lib/openstudio-standards/standards/Standards.Model.rb in openstudio-standards-0.2.2 vs lib/openstudio-standards/standards/Standards.Model.rb in openstudio-standards-0.2.3

- old
+ new

@@ -1,5 +1,9 @@ +require 'csv' + + + class Standard attr_accessor :space_multiplier_map def define_space_multiplier return @space_multiplier_map @@ -9,17 +13,16 @@ # Creates a Performance Rating Method (aka Appendix G aka LEED) baseline building model # based on the inputs currently in the model. # the current model with this model. # - # @note Per 90.1, the Performance Rating Method "does NOT offer an alternative - # compliance path for minimum standard compliance." This means you can't use - # this method for code compliance to get a permit. + # @note Per 90.1, the Performance Rating Method "does NOT offer an alternative compliance path for minimum standard compliance." + # This means you can't use this method for code compliance to get a permit. # @param building_type [String] the building type # @param climate_zone [String] the climate zone # @param custom [String] the custom logic that will be applied during baseline creation. Valid choices are 'Xcel Energy CO EDA' or '90.1-2007 with addenda dn'. - # If nothing is specified, no custom logic will be applied; the process will follow the template logic explicitly. + # If nothing is specified, no custom logic will be applied; the process will follow the template logic explicitly. # @param sizing_run_dir [String] the directory where the sizing runs will be performed # @param debug [Boolean] If true, will report out more detailed debugging output # @return [Bool] returns true if successful, false if not def model_create_prm_baseline_building(model, building_type, climate_zone, custom = nil, sizing_run_dir = Dir.pwd, debug = false) model.getBuilding.setName("#{template}-#{building_type}-#{climate_zone} PRM baseline created: #{Time.new}") @@ -31,16 +34,14 @@ # Reduce the WWR and SRR, if necessary OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Adjusting Window and Skylight Ratios ***') model_apply_prm_baseline_window_to_wall_ratio(model, climate_zone) model_apply_prm_baseline_skylight_to_roof_ratio(model) - # Assign building stories to spaces in the building - # where stories are not yet assigned. + # Assign building stories to spaces in the building where stories are not yet assigned. model_assign_spaces_to_stories(model) - # Modify the internal loads in each space type, - # keeping user-defined schedules. + # Modify the internal loads in each space type, keeping user-defined schedules. OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Changing Lighting Loads ***') model.getSpaceTypes.sort.each do |space_type| set_people = false set_lights = true set_electric_equipment = false @@ -48,13 +49,12 @@ set_ventilation = false set_infiltration = false space_type_apply_internal_loads(space_type, set_people, set_lights, set_electric_equipment, set_gas_equipment, set_ventilation, set_infiltration) end - # If any of the lights are missing schedules, assign an - # always-off schedule to those lights. This is assumed to - # be the user's intent in the proposed model. + # If any of the lights are missing schedules, assign an always-off schedule to those lights. + # This is assumed to be the user's intent in the proposed model. model.getLightss.sort.each do |lights| if lights.schedule.empty? lights.setSchedule(model.alwaysOffDiscreteSchedule) end end @@ -79,28 +79,23 @@ model_apply_prm_construction_types(model) # Set the construction properties of all the surfaces in the model model_apply_standard_constructions(model, climate_zone) - # Get the groups of zones that define the - # baseline HVAC systems for later use. - # This must be done before removing the HVAC systems - # because it requires knowledge of proposed HVAC fuels. + # Get the groups of zones that define the baseline HVAC systems for later use. + # This must be done before removing the HVAC systems because it requires knowledge of proposed HVAC fuels. OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Grouping Zones by Fuel Type and Occupancy Type ***') sys_groups = model_prm_baseline_system_groups(model, custom) - # Remove all HVAC from model, - # excluding service water heating + # Remove all HVAC from model, excluding service water heating model_remove_prm_hvac(model) - # Modify the service water heating loops - # per the baseline rules + # Modify the service water heating loops per the baseline rules OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Cleaning up Service Water Heating Loops ***') model_apply_baseline_swh_loops(model, building_type) - # Determine the baseline HVAC system type for each of - # the groups of zones and add that system type. + # Determine the baseline HVAC system type for each of the groups of zones and add that system type. OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Adding Baseline HVAC Systems ***') sys_groups.each do |sys_group| # Determine the primary baseline system type system_type = model_prm_baseline_system_type(model, climate_zone, @@ -162,12 +157,11 @@ # Run sizing run with the HVAC equipment if model_run_sizing_run(model, "#{sizing_run_dir}/SR1") == false return false end - # If there are any multizone systems, reset damper positions - # to achieve a 60% ventilation effectiveness minimum for the system + # 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 model_apply_multizone_vav_outdoor_air_sizing(model) # Set the baseline fan power for all airloops model.getAirLoopHVACs.sort.each do |air_loop| @@ -193,12 +187,11 @@ # Skip the SWH loops next if plant_loop_swh_loop?(plant_loop) plant_loop_apply_prm_number_of_cooling_towers(plant_loop) end - # Run sizing run with the new chillers, boilers, and - # cooling towers to determine capacities + # Run sizing run with the new chillers, boilers, and cooling towers to determine capacities if model_run_sizing_run(model, "#{sizing_run_dir}/SR2") == false return false end # Set the pumping control strategy and power @@ -217,17 +210,12 @@ # Fix EMS references. # Temporary workaround for OS issue #2598 model_temp_fix_ems_references(model) - # Delete all the unused curves - model.getCurves.sort.each do |curve| - if curve.directUseCount == 0 - OpenStudio::logFree(OpenStudio::Debug, 'openstudio.standards.Model', "#{curve.name} is unused; it will be removed.") - model.removeObject(curve.handle) - end - end + # Delete all the unused resource objects + model_remove_unused_resource_objects(model) # TODO: turn off self shading # Set Solar Distribution to MinimalShadowing... problem is when you also have detached shading such as surrounding buildings etc # It won't be taken into account, while it should: only self shading from the building itself should be turned off but to my knowledge there isn't a way to do this in E+ @@ -241,20 +229,18 @@ idf.save(idf_path, true) return true end - # Determine if there needs to be a sizing run after constructions - # are added so that EnergyPlus can calculate the VLTs of - # layer-by-layer glazing constructions. These VLT values are - # needed for the daylighting controls logic for some templates. + # Determine if there needs to be a sizing run after constructions are added + # so that EnergyPlus can calculate the VLTs of layer-by-layer glazing constructions. + # These VLT values are needed for the daylighting controls logic for some templates. def model_create_prm_baseline_building_requires_vlt_sizing_run(model) return false # Not required for most templates end - # Determine the residential and nonresidential floor areas - # based on the space type properties for each space. + # Determine the residential and nonresidential floor areas based on the space type properties for each space. # For spaces with no space type, assume nonresidential. # # @return [Hash] keys are 'residential' and 'nonresidential', units are m^2 def model_residential_and_nonresidential_floor_areas(model) res_area_m2 = 0 @@ -268,14 +254,13 @@ end return { 'residential' => res_area_m2, 'nonresidential' => nonres_area_m2 } end - # Determine the number of stories spanned by the - # supplied zones. If all zones on one of the stories have - # an indentical multiplier, assume that the multiplier is a - # floor multiplier and increase the number of stories accordingly. + # Determine the number of stories spanned by the supplied zones. + # If all zones on one of the stories have an indentical multiplier, + # assume that the multiplier is a floor multiplier and increase the number of stories accordingly. # Stories do not have to be contiguous. # # @param zones [Array<OpenStudio::Model::ThermalZone>] an array of zones # @return [Integer] the number of stories spanned def model_num_stories_spanned(model, zones) @@ -299,15 +284,13 @@ end return num_stories end - # Categorize zones by occupancy type and fuel type, - # where the types depend on the standard. + # Categorize zones by occupancy type and fuel type, where the types depend on the standard. # - # @return [Array<Hash>] an array of hashes, one for each zone, - # with the keys 'zone', 'type' (occ type), 'fuel', and 'area' + # @return [Array<Hash>] an array of hashes, one for each zone, with the keys 'zone', 'type' (occ type), 'fuel', and 'area' def model_zones_with_occ_and_fuel_type(model, custom) zones = [] model.getThermalZones.sort.each do |zone| # Skip plenums @@ -333,10 +316,13 @@ zn_hash['area'] = zone.floorArea # Occupancy type zn_hash['occ'] = thermal_zone_occupancy_type(zone) + # Building type + zn_hash['bldg_type'] = thermal_zone_building_type(zone) + # Fuel type zn_hash['fuel'] = thermal_zone_fossil_or_electric_type(zone, custom) zones << zn_hash end @@ -390,15 +376,13 @@ dom_occ = type_to_area.sort_by { |k, v| v }.reverse[0][0] # Get the dominant occupancy type group dom_occ_group = zones_grouped_by_occ[dom_occ] - # Check the non-dominant occupancy type groups to see if they - # are big enough to trigger the occupancy exception. + # Check the non-dominant occupancy type groups to see if they are big enough to trigger the occupancy exception. # If they are, leave the group standing alone. - # If they are not, add the zones in that group - # back to the dominant occupancy type group. + # If they are not, add the zones in that group back to the dominant occupancy type group. occ_groups = [] zones_grouped_by_occ.each do |occ_type, zns| # Skip the dominant occupancy type next if occ_type == dom_occ @@ -419,14 +403,12 @@ end end # Add the dominant occupancy group to the list occ_groups << [dom_occ, dom_occ_group] - # Inside of each remaining occupancy group, - # determine the dominant fuel type. This determination - # should only include zones that are part of the - # dominant area type inside of this group. + # Inside of each remaining occupancy group, determine the dominant fuel type. + # This determination should only include zones that are part of the dominant area type inside of this group. occ_and_fuel_groups = [] occ_groups.each do |occ_type, zns| # Separate the zones that are part of the dominant occ type dom_occ_zns = [] nondom_occ_zns = [] @@ -436,12 +418,11 @@ else nondom_occ_zns << zn end end - # Determine the dominant fuel type - # from the subset of the dominant area type zones + # Determine the dominant fuel type from the subset of the dominant area type zones fuel_to_area = Hash.new { 0.0 } zones_grouped_by_fuel = dom_occ_zns.group_by { |z| z['fuel'] } zones_grouped_by_fuel.each do |fuel, zns_by_fuel| zns_by_fuel.each do |zn| fuel_to_area[fuel] += zn['area'] @@ -449,12 +430,11 @@ end sorted_by_area = fuel_to_area.sort_by { |k, v| v }.reverse dom_fuel = sorted_by_area[0][0] - # Don't allow unconditioned to be the dominant fuel, - # go to the next biggest + # Don't allow unconditioned to be the dominant fuel, go to the next biggest if dom_fuel == 'unconditioned' if sorted_by_area.size > 1 dom_fuel = sorted_by_area[1][0] else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', 'The fuel type was not able to be determined for any zones in this model. Run with debug messages enabled to see possible reasons.') @@ -466,19 +446,16 @@ dom_fuel_group = {} dom_fuel_group['occ'] = occ_type dom_fuel_group['fuel'] = dom_fuel dom_fuel_group['zones'] = zones_grouped_by_fuel[dom_fuel] - # The zones that aren't part of the dominant occ type - # are automatically added to the dominant fuel group + # The zones that aren't part of the dominant occ type are automatically added to the dominant fuel group dom_fuel_group['zones'] += nondom_occ_zns - # Check the non-dominant occupancy type groups to see if they - # are big enough to trigger the occupancy exception. + # Check the non-dominant occupancy type groups to see if they are big enough to trigger the occupancy exception. # If they are, leave the group standing alone. - # If they are not, add the zones in that group - # back to the dominant occupancy type group. + # If they are not, add the zones in that group back to the dominant occupancy type group. zones_grouped_by_fuel.each do |fuel_type, zns_by_fuel| # Skip the dominant occupancy type next if fuel_type == dom_fuel # Add up the floor area of the group @@ -504,14 +481,12 @@ # Add the dominant occupancy group to the list occ_and_fuel_groups << dom_fuel_group end # Moved heated-only zones into their own groups. - # Per the PNNL PRM RM, this must be done AFTER - # the dominant occ and fuel types are determined - # so that heated-only zone areas are part of - # the determination. + # Per the PNNL PRM RM, this must be done AFTER the dominant occ and fuel types are determined + # so that heated-only zone areas are part of the determination. final_groups = [] occ_and_fuel_groups.each do |gp| # Skip unconditioned groups next if gp['fuel'] == 'unconditioned' @@ -527,23 +502,21 @@ gp['zones'] = heated_cooled_zones # Add the group (less unheated zones) to the final list final_groups << gp - # If there are any heated-only zones, create - # a new group for them. + # If there are any heated-only zones, create a new group for them. unless heated_only_zones.empty? htd_only_group = {} htd_only_group['occ'] = 'heatedonly' htd_only_group['fuel'] = gp['fuel'] htd_only_group['zones'] = heated_only_zones final_groups << htd_only_group end end - # Calculate the area for each of the final groups - # and replace the zone hashes with the zone objects + # Calculate the area for each of the final groups and replace the zone hashes with the zone objects final_groups.each do |gp| area_m2 = 0.0 gp_zns = [] gp['zones'].each do |zn| area_m2 += zn['area'] @@ -553,17 +526,14 @@ gp['area_ft2'] = area_ft2 gp['zones'] = gp_zns end # TODO: Remove the secondary zones before - # determining the area used to pick the HVAC - # system, per PNNL PRM RM + # determining the area used to pick the HVAC system, per PNNL PRM RM - # If there is any district heating or district cooling - # in the proposed building, the heating and cooling - # fuels in the entire baseline building are changed - # for the purposes of HVAC system assignment + # If there is any district heating or district cooling in the proposed building, the heating and cooling + # fuels in the entire baseline building are changed for the purposes of HVAC system assignment all_htg_fuels = [] all_clg_fuels = [] model.getThermalZones.sort.each do |zone| all_htg_fuels += zone.heating_fuels all_clg_fuels += zone.cooling_fuels @@ -593,20 +563,18 @@ elsif !purchased_heating && purchased_cooling district_fuel = 'purchasedcooling' OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', 'The proposed model included purchased cooling. All baseline building system selection will be based on this information.') end - # Change the fuel in all final groups - # if district systems were found. + # Change the fuel in all final groups if district systems were found. if district_fuel final_groups.each do |gp| gp['fuel'] = district_fuel end end - # Determine the number of stories spanned - # by each group and report out info. + # Determine the number of stories spanned by each group and report out info. final_groups.each do |group| # Determine the number of stories this group spans num_stories = model_num_stories_spanned(model, group['zones']) group['stories'] = num_stories # Report out the final grouping @@ -630,25 +598,21 @@ exception_min_area_ft2 = 20_000 exception_min_area_m2 = OpenStudio.convert(exception_min_area_ft2, 'ft^2', 'm^2').get return exception_min_area_m2 end - # Determine the baseline system type given the - # inputs. Logic is different for different standards. + # Determine the baseline system type given the inputs. Logic is different for different standards. # # 90.1-2007, 90.1-2010, 90.1-2013 - # @param area_type [String] Valid choices are residential, - # nonresidential, and heatedonly - # @param fuel_type [String] Valid choices are - # electric, fossil, fossilandelectric, - # purchasedheat, purchasedcooling, purchasedheatandcooling + # @param area_type [String] Valid choices are residential, nonresidential, and heatedonly + # @param fuel_type [String] Valid choices are electric, fossil, fossilandelectric, + # purchasedheat, purchasedcooling, purchasedheatandcooling # @param area_ft2 [Double] Area in ft^2 # @param num_stories [Integer] Number of stories - # @return [String] The system type. Possibilities are - # PTHP, PTAC, PSZ_AC, PSZ_HP, PVAV_Reheat, PVAV_PFP_Boxes, - # VAV_Reheat, VAV_PFP_Boxes, Gas_Furnace, Electric_Furnace - # @todo add 90.1-2013 systems 11-13 + # @return [String] The system type. Possibilities are PTHP, PTAC, PSZ_AC, PSZ_HP, PVAV_Reheat, PVAV_PFP_Boxes, + # VAV_Reheat, VAV_PFP_Boxes, Gas_Furnace, Electric_Furnace + # TODO: add 90.1-2013 systems 11-13 def model_prm_baseline_system_type(model, climate_zone, area_type, fuel_type, area_ft2, num_stories, custom) # [type, central_heating_fuel, zone_heating_fuel, cooling_fuel] system_type = [nil, nil, nil, nil] # Get the row from TableG3.1.1A @@ -707,14 +671,12 @@ end return system_type end - # Determines which system number is used - # for the baseline system. Default is 90.1-2004 approach. - # @return [String] the system number: 1_or_2, 3_or_4, - # 5_or_6, 7_or_8, 9_or_10 + # Determines which system number is used for the baseline system. Default is 90.1-2004 approach. + # @return [String] the system number: 1_or_2, 3_or_4, 5_or_6, 7_or_8, 9_or_10 def model_prm_baseline_system_number(model, climate_zone, area_type, fuel_type, area_ft2, num_stories, custom) sys_num = nil # Set the area limit limit_ft2 = 75_000 @@ -740,81 +702,60 @@ end return sys_num end - # Change the fuel type based on climate zone, depending on the standard. - # Defaults to no change. + # Change the fuel type based on climate zone, depending on the standard. Defaults to no change. # @return [String] the revised fuel type def model_prm_baseline_system_change_fuel_type(model, fuel_type, climate_zone, custom = nil) return fuel_type # Don't change fuel type for most templates end - # Add the specified baseline system type to the - # specified zons based on the specified template. - # For some multi-zone system types, the standards require - # identifying zones whose loads or schedules - # are outliers and putting these systems on separate - # single-zone systems. This method does that. + # Add the specified baseline system type to the specified zones based on the specified template. + # For some multi-zone system types, the standards require identifying zones whose loads or schedules + # are outliers and putting these systems on separate single-zone systems. This method does that. # - # @param system_type [String] The system type. Valid choices are - # PTHP, PTAC, PSZ_AC, PSZ_HP, PVAV_Reheat, PVAV_PFP_Boxes, - # VAV_Reheat, VAV_PFP_Boxes, Gas_Furnace, Electric_Furnace, - # which are also returned by the method - # OpenStudio::Model::Model.prm_baseline_system_type. - # @param main_heat_fuel [String] main heating fuel. Valid choices are - # Electricity, NaturalGas, DistrictHeating - # @param zone_heat_fuel [String] zone heating/reheat fuel. Valid choices are - # Electricity, NaturalGas, DistrictHeating - # @param cool_fuel [String] cooling fuel. Valid choices are - # Electricity, DistrictCooling - # @todo add 90.1-2013 systems 11-13 + # @param system_type [String] The system type. Valid choices are PTHP, PTAC, PSZ_AC, PSZ_HP, PVAV_Reheat, + # PVAV_PFP_Boxes, VAV_Reheat, VAV_PFP_Boxes, Gas_Furnace, Electric_Furnace, + # which are also returned by the method OpenStudio::Model::Model.prm_baseline_system_type. + # @param main_heat_fuel [String] main heating fuel. Valid choices are Electricity, NaturalGas, DistrictHeating + # @param zone_heat_fuel [String] zone heating/reheat fuel. Valid choices are Electricity, NaturalGas, DistrictHeating + # @param cool_fuel [String] cooling fuel. Valid choices are Electricity, DistrictCooling + # TODO: Add 90.1-2013 systems 11-13 def model_add_prm_baseline_system(model, system_type, main_heat_fuel, zone_heat_fuel, cool_fuel, zones) case system_type when 'PTAC' # System 1 - unless zones.empty? - - # Retrieve the existing hot water loop - # or add a new one if necessary. + # Retrieve the existing hot water loop or add a new one if necessary. hot_water_loop = nil hot_water_loop = if model.getPlantLoopByName('Hot Water Loop').is_initialized model.getPlantLoopByName('Hot Water Loop').get else model_add_hw_loop(model, main_heat_fuel) end # Add a hot water PTAC to each zone model_add_ptac(model, - nil, - hot_water_loop, zones, - 'ConstantVolume', - 'Water', - 'Single Speed DX AC') + cooling_type: "Single Speed DX AC", + heating_type: "Water", + hot_water_loop: hot_water_loop, + fan_type: "ConstantVolume") end when 'PTHP' # System 2 - unless zones.empty? - - # Add an air-source packaged terminal - # heat pump with electric supplemental heat - # to each zone. + # add an air-source packaged terminal heat pump with electric supplemental heat to each zone. model_add_pthp(model, - nil, zones, - 'ConstantVolume') - + fan_type: 'ConstantVolume') end when 'PSZ_AC' # System 3 - unless zones.empty? - heating_type = 'Gas' - # If district heating + # if district heating hot_water_loop = nil if main_heat_fuel == 'DistrictHeating' heating_type = 'Water' hot_water_loop = if model.getPlantLoopByName('Hot Water Loop').is_initialized model.getPlantLoopByName('Hot Water Loop').get @@ -830,67 +771,42 @@ cooling_type = 'Water' chilled_water_loop = if model.getPlantLoopByName('Chilled Water Loop').is_initialized model.getPlantLoopByName('Chilled Water Loop').get else model_add_chw_loop(model, - 'const_pri', - chiller_cooling_type = nil, - chiller_condenser_type = nil, - chiller_compressor_type = nil, - cool_fuel, - condenser_water_loop = nil, - building_type = nil) - + cooling_fuel: cool_fuel, + chw_pumping_type: 'const_pri') end end - # Add a gas-fired PSZ-AC to each zone - # hvac_op_sch=nil means always on - # oa_damper_sch to nil means always open + # Add a PSZ-AC to each zone model_add_psz_ac(model, - sys_name = nil, - hot_water_loop, - chilled_water_loop, zones, - hvac_op_sch = nil, - oa_damper_sch = nil, - fan_location = 'DrawThrough', - fan_type = 'ConstantVolume', - heating_type, - supplemental_heating_type = 'Gas', # Should we really add supplemental heating here? - cooling_type, - building_type = nil) - + cooling_type: cooling_type, + chilled_water_loop: chilled_water_loop, + heating_type: heating_type, + supplemental_heating_type: "Gas", + hot_water_loop: hot_water_loop, + fan_location: 'DrawThrough', + fan_type: 'ConstantVolume') end when 'PSZ_HP' # System 4 - unless zones.empty? - - # Add an air-source packaged single zone - # heat pump with electric supplemental heat - # to each zone. + # Add an air-source packaged single zone heat pump with electric supplemental heat to each zone. model_add_psz_ac(model, - 'PSZ-HP', - nil, - nil, zones, - nil, - nil, - 'DrawThrough', - 'ConstantVolume', - 'Single Speed Heat Pump', - 'Electric', - 'Single Speed Heat Pump', - building_type = nil) - + system_name: 'PSZ-HP', + cooling_type: 'Single Speed Heat Pump', + heating_type: 'Single Speed Heat Pump', + supplemental_heating_type: 'Electric', + fan_location: 'DrawThrough', + fan_type: 'ConstantVolume') end when 'PVAV_Reheat' # System 5 - - # Retrieve the existing hot water loop - # or add a new one if necessary. + # Retrieve the existing hot water loop or add a new one if necessary. hot_water_loop = nil hot_water_loop = if model.getPlantLoopByName('Hot Water Loop').is_initialized model.getPlantLoopByName('Hot Water Loop').get else model_add_hw_loop(model, main_heat_fuel) @@ -901,17 +817,12 @@ if cool_fuel == 'DistrictCooling' chilled_water_loop = if model.getPlantLoopByName('Chilled Water Loop').is_initialized model.getPlantLoopByName('Chilled Water Loop').get else model_add_chw_loop(model, - 'const_pri', - chiller_cooling_type = nil, - chiller_condenser_type = nil, - chiller_compressor_type = nil, - cool_fuel, - condenser_water_loop = nil, - building_type = nil) + cooling_fuel: cool_fuel, + chw_pumping_type: 'const_pri') end end # If electric zone heat electric_reheat = false @@ -936,50 +847,39 @@ stories = [] story_group[0].spaces.each do |space| stories << [space.buildingStory.get.name.get, building_story_minimum_z_value(space.buildingStory.get)] end story_name = stories.sort_by { |nm, z| z }[0][0] - sys_name = "#{story_name} PVAV_Reheat (Sys5)" + system_name = "#{story_name} PVAV_Reheat (Sys5)" # If and only if there are primary zones to attach to the loop # counter example: floor with only one elevator machine room that get classified as sec_zones unless pri_zones.empty? - model_add_pvav(model, - sys_name, pri_zones, - nil, - nil, - electric_reheat, - hot_water_loop, - chilled_water_loop, - nil, - nil) + system_name: system_name, + hot_water_loop: hot_water_loop, + chilled_water_loop: chilled_water_loop, + electric_reheat: electric_reheat) end # Add a PSZ_AC for each secondary zone unless sec_zones.empty? model_add_prm_baseline_system(model, 'PSZ_AC', main_heat_fuel, zone_heat_fuel, cool_fuel, sec_zones) end end when 'PVAV_PFP_Boxes' # System 6 - # If district cooling chilled_water_loop = nil if cool_fuel == 'DistrictCooling' chilled_water_loop = if model.getPlantLoopByName('Chilled Water Loop').is_initialized model.getPlantLoopByName('Chilled Water Loop').get else model_add_chw_loop(model, - 'const_pri', - chiller_cooling_type = nil, - chiller_condenser_type = nil, - chiller_compressor_type = nil, - cool_fuel, - condenser_water_loop = nil, - building_type = nil) + cooling_fuel: cool_fuel, + chw_pumping_type: 'const_pri') end end # Group zones by story story_zone_lists = model_group_zones_by_story(model, zones) @@ -998,73 +898,58 @@ stories = [] story_group[0].spaces.each do |space| stories << [space.buildingStory.get.name.get, building_story_minimum_z_value(space.buildingStory.get)] end story_name = stories.sort_by { |nm, z| z }[0][0] - sys_name = "#{story_name} PVAV_PFP_Boxes (Sys6)" + system_name = "#{story_name} PVAV_PFP_Boxes (Sys6)" # If and only if there are primary zones to attach to the loop unless pri_zones.empty? model_add_pvav_pfp_boxes(model, - sys_name, pri_zones, - nil, - nil, - 0.62, - 0.9, - OpenStudio.convert(4.0, 'inH_{2}O', 'Pa').get, - chilled_water_loop, - nil) + system_name: system_name, + chilled_water_loop: chilled_water_loop, + fan_efficiency: 0.62, + fan_motor_efficiency: 0.9, + fan_pressure_rise: 4.0) end # Add a PSZ_HP for each secondary zone unless sec_zones.empty? model_add_prm_baseline_system(model, 'PSZ_HP', main_heat_fuel, zone_heat_fuel, cool_fuel, sec_zones) end end when 'VAV_Reheat' # System 7 - - # Retrieve the existing hot water loop - # or add a new one if necessary. + # Retrieve the existing hot water loop or add a new one if necessary. hot_water_loop = nil hot_water_loop = if model.getPlantLoopByName('Hot Water Loop').is_initialized model.getPlantLoopByName('Hot Water Loop').get else model_add_hw_loop(model, main_heat_fuel) end - # Retrieve the existing chilled water loop - # or add a new one if necessary. + # Retrieve the existing chilled water loop or add a new one if necessary. chilled_water_loop = nil if model.getPlantLoopByName('Chilled Water Loop').is_initialized chilled_water_loop = model.getPlantLoopByName('Chilled Water Loop').get else if cool_fuel == 'DistrictCooling' chilled_water_loop = model_add_chw_loop(model, - 'const_pri', - chiller_cooling_type = nil, - chiller_condenser_type = nil, - chiller_compressor_type = nil, - cool_fuel, - condenser_water_loop = nil, - building_type = nil) + cooling_fuel: cool_fuel, + chw_pumping_type: 'const_pri') else fan_type = model_cw_loop_cooling_tower_fan_type(model) condenser_water_loop = model_add_cw_loop(model, - 'Open Cooling Tower', - 'Propeller or Axial', - fan_type, - 1, - 1, - nil) + cooling_tower_type: 'Open Cooling Tower', + cooling_tower_fan_type: 'Propeller or Axial', + cooling_tower_capacity_control: fan_type, + number_of_cells_per_tower: 1, + number_cooling_towers: 1) chilled_water_loop = model_add_chw_loop(model, - 'const_pri_var_sec', - 'WaterCooled', - chiller_condenser_type = nil, - 'Rotary Screw', - cooling_fuel = nil, - condenser_water_loop, - building_type = nil) + chw_pumping_type: 'const_pri_var_sec', + chiller_cooling_type: 'WaterCooled', + chiller_compressor_type: 'Rotary Screw', + condenser_water_loop: condenser_water_loop) end end # If electric zone heat reheat_type = 'Water' @@ -1073,14 +958,12 @@ end # Group zones by story story_zone_lists = model_group_zones_by_story(model, zones) - # For the array of zones on each story, - # separate the primary zones from the secondary zones. - # Add the baseline system type to the primary zones - # and add the suplemental system type to the secondary zones. + # For the array of zones on each story, separate the primary zones from the secondary zones. + # Add the baseline system type to the primary zones and add the suplemental system type to the secondary zones. story_zone_lists.each do |story_group| # The model_group_zones_by_story(model) NO LONGER returns empty lists when a given floor doesn't have any of the zones # So NO need to filter it out otherwise you get an error undefined method `spaces' for nil:NilClass # next if zones.empty? @@ -1093,70 +976,55 @@ stories = [] story_group[0].spaces.each do |space| stories << [space.buildingStory.get.name.get, building_story_minimum_z_value(space.buildingStory.get)] end story_name = stories.sort_by { |nm, z| z }[0][0] - sys_name = "#{story_name} VAV_Reheat (Sys7)" + system_name = "#{story_name} VAV_Reheat (Sys7)" # If and only if there are primary zones to attach to the loop # counter example: floor with only one elevator machine room that get classified as sec_zones unless pri_zones.empty? model_add_vav_reheat(model, - sys_name, - hot_water_loop, - chilled_water_loop, pri_zones, - nil, - nil, - 0.62, - 0.9, - OpenStudio.convert(4.0, 'inH_{2}O', 'Pa').get, - nil, - reheat_type, - nil) + system_name: system_name, + reheat_type: reheat_type, + hot_water_loop: hot_water_loop, + chilled_water_loop: chilled_water_loop, + fan_efficiency: 0.62, + fan_motor_efficiency: 0.9, + fan_pressure_rise: 4.0) end # Add a PSZ_AC for each secondary zone unless sec_zones.empty? model_add_prm_baseline_system(model, 'PSZ_AC', main_heat_fuel, zone_heat_fuel, cool_fuel, sec_zones) end end when 'VAV_PFP_Boxes' # System 8 - - # Retrieve the existing chilled water loop - # or add a new one if necessary. + # Retrieve the existing chilled water loop or add a new one if necessary. chilled_water_loop = nil if model.getPlantLoopByName('Chilled Water Loop').is_initialized chilled_water_loop = model.getPlantLoopByName('Chilled Water Loop').get else if cool_fuel == 'DistrictCooling' chilled_water_loop = model_add_chw_loop(model, - 'const_pri', - chiller_cooling_type = nil, - chiller_condenser_type = nil, - chiller_compressor_type = nil, - cool_fuel, - condenser_water_loop = nil, - building_type = nil) + cooling_fuel: cool_fuel, + chw_pumping_type: 'const_pri') else fan_type = model_cw_loop_cooling_tower_fan_type(model) condenser_water_loop = model_add_cw_loop(model, - 'Open Cooling Tower', - 'Propeller or Axial', - fan_type, - 1, - 1, - nil) + cooling_tower_type: 'Open Cooling Tower', + cooling_tower_fan_type: 'Propeller or Axial', + cooling_tower_capacity_control: fan_type, + number_of_cells_per_tower: 1, + number_cooling_towers: 1) chilled_water_loop = model_add_chw_loop(model, - 'const_pri_var_sec', - 'WaterCooled', - chiller_condenser_type = nil, - 'Rotary Screw', - cool_fueling = nil, - condenser_water_loop, - building_type = nil) + chw_pumping_type: 'const_pri_var_sec', + chiller_cooling_type: 'WaterCooled', + chiller_compressor_type: 'Rotary Screw', + condenser_water_loop: condenser_water_loop) end end # Group zones by story story_zone_lists = model_group_zones_by_story(model, zones) @@ -1175,78 +1043,60 @@ stories = [] story_group[0].spaces.each do |space| stories << [space.buildingStory.get.name.get, building_story_minimum_z_value(space.buildingStory.get)] end story_name = stories.sort_by { |nm, z| z }[0][0] - sys_name = "#{story_name} VAV_PFP_Boxes (Sys8)" + system_name = "#{story_name} VAV_PFP_Boxes (Sys8)" # If and only if there are primary zones to attach to the loop unless pri_zones.empty? model_add_vav_pfp_boxes(model, - sys_name, - chilled_water_loop, pri_zones, - nil, - nil, - 0.62, - 0.9, - OpenStudio.convert(4.0, 'inH_{2}O', 'Pa').get) + system_name: system_name, + chilled_water_loop: chilled_water_loop, + fan_efficiency:0.62, + fan_motor_efficiency: 0.9, + fan_pressure_rise: 4.0) end # Add a PSZ_HP for each secondary zone unless sec_zones.empty? model_add_prm_baseline_system(model, 'PSZ_HP', main_heat_fuel, zone_heat_fuel, cool_fuel, sec_zones) end end when 'Gas_Furnace' # System 9 - unless zones.empty? - # If district heating hot_water_loop = nil if main_heat_fuel == 'DistrictHeating' hot_water_loop = if model.getPlantLoopByName('Hot Water Loop').is_initialized model.getPlantLoopByName('Hot Water Loop').get else model_add_hw_loop(model, main_heat_fuel) end end - # Add a System 9 - Gas Unit Heater to each zone model_add_unitheater(model, - nil, zones, - nil, - 'ConstantVolume', - OpenStudio.convert(0.2, 'inH_{2}O', 'Pa').get, - main_heat_fuel, - hot_water_loop, - nil) - + fan_control_type: 'ConstantVolume', + fan_pressure_rise: 0.2, + heating_type: main_heat_fuel, + hot_water_loop: hot_water_loop) end when 'Electric_Furnace' # System 10 - unless zones.empty? - # Add a System 10 - Electric Unit Heater to each zone model_add_unitheater(model, - nil, zones, - nil, - 'ConstantVolume', - OpenStudio.convert(0.2, 'inH_{2}O', 'Pa').get, - main_heat_fuel, - nil, - nil) - + fan_control_type: 'ConstantVolume', + fan_pressure_rise: 0.2, + heating_type: main_heat_fuel) end else - OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "System type #{system_type} is not a valid choice, nothing will be added to the model.") return false - end return true end # Determines the fan type used by VAV_Reheat and VAV_PFP_Boxes systems. @@ -1255,12 +1105,11 @@ def model_baseline_system_vav_fan_type(model) fan_type = 'TwoSpeed Fan' return fan_type end - # Looks through the model and creates an hash of what the baseline - # system type should be for each zone. + # Looks through the model and creates an hash of what the baseline system type should be for each zone. # # @return [Hash] keys are zones, values are system type strings # PTHP, PTAC, PSZ_AC, PSZ_HP, PVAV_Reheat, PVAV_PFP_Boxes, # VAV_Reheat, VAV_PFP_Boxes, Gas_Furnace, Electric_Furnace def model_get_baseline_system_type_by_zone(model, climate_zone, custom = nil) @@ -1329,12 +1178,11 @@ end return zone_to_sys_type end - # @param array_of_zones [Array] an array of Hashes for each zone, - # with the keys 'zone', + # @param array_of_zones [Array] an array of Hashes for each zone, with the keys 'zone', def model_eliminate_outlier_zones(model, array_of_zones, key_to_inspect, tolerance, field_name, units) # Sort the zones by the desired key array_of_zones = array_of_zones.sort_by { |hsh| hsh[key_to_inspect] } # Calculate the area-weighted average @@ -1356,12 +1204,11 @@ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Model', "Values for #{field_name}, tol = #{tolerance} #{units}, area ft2:") OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Model', "vals #{all_vals.join(', ')}") OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Model', "areas #{all_areas.join(', ')}") OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Model', "names #{all_zn_names.join(', ')}") - # Calculate the biggest delta - # and the index of the biggest delta + # Calculate the biggest delta and the index of the biggest delta biggest_delta_i = nil biggest_delta = 0.0 worst = nil array_of_zones.each_with_index do |zn, i| val = zn[key_to_inspect] @@ -1373,37 +1220,30 @@ end end # puts " #{worst} - #{avg.round} = #{biggest_delta.round} biggest delta" - # Compare the biggest delta - # against the difference and - # eliminate that zone if higher - # than the limit. + # Compare the biggest delta against the difference and eliminate that zone if higher than the limit. if biggest_delta > tolerance zn_name = array_of_zones[biggest_delta_i]['zone'].name.get.to_s OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "For zone #{zn_name}, the #{field_name} of #{worst.round(1)} #{units} is more than #{tolerance} #{units} outside the area-weighted average of #{avg.round(1)} #{units}; it will be placed on its own secondary system.") array_of_zones.delete_at(biggest_delta_i) # Call method recursively if something was eliminated array_of_zones = model_eliminate_outlier_zones(model, array_of_zones, key_to_inspect, tolerance, field_name, units) else - # OpenStudio::logFree(OpenStudio::Debug, 'openstudio.standards.Model', "#{worst.round(1)} - #{avg.round(1)} = #{biggest_delta.round(1)} #{units} < tolerance of #{tolerance} #{units}, stopping elimination process.") - OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Model', "#{worst} - #{avg} = #{biggest_delta} #{units} < tolerance of #{tolerance} #{units}, stopping elimination process.") + zn_name = array_of_zones[biggest_delta_i]['zone'].name.get.to_s + OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "For zone #{zn_name}, the #{field_name} #{worst.round(2)} #{units} - average #{field_name} #{avg.round(2)} #{units} = #{biggest_delta.round(2)} #{units} < tolerance of #{tolerance} #{units}, stopping elimination process.") end return array_of_zones end - # Determine which of the zones - # should be served by the primary HVAC system. - # First, eliminate zones that differ by more - # than 40 full load hours per week. In this case, - # lighting schedule is used as the proxy for operation - # instead of occupancy to avoid accidentally removing - # transition spaces. Second, eliminate zones whose - # design internal loads differ from the - # area-weighted average of all other zones + # Determine which of the zones should be served by the primary HVAC system. + # First, eliminate zones that differ by more# than 40 full load hours per week. + # In this case, lighting schedule is used as the proxy for operation instead + # of occupancy to avoid accidentally removing transition spaces. + # Second, eliminate zones whose design internal loads differ from the area-weighted average of all other zones # on the system by more than 10 Btu/hr*ft^2. # # @return [Hash] A hash of two arrays of ThermalZones, # where the keys are 'primary' and 'secondary' def model_differentiate_primary_secondary_thermal_zones(model, zones) @@ -1462,13 +1302,12 @@ end zone_data_1 << data end - # Filter out any zones that operate differently by more - # than 40hrs/wk. This will be determined by a difference of more - # than (40 hrs/wk * 52 wks/yr) = 2080 annual full load hrs. + # Filter out any zones that operate differently by more than 40hrs/wk. + # This will be determined by a difference of more than (40 hrs/wk * 52 wks/yr) = 2080 annual full load hrs. zones_same_hrs = model_eliminate_outlier_zones(model, zone_data_1, 'wk_op_hrs', 40, 'weekly operating hrs', 'hrs') # Get the internal loads for # all remaining zones. zone_data_2 = [] @@ -1479,11 +1318,11 @@ # Get the area area_m2 = zone.floorArea * zone.multiplier area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get data['area_ft2'] = area_ft2 # Get the internal loads - int_load_w = thermal_zone_design_internal_load(zone) + int_load_w = thermal_zone_design_internal_load(zone) * zone.multiplier # Normalize per-area int_load_w_per_m2 = int_load_w / area_m2 int_load_btu_per_ft2 = OpenStudio.convert(int_load_w_per_m2, 'W/m^2', 'Btu/hr*ft^2').get data['int_load_btu_per_ft2'] = int_load_btu_per_ft2 zone_data_2 << data @@ -1519,13 +1358,12 @@ end return { 'primary' => pri_zones, 'secondary' => sec_zones } end - # Group an array of zones into multiple arrays, one - # for each story in the building. Zones with spaces on multiple stories - # will be assigned to only one of the stories. + # Group an array of zones into multiple arrays, one for each story in the building. + # Zones with spaces on multiple stories will be assigned to only one of the stories. # Removes empty array (when the story doesn't contain any of the zones) # @return [Array<Array<OpenStudio::Model::ThermalZone>>] array of arrays of zones def model_group_zones_by_story(model, zones) story_zone_lists = [] zones_already_assigned = [] @@ -1562,15 +1400,13 @@ end return story_zone_lists end - # Assign each space in the model to a building story - # based on common z (height) values. If no story - # object is found for a particular height, create a new one - # and assign it to the space. Does not assign a story - # to plenum spaces. + # Assign each space in the model to a building story based on common z (height) values. + # If no story object is found for a particular height, create a new one and assign it to the space. + # Does not assign a story to plenum spaces. # # @return [Bool] returns true if successful, false if not. def model_assign_spaces_to_stories(model) # Make hash of spaces and minz values sorted_spaces = {} @@ -1604,226 +1440,41 @@ end return true end - # Creates a construction set with the construction types specified in the - # Performance Rating Method (aka Appendix G aka LEED) and adds it to the model. - # This method creates and adds the constructions and their materials as well. - # - # @param category [String] the construction set category desired. - # Valid choices are Nonresidential, Residential, and Semiheated - # @return [OpenStudio::Model::DefaultConstructionSet] returns a default - # construction set populated with the specified constructions. - def model_add_prm_construction_set(model, category) - construction_set = OpenStudio::Model::OptionalDefaultConstructionSet.new - - # Find the climate zone set that this climate zone falls into - climate_zone_set = model_find_climate_zone_set(model, clim) - unless climate_zone_set - return construction_set - end - - # Get the object data - data = model_find_object(standards_data['construction_sets'], 'template' => template, 'climate_zone_set' => climate_zone_set, 'building_type' => building_type, 'space_type' => spc_type, 'is_residential' => is_residential) - unless data - data = model_find_object(standards_data['construction_sets'], 'template' => template, 'climate_zone_set' => climate_zone_set, 'building_type' => building_type, 'space_type' => spc_type) - unless data - return construction_set - end - end - - OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Adding construction set: #{template}-#{clim}-#{building_type}-#{spc_type}-is_residential#{is_residential}") - - name = model_make_name(model, clim, building_type, spc_type) - - # Create a new construction set and name it - construction_set = OpenStudio::Model::DefaultConstructionSet.new(model) - construction_set.setName(name) - - # Specify the types of constructions - # Exterior surfaces constructions - exterior_floor_standards_construction_type = 'SteelFramed' - exterior_wall_standards_construction_type = 'SteelFramed' - exterior_roof_standards_construction_type = 'IEAD' - - # Ground contact surfaces constructions - ground_contact_floor_standards_construction_type = 'Unheated' - ground_contact_wall_standards_construction_type = 'Mass' - - # Exterior sub surfaces constructions - exterior_fixed_window_standards_construction_type = 'IEAD' - exterior_operable_window_standards_construction_type = 'IEAD' - exterior_door_standards_construction_type = 'IEAD' - exterior_overhead_door_standards_construction_type = 'IEAD' - exterior_skylight_standards_construction_type = 'IEAD' - - # Exterior surfaces constructions - exterior_surfaces = OpenStudio::Model::DefaultSurfaceConstructions.new(model) - construction_set.setDefaultExteriorSurfaceConstructions(exterior_surfaces) - exterior_surfaces.setFloorConstruction(model_find_and_add_construction(model, - climate_zone_set, - 'ExteriorFloor', - exterior_floor_standards_construction_type, - category)) - - exterior_surfaces.setWallConstruction(model_find_and_add_construction(model, - climate_zone_set, - 'ExteriorWall', - exterior_wall_standards_construction_type, - category)) - - exterior_surfaces.setRoofCeilingConstruction(model_find_and_add_construction(model, - climate_zone_set, - 'ExteriorRoof', - exterior_roof_standards_construction_type, - category)) - - # Interior surfaces constructions - interior_surfaces = OpenStudio::Model::DefaultSurfaceConstructions.new(model) - construction_set.setDefaultInteriorSurfaceConstructions(interior_surfaces) - construction_name = interior_floors - unless construction_name.nil? - interior_surfaces.setFloorConstruction(model_add_construction(model, construction_name)) - end - construction_name = interior_walls - unless construction_name.nil? - interior_surfaces.setWallConstruction(model_add_construction(model, construction_name)) - end - construction_name = interior_ceilings - unless construction_name.nil? - interior_surfaces.setRoofCeilingConstruction(model_add_construction(model, construction_name)) - end - - # Ground contact surfaces constructions - ground_surfaces = OpenStudio::Model::DefaultSurfaceConstructions.new(model) - construction_set.setDefaultGroundContactSurfaceConstructions(ground_surfaces) - ground_surfaces.setFloorConstruction(model_find_and_add_construction(model, - climate_zone_set, - 'GroundContactFloor', - ground_contact_floor_standards_construction_type, - category)) - - ground_surfaces.setWallConstruction(model_find_and_add_construction(model, - climate_zone_set, - 'GroundContactWall', - ground_contact_wall_standards_construction_type, - category)) - - # Exterior sub surfaces constructions - exterior_subsurfaces = OpenStudio::Model::DefaultSubSurfaceConstructions.new(model) - construction_set.setDefaultExteriorSubSurfaceConstructions(exterior_subsurfaces) - if exterior_fixed_window_standards_construction_type && exterior_fixed_window_building_category - exterior_subsurfaces.setFixedWindowConstruction(model_find_and_add_construction(model, - climate_zone_set, - 'ExteriorWindow', - exterior_fixed_window_standards_construction_type, - category)) - end - if exterior_operable_window_standards_construction_type && exterior_operable_window_building_category - exterior_subsurfaces.setOperableWindowConstruction(model_find_and_add_construction(model, - climate_zone_set, - 'ExteriorWindow', - exterior_operable_window_standards_construction_type, - category)) - end - if exterior_door_standards_construction_type && exterior_door_building_category - exterior_subsurfaces.setDoorConstruction(model_find_and_add_construction(model, - climate_zone_set, - 'ExteriorDoor', - exterior_door_standards_construction_type, - category)) - end - construction_name = exterior_glass_doors - unless construction_name.nil? - exterior_subsurfaces.setGlassDoorConstruction(model_add_construction(model, construction_name)) - end - if exterior_overhead_door_standards_construction_type && exterior_overhead_door_building_category - exterior_subsurfaces.setOverheadDoorConstruction(model_find_and_add_construction(model, - climate_zone_set, - 'ExteriorDoor', - exterior_overhead_door_standards_construction_type, - category)) - end - if exterior_skylight_standards_construction_type && exterior_skylight_building_category - exterior_subsurfaces.setSkylightConstruction(model_find_and_add_construction(model, - climate_zone_set, - 'Skylight', - exterior_skylight_standards_construction_type, - category)) - end - if construction_name == tubular_daylight_domes - exterior_subsurfaces.setTubularDaylightDomeConstruction(model_add_construction(model, construction_name)) - end - if construction_name == tubular_daylight_diffusers - exterior_subsurfaces.setTubularDaylightDiffuserConstruction(model_add_construction(model, construction_name)) - end - - # Interior sub surfaces constructions - interior_subsurfaces = OpenStudio::Model::DefaultSubSurfaceConstructions.new(model) - construction_set.setDefaultInteriorSubSurfaceConstructions(interior_subsurfaces) - if construction_name == interior_fixed_windows - interior_subsurfaces.setFixedWindowConstruction(model_add_construction(model, construction_name)) - end - if construction_name == interior_operable_windows - interior_subsurfaces.setOperableWindowConstruction(model_add_construction(model, construction_name)) - end - if construction_name == interior_doors - interior_subsurfaces.setDoorConstruction(model_add_construction(model, construction_name)) - end - - # Other constructions - if construction_name == interior_partitions - construction_set.setInteriorPartitionConstruction(model_add_construction(model, construction_name)) - end - if construction_name == space_shading - construction_set.setSpaceShadingConstruction(model_add_construction(model, construction_name)) - end - if construction_name == building_shading - construction_set.setBuildingShadingConstruction(model_add_construction(model, construction_name)) - end - if construction_name == site_shading - construction_set.setSiteShadingConstruction(model_add_construction(model, construction_name)) - end - - # componentize the construction set - # construction_set_component = construction_set.createComponent - - # Return the construction set - return OpenStudio::Model::OptionalDefaultConstructionSet.new(construction_set) - - # Create a constuction set that is all - end - # Applies the multi-zone VAV outdoor air sizing requirements # to all applicable air loops in the model. # - # @note This must be performed before the sizing run because - # it impacts component sizes, which in turn impact efficiencies. + # @note This must be performed before the sizing run because it impacts component sizes, which in turn impact efficiencies. def model_apply_multizone_vav_outdoor_air_sizing(model) OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Started applying multizone vav OA sizing.') # Multi-zone VAV outdoor air sizing - model.getAirLoopHVACs.sort.each { |obj| air_loop_hvac_apply_multizone_vav_outdoor_air_sizing(obj) } + model.getAirLoopHVACs.sort.each {|obj| air_loop_hvac_apply_multizone_vav_outdoor_air_sizing(obj)} OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Finished applying multizone vav OA sizing.') end - # Applies the HVAC parts of the template to all objects in the model - # using the the template specified in the model. - def model_apply_hvac_efficiency_standard(model, climate_zone) + # Applies the HVAC parts of the template to all objects in the model using the the template specified in the model. + def model_apply_hvac_efficiency_standard(model, climate_zone, apply_controls: true) sql_db_vars_map = {} OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Started applying HVAC efficiency standards.') # Air Loop Controls - model.getAirLoopHVACs.sort.each { |obj| air_loop_hvac_apply_standard_controls(obj, climate_zone) } + if apply_controls.nil? || apply_controls == true + model.getAirLoopHVACs.sort.each { |obj| air_loop_hvac_apply_standard_controls(obj, climate_zone) } + end # Plant Loop Controls # TODO refactor: enable this code (missing before refactor) # getPlantLoops.sort.each { |obj| plant_loop_apply_standard_controls(obj, template, climate_zone) } + # Zone HVAC Controls + model.getZoneHVACComponents.sort.each { |obj| zone_hvac_component_apply_standard_controls(obj) } + ##### Apply equipment efficiencies # Fans model.getFanVariableVolumes.sort.each { |obj| fan_apply_standard_minimum_motor_efficiency(obj, fan_brake_horsepower(obj)) } model.getFanConstantVolumes.sort.each { |obj| fan_apply_standard_minimum_motor_efficiency(obj, fan_brake_horsepower(obj)) } @@ -1865,12 +1516,11 @@ model.getHeatExchangerAirToAirSensibleAndLatents.each { |obj| heat_exchanger_air_to_air_sensible_and_latent_apply_efficiency(obj) } OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Finished applying HVAC efficiency standards.') end - # Applies daylighting controls to each space in the model - # per the standard. + # Applies daylighting controls to each space in the model per the standard. def model_add_daylighting_controls(model) OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Started adding daylighting controls.') # Add daylighting controls to each space model.getSpaces.sort.each do |space| @@ -1901,19 +1551,17 @@ end return true end - # Method to search through a hash for the objects that meets the - # desired search criteria, as passed via a hash. + # Method to search through a hash for the objects that meets the desired search criteria, as passed via a hash. # Returns an Array (empty if nothing found) of matching objects. # # @param hash_of_objects [Hash] hash of objects to search through # @param search_criteria [Hash] hash of search criteria # @param capacity [Double] capacity of the object in question. If capacity is supplied, - # the objects will only be returned if the specified capacity is between - # the minimum_capacity and maximum_capacity values. + # the objects will only be returned if the specified capacity is between the minimum_capacity and maximum_capacity values. # @return [Array] returns an array of hashes, one hash per object. Array is empty if no results. # @example Find all the schedule rules that match the name # rules = model_find_objects(self, standards_data['schedules'], {'name'=>schedule_name}) # if rules.size == 0 # OpenStudio::logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Cannot find data for schedule: #{schedule_name}, will not be created.") @@ -2032,16 +1680,13 @@ # raise ("Hell") # end return matching_objects end - # Method to search through a hash for an object that meets the - # desired search criteria, as passed via a hash. If capacity is supplied, - # the object will only be returned if the specified capacity is between - # the minimum_capacity and maximum_capacity values. + # Method to search through a hash for an object that meets the desired search criteria, as passed via a hash. + # If capacity is supplied, the object will only be returned if the specified capacity is between the minimum_capacity and maximum_capacity values. # - # # @param hash_of_objects [Hash] hash of objects to search through # @param search_criteria [Hash] hash of search criteria # @param capacity [Double] capacity of the object in question. If capacity is supplied, # the objects will only be returned if the specified capacity is between # the minimum_capacity and maximum_capacity values. @@ -2154,21 +1799,126 @@ # Create constant ScheduleRuleset # # @param value [double] the value to use, 24-7, 365 # @param name [string] the name of the schedule + # @param sch_type_limit [string] the name of a schedule type limit + # options are Temperature, Humidity Ratio, Fractional, OnOff, and Activity # @return schedule - def model_add_constant_schedule_ruleset(model, value, name = nil) + def model_add_constant_schedule_ruleset(model, + value, + name = nil, + sch_type_limit: 'Temperature') + # check to see if schedule exists with same name and constant value and return if true + unless name.nil? + existing_sch = model.getScheduleRulesetByName(name) + if existing_sch.is_initialized + existing_sch = existing_sch.get + existing_day_sch_vals = existing_sch.defaultDaySchedule.values + if existing_day_sch_vals.size == 1 && existing_day_sch_vals[0] == value + return existing_sch + end + end + end + schedule = OpenStudio::Model::ScheduleRuleset.new(model) unless name.nil? schedule.setName(name) schedule.defaultDaySchedule.setName("#{name} Default") end + + if !sch_type_limit.nil? + sch_type_limits_obj = model_add_schedule_type_limits(model, standard_sch_type_limit: sch_type_limit) + schedule.setScheduleTypeLimits(sch_type_limits_obj) + end + schedule.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), value) return schedule end + # Create ScheduleTypeLimits + # + # @param standard_sch_type_limit [string] the name of a standard schedule type limit with predefined limits + # options are Temperature, Humidity Ratio, Fractional, OnOff, and Activity + # @param name[string] the name of the schedule type limits + # @param lower_limit_value [double] the lower limit value for the schedule type + # @param upper_limit_value [double] the upper limit value for the schedule type + # @param numeric_type [string] the numeric type, options are Continuous or Discrete + # @param unit_type [string] the unit type, options are defined in EnergyPlus I/O reference + # @return [<OpenStudio::Model::ScheduleTypeLimits>] + def model_add_schedule_type_limits(model, + standard_sch_type_limit: nil, + name: nil, + lower_limit_value: nil, + upper_limit_value: nil, + numeric_type: nil, + unit_type: nil) + + if standard_sch_type_limit.nil? + if lower_limit_value.nil? || upper_limit_value.nil? || numeric_type.nil? || unit_type.nil? + OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "If calling model_add_schedule_type_limits without a standard_sch_type_limit, you must specify all properties of ScheduleTypeLimits.") + return false + end + schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model) + schedule_type_limits.setName(name) if !name.nil? + schedule_type_limits.setLowerLimitValue(lower_limit_value) + schedule_type_limits.setUpperLimitValue(upper_limit_value) + schedule_type_limits.setNumericType(numeric_type) + schedule_type_limits.setUnitType(unit_type) + else + schedule_type_limits = model.getScheduleTypeLimitsByName(standard_sch_type_limit) + if !schedule_type_limits.empty? + schedule_type_limits = schedule_type_limits.get + else + case standard_sch_type_limit.downcase + when 'temperature' + schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model) + schedule_type_limits.setName("Temperature") + schedule_type_limits.setLowerLimitValue(0.0) + schedule_type_limits.setUpperLimitValue(100.0) + schedule_type_limits.setNumericType("Continuous") + schedule_type_limits.setUnitType("Temperature") + + when 'humidity ratio' + schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model) + schedule_type_limits.setName("Humidity Ratio") + schedule_type_limits.setLowerLimitValue(0.0) + schedule_type_limits.setUpperLimitValue(0.3) + schedule_type_limits.setNumericType("Continuous") + schedule_type_limits.setUnitType("Dimensionless") + + when 'fraction', 'fractional' + schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model) + schedule_type_limits.setName("Fraction") + schedule_type_limits.setLowerLimitValue(0.0) + schedule_type_limits.setUpperLimitValue(1.0) + schedule_type_limits.setNumericType("Continuous") + schedule_type_limits.setUnitType("Dimensionless") + + when 'onoff' + schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model) + schedule_type_limits.setName("OnOff") + schedule_type_limits.setLowerLimitValue(0) + schedule_type_limits.setUpperLimitValue(1) + schedule_type_limits.setNumericType("Discrete") + schedule_type_limits.setUnitType("Availability") + + when 'activity' + schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model) + schedule_type_limits.setName("Activity") + schedule_type_limits.setLowerLimitValue(70.0) + schedule_type_limits.setUpperLimitValue(1000.0) + schedule_type_limits.setNumericType("Continuous") + schedule_type_limits.setUnitType("ActivityLevel") + else + OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "Invalid standard_sch_type_limit for method model_add_schedule_type_limits.") + end + end + end + return schedule_type_limits + end + # Create a schedule from the openstudio standards dataset and # add it to the model. # # @param schedule_name [String} name of the schedule # @return [ScheduleRuleset] the resulting schedule ruleset @@ -2189,17 +1939,15 @@ # Find all the schedule rules that match the name rules = model_find_objects(standards_data['schedules'], 'name' => schedule_name) if rules.size.zero? OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "Cannot find data for schedule: #{schedule_name}, will not be created.") - sch_ruleset = OpenStudio::Model::ScheduleRuleset.new(model) - sch_ruleset.setName("NOT ACTUALLY #{schedule_name}") - return sch_ruleset + return false end # Make a schedule ruleset - sch_ruleset = OpenStudio::Model::ScheduleRuleset.new( model ) + sch_ruleset = OpenStudio::Model::ScheduleRuleset.new(model) sch_ruleset.setName(schedule_name.to_s) # Loop through the rules, making one for each row in the spreadsheet rules.each do |rule| day_types = rule['day_types'] @@ -2373,11 +2121,11 @@ else material.setSolarDiffusing(false) end else - puts "Unknown material type #{material_type}" + OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "Unknown material type #{material_type}, cannot add material called #{material_name}.") exit end return material end @@ -2469,10 +2217,47 @@ # construction_set_underground_wall_c_factor(construction, target_c_factor_ip.to_f, data['insulation_layer']) construction_set_u_value(construction, 0.0, data['insulation_layer'], data['intended_surface_type'], u_includes_int_film, u_includes_ext_film) end + # If the construction has a skylight framing material specified, + # get the skylight frame material properties and add frame to + # all skylights in the model. + if data['skylight_framing'] + # Get the skylight framing material + framing_name = data['skylight_framing'] + frame_data = model_find_object(standards_data['materials'], 'name' => framing_name) + if frame_data + frame_width_in = frame_data['frame_width'].to_f + frame_with_m = OpenStudio.convert(frame_width_in, 'in', 'm').get + frame_resistance_ip = frame_data['resistance'].to_f + frame_resistance_si = OpenStudio.convert(frame_resistance_ip, 'hr*ft^2*R/Btu', 'm^2*K/W').get + frame_conductance_si = 1.0/frame_resistance_si + frame = OpenStudio::Model::WindowPropertyFrameAndDivider.new(model) + frame.setName("Skylight frame R-#{frame_resistance_ip.round(2)} #{frame_width_in.round(1)} in. wide") + frame.setFrameWidth(frame_with_m) + frame.setFrameConductance(frame_conductance_si) + skylights_frame_added = 0 + model.getSubSurfaces.each do |sub_surface| + next unless sub_surface.outsideBoundaryCondition == 'Outdoors' && sub_surface.subSurfaceType == 'Skylight' + # todo enable proper window frame setting after https://github.com/NREL/OpenStudio/issues/2895 is fixed + sub_surface.setString(8, frame.name.get.to_s) + skylights_frame_added += 1 + # if sub_surface.allowWindowPropertyFrameAndDivider + # sub_surface.setWindowPropertyFrameAndDivider(frame) + # skylights_frame_added += 1 + # else + # OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "For #{sub_surface.name}: cannot add a frame to this skylight.") + # end + end + OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Adding #{frame.name} to #{skylights_frame_added} skylights.") if skylights_frame_added > 0 + else + OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Cannot find skylight framing data for: #{framing_name}, will not be created.") + return false # TODO: change to return empty optional material + end + end + end # # Check if the construction with the modified name was already in the model. # # If it was, delete this new construction and return the copy already in the model. # m = construction.name.get.to_s.match(/\s(\d+)/) # if m @@ -2500,22 +2285,22 @@ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Adding construction #{construction.name}.") return construction end - # Helper method to find a particular construction and add it to the model - # after modifying the insulation value if necessary. + # Helper method to find a particular construction and add it to the model after modifying the insulation value if necessary. def model_find_and_add_construction(model, climate_zone_set, intended_surface_type, standards_construction_type, building_category) # Get the construction properties, # which specifies properties by construction category by climate zone set. # AKA the info in Tables 5.5-1-5.5-8 - props = model_find_object(standards_data['construction_properties'], 'template' => template, - 'climate_zone_set' => climate_zone_set, - 'intended_surface_type' => intended_surface_type, - 'standards_construction_type' => standards_construction_type, - 'building_category' => building_category) + props = model_find_object(standards_data['construction_properties'], + 'template' => template, + 'climate_zone_set' => climate_zone_set, + 'intended_surface_type' => intended_surface_type, + 'standards_construction_type' => standards_construction_type, + 'building_category' => building_category) if !props OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "Could not find construction properties for: #{template}-#{climate_zone_set}-#{intended_surface_type}-#{standards_construction_type}-#{building_category}.") # Return an empty construction construction = OpenStudio::Model::Construction.new(model) @@ -2552,16 +2337,16 @@ unless climate_zone_set return construction_set end # Get the object data - data = model_find_object(standards_data['construction_sets'], 'template' => template, 'climate_zone_set' => climate_zone_set, 'building_type' => building_type, 'space_type' => spc_type, 'is_residential' => is_residential) unless data + # Search again without the is_residential criteria in the case that this field is not specified for a standard data = model_find_object(standards_data['construction_sets'], 'template' => template, 'climate_zone_set' => climate_zone_set, 'building_type' => building_type, 'space_type' => spc_type) unless data - # if nothing matches say that we could not find it. + # if nothing matches say that we could not find it OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', "Construction set for template =#{template}, climate zone set =#{climate_zone_set}, building type = #{building_type}, space type = #{spc_type}, is residential = #{is_residential} was not found in standards_data['construction_sets']") return construction_set end end @@ -2574,38 +2359,61 @@ construction_set.setName(name) # Exterior surfaces constructions exterior_surfaces = OpenStudio::Model::DefaultSurfaceConstructions.new(model) construction_set.setDefaultExteriorSurfaceConstructions(exterior_surfaces) - if data['exterior_floor_standards_construction_type'] && data['exterior_floor_building_category'] - exterior_surfaces.setFloorConstruction(model_find_and_add_construction(model, - climate_zone_set, - 'ExteriorFloor', - data['exterior_floor_standards_construction_type'], - data['exterior_floor_building_category'])) + # Special condition for attics, where the insulation is actually on the floor but the soffit is uninsulated + if spc_type == 'Attic' + exterior_surfaces.setFloorConstruction(model_add_construction(model, 'Typical Attic Soffit')) + else + if data['exterior_floor_standards_construction_type'] && data['exterior_floor_building_category'] + exterior_surfaces.setFloorConstruction(model_find_and_add_construction(model, + climate_zone_set, + 'ExteriorFloor', + data['exterior_floor_standards_construction_type'], + data['exterior_floor_building_category'])) + end end if data['exterior_wall_standards_construction_type'] && data['exterior_wall_building_category'] exterior_surfaces.setWallConstruction(model_find_and_add_construction(model, climate_zone_set, 'ExteriorWall', data['exterior_wall_standards_construction_type'], data['exterior_wall_building_category'])) end - if data['exterior_roof_standards_construction_type'] && data['exterior_roof_building_category'] - exterior_surfaces.setRoofCeilingConstruction(model_find_and_add_construction(model, - climate_zone_set, - 'ExteriorRoof', - data['exterior_roof_standards_construction_type'], - data['exterior_roof_building_category'])) + # Special condition for attics, where the insulation is actually on the floor and the roof itself is uninsulated + if spc_type == 'Attic' + if data['exterior_roof_standards_construction_type'] && data['exterior_roof_building_category'] + exterior_surfaces.setRoofCeilingConstruction(model_add_construction(model, 'Typical Uninsulated Wood Joist Attic Roof')) + end + else + if data['exterior_roof_standards_construction_type'] && data['exterior_roof_building_category'] + exterior_surfaces.setRoofCeilingConstruction(model_find_and_add_construction(model, + climate_zone_set, + 'ExteriorRoof', + data['exterior_roof_standards_construction_type'], + data['exterior_roof_building_category'])) + end end - # Interior surfaces constructions interior_surfaces = OpenStudio::Model::DefaultSurfaceConstructions.new(model) construction_set.setDefaultInteriorSurfaceConstructions(interior_surfaces) construction_name = data['interior_floors'] - unless construction_name.nil? - interior_surfaces.setFloorConstruction(model_add_construction(model, construction_name)) + # Special condition for attics, where the insulation is actually on the floor and the roof itself is uninsulated + if spc_type == 'Attic' + if data['exterior_roof_standards_construction_type'] && data['exterior_roof_building_category'] + interior_surfaces.setFloorConstruction(model_find_and_add_construction(model, + climate_zone_set, + 'ExteriorRoof', + data['exterior_roof_standards_construction_type'], + data['exterior_roof_building_category'])) + + end + else + unless construction_name.nil? + interior_surfaces.setFloorConstruction(model_add_construction(model, construction_name)) + end end construction_name = data['interior_walls'] unless construction_name.nil? interior_surfaces.setWallConstruction(model_add_construction(model, construction_name)) end @@ -2662,13 +2470,16 @@ climate_zone_set, 'ExteriorDoor', data['exterior_door_standards_construction_type'], data['exterior_door_building_category'])) end - construction_name = data['exterior_glass_doors'] - unless construction_name.nil? - exterior_subsurfaces.setGlassDoorConstruction(model_add_construction(model, construction_name)) + if data['exterior_glass_door_standards_construction_type'] && data['exterior_glass_door_building_category'] + exterior_subsurfaces.setGlassDoorConstruction(model_find_and_add_construction(model, + climate_zone_set, + 'GlassDoor', + data['exterior_glass_door_standards_construction_type'], + data['exterior_glass_door_building_category'])) end if data['exterior_overhead_door_standards_construction_type'] && data['exterior_overhead_door_building_category'] exterior_subsurfaces.setOverheadDoorConstruction(model_find_and_add_construction(model, climate_zone_set, 'ExteriorDoor', @@ -2749,95 +2560,95 @@ return nil end # Make the correct type of curve case data['form'] - when 'Linear' - curve = OpenStudio::Model::CurveLinear.new(model) - curve.setName(data['name']) - curve.setCoefficient1Constant(data['coeff_1']) - curve.setCoefficient2x(data['coeff_2']) - curve.setMinimumValueofx(data['minimum_independent_variable_1']) if data['minimum_independent_variable_1'] - curve.setMaximumValueofx(data['maximum_independent_variable_1']) if data['maximum_independent_variable_1'] - curve.setMinimumCurveOutput(data['minimum_dependent_variable_output']) if data['minimum_dependent_variable_output'] - curve.setMaximumCurveOutput(data['maximum_dependent_variable_output']) if data['maximum_dependent_variable_output'] - return curve - when 'Cubic' - curve = OpenStudio::Model::CurveCubic.new(model) - curve.setName(data['name']) - curve.setCoefficient1Constant(data['coeff_1']) - curve.setCoefficient2x(data['coeff_2']) - curve.setCoefficient3xPOW2(data['coeff_3']) - curve.setCoefficient4xPOW3(data['coeff_4']) - curve.setMinimumValueofx(data['minimum_independent_variable_1']) if data['minimum_independent_variable_1'] - curve.setMaximumValueofx(data['maximum_independent_variable_1']) if data['maximum_independent_variable_1'] - curve.setMinimumCurveOutput(data['minimum_dependent_variable_output']) if data['minimum_dependent_variable_output'] - curve.setMaximumCurveOutput(data['maximum_dependent_variable_output']) if data['maximum_dependent_variable_output'] - return curve - when 'Quadratic' - curve = OpenStudio::Model::CurveQuadratic.new(model) - curve.setName(data['name']) - curve.setCoefficient1Constant(data['coeff_1']) - curve.setCoefficient2x(data['coeff_2']) - curve.setCoefficient3xPOW2(data['coeff_3']) - curve.setMinimumValueofx(data['minimum_independent_variable_1']) if data['minimum_independent_variable_1'] - curve.setMaximumValueofx(data['maximum_independent_variable_1']) if data['maximum_independent_variable_1'] - curve.setMinimumCurveOutput(data['minimum_dependent_variable_output']) if data['minimum_dependent_variable_output'] - curve.setMaximumCurveOutput(data['maximum_dependent_variable_output']) if data['maximum_dependent_variable_output'] - return curve - when 'BiCubic' - curve = OpenStudio::Model::CurveBicubic.new(model) - curve.setName(data['name']) - curve.setCoefficient1Constant(data['coeff_1']) - curve.setCoefficient2x(data['coeff_2']) - curve.setCoefficient3xPOW2(data['coeff_3']) - curve.setCoefficient4y(data['coeff_4']) - curve.setCoefficient5yPOW2(data['coeff_5']) - curve.setCoefficient6xTIMESY(data['coeff_6']) - curve.setCoefficient7xPOW3(data['coeff_7']) - curve.setCoefficient8yPOW3(data['coeff_8']) - curve.setCoefficient9xPOW2TIMESY(data['coeff_9']) - curve.setCoefficient10xTIMESYPOW2(data['coeff_10']) - curve.setMinimumValueofx(data['minimum_independent_variable_1']) if data['minimum_independent_variable_1'] - curve.setMaximumValueofx(data['maximum_independent_variable_1']) if data['maximum_independent_variable_1'] - curve.setMinimumValueofy(data['minimum_independent_variable_2']) if data['minimum_independent_variable_2'] - curve.setMaximumValueofy(data['maximum_independent_variable_2']) if data['maximum_independent_variable_2'] - curve.setMinimumCurveOutput(data['minimum_dependent_variable_output']) if data['minimum_dependent_variable_output'] - curve.setMaximumCurveOutput(data['maximum_dependent_variable_output']) if data['maximum_dependent_variable_output'] - return curve - when 'BiQuadratic' - curve = OpenStudio::Model::CurveBiquadratic.new(model) - curve.setName(data['name']) - curve.setCoefficient1Constant(data['coeff_1']) - curve.setCoefficient2x(data['coeff_2']) - curve.setCoefficient3xPOW2(data['coeff_3']) - curve.setCoefficient4y(data['coeff_4']) - curve.setCoefficient5yPOW2(data['coeff_5']) - curve.setCoefficient6xTIMESY(data['coeff_6']) - curve.setMinimumValueofx(data['minimum_independent_variable_1']) if data['minimum_independent_variable_1'] - curve.setMaximumValueofx(data['maximum_independent_variable_1']) if data['maximum_independent_variable_1'] - curve.setMinimumValueofy(data['minimum_independent_variable_2']) if data['minimum_independent_variable_2'] - curve.setMaximumValueofy(data['maximum_independent_variable_2']) if data['maximum_independent_variable_2'] - curve.setMinimumCurveOutput(data['minimum_dependent_variable_output']) if data['minimum_dependent_variable_output'] - curve.setMaximumCurveOutput(data['maximum_dependent_variable_output']) if data['maximum_dependent_variable_output'] - return curve - when 'BiLinear' - curve = OpenStudio::Model::CurveBiquadratic.new(model) - curve.setName(data['name']) - curve.setCoefficient1Constant(data['coeff_1']) - curve.setCoefficient2x(data['coeff_2']) - curve.setCoefficient4y(data['coeff_3']) - curve.setMinimumValueofx(data['minimum_independent_variable_1']) if data['minimum_independent_variable_1'] - curve.setMaximumValueofx(data['maximum_independent_variable_1']) if data['maximum_independent_variable_1'] - curve.setMinimumValueofy(data['minimum_independent_variable_2']) if data['minimum_independent_variable_2'] - curve.setMaximumValueofy(data['maximum_independent_variable_2']) if data['maximum_independent_variable_2'] - curve.setMinimumCurveOutput(data['minimum_dependent_variable_output']) if data['minimum_dependent_variable_output'] - curve.setMaximumCurveOutput(data['maximum_dependent_variable_output']) if data['maximum_dependent_variable_output'] - return curve - else - OpenStudio::logFree(OpenStudio::Error, "openstudio.Model.Model", "#{curve_name}' has an invalid form: #{data['form']}', cannot create this curve.") - return nil + when 'Linear' + curve = OpenStudio::Model::CurveLinear.new(model) + curve.setName(data['name']) + curve.setCoefficient1Constant(data['coeff_1']) + curve.setCoefficient2x(data['coeff_2']) + curve.setMinimumValueofx(data['minimum_independent_variable_1']) if data['minimum_independent_variable_1'] + curve.setMaximumValueofx(data['maximum_independent_variable_1']) if data['maximum_independent_variable_1'] + curve.setMinimumCurveOutput(data['minimum_dependent_variable_output']) if data['minimum_dependent_variable_output'] + curve.setMaximumCurveOutput(data['maximum_dependent_variable_output']) if data['maximum_dependent_variable_output'] + return curve + when 'Cubic' + curve = OpenStudio::Model::CurveCubic.new(model) + curve.setName(data['name']) + curve.setCoefficient1Constant(data['coeff_1']) + curve.setCoefficient2x(data['coeff_2']) + curve.setCoefficient3xPOW2(data['coeff_3']) + curve.setCoefficient4xPOW3(data['coeff_4']) + curve.setMinimumValueofx(data['minimum_independent_variable_1']) if data['minimum_independent_variable_1'] + curve.setMaximumValueofx(data['maximum_independent_variable_1']) if data['maximum_independent_variable_1'] + curve.setMinimumCurveOutput(data['minimum_dependent_variable_output']) if data['minimum_dependent_variable_output'] + curve.setMaximumCurveOutput(data['maximum_dependent_variable_output']) if data['maximum_dependent_variable_output'] + return curve + when 'Quadratic' + curve = OpenStudio::Model::CurveQuadratic.new(model) + curve.setName(data['name']) + curve.setCoefficient1Constant(data['coeff_1']) + curve.setCoefficient2x(data['coeff_2']) + curve.setCoefficient3xPOW2(data['coeff_3']) + curve.setMinimumValueofx(data['minimum_independent_variable_1']) if data['minimum_independent_variable_1'] + curve.setMaximumValueofx(data['maximum_independent_variable_1']) if data['maximum_independent_variable_1'] + curve.setMinimumCurveOutput(data['minimum_dependent_variable_output']) if data['minimum_dependent_variable_output'] + curve.setMaximumCurveOutput(data['maximum_dependent_variable_output']) if data['maximum_dependent_variable_output'] + return curve + when 'BiCubic' + curve = OpenStudio::Model::CurveBicubic.new(model) + curve.setName(data['name']) + curve.setCoefficient1Constant(data['coeff_1']) + curve.setCoefficient2x(data['coeff_2']) + curve.setCoefficient3xPOW2(data['coeff_3']) + curve.setCoefficient4y(data['coeff_4']) + curve.setCoefficient5yPOW2(data['coeff_5']) + curve.setCoefficient6xTIMESY(data['coeff_6']) + curve.setCoefficient7xPOW3(data['coeff_7']) + curve.setCoefficient8yPOW3(data['coeff_8']) + curve.setCoefficient9xPOW2TIMESY(data['coeff_9']) + curve.setCoefficient10xTIMESYPOW2(data['coeff_10']) + curve.setMinimumValueofx(data['minimum_independent_variable_1']) if data['minimum_independent_variable_1'] + curve.setMaximumValueofx(data['maximum_independent_variable_1']) if data['maximum_independent_variable_1'] + curve.setMinimumValueofy(data['minimum_independent_variable_2']) if data['minimum_independent_variable_2'] + curve.setMaximumValueofy(data['maximum_independent_variable_2']) if data['maximum_independent_variable_2'] + curve.setMinimumCurveOutput(data['minimum_dependent_variable_output']) if data['minimum_dependent_variable_output'] + curve.setMaximumCurveOutput(data['maximum_dependent_variable_output']) if data['maximum_dependent_variable_output'] + return curve + when 'BiQuadratic' + curve = OpenStudio::Model::CurveBiquadratic.new(model) + curve.setName(data['name']) + curve.setCoefficient1Constant(data['coeff_1']) + curve.setCoefficient2x(data['coeff_2']) + curve.setCoefficient3xPOW2(data['coeff_3']) + curve.setCoefficient4y(data['coeff_4']) + curve.setCoefficient5yPOW2(data['coeff_5']) + curve.setCoefficient6xTIMESY(data['coeff_6']) + curve.setMinimumValueofx(data['minimum_independent_variable_1']) if data['minimum_independent_variable_1'] + curve.setMaximumValueofx(data['maximum_independent_variable_1']) if data['maximum_independent_variable_1'] + curve.setMinimumValueofy(data['minimum_independent_variable_2']) if data['minimum_independent_variable_2'] + curve.setMaximumValueofy(data['maximum_independent_variable_2']) if data['maximum_independent_variable_2'] + curve.setMinimumCurveOutput(data['minimum_dependent_variable_output']) if data['minimum_dependent_variable_output'] + curve.setMaximumCurveOutput(data['maximum_dependent_variable_output']) if data['maximum_dependent_variable_output'] + return curve + when 'BiLinear' + curve = OpenStudio::Model::CurveBiquadratic.new(model) + curve.setName(data['name']) + curve.setCoefficient1Constant(data['coeff_1']) + curve.setCoefficient2x(data['coeff_2']) + curve.setCoefficient4y(data['coeff_3']) + curve.setMinimumValueofx(data['minimum_independent_variable_1']) if data['minimum_independent_variable_1'] + curve.setMaximumValueofx(data['maximum_independent_variable_1']) if data['maximum_independent_variable_1'] + curve.setMinimumValueofy(data['minimum_independent_variable_2']) if data['minimum_independent_variable_2'] + curve.setMaximumValueofy(data['maximum_independent_variable_2']) if data['maximum_independent_variable_2'] + curve.setMinimumCurveOutput(data['minimum_dependent_variable_output']) if data['minimum_dependent_variable_output'] + curve.setMaximumCurveOutput(data['maximum_dependent_variable_output']) if data['maximum_dependent_variable_output'] + return curve + else + OpenStudio::logFree(OpenStudio::Error, "openstudio.Model.Model", "#{curve_name}' has an invalid form: #{data['form']}', cannot create this curve.") + return nil end end # Get the full path to the weather file that is specified in the model. # @@ -2868,51 +2679,82 @@ end return full_epw_path end + # Find the legacy simulation results from a CSV of previously created results. + # + # @return [Hash] a hash of results for each fuel, where the keys are in the form 'End Use|Fuel Type', + # e.g. Heating|Electricity, Exterior Equipment|Water. All end use/fuel type combos are present, with + # values of 0.0 if none of this end use/fuel type combo was used by the simulation. Returns nil + # if the legacy results couldn't be found. + def model_legacy_results_by_end_use_and_fuel_type(model, climate_zone, building_type) + # Load the legacy idf results CSV file into a ruby hash + top_dir = File.expand_path('../../..', File.dirname(__FILE__)) + standards_data_dir = "#{top_dir}/data/standards" + temp = '' + # Run differently depending on whether running from embedded filesystem in OpenStudio CLI or not + if __dir__[0] == ':' # Running from OpenStudio CLI + # load file from embedded files + temp = load_resource_relative('../../../data/standards/legacy_idf_results.csv', 'r:UTF-8') + else + # loaded gem from system path + temp = File.read("#{standards_data_dir}/legacy_idf_results.csv") + end + legacy_idf_csv = CSV.new(temp, :headers => true, :converters => :all) + legacy_idf_results = legacy_idf_csv.to_a.map {|row| row.to_hash } + + # Get the results for this building + search_criteria = { + 'Building Type' => building_type, + 'Template' => template, + 'Climate Zone' => climate_zone + } + energy_values = model_find_object(legacy_idf_results, search_criteria) + if energy_values.nil? + OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "Could not find legacy simulation results for #{search_criteria}") + return {} + end + + return energy_values + end + # Method to gather prototype simulation results for a specific climate zone, building type, and template # # @param climate_zone [String] string for the ASHRAE climate zone. # @param building_type [String] string for prototype building type. # @return [Hash] Returns a hash with data presented in various bins. Returns nil if no search results def model_process_results_for_datapoint(model, climate_zone, building_type) - # Combine the data from the JSON files into a single hash - top_dir = File.expand_path('../../..', File.dirname(__FILE__)) - standards_data_dir = "#{top_dir}/data/standards" + # Hash to store the legacy results by fuel and by end use + legacy_results_hash = {} + legacy_results_hash['total_legacy_energy_val'] = 0 + legacy_results_hash['total_legacy_water_val'] = 0 + legacy_results_hash['total_energy_by_fuel'] = {} + legacy_results_hash['total_energy_by_end_use'] = {} - # Load the legacy idf results JSON file into a ruby hash - temp = '' - begin - temp = load_resource_relative('../../../data/standards/legacy_idf_results.json', 'r:UTF-8') - rescue NoMethodError - temp = File.read("#{standards_data_dir}/legacy_idf_results.json") + # Get the lecay simulation results + legacy_values = model_legacy_results_by_end_use_and_fuel_type(model, climate_zone, building_type) + if legacy_values.nil? + OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "Could not find legacy idf results for #{search_criteria}") + return legacy_results_hash end - legacy_idf_results = JSON.parse(temp) # List of all fuel types fuel_types = ['Electricity', 'Natural Gas', 'Additional Fuel', 'District Cooling', 'District Heating', 'Water'] # List of all end uses - end_uses = ['Heating', 'Cooling', 'Interior Lighting', 'Exterior Lighting', 'Interior Equipment', 'Exterior Equipment', 'Fans', 'Pumps', 'Heat Rejection', 'Humidification', 'Heat Recovery', 'Water Systems', 'Refrigeration', 'Generators'] + end_uses = ['Heating', 'Cooling', 'Interior Lighting', 'Exterior Lighting', 'Interior Equipment', 'Exterior Equipment', 'Fans', 'Pumps', 'Heat Rejection','Humidification', 'Heat Recovery', 'Water Systems', 'Refrigeration', 'Generators'] - # Get legacy idf results - legacy_results_hash = {} - legacy_results_hash['total_legacy_energy_val'] = 0 - legacy_results_hash['total_legacy_water_val'] = 0 - legacy_results_hash['total_energy_by_fuel'] = {} - legacy_results_hash['total_energy_by_end_use'] = {} + # Sum the legacy results up by fuel and by end use fuel_types.each do |fuel_type| end_uses.each do |end_use| next if end_use == 'Exterior Equipment' + legacy_val = legacy_values["#{end_use}|#{fuel_type}"] - # Get the legacy results number - legacy_val = legacy_idf_results.dig(building_type, template, climate_zone, fuel_type, end_use) - # Combine the exterior lighting and exterior equipment if end_use == 'Exterior Lighting' - legacy_exterior_equipment = legacy_idf_results.dig(building_type, template, climate_zone, fuel_type, 'Exterior Equipment') + legacy_exterior_equipment = legacy_values["Exterior Equipment|#{fuel_type}"] unless legacy_exterior_equipment.nil? legacy_val += legacy_exterior_equipment end end @@ -2996,29 +2838,17 @@ end return result end - # this is used by other methods to get the clinzte aone and building type from a model. + # this is used by other methods to get the climate zone and building type from a model. # it has logic to break office into small, medium or large based on building area that can be turned off # @param remap_office [bool] re-map small office or leave it alone # @return [hash] key for climate zone and building type, both values are strings def model_get_building_climate_zone_and_building_type(model, remap_office = true) # get climate zone from model - # get ashrae climate zone from model - climate_zone = '' - model.getClimateZones.climateZones.each do |cz| - if cz.institution == 'ASHRAE' - climate_zone = if cz.value == '7' || cz.value == '8' - "ASHRAE 169-2006-#{cz.value}A" - else - "ASHRAE 169-2006-#{cz.value}" - end - elsif cz.institution == 'CEC' - climate_zone = "CEC T24-CEC#{cz.value}" - end - end + climate_zone = model_standards_climate_zone(model) # get building type from model building_type = '' if model.getBuilding.standardsBuildingType.is_initialized building_type = model.getBuilding.standardsBuildingType.get @@ -3117,14 +2947,12 @@ end return result end - # Get a unique list of constructions with given - # boundary condition and a given type of surface. - # Pulls from both default construction sets and - # hard-assigned constructions. + # Get a unique list of constructions with given boundary condition and a given type of surface. + # Pulls from both default construction sets and hard-assigned constructions. # # @param boundary_condition [String] the desired boundary condition # valid choices are: # Adiabatic # Surface @@ -3274,15 +3102,13 @@ all_constructions = all_constructions.sort return all_constructions end - # Go through the default construction sets and hard-assigned - # constructions. Clone the existing constructions and set their - # intended surface type and standards construction type per - # the PRM. For some standards, this will involve making - # modifications. For others, it will not. + # Go through the default construction sets and hard-assigned constructions. + # Clone the existing constructions and set their intended surface type and standards construction type per the PRM. + # For some standards, this will involve making modifications. For others, it will not. # # 90.1-2007, 90.1-2010, 90.1-2013 # @return [Bool] returns true if successful, false if not def model_apply_prm_construction_types(model) types_to_modify = [] @@ -3363,12 +3189,11 @@ end return true end - # Apply the standard construction to each surface in the - # model, based on the construction type currently assigned. + # Apply the standard construction to each surface in the model, based on the construction type currently assigned. # # @return [Bool] true if successful, false if not def model_apply_standard_constructions(model, climate_zone) types_to_modify = [] @@ -3464,19 +3289,17 @@ construction_properties = model_find_object(standards_data['construction_properties'], search_criteria) return construction_properties end - # Reduces the WWR to the values specified by the PRM. WWR reduction - # will be done by moving vertices inward toward centroid. This causes the least impact - # on the daylighting area calculations and controls placement. + # Reduces the WWR to the values specified by the PRM. + # WWR reduction will be done by moving vertices inward toward centroid. + # This causes the least impact on the daylighting area calculations and controls placement. # - # @todo add proper support for 90.1-2013 with all those building - # type specific values - # @todo support 90.1-2004 requirement that windows be modeled as - # horizontal bands. Currently just using existing window geometry, - # and shrinking as necessary if WWR is above limit. + # @todo add proper support for 90.1-2013 with all those building type specific values + # @todo support 90.1-2004 requirement that windows be modeled as horizontal bands. + # Currently just using existing window geometry, and shrinking as necessary if WWR is above limit. # @todo support semiheated spaces as a separate WWR category # @todo add window frame area to calculation of WWR def model_apply_prm_baseline_window_to_wall_ratio(model, climate_zone) # Loop through all spaces in the model, and # per the PNNL PRM Reference Manual, find the areas @@ -3509,15 +3332,14 @@ wind_area_m2 += ss.netArea * space.multiplier end end # Determine the space category - # TODO This should really use the heating/cooling loads - # from the proposed building. However, in an attempt - # to avoid another sizing run just for this purpose, - # conditioned status is based on heating/cooling - # setpoints. If heated-only, will be assumed Semiheated. + # TODO: This should really use the heating/cooling loads from the proposed building. + # However, in an attempt to avoid another sizing run just for this purpose, + # conditioned status is based on heating/cooling setpoints. + # If heated-only, will be assumed Semiheated. # The full-bore method is on the next line in case needed. # cat = thermal_zone_conditioning_category(space, template, climate_zone) cooled = space_cooled?(space) heated = space_heated?(space) cat = 'Unconditioned' @@ -3634,12 +3456,11 @@ end return true end - # Reduces the SRR to the values specified by the PRM. SRR reduction - # will be done by shrinking vertices toward the centroid. + # Reduces the SRR to the values specified by the PRM. SRR reduction will be done by shrinking vertices toward the centroid. # # @todo support semiheated spaces as a separate SRR category # @todo add skylight frame area to calculation of SRR def model_apply_prm_baseline_skylight_to_roof_ratio(model) # Loop through all spaces in the model, and @@ -3771,14 +3592,12 @@ def model_prm_skylight_to_roof_ratio_limit(model) srr_lim = 5.0 return srr_lim end - # Remove all HVAC that will be replaced during the - # performance rating method baseline generation. - # This does not include plant loops that serve - # WaterUse:Equipment or Fan:ZoneExhaust + # Remove all HVAC that will be replaced during the performance rating method baseline generation. + # This does not include plant loops that serve WaterUse:Equipment or Fan:ZoneExhaust # # @return [Bool] true if successful, false if not def model_remove_prm_hvac(model) # Plant loops model.getPlantLoops.sort.each do |loop| @@ -3802,12 +3621,11 @@ model.getAirConditionerVariableRefrigerantFlows.each(&:remove) return true end - # Remove external shading devices. - # Site shading will not be impacted. + # Remove external shading devices. Site shading will not be impacted. # @return [Bool] returns true if successful, false if not. def model_remove_external_shading_devices(model) shading_surfaces_removed = 0 model.getShadingSurfaceGroups.sort.each do |shade_group| # Skip Site shading @@ -3832,19 +3650,16 @@ sizing_params.setCoolingSizingFactor(clg) OpenStudio.logFree(OpenStudio::Info, 'openstudio.prototype.Model', "Set sizing factors to #{htg} for heating and #{clg} for cooling.") end - # Helper method to get the story object that - # cooresponds to a specific minimum z value. + # Helper method to get the story object that corresponds to a specific minimum z value. # Makes a new story if none found at this height. # # - # @param minz [Double] the z value (height) of the - # desired story, in meters. - # @param tolerance [Double] tolerance for comparison, in m. - # Default is 0.3 m ~1ft + # @param minz [Double] the z value (height) of the desired story, in meters. + # @param tolerance [Double] tolerance for comparison, in m. Default is 0.3 m ~1ft # @return [OpenStudio::Model::BuildingStory] the story def model_get_story_for_nominal_z_coordinate(model, minz, tolerance = 0.3) model.getBuildingStorys.sort.each do |story| z = building_story_minimum_z_value(story) @@ -3860,11 +3675,11 @@ return story end # Returns average daily hot water consumption by building type - # recommendations from 2011 ASHRAE Handobook - HVAC Applications Table 7 section 60.14 + # recommendations from 2011 ASHRAE Handbook - HVAC Applications Table 7 section 60.14 # Not all building types are included in lookup # some recommendations have multiple values based on number of units. # Will return an array of hashes. Many may have one array entry. # all values other than block size are gallons. # @@ -3926,12 +3741,11 @@ swh_gal_per_day = units_per_bldg * (30.0 + (10.0 * bedrooms_per_unit)) return swh_gal_per_day end - # Returns average daily internal loads for residential buildings - # from Table R405.5.2(1) + # Returns average daily internal loads for residential buildings from Table R405.5.2(1) # # @return [Hash] mech_vent_cfm, infiltration_ach, igain_btu_per_day, internal_mass_lbs def model_find_icc_iecc_2015_internal_loads(model, units_per_bldg, bedrooms_per_unit) # get total and conditioned floor area total_floor_area = model.getBuilding.floorArea @@ -3941,17 +3755,11 @@ OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', 'Cannot find conditioned floor area, will use total floor area.') conditioned_floor_area = total_floor_area end # get climate zone value - climate_zone_value = '' - model.getClimateZones.climateZones.each do |cz| - if cz.institution == 'ASHRAE' - climate_zone_value = cz.value - next - end - end + climate_zone = model_standards_climate_zone(model) internal_loads = {} internal_loads['mech_vent_cfm'] = units_per_bldg * (0.01 * conditioned_floor_area + 7.5 * (bedrooms_per_unit + 1.0)) internal_loads['infiltration_ach'] = if ['1A', '1B', '2A', '2B'].include? climate_zone_value 5.0 @@ -3962,12 +3770,11 @@ internal_loads['internal_mass_lbs'] = total_floor_area * 8.0 return internal_loads end - # Helper method to make a shortened version of a name - # that will be readable in a GUI. + # Helper method to make a shortened version of a name that will be readable in a GUI. def model_make_name(model, clim, building_type, spc_type) clim = clim.gsub('ClimateZone ', 'CZ') if clim == 'CZ1-8' clim = '' end @@ -4065,13 +3872,12 @@ def model_get_climate_zone_set_from_list(model, possible_climate_zone_sets) climate_zone_set = possible_climate_zone_sets.sort.first return climate_zone_set end - # This method ensures that all spaces with spacetypes defined contain at least - # a standardSpaceType appropriate for the template. So, if any space - # with a space type defined does not have a Stnadard spacetype, or is undefined, an error will stop + # This method ensures that all spaces with spacetypes defined contain at least a standardSpaceType appropriate for the template. + # So, if any space with a space type defined does not have a Stnadard spacetype, or is undefined, an error will stop # with information that the spacetype needs to be defined. def model_validate_standards_spacetypes_in_model(model) error_string = '' # populate search hash model.getSpaces.sort.each do |space| @@ -4304,10 +4110,242 @@ end return space_type_hash.sort.to_h end + + # This method will apply the a FDWR to a model. It will remove any existing windows and doors and use the + # Default contruction to set to apply the window construction. Sill height is in meters + def apply_max_fdwr(model, runner, sillHeight_si, wwr) + empty_const_warning = false + model.getSpaces.sort.each do |space| + space.surfaces.sort.each do |surface| + zone = surface.space.get.thermalZone + zone_multiplier = nil + next if zone.empty? + if surface.outsideBoundaryCondition == 'Outdoors' and surface.surfaceType == "Wall" + surface.subSurfaces.each {|ss| ss.remove} + new_window = surface.setWindowToWallRatio(wwr, sillHeight_si, true) + raise "#{surface.name.get} did not get set to #{wwr}. The size of the surface is #{surface.grossArea}" unless surface.windowToWallRatio.round(3) == wwr.round(3) + if new_window.empty? + runner.registerWarning("The requested window to wall ratio for surface '#{surface.name}' was too large. Fenestration was not altered for this surface.") + else + windows_added = true + # warn user if resulting window doesn't have a construction, as it will result in failed simulation. In the future may use logic from starting windows to apply construction to new window. + if new_window.get.construction.empty? && (empty_const_warning == false) + runner.registerWarning('one or more resulting windows do not have constructions. This script is intended to be used with models using construction sets versus hard assigned constructions.') + empty_const_warning = true + end + end + end + end + end + end + + # This method will apply the a SRR to a model. It will remove any existing skylights and use the + # Default contruction to set to apply the skylight construction. A default skylight square area of 0.25^2 is used. + def apply_max_srr(model, runner, srr, skylight_area = 0.25 * 0.25) + spaces = [] + surface_type = "RoofCeiling" + model.getSpaces.sort.each do |space| + space.surfaces.sort.each do |surface| + if surface.outsideBoundaryCondition == 'Outdoors' and surface.surfaceType == surface_type + spaces << space + break + end + end + end + pattern = OpenStudio::Model.generateSkylightPattern(spaces, spaces[0].directionofRelativeNorth, srr, Math.sqrt(skylight_area), Math.sqrt(skylight_area)) # ratio, x value, y value + # applying skylight pattern + skylights = OpenStudio::Model.applySkylightPattern(pattern, spaces, OpenStudio::Model::OptionalConstructionBase.new) + spacenames = spaces.map {|space| space.name.get} + runner.registerInfo("Adding #{skylights.size} skylights to #{spacenames}") + end + + # This method will limit the subsurface of a given surface_type ("Wall" or "RoofCeiling") to the ratio for the building. + # This method only reduces subsurface sizes at most. + def apply_limit_to_subsurface_ratio(model, ratio, surface_type = "Wall") + fdwr = get_outdoor_subsurface_ratio(model, surface_type) + if fdwr <= ratio + OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Building FDWR of #{fdwr} is already lower than limit of #{ratio.round}%.") + return true + end + OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Reducing the size of all windows (by shrinking to centroid) to reduce window area down to the limit of #{ratio.round}%.") + # Determine the factors by which to reduce the window / door area + mult = ratio / fdwr + # Reduce the window area if any of the categories necessary + model.getSpaces.sort.each do |space| + # Loop through all surfaces in this space + space.surfaces.sort.each do |surface| + # Skip non-outdoor surfaces + next unless surface.outsideBoundaryCondition == 'Outdoors' + # Skip non-walls + next unless surface.surfaceType == surface_type + # Subsurfaces in this surface + surface.subSurfaces.sort.each do |ss| + # Reduce the size of the window + red = 1.0 - mult + sub_surface_reduce_area_by_percent_by_shrinking_toward_centroid(ss, red) + end + end + end + return true + end + + # Converts the climate zone in the model into the format used + # by the openstudio-standards lookup tables. For example: + # institution: ASHRAE, value: 6A becomes: ASHRAE 169-2006-6A. + # institution: CEC, value: 3 becomes: CEC T24-CEC3. + # + # @param model [OpenStudio::Model::Model] the model + # @return [String] the string representation of the climate zone, + # empty string if no climate zone is present in the model. + def model_standards_climate_zone(model) + climate_zone = '' + model.getClimateZones.climateZones.each do |cz| + if cz.institution == 'ASHRAE' + next if cz.value == '' # Skip blank ASHRAE climate zones put in by OpenStudio Application + climate_zone = if cz.value == '7' || cz.value == '8' + "ASHRAE 169-2006-#{cz.value}A" + else + "ASHRAE 169-2006-#{cz.value}" + end + elsif cz.institution == 'CEC' + next if cz.value == '' # Skip blank ASHRAE climate zones put in by OpenStudio Application + climate_zone = "CEC T24-CEC#{cz.value}" + end + end + return climate_zone + end + + # Sets the climate zone object in the model using + # the correct institution based on the climate zone specified + # in the format used by the openstudio-standards lookups. + # Clears out any climate zones previously added to the model. + # + # @param model [OpenStudio::Model::Model] the model + # @param climate_zone [String] the climate zone in openstudio-standards format. + # For example: ASHRAE 169-2006-2A, CEC T24-CEC3 + # @return [Boolean] returns true if successful, false if not + def model_set_climate_zone(model, climate_zone) + # Remove previous climate zones from the model + model.getClimateZones.clear + # Split the string into the correct institution and value + if climate_zone.include? 'ASHRAE 169-2006-' + model.getClimateZones.setClimateZone('ASHRAE', climate_zone.gsub('ASHRAE 169-2006-', '')) + elsif climate_zone.include? 'CEC T24-CEC' + model.getClimateZones.setClimateZone('CEC', climate_zone.gsub('CEC T24-CEC', '')) + + end + return true + end + + + # This method return the building ratio of subsurface_area / surface_type_area where surface_type can be "Wall" or "RoofCeiling" + def get_outdoor_subsurface_ratio(model, surface_type = "Wall") + surface_area = 0.0 + sub_surface_area = 0 + all_surfaces = [] + all_sub_surfaces = [] + model.getSpaces.sort.each do |space| + zone = space.thermalZone + zone_multiplier = nil + next if zone.empty? + zone_multiplier = zone.get.multiplier + space.surfaces.sort.each do |surface| + if surface.outsideBoundaryCondition == 'Outdoors' and surface.surfaceType == surface_type + surface_area += surface.grossArea * zone_multiplier + surface.subSurfaces.sort.each do |sub_surface| + sub_surface_area += sub_surface.grossArea * sub_surface.multiplier * zone_multiplier + end + end + end + end + return fdwr = (sub_surface_area / surface_area) + end + + # Loads a osm as a starting point. + # + # @return [Bool] returns true if successful, false if not + def load_initial_osm(osm_file) + # Load the geometry .osm + unless File.exist?(osm_file) + raise("The initial osm path: #{osm_file} does not exist.") + end + osm_model_path = OpenStudio::Path.new(osm_file.to_s) + # Upgrade version if required. + version_translator = OpenStudio::OSVersion::VersionTranslator.new + model = version_translator.loadModel(osm_model_path).get + validate_initial_model(model) + return model + end + + def validate_initial_model(model) + if model.getBuildingStorys.empty? + OpenStudio.logFree(OpenStudio::Error, 'openstudio.model.Model', "Please assign Spaces to BuildingStorys the geometry model.") + end + if model.getThermalZones.empty? + OpenStudio.logFree(OpenStudio::Error, 'openstudio.model.Model', "Please assign Spaces to ThermalZones the geometry model.") + end + if model.getBuilding.standardsNumberOfStories.empty? + OpenStudio.logFree(OpenStudio::Error, 'openstudio.model.Model', "Please define Building.standardsNumberOfStories the geometry model.") + end + if model.getBuilding.standardsNumberOfAboveGroundStories.empty? + OpenStudio.logFree(OpenStudio::Error, 'openstudio.model.Model', "Please define Building.standardsNumberOfAboveStories in the geometry model.") + end + + if @space_type_map.nil? || @space_type_map.empty? + @space_type_map = get_space_type_maps_from_model(model) + if @space_type_map.nil? || @space_type_map.empty? + OpenStudio.logFree(OpenStudio::Error, 'openstudio.model.Model', "Please assign SpaceTypes in the geometry model or in standards database #{@space_type_map}.") + else + @space_type_map = @space_type_map.sort.to_h + OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', "Loaded space type map from model") + end + end + + # ensure that model is intersected correctly. + model.getSpaces.each {|space1| model.getSpaces.each {|space2| space1.intersectSurfaces(space2)}} + # Get multipliers from TZ in model. Need this for HVAC contruction. + @space_multiplier_map = {} + model.getSpaces.sort.each do |space| + @space_multiplier_map[space.name.get] = space.multiplier if space.multiplier > 1 + end + OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Finished adding geometry') + unless @space_multiplier_map.empty? + OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', "Found mulitpliers for space #{@space_multiplier_map}") + end + end + + + # Determines how ventilation for the standard is specified. + # When 'Sum', all min OA flow rates are added up. Commonly used by 90.1. + # When 'Maximum', only the biggest OA flow rate. Used by T24. + # + # @param model [OpenStudio::Model::Model] the model + # @return [String] the ventilation method, either Sum or Maximum + def model_ventilation_method(model) + ventilation_method = 'Sum' + return ventilation_method + end + + # Removes all of the unused ResourceObjects + # (Curves, ScheduleDay, Material, etc.) from the model. + # + # @return [Bool] returns true if successful, false if not + def model_remove_unused_resource_objects(model) + model.getResourceObjects.sort.each do |obj| + if obj.directUseCount.zero? + OpenStudio::logFree(OpenStudio::Debug, 'openstudio.standards.Model', "#{obj.name} is unused; it will be removed.") + model.removeObject(obj.handle) + end + end + + return true + end + + private # Helper method to fill in hourly values def model_add_vals_to_sch(model, day_sch, sch_type, values) if sch_type == 'Constant' @@ -4320,21 +4358,19 @@ else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "Schedule type: #{sch_type} is not recognized. Valid choices are 'Constant' and 'Hourly'.") end end - # Modify the existing service water heating loops - # to match the baseline required heating type. + # Modify the existing service water heating loops to match the baseline required heating type. # @return [Bool] return true if successful, false if not # @author Julien Marrec def model_apply_baseline_swh_loops(model, building_type) model.getPlantLoops.sort.each do |plant_loop| # Skip non service water heating loops next unless plant_loop_swh_loop?(plant_loop) - # Rename the loop to avoid accidentally hooking - # up the HVAC systems to this loop later. + # Rename the loop to avoid accidentally hooking up the HVAC systems to this loop later. plant_loop.setName('Service Water Heating Loop') htg_fuels, combination_system, storage_capacity, total_heating_capacity = plant_loop_swh_system_type(plant_loop) # htg_fuels.size == 0 shoudln't happen @@ -4349,13 +4385,12 @@ htg_fuels.include?('Diesel') || htg_fuels.include?('Gasoline') electric = false end - # Per Table G3.1 11.e, if the baseline system was a combination of - # heating and service water heating, delete all heating equipment - # and recreate a WaterHeater:Mixed. + # Per Table G3.1 11.e, if the baseline system was a combination of heating and service water heating, + # delete all heating equipment and recreate a WaterHeater:Mixed. if combination_system plant_loop.supplyComponents.each do |component| # Get the object type obj_type = component.iddObjectType.valueName.to_s next if ['OS_Node', 'OS_Pump_ConstantSpeed', 'OS_Pump_VariableSpeed', 'OS_Connector_Splitter', 'OS_Connector_Mixer', 'OS_Pipe_Adiabatic'].include?(obj_type) @@ -4404,17 +4439,15 @@ end return true end - # This method goes through certain types of EnergyManagementSystem - # variables and replaces UIDs with object names. This should - # be done by the forward translator, and this code should be - # removed after this bug is fixed: + # This method goes through certain types of EnergyManagementSystem variables and replaces UIDs with object names. + # This should be done by the forward translator, and this code should be removed after this bug is fixed: # https://github.com/NREL/OpenStudio/issues/2598 # - # @todo remove this method after OpenStudio issue #2598 is fixed. + # TODO: remove this method after OpenStudio issue #2598 is fixed. def model_temp_fix_ems_references(model) # Internal Variables model.getEnergyManagementSystemInternalVariables.sort.each do |var| # Get the reference field value ref = var.internalDataIndexKeyName @@ -4480,20 +4513,9 @@ else @space_type_map = @space_type_map.sort.to_h OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', "Loaded space type map from osm file: #{osm_model_path}") end end - - # ensure that model is intersected correctly. - model.getSpaces.each { |space1| model.getSpaces.each { |space2| space1.intersectSurfaces(space2) } } - # Get multipliers from TZ in model. Need this for HVAC contruction. - @space_multiplier_map = {} - model.getSpaces.sort.each do |space| - @space_multiplier_map[space.name.get] = space.multiplier if space.multiplier > 1 - end - OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', 'Finished adding geometry') - unless @space_multiplier_map.empty? - OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', "Found mulitpliers for space #{@space_multiplier_map}") - end return model + end end