lib/openstudio-standards/standards/Standards.Model.rb in openstudio-standards-0.5.0 vs lib/openstudio-standards/standards/Standards.Model.rb in openstudio-standards-0.6.0.rc1

- old
+ new

@@ -61,10 +61,26 @@ # @param sizing_run_dir [String] the directory where the sizing runs will be performed # @param run_all_orients [Boolean] indicate weather a baseline model should be created for all 4 orientations: same as user model, +90 deg, +180 deg, +270 deg # @param debug [Boolean] If true, will report out more detailed debugging output # @return [Boolean] returns true if successful, false if not def model_create_prm_any_baseline_building(user_model, building_type, climate_zone, hvac_building_type = 'All others', wwr_building_type = 'All others', swh_building_type = 'All others', model_deep_copy = false, create_proposed_model = false, custom = nil, sizing_run_dir = Dir.pwd, run_all_orients = false, unmet_load_hours_check = true, debug = false) + # User data process + # bldg_type_hvac_zone_hash could be an empty hash if all zones in the models are unconditioned + # TODO - move this portion to the top of the function + bldg_type_hvac_zone_hash = {} + handle_user_input_data(user_model, climate_zone, sizing_run_dir, hvac_building_type, wwr_building_type, swh_building_type, bldg_type_hvac_zone_hash) + + # enforce the user model to be a non-leap year, defaulting to 2009 if the model year is a leap year + if user_model.yearDescription.is_initialized + year_description = user_model.yearDescription.get + if year_description.isLeapYear + OpenStudio.logFree(OpenStudio::Warn, 'prm.log', + "The user model year #{year_description.assumedYear} is a leap year. Changing to 2009, a non-leap year, as required by PRM guidelines.") + year_description.setCalendarYear(2009) + end + end + if create_proposed_model # Perform a user model design day run only to make sure # that the user model is valid, i.e. can run without major # errors if !model_run_sizing_run(user_model, "#{sizing_run_dir}/USER-SR") @@ -87,11 +103,11 @@ # Check proposed model unmet load hours if unmet_load_hours_check # Run user model; need annual simulation to get unmet load hours if model_run_simulation_and_log_errors(proposed_model, run_dir = "#{sizing_run_dir}/PROP") - umlh = model_get_unmet_load_hours(proposed_model) + umlh = OpenstudioStandards::SqlFile.model_get_annual_occupied_unmet_hours(proposed_model) if umlh > 300 OpenStudio.logFree(OpenStudio::Warn, 'prm.log', "Proposed model unmet load hours (#{umlh}) exceed 300. Baseline model(s) won't be created.") prm_raise(false, sizing_run_dir, @@ -116,16 +132,10 @@ idf = forward_translator.translateModel(proposed_model) idf_path = OpenStudio::Path.new("#{sizing_run_dir}/proposed_final.idf") idf.save(idf_path, true) end - # User data process - # bldg_type_hvac_zone_hash could be an empty hash if all zones in the models are unconditioned - # TODO - move this portion to the top of the function - bldg_type_hvac_zone_hash = {} - handle_user_input_data(user_model, climate_zone, sizing_run_dir, hvac_building_type, wwr_building_type, swh_building_type, bldg_type_hvac_zone_hash) - # Define different orientation from original orientation # for each individual baseline models # Need to run proposed model sizing simulation if no sql data is available degs_from_org = run_all_orientations(run_all_orients, user_model) ? [0, 90, 180, 270] : [0] @@ -199,11 +209,11 @@ OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', '*** Adjusting Window and Skylight Ratios ***') success, wwr_info = model_apply_prm_baseline_window_to_wall_ratio(model, climate_zone, wwr_building_type: wwr_building_type) model_apply_prm_baseline_skylight_to_roof_ratio(model) # Assign building stories to spaces in the building where stories are not yet assigned. - model_assign_spaces_to_stories(model) + OpenstudioStandards::Geometry.model_assign_spaces_to_building_stories(model) # 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 @@ -509,53 +519,54 @@ sql = model.sqlFile.get if sql.connectionOpen sql.close end - if model_run_simulation_and_log_errors(model, "#{sizing_run_dir}/final#{degs}") - # If UMLH are greater than the threshold allowed by Appendix G, - # increase zone air flow and load as per the recommendation in - # the PRM-RM; Note that the PRM-RM only suggest to increase - # air zone air flow, but the zone sizing factor in EnergyPlus - # increase both air flow and load. - if model_get_unmet_load_hours(model) > 300 - model.getThermalZones.each do |thermal_zone| - # Cooling adjustments - clg_umlh = thermal_zone_get_unmet_load_hours(thermal_zone, 'Cooling') - if clg_umlh > 50 - sizing_factor = 1.0 - if thermal_zone.sizingZone.zoneCoolingSizingFactor.is_initialized - sizing_factor = thermal_zone.sizingZone.zoneCoolingSizingFactor.get - end - # Make adjustment to zone cooling sizing factor - # Do not adjust factors greater or equal to 2 - clg_umlh > 150 ? sizing_factor = [2.0, sizing_factor * 1.1].min : sizing_factor = [2.0, sizing_factor * 1.05].min - thermal_zone.sizingZone.setZoneCoolingSizingFactor(sizing_factor) + # simulation failure, raise the exception + unless model_run_simulation_and_log_errors(model, "#{sizing_run_dir}/final#{degs}") + raise('OpenStudio simulation failed.') + end + + # If UMLH are greater than the threshold allowed by Appendix G, + # increase zone air flow and load as per the recommendation in + # the PRM-RM; Note that the PRM-RM only suggest to increase + # air zone air flow, but the zone sizing factor in EnergyPlus + # increase both air flow and load. + umlh = OpenstudioStandards::SqlFile.model_get_annual_occupied_unmet_hours(proposed_model) + if umlh > 300 + model.getThermalZones.each do |thermal_zone| + # Cooling adjustments + clg_umlh = OpenstudioStandards::SqlFile.thermal_zone_get_annual_occupied_unmet_cooling_hours(thermal_zone) + if clg_umlh > 50 + sizing_factor = 1.0 + if thermal_zone.sizingZone.zoneCoolingSizingFactor.is_initialized + sizing_factor = thermal_zone.sizingZone.zoneCoolingSizingFactor.get end + # Make adjustment to zone cooling sizing factor + # Do not adjust factors greater or equal to 2 + clg_umlh > 150 ? sizing_factor = [2.0, sizing_factor * 1.1].min : sizing_factor = [2.0, sizing_factor * 1.05].min + thermal_zone.sizingZone.setZoneCoolingSizingFactor(sizing_factor) + end - # Heating adjustments - # Reset sizing factor - htg_umlh = thermal_zone_get_unmet_load_hours(thermal_zone, 'Heating') - if htg_umlh > 50 - sizing_factor = 1.0 - if thermal_zone.sizingZone.zoneHeatingSizingFactor.is_initialized - # Get zone heating sizing factor - sizing_factor = thermal_zone.sizingZone.zoneHeatingSizingFactor.get - end - - # Make adjustment to zone heating sizing factor - # Do not adjust factors greater or equal to 2 - htg_umlh > 150 ? sizing_factor = [2.0, sizing_factor * 1.1].min : sizing_factor = [2.0, sizing_factor * 1.05].min - thermal_zone.sizingZone.setZoneHeatingSizingFactor(sizing_factor) + # Heating adjustments + # Reset sizing factor + htg_umlh = OpenstudioStandards::SqlFile.thermal_zone_get_annual_occupied_unmet_heating_hours(thermal_zone) + if htg_umlh > 50 + sizing_factor = 1.0 + if thermal_zone.sizingZone.zoneHeatingSizingFactor.is_initialized + # Get zone heating sizing factor + sizing_factor = thermal_zone.sizingZone.zoneHeatingSizingFactor.get end + + # Make adjustment to zone heating sizing factor + # Do not adjust factors greater or equal to 2 + htg_umlh > 150 ? sizing_factor = [2.0, sizing_factor * 1.1].min : sizing_factor = [2.0, sizing_factor * 1.05].min + thermal_zone.sizingZone.setZoneHeatingSizingFactor(sizing_factor) end end - else - # simulation failure, raise the exception. - # OpenStudio.logFree(OpenStudio::Error, 'openstudio.model.Model', 'OpenStudio simulation failed.') - raise('OpenStudio simulation failed.') end + nb_adjustments += 1 end end end @@ -616,11 +627,11 @@ # TODO: Once data refactoring has been completed lookup values from the database; # For now, hard-code LPD for selected spaces. Current Standards Space Type # of OS:SpaceType is the PRM interior lighting space type. These values are # from Table 9.6.1 as required by Section G3.1.6.e. proposed_lpd_residential_spaces = { - 'dormitory - living quarters' => 0.5, # "primary_space_type": "Dormitory—Living Quarters", + 'dormitory - living quarters' => 0.5, # "primary_space_type": "Dormitory - Living Quarters", 'apartment - hardwired' => 0.6, # "primary_space_type": "Dwelling Unit" 'guest room' => 0.41 # "primary_space_type": "Guest Room", } # Make proposed model space related adjustments @@ -711,61 +722,10 @@ # @return [Boolean] Returns true if a sizing run is required def model_create_prm_baseline_building_requires_proposed_model_sizing_run(model) return false end - # Determine the residential and nonresidential floor areas based on the space type properties for each space. - # For spaces with no space type, assume nonresidential. - # - # @param model [OpenStudio::Model::Model] OpenStudio model object - # @return [Hash] keys are 'residential' and 'nonresidential', units are m^2 - def model_residential_and_nonresidential_floor_areas(model) - res_area_m2 = 0 - nonres_area_m2 = 0 - model.getSpaces.sort.each do |space| - if thermal_zone_residential?(space) - res_area_m2 += space.floorArea - else - nonres_area_m2 += space.floorArea - end - 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 identical multiplier, - # assume that the multiplier is a floor multiplier and increase the number of stories accordingly. - # Stories do not have to be contiguous. - # - # @param model [OpenStudio::Model::Model] OpenStudio model object - # @param zones [Array<OpenStudio::Model::ThermalZone>] an array of zones - # @return [Integer] the number of stories spanned - def model_num_stories_spanned(model, zones) - # Get the story object for all zones - stories = [] - zones.each do |zone| - zone.spaces.each do |space| - story = space.buildingStory - next if story.empty? - - stories << story.get - end - end - - # Reduce down to the unique set of stories - stories = stories.uniq - - # Tally up stories including multipliers - num_stories = 0 - stories.each do |story| - num_stories += building_story_floor_multiplier(story) - end - - return num_stories - end - # Add design day schedule objects for space loads, # not used for 2013 and earlier # @author Xuechen (Jerry) Lei, PNNL # @param model [OpenStudio::Model::Model] OpenStudio model object # @@ -783,11 +743,11 @@ def model_zones_with_occ_and_fuel_type(model, custom, applicable_zones = nil) zones = [] model.getThermalZones.sort.each do |zone| # Skip plenums - if thermal_zone_plenum?(zone) + if OpenstudioStandards::ThermalZone.thermal_zone_plenum?(zone) OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Zone #{zone.name} is a plenum. It will not be assigned a baseline system.") next end if !applicable_zones.nil? @@ -797,12 +757,12 @@ next end end # Skip unconditioned zones - heated = thermal_zone_heated?(zone) - cooled = thermal_zone_cooled?(zone) + heated = OpenstudioStandards::ThermalZone.thermal_zone_heated?(zone) + cooled = OpenstudioStandards::ThermalZone.thermal_zone_cooled?(zone) if !heated && !cooled OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Zone #{zone.name} is unconditioned. It will not be assigned a baseline system.") next end @@ -816,11 +776,11 @@ # Occupancy type zn_hash['occ'] = thermal_zone_occupancy_type(zone) # Building type - zn_hash['bldg_type'] = thermal_zone_building_type(zone) + zn_hash['bldg_type'] = OpenstudioStandards::ThermalZone.thermal_zone_get_building_type(zone) # Fuel type # for 2013 and prior, baseline fuel = proposed fuel # for 2016 and later, use fuel to identify zones with district energy zn_hash['fuel'] = thermal_zone_get_zone_fuels_for_occ_and_fuel_type(zone) @@ -999,11 +959,11 @@ next if gp['fuel'] == 'unconditioned' heated_only_zones = [] heated_cooled_zones = [] gp['zones'].each do |zn| - if thermal_zone_heated?(zn['zone']) && !thermal_zone_cooled?(zn['zone']) + if OpenstudioStandards::ThermalZone.thermal_zone_heated?(zn['zone']) && !OpenstudioStandards::ThermalZone.thermal_zone_cooled?(zn['zone']) heated_only_zones << zn else heated_cooled_zones << zn end end @@ -1087,12 +1047,11 @@ end # 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 + group['stories'] = OpenstudioStandards::Geometry.thermal_zones_get_number_of_stories_spanned(group['zones']) # Report out the final grouping OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Final system type group: occ = #{group['occ']}, fuel = #{group['fuel']}, area = #{group['area_ft2'].round} ft2, num stories = #{group['stories']}, zones:") group['zones'].sort.each_slice(5) do |zone_list| zone_names = [] zone_list.each do |zone| @@ -1133,36 +1092,30 @@ # @return [String concatenated string showing different fuel types in a group of zones def get_group_heat_types(model, zones) heat_list = '' has_district_heat = false has_fuel_heat = false - has_elec_heat = false - - # error if HVACComponent heating fuels method is not available - if model.version < OpenStudio::VersionString.new('3.6.0') - OpenStudio.logFree(OpenStudio::Error, 'openstudio.Standards.Model', 'Required HVACComponent method .heatingFuelTypes is not available in pre-OpenStudio 3.6.0 versions. Use a more recent version of OpenStudio.') - end - + has_electric_heat = false zones.each do |zone| - htg_fuels = zone.heatingFuelTypes.map(&:valueName) - if htg_fuels.include?('DistrictHeating') || htg_fuels.include?('DistrictHeatingWater') || htg_fuels.include?('DistrictHeatingSteam') + if OpenstudioStandards::ThermalZone.thermal_zone_district_heat?(zone) has_district_heat = true end - other_heat = thermal_zone_fossil_or_electric_type(zone, '') - if other_heat == 'fossil' + if OpenstudioStandards::ThermalZone.thermal_zone_fossil_heat?(zone) has_fuel_heat = true - elsif other_heat == 'electric' - has_elec_heat = true end + if OpenstudioStandards::ThermalZone.thermal_zone_electric_heat?(zone) + has_electric_heat = true + end end + if has_district_heat heat_list = 'districtheating' end if has_fuel_heat heat_list += '_fuel' end - if has_elec_heat + if has_electric_heat heat_list += '_electric' end return heat_list end @@ -1189,11 +1142,11 @@ if avail_mgr.to_AvailabilityManagerScheduled.is_initialized avail_mgr = avail_mgr.to_AvailabilityManagerScheduled.get fan_schedule = avail_mgr.schedule # fan_sch_translator = ScheduleTranslator.new(model, fan_schedule) # fan_sch_ruleset = fan_sch_translator.translate - fan_schedule_8760 = get_8760_values_from_schedule(model, fan_schedule) + fan_schedule_8760 = OpenstudioStandards::Schedules.schedule_get_hourly_values(fan_schedule) end end end if fan_schedule_8760.empty? # If there are no availability managers, then use the schedule in the supply fan object @@ -1206,11 +1159,11 @@ # fan_schedule = fan_object.availabilitySchedule fan_schedule = air_loop_hvac.availabilitySchedule else OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Failed to retreive fan object for AirLoop #{air_loop_hvac.name}") end - fan_schedule_8760 = get_8760_values_from_schedule(model, fan_schedule) + fan_schedule_8760 = OpenstudioStandards::Schedules.schedule_get_hourly_values(fan_schedule) end # Assign this schedule to each zone on this air loop air_loop_hvac.thermalZones.each do |zone| fan_sch_names[zone.name.get] = fan_schedule_8760 @@ -1227,11 +1180,11 @@ # get fan schedule fan_object = zone_hvac_get_fan_object(zone_equipment) if !fan_object.nil? fan_schedule = fan_object.availabilitySchedule - fan_schedule_8760 = get_8760_values_from_schedule(model, fan_schedule) + fan_schedule_8760 = OpenstudioStandards::Schedules.schedule_get_hourly_values(fan_schedule) fan_sch_names[zone.name.get] = fan_schedule_8760 break end end end @@ -1289,40 +1242,10 @@ fan_obj = fan_component.to_FanVariableVolume.get end return fan_obj end - # Convert from schedule object to array of hourly values for entire year - # Array will include extra 24 values for leap year - # Array will also include extra 24 values at end for holiday day type - # @author: Doug Maddox, PNNL - # @todo consider moving this to Standards.Schedule.rb - # @param: model [Object] - # @param: fan_schedule [Object] - # @return: [Array<String>] annual hourly values from schedule - def get_8760_values_from_schedule(model, fan_schedule) - sch_object_type = fan_schedule.iddObjectType.valueName.to_s - fan_8760 = nil - case sch_object_type - when 'OS_Schedule_Ruleset' - fan_8760 = get_8760_values_from_schedule_ruleset(model, fan_schedule) - when 'OS_Schedule_Constant' - fan_schedule_constant = fan_schedule.to_ScheduleConstant.get - fan_8760 = get_8760_values_from_schedule_constant(model, fan_schedule_constant) - when 'OS_Schedule_Compact' - # First convert to ScheduleRuleset - sch_translator = ScheduleTranslator.new(model, fan_schedule) - fan_schedule_ruleset = sch_translator.convert_schedule_compact_to_schedule_ruleset - fan_8760 = get_8760_values_from_schedule_ruleset(model, fan_schedule_ruleset) - when 'OS_Schedule_Year' - # @todo add function for ScheduleYear - # fan_8760 = get_8760_values_from_schedule_year(model, fan_schedule) - OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', 'Automated baseline measure does not support use of Schedule Year') - end - return fan_8760 - end - # Determine the baseline system type given the inputs. Logic is different for different standards. # # 90.1-2007, 90.1-2010, 90.1-2013 # # @param model [OpenStudio::Model::Model] OpenStudio model object @@ -1454,45 +1377,10 @@ def model_prm_baseline_system_change_fuel_type(model, fuel_type, climate_zone) # Don't change fuel type for most templates return fuel_type end - # Get ASHRAE ID code for climate zone - # @param climate_zone [String] full name of climate zone - # @return [String] ASHRAE ID code for climate zone - def get_climate_zone_code(climate_zone) - cz_codes = [] - cz_codes << '0A' - cz_codes << '0B' - cz_codes << '1A' - cz_codes << '1B' - cz_codes << '2A' - cz_codes << '2B' - cz_codes << '3A' - cz_codes << '3B' - cz_codes << '3C' - cz_codes << '4A' - cz_codes << '4B' - cz_codes << '4C' - cz_codes << '5A' - cz_codes << '5B' - cz_codes << '5C' - cz_codes << '6A' - cz_codes << '6B' - cz_codes << '7A' - cz_codes << '7B' - cz_codes << '8A' - cz_codes << '8B' - - cz_codes.each do |cz| - pattern = Regexp.new(cz, true) - if pattern =~ climate_zone - return cz.to_s - end - end - end - # 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 model [OpenStudio::Model::Model] OpenStudio model object @@ -1613,11 +1501,11 @@ if zone_heat_fuel == 'Electricity' electric_reheat = true end # Group zones by story - story_zone_lists = model_group_zones_by_story(model, zones) + story_zone_lists = OpenstudioStandards::Geometry.model_group_thermal_zones_by_building_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. @@ -1629,11 +1517,12 @@ zone_op_hrs = pri_sec_zone_lists['zone_op_hrs'] # Add a PVAV with Reheat for the primary zones stories = [] story_group[0].spaces.each do |space| - stories << [space.buildingStory.get.name.get, building_story_minimum_z_value(space.buildingStory.get)] + min_z = OpenstudioStandards::Geometry.building_story_get_minimum_height(space.buildingStory.get) + stories << [space.buildingStory.get.name.get, min_z] end story_name = stories.min_by { |nm, z| z }[0] system_name = "#{story_name} PVAV_Reheat (Sys5)" # If and only if there are primary zones to attach to the loop @@ -1668,11 +1557,11 @@ chw_pumping_type: 'const_pri') end end # Group zones by story - story_zone_lists = model_group_zones_by_story(model, zones) + story_zone_lists = OpenstudioStandards::Geometry.model_group_thermal_zones_by_building_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. @@ -1684,11 +1573,12 @@ zone_op_hrs = pri_sec_zone_lists['zone_op_hrs'] # Add an VAV for the primary zones stories = [] story_group[0].spaces.each do |space| - stories << [space.buildingStory.get.name.get, building_story_minimum_z_value(space.buildingStory.get)] + min_z = OpenstudioStandards::Geometry.building_story_get_minimum_height(space.buildingStory.get) + stories << [space.buildingStory.get.name.get, min_z] end story_name = stories.min_by { |nm, z| z }[0] 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? @@ -1746,16 +1636,16 @@ if zone_heat_fuel == 'Electricity' reheat_type = 'Electricity' end # Group zones by story - story_zone_lists = model_group_zones_by_story(model, zones) + story_zone_lists = OpenstudioStandards::Geometry.model_group_thermal_zones_by_building_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. 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 + # The OpenstudioStandards::Geometry.model_group_thermal_zones_by_building_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? # Differentiate primary and secondary zones pri_sec_zone_lists = model_differentiate_primary_secondary_thermal_zones(model, story_group, zone_fan_scheds) @@ -1764,11 +1654,12 @@ zone_op_hrs = pri_sec_zone_lists['zone_op_hrs'] # Add a VAV for the primary zones stories = [] story_group[0].spaces.each do |space| - stories << [space.buildingStory.get.name.get, building_story_minimum_z_value(space.buildingStory.get)] + min_z = OpenstudioStandards::Geometry.building_story_get_minimum_height(space.buildingStory.get) + stories << [space.buildingStory.get.name.get, min_z] end story_name = stories.min_by { |nm, z| z }[0] system_name = "#{story_name} VAV_Reheat (Sys7)" # If and only if there are primary zones to attach to the loop @@ -1823,11 +1714,11 @@ condenser_water_loop: condenser_water_loop) end end # Group zones by story - story_zone_lists = model_group_zones_by_story(model, zones) + story_zone_lists = OpenstudioStandards::Geometry.model_group_thermal_zones_by_building_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. @@ -1839,11 +1730,12 @@ zone_op_hrs = pri_sec_zone_lists['zone_op_hrs'] # Add an VAV for the primary zones stories = [] story_group[0].spaces.each do |space| - stories << [space.buildingStory.get.name.get, building_story_minimum_z_value(space.buildingStory.get)] + min_z = OpenstudioStandards::Geometry.building_story_get_minimum_height(space.buildingStory.get) + stories << [space.buildingStory.get.name.get, min_z] end story_name = stories.min_by { |nm, z| z }[0] 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? @@ -1903,11 +1795,11 @@ heating_type = 'Water' 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 else # If no hot water loop is defined, heat will default to electric resistance heating_type = 'Electric' end cooling_type = 'Water' @@ -1915,11 +1807,11 @@ model.getPlantLoopByName('Chilled Water Loop').get else model_add_chw_loop(model, cooling_fuel: cool_fuel, chw_pumping_type: 'const_pri') - end + end model_add_four_pipe_fan_coil(model, zones, chilled_water_loop, hot_water_loop: hot_water_loop, @@ -1927,11 +1819,11 @@ capacity_control_method: 'ConstantVolume') end when 'SZ_VAV' # System 11, chilled water, heating type varies by climate zone unless zones.empty? # htg type - climate_zone = model_standards_climate_zone(model) + climate_zone = OpenstudioStandards::Weather.model_get_climate_zone(model) case climate_zone when 'ASHRAE 169-2006-0A', 'ASHRAE 169-2006-0B', 'ASHRAE 169-2006-1A', 'ASHRAE 169-2006-1B', @@ -2007,11 +1899,11 @@ # because it requires knowledge of proposed HVAC fuels. sys_groups = model_prm_baseline_system_groups(model, custom) # Assign building stories to spaces in the building # where stories are not yet assigned. - model_assign_spaces_to_stories(model) + OpenstudioStandards::Geometry.model_assign_spaces_to_building_stories(model) # Determine the baseline HVAC system type for each of # the groups of zones and add that system type. sys_groups.each do |sys_group| # Determine the primary baseline system type @@ -2035,11 +1927,11 @@ when 'PVAV_PFP_Boxes', 'VAV_PFP_Boxes' sec_system_type = 'PSZ_HP' end # Group zones by story - story_zone_lists = model_group_zones_by_story(model, sys_group['zones']) + story_zone_lists = OpenstudioStandards::Geometry.model_group_thermal_zones_by_building_story(model, sys_group['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| @@ -2182,24 +2074,14 @@ full_load_hrs = 0.0 # Skip lights with no schedule next if lights_sch.empty? lights_sch = lights_sch.get - if lights_sch.to_ScheduleRuleset.is_initialized - lights_sch = lights_sch.to_ScheduleRuleset.get - full_load_hrs = schedule_ruleset_annual_equivalent_full_load_hrs(lights_sch) - if full_load_hrs > 0 - ann_op_hrs = full_load_hrs - break # Stop after the first schedule with more than 0 hrs - end - elsif lights_sch.to_ScheduleConstant.is_initialized - lights_sch = lights_sch.to_ScheduleConstant.get - full_load_hrs = schedule_constant_annual_equivalent_full_load_hrs(lights_sch) - if full_load_hrs > 0 - ann_op_hrs = full_load_hrs - break # Stop after the first schedule with more than 0 hrs - end + full_load_hrs = OpenstudioStandards::Schedules.schedule_get_equivalent_full_load_hours(lights_sch) + if full_load_hrs > 0 + ann_op_hrs = full_load_hrs + break # Stop after the first schedule with more than 0 hrs end end wk_op_hrs = ann_op_hrs / 52.0 data['wk_op_hrs'] = wk_op_hrs # OpenStudio::logFree(OpenStudio::Info, "openstudio.Standards.Model", "******wk_op_hrs = #{wk_op_hrs.round}") @@ -2222,11 +2104,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) * zone.multiplier + int_load_w = OpenstudioStandards::ThermalZone.thermal_zone_get_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 @@ -2313,97 +2195,10 @@ def model_create_multizone_fan_schedule(model, zone_op_hrs, pri_zones, system_name) # Not applicable if not stable baseline return 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. - # Removes empty array (when the story doesn't contain any of the zones) - # - # @param model [OpenStudio::Model::Model] OpenStudio model object - # @param zones [Array<OpenStudio::Model::ThermalZone>] an array of 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 = [] - model.getBuildingStorys.sort.each do |story| - # Get all the spaces on this story - spaces = story.spaces - - # Get all the thermal zones that serve these spaces - all_zones_on_story = [] - spaces.each do |space| - if space.thermalZone.is_initialized - all_zones_on_story << space.thermalZone.get - else - OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Space #{space.name} has no thermal zone, it is not included in the simulation.") - end - end - - # Find zones in the list that are on this story - zones_on_story = [] - zones.each do |zone| - if all_zones_on_story.include?(zone) - # Skip zones that were already assigned to a story. - # This can happen if a zone has multiple spaces on multiple stories. - # Stairwells and atriums are typical scenarios. - next if zones_already_assigned.include?(zone) - - zones_on_story << zone - zones_already_assigned << zone - end - end - - unless zones_on_story.empty? - story_zone_lists << zones_on_story - end - 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. - # - # @param model [OpenStudio::Model::Model] OpenStudio model object - # @return [Boolean] returns true if successful, false if not - def model_assign_spaces_to_stories(model) - # Make hash of spaces and minz values - sorted_spaces = {} - model.getSpaces.sort.each do |space| - # Skip plenum spaces - next if space_plenum?(space) - - # loop through space surfaces to find min z value - z_points = [] - space.surfaces.each do |surface| - surface.vertices.each do |vertex| - z_points << vertex.z - end - end - minz = z_points.min + space.zOrigin - sorted_spaces[space] = minz - end - - # Pre-sort spaces - sorted_spaces = sorted_spaces.sort_by { |a| a[1] } - - # Take the sorted list and assign/make stories - sorted_spaces.each do |space| - space_obj = space[0] - space_minz = space[1] - if space_obj.buildingStory.empty? - story = model_get_story_for_nominal_z_coordinate(model, space_minz) - space_obj.setBuildingStory(story) - OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Space #{space[0].name} was not assigned to a story by the user. It has been assigned to #{story.name}.") - end - end - - return true - 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. # # @param model [OpenStudio::Model::Model] OpenStudio model object # @return [Boolean] returns true if successful, false if not @@ -2559,18 +2354,21 @@ # the objects will only be returned if the specified date is between the start_date and end_date. # @param area [Double] area of the object in question. If area is supplied, # the objects will only be returned if the specified area is between the minimum_area and maximum_area values. # @param num_floors [Double] capacity of the object in question. If num_floors is supplied, # the objects will only be returned if the specified num_floors is between the minimum_floors and maximum_floors values. + # @param fan_motor_bhp [Double] fan motor brake horsepower. + # @param volume [Double] Equipment storage capacity in gallons. + # @param capacity_per_volume [Double] Equipment capacity per storage capacity in Btu/h/gal. # @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(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.") # return false # end - def model_find_objects(hash_of_objects, search_criteria, capacity = nil, date = nil, area = nil, num_floors = nil, fan_motor_bhp = nil) + def model_find_objects(hash_of_objects, search_criteria, capacity = nil, date = nil, area = nil, num_floors = nil, fan_motor_bhp = nil, volume = nil, capacity_per_volume = nil) matching_objects = [] if hash_of_objects.is_a?(Hash) && hash_of_objects.key?('table') hash_of_objects = hash_of_objects['table'] end @@ -2620,10 +2418,52 @@ else matching_objects = matching_capacity_objects end end + # If volume was specified, narrow down the matching objects + unless volume.nil? + # Skip objects that don't have fields for minimum_storage and maximum_storage + matching_objects = matching_objects.reject { |object| !object.key?('minimum_storage') || !object.key?('maximum_storage') } + + # Skip objects that don't have values specified for minimum_storage and maximum_storage + matching_objects = matching_objects.reject { |object| object['minimum_storage'].nil? || object['maximum_storage'].nil? } + + # Skip objects whose the minimum volume is below or maximum volume above the specified volume + matching_volume_objects = matching_objects.reject { |object| volume.to_f < object['minimum_storage'].to_f || volume.to_f > object['maximum_storage'].to_f } + + # If no object was found, round the volume down in case the number fell between the limits in the json file. + if matching_volume_objects.size.zero? + volume *= 0.99 + # Skip objects whose minimum volume is below or maximum volume above the specified volume + matching_objects = matching_objects.reject { |object| volume.to_f <= object['minimum_storage'].to_f || volume.to_f >= object['maximum_storage'].to_f } + else + matching_objects = matching_volume_objects + end + end + + # If capacity_per_volume was specified, narrow down the matching objects + unless capacity_per_volume.nil? + # Skip objects that don't have fields for minimum_capacity_per_storage and maximum_capacity_per_storage + matching_objects = matching_objects.reject { |object| !object.key?('minimum_capacity_per_storage') || !object.key?('maximum_capacity_per_storage') } + + # Skip objects that don't have values specified for minimum_capacity_per_storage and maximum_capacity_per_storage + matching_objects = matching_objects.reject { |object| object['minimum_capacity_per_storage'].nil? || object['maximum_capacity_per_storage'].nil? } + + # Skip objects whose the minimum capacity_per_volume is below or maximum capacity_per_volume above the specified capacity_per_volume + matching_capacity_per_volume_objects = matching_objects.reject { |object| capacity_per_volume.to_f <= object['minimum_capacity_per_storage'].to_f || capacity_per_volume.to_f >= object['maximum_capacity_per_storage'].to_f } + + # If no object was found, round the volume down in case the number fell between the limits in the json file. + if matching_capacity_per_volume_objects.size.zero? + capacity_per_volume *= 0.99 + # Skip objects whose minimum capacity_per_volume is below or maximum capacity_per_volume above the specified capacity_per_volume + matching_objects = matching_objects.reject { |object| capacity_per_volume.to_f <= object['minimum_capacity_per_storage'].to_f || capacity_per_volume.to_f >= object['maximum_capacity_per_storage'].to_f } + else + matching_objects = matching_capacity_per_volume_objects + end + end + # If fan_motor_bhp was specified, narrow down the matching objects unless fan_motor_bhp.nil? # Skip objects that don't have fields for minimum_capacity and maximum_capacity matching_objects = matching_objects.reject { |object| !object.key?('minimum_capacity') || !object.key?('maximum_capacity') } @@ -2709,12 +2549,12 @@ # 'template' => template, # 'number_of_poles' => 4.0, # 'type' => 'Enclosed', # } # motor_properties = self.model.find_object(motors, search_criteria, capacity: 2.5) - def model_find_object(hash_of_objects, search_criteria, capacity = nil, date = nil, area = nil, num_floors = nil, fan_motor_bhp = nil) - matching_objects = model_find_objects(hash_of_objects, search_criteria, capacity, date, area, num_floors, fan_motor_bhp) + def model_find_object(hash_of_objects, search_criteria, capacity = nil, date = nil, area = nil, num_floors = nil, fan_motor_bhp = nil, volume = nil, capacity_per_volume = nil) + matching_objects = model_find_objects(hash_of_objects, search_criteria, capacity, date, area, num_floors, fan_motor_bhp, volume, capacity_per_volume) # Check the number of matching objects found if matching_objects.size.zero? desired_object = nil OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Model', "Find object search criteria returned no results. Search criteria: #{search_criteria}. Called from #{caller(0)[1]}") @@ -2903,146 +2743,10 @@ end return desired_object end - # Create constant ScheduleRuleset - # - # @param model [OpenStudio::Model::Model] OpenStudio model object - # @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 [OpenStudio::Model::ScheduleRuleset] schedule ruleset object - 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).abs < 1.0e-6 - 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 model [OpenStudio::Model::Model] OpenStudio model object - # @param standard_sch_type_limit [String] the name of a standard schedule type limit with predefined limits - # options are Dimensionless, 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] schedule type limits - 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 - if schedule_type_limits.name.to_s.downcase == 'temperature' - schedule_type_limits.resetLowerLimitValue - schedule_type_limits.resetUpperLimitValue - schedule_type_limits.setNumericType('Continuous') - schedule_type_limits.setUnitType('Temperature') - end - else - case standard_sch_type_limit.downcase - when 'dimensionless' - schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model) - schedule_type_limits.setName('Dimensionless') - schedule_type_limits.setLowerLimitValue(0.0) - schedule_type_limits.setUpperLimitValue(1000.0) - schedule_type_limits.setNumericType('Continuous') - schedule_type_limits.setUnitType('Dimensionless') - - 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 model [OpenStudio::Model::Model] OpenStudio model object # @param schedule_name [String} name of the schedule # @return [ScheduleRuleset] the resulting schedule ruleset @@ -3158,33 +2862,77 @@ # Create a material from the openstudio standards dataset. # # @param model [OpenStudio::Model::Model] OpenStudio model object # @param material_name [String] name of the material # @return [OpenStudio::Model::Material] material object - # @todo make return an OptionalMaterial def model_add_material(model, material_name) # First check model and return material if it already exists model.getMaterials.sort.each do |material| if material.name.get.to_s == material_name OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Model', "Already added material: #{material_name}") return material end end - # OpenStudio::logFree(OpenStudio::Info, 'openstudio.standards.Model', "Adding material: #{material_name}") - # Get the object data - data = model_find_object(standards_data['materials'], 'name' => material_name) - unless data - OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Cannot find data for material: #{material_name}, will not be created.") - return false - # @todo change to return empty optional material + # For Simple Glazing materials: + # Attempt to get properties from the name of the material + material_type = nil + if material_name.downcase.include?('simple glazing') + material_type = 'SimpleGlazing' + u_factor = nil + shgc = nil + vt = nil + material_name.split.each_with_index do |item, i| + prop_value = material_name.split[i + 1].to_f + if item == 'U' + unless u_factor.nil? + OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Multiple U-Factor values have been identified for #{material_name}: previous = #{u_factor}, new = #{prop_value}. Please check the material name. New U-Factor will be used.") + end + u_factor = prop_value + elsif item == 'SHGC' + unless shgc.nil? + OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Multiple SHGC values have been identified for #{material_name}: previous = #{shgc}, new = #{prop_value}. Please check the material name. New SHGC will be used.") + end + shgc = prop_value + elsif item == 'VT' + unless vt.nil? + OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Multiple VT values have been identified for #{material_name}: previous = #{vt}, new = #{prop_value}. Please check the material name. New SHGC will be used.") + end + vt = prop_value + end + end + if u_factor.nil? && shgc.nil? && vt.nil? + material_type = nil + OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Properties of the simple glazing material named #{material_name} could not be identified from its name.") + else + if u_factor.nil? + u_factor = 1.23 + OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Cannot find the U-Factor for the simple glazing material named #{material_name}, a default value of 1.23 is used.") + end + if shgc.nil? + shgc = 0.61 + OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Cannot find the SHGC for the simple glazing material named #{material_name}, a default value of 0.61 is used.") + end + if vt.nil? + vt = 0.81 + OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Cannot find the VT for the simple glazing material named #{material_name}, a default value of 0.81 is used.") + end + end end + # If no properties could be found or the material + # is not of the simple glazing type, search the database + if material_type.nil? + data = model_find_object(standards_data['materials'], 'name' => material_name) + unless data + OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Cannot find data for material: #{material_name}, will not be created.") + return OpenStudio::Model::OptionalMaterial.new + end + material_type = data['material_type'] + end material = nil - material_type = data['material_type'] - if material_type == 'StandardOpaqueMaterial' material = OpenStudio::Model::StandardOpaqueMaterial.new(model) material.setName(material_name) material.setRoughness(data['roughness'].to_s) @@ -3220,13 +2968,13 @@ elsif material_type == 'SimpleGlazing' material = OpenStudio::Model::SimpleGlazing.new(model) material.setName(material_name) - material.setUFactor(OpenStudio.convert(data['u_factor'].to_f, 'Btu/hr*ft^2*R', 'W/m^2*K').get) - material.setSolarHeatGainCoefficient(data['solar_heat_gain_coefficient'].to_f) - material.setVisibleTransmittance(data['visible_transmittance'].to_f) + material.setUFactor(OpenStudio.convert(u_factor.to_f, 'Btu/hr*ft^2*R', 'W/m^2*K').get) + material.setSolarHeatGainCoefficient(shgc.to_f) + material.setVisibleTransmittance(vt.to_f) elsif material_type == 'StandardGlazing' material = OpenStudio::Model::StandardGlazing.new(model) material.setName(material_name) @@ -3332,50 +3080,71 @@ OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Model', "#{data['intended_surface_type']} u_val #{target_u_value_ip} f_fac #{target_f_factor_ip} c_fac #{target_c_factor_ip}") if target_u_value_ip # Handle Opaque and Fenestration Constructions differently - # if construction.isFenestration && construction_simple_glazing?(construction) + # if construction.isFenestration && OpenstudioStandards::Constructions.construction_simple_glazing?(construction) if construction.isFenestration - if construction_simple_glazing?(construction) + if OpenstudioStandards::Constructions.construction_simple_glazing?(construction) # Set the U-Value and SHGC - construction_set_glazing_u_value(construction, target_u_value_ip.to_f, data['intended_surface_type'], u_includes_int_film, u_includes_ext_film) - construction_set_glazing_shgc(construction, target_shgc.to_f) + OpenstudioStandards::Constructions.construction_set_glazing_u_value(construction, target_u_value_ip.to_f, + target_includes_interior_film_coefficients: u_includes_int_film, + target_includes_exterior_film_coefficients: u_includes_ext_film) + simple_glazing = construction.layers.first.to_SimpleGlazing + unless simple_glazing.is_initialized && !target_shgc.nil? + simple_glazing.get.setSolarHeatGainCoefficient(target_shgc.to_f) + end else # if !data['intended_surface_type'] == 'ExteriorWindow' && !data['intended_surface_type'] == 'Skylight' # Set the U-Value - construction_set_u_value(construction, target_u_value_ip.to_f, data['insulation_layer'], data['intended_surface_type'], u_includes_int_film, u_includes_ext_film) + OpenstudioStandards::Constructions.construction_set_u_value(construction, target_u_value_ip.to_f, + insulation_layer_name: data['insulation_layer'], + intended_surface_type: data['intended_surface_type'], + target_includes_interior_film_coefficients: u_includes_int_film, + target_includes_exterior_film_coefficients: u_includes_ext_film) # else # OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Not modifying U-value for #{data['intended_surface_type']} u_val #{target_u_value_ip} f_fac #{target_f_factor_ip} c_fac #{target_c_factor_ip}") end else # Set the U-Value - construction_set_u_value(construction, target_u_value_ip.to_f, data['insulation_layer'], data['intended_surface_type'], u_includes_int_film, u_includes_ext_film) + OpenstudioStandards::Constructions.construction_set_u_value(construction, target_u_value_ip.to_f, + insulation_layer_name: data['insulation_layer'], + intended_surface_type: data['intended_surface_type'], + target_includes_interior_film_coefficients: u_includes_int_film, + target_includes_exterior_film_coefficients: u_includes_ext_film) # else # OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Not modifying U-value for #{data['intended_surface_type']} u_val #{target_u_value_ip} f_fac #{target_f_factor_ip} c_fac #{target_c_factor_ip}") end elsif target_f_factor_ip && data['intended_surface_type'] == 'GroundContactFloor' # F-factor objects are unique to each surface, so a surface needs to be passed # If not surface is passed, use the older approach to model ground contact floors if surface.nil? # Set the F-Factor (only applies to slabs on grade) # @todo figure out what the prototype buildings did about ground heat transfer - # construction_set_slab_f_factor(construction, target_f_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) + # OpenstudioStandards::Constructions.construction_set_slab_f_factor(construction, target_f_factor_ip.to_f, insulation_layer_name: data['insulation_layer']) + OpenstudioStandards::Constructions.construction_set_u_value(construction, 0.0, + insulation_layer_name: data['insulation_layer'], + intended_surface_type: data['intended_surface_type'], + target_includes_interior_film_coefficients: u_includes_int_film, + target_includes_exterior_film_coefficients: u_includes_ext_film) else - construction_set_surface_slab_f_factor(construction, target_f_factor_ip, surface) + OpenstudioStandards::Constructions.construction_set_surface_slab_f_factor(construction, target_f_factor_ip, surface) end elsif target_c_factor_ip && (data['intended_surface_type'] == 'GroundContactWall' || data['intended_surface_type'] == 'GroundContactRoof') # C-factor objects are unique to each surface, so a surface needs to be passed # If not surface is passed, use the older approach to model ground contact walls if surface.nil? # Set the C-Factor (only applies to underground walls) # @todo figure out what the prototype buildings did about ground heat transfer - # 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) + # OpenstudioStandards::Constructions.construction_set_underground_wall_c_factor(construction, target_c_factor_ip.to_f, insulation_layer_name: data['insulation_layer']) + OpenstudioStandards::Constructions.construction_set_u_value(construction, 0.0, + insulation_layer_name: data['insulation_layer'], + intended_surface_type: data['intended_surface_type'], + target_includes_interior_film_coefficients: u_includes_int_film, + target_includes_exterior_film_coefficients: u_includes_ext_film) else - construction_set_surface_underground_wall_c_factor(construction, target_c_factor_ip, surface) + OpenstudioStandards::Constructions.construction_set_surface_underground_wall_c_factor(construction, target_c_factor_ip, surface) end end # If the construction is fenestration, # also set the frame type for use in future lookups @@ -3756,19 +3525,19 @@ existing_curves += model.getCurveCubics existing_curves += model.getCurveQuadratics existing_curves += model.getCurveBicubics existing_curves += model.getCurveBiquadratics existing_curves += model.getCurveQuadLinears + existing_curves += model.getTableMultiVariableLookups + existing_curves += model.getTableLookups existing_curves.sort.each do |curve| if curve.name.get.to_s == curve_name OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Model', "Already added curve: #{curve_name}") return curve end end - # OpenStudio::logFree(OpenStudio::Info, "openstudio.prototype.addCurve", "Adding curve '#{curve_name}' to the model.") - # Find curve data data = model_find_object(standards_data['curves'], 'name' => curve_name) if data.nil? OpenStudio.logFree(OpenStudio::Warn, 'openstudio.Model.Model', "Could not find a curve called '#{curve_name}' in the standards.") return nil @@ -3875,75 +3644,77 @@ curve.setMinimumValueofz(data['minimum_independent_variable_z']) curve.setMaximumValueofz(data['maximum_independent_variable_z']) curve.setMinimumCurveOutput(data['minimum_dependent_variable_output']) curve.setMaximumCurveOutput(data['maximum_dependent_variable_output']) return curve - when 'MultiVariableLookupTable' + when 'TableLookup', 'LookupTable', 'TableMultiVariableLookup', 'MultiVariableLookupTable' num_ind_var = data['number_independent_variables'].to_i - table = OpenStudio::Model::TableMultiVariableLookup.new(model, num_ind_var) - table.setName(data['name']) - table.setInterpolationMethod(data['interpolation_method']) - table.setNumberofInterpolationPoints(data['number_of_interpolation_points']) - table.setCurveType(data['curve_type']) - table.setTableDataFormat('SingleLineIndependentVariableWithMatrix') - table.setNormalizationReference(data['normalization_reference'].to_f) - table.setOutputUnitType(data['output_unit_type']) - table.setMinimumValueofX1(data['minimum_independent_variable_1'].to_f) - table.setMaximumValueofX1(data['maximum_independent_variable_1'].to_f) - table.setInputUnitTypeforX1(data['input_unit_type_x1']) - if num_ind_var == 2 - table.setMinimumValueofX2(data['minimum_independent_variable_2'].to_f) - table.setMaximumValueofX2(data['maximum_independent_variable_2'].to_f) - table.setInputUnitTypeforX2(data['input_unit_type_x2']) - end - data_points = data.each.select { |key, value| key.include? 'data_point' } - data_points.each do |key, value| - if num_ind_var == 1 - table.addPoint(value.split(',')[0].to_f, value.split(',')[1].to_f) - elsif num_ind_var == 2 - table.addPoint(value.split(',')[0].to_f, value.split(',')[1].to_f, value.split(',')[2].to_f) + if model.version < OpenStudio::VersionString.new('3.7.0') + # Use TableMultiVariableLookup object + table = OpenStudio::Model::TableMultiVariableLookup.new(model, num_ind_var) + table.setInterpolationMethod(data['interpolation_method']) + table.setNumberofInterpolationPoints(data['number_of_interpolation_points']) + table.setCurveType(data['curve_type']) + table.setTableDataFormat('SingleLineIndependentVariableWithMatrix') + table.setNormalizationReference(data['normalization_reference'].to_f) + + # set table limits + table.setMinimumValueofX1(data['minimum_independent_variable_1'].to_f) + table.setMaximumValueofX1(data['maximum_independent_variable_1'].to_f) + table.setInputUnitTypeforX1(data['input_unit_type_x1']) + if num_ind_var == 2 + table.setMinimumValueofX2(data['minimum_independent_variable_2'].to_f) + table.setMaximumValueofX2(data['maximum_independent_variable_2'].to_f) + table.setInputUnitTypeforX2(data['input_unit_type_x2']) end + + # add data points + data_points = data.each.select { |key, value| key.include? 'data_point' } + data_points.each do |key, value| + if num_ind_var == 1 + table.addPoint(value.split(',')[0].to_f, value.split(',')[1].to_f) + elsif num_ind_var == 2 + table.addPoint(value.split(',')[0].to_f, value.split(',')[1].to_f, value.split(',')[2].to_f) + end + end + else + # Use TableLookup Object + table = OpenStudio::Model::TableLookup.new(model) + table.setNormalizationDivisor(data['normalization_reference'].to_f) + + # sorting data in ascending order + data_points = data.each.select { |key, value| key.include? 'data_point' } + data_points = data_points.sort_by { |item| item[1].split(',').map(&:to_f) } + data_points.each do |key, value| + var_dep = value.split(',')[2].to_f + table.addOutputValue(var_dep) + end + num_ind_var.times do |i| + table_indvar = OpenStudio::Model::TableIndependentVariable.new(model) + table_indvar.setName(data['name'] + "_ind_#{i + 1}") + table_indvar.setInterpolationMethod(data['interpolation_method']) + + # set table limits + table_indvar.setMinimumValue(data["minimum_independent_variable_#{i + 1}"].to_f) + table_indvar.setMaximumValue(data["maximum_independent_variable_#{i + 1}"].to_f) + table_indvar.setUnitType(data["input_unit_type_x#{i + 1}"].to_s) + + # add data points + var_ind_unique = data_points.map { |key, value| value.split(',')[i].to_f }.uniq + var_ind_unique.each { |var_ind| table_indvar.addValue(var_ind) } + table.addIndependentVariable(table_indvar) + end end + table.setName(data['name']) + table.setOutputUnitType(data['output_unit_type']) return table 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 - # - # @param model [OpenStudio::Model::Model] OpenStudio model object - # @return [OpenStudio::OptionalPath] path to weather file - def model_get_full_weather_file_path(model) - full_epw_path = OpenStudio::OptionalPath.new - - if model.weatherFile.is_initialized - epw_path = model.weatherFile.get.path - if epw_path.is_initialized - if File.exist?(epw_path.get.to_s) - full_epw_path = OpenStudio::OptionalPath.new(epw_path.get) - else - # If this is an always-run Measure, need to check a different path - alt_weath_path = File.expand_path(File.join(Dir.pwd, '../../resources')) - alt_epw_path = File.expand_path(File.join(alt_weath_path, epw_path.get.to_s)) - if File.exist?(alt_epw_path) - full_epw_path = OpenStudio::OptionalPath.new(OpenStudio::Path.new(alt_epw_path)) - else - OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', "Model has been assigned a weather file, but the file is not in the specified location of '#{epw_path.get}'.") - end - end - else - OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', 'Model has a weather file assigned, but the weather file path has been deleted.') - end - else - OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Model', 'Model has not been assigned a weather file.') - end - - return full_epw_path - end - # Find the legacy simulation results from a CSV of previously created results. # # @param model [OpenStudio::Model::Model] OpenStudio model object # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A' # @param building_type [String] the building type @@ -4143,11 +3914,11 @@ # @param model [OpenStudio::Model::Model] OpenStudio model object # @param remap_office [Boolean] re-map small office or leave it alone # @return [Hash] key for climate zone, building type, and standards template. All values are strings. def model_get_building_properties(model, remap_office = true) # get climate zone from model - climate_zone = model_standards_climate_zone(model) + climate_zone = OpenstudioStandards::Weather.model_get_climate_zone(model) # get building type from model building_type = '' if model.getBuilding.standardsBuildingType.is_initialized building_type = model.getBuilding.standardsBuildingType.get @@ -4260,166 +4031,10 @@ 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. - # - # @param model [OpenStudio::Model::Model] OpenStudio model object - # @param boundary_condition [String] the desired boundary condition. valid choices are: - # Adiabatic - # Surface - # Outdoors - # Ground - # @param type [String] the type of surface to find. valid choices are: - # AtticFloor - # AtticWall - # AtticRoof - # DemisingFloor - # DemisingWall - # DemisingRoof - # ExteriorFloor - # ExteriorWall - # ExteriorRoof - # ExteriorWindow - # ExteriorDoor - # GlassDoor - # GroundContactFloor - # GroundContactWall - # GroundContactRoof - # InteriorFloor - # InteriorWall - # InteriorCeiling - # InteriorPartition - # InteriorWindow - # InteriorDoor - # OverheadDoor - # Skylight - # TubularDaylightDome - # TubularDaylightDiffuser - # return [Array<OpenStudio::Model::ConstructionBase>] an array of all constructions. - def model_find_constructions(model, boundary_condition, type) - constructions = [] - - # From default construction sets - model.getDefaultConstructionSets.sort.each do |const_set| - ext_surfs = const_set.defaultExteriorSurfaceConstructions - int_surfs = const_set.defaultInteriorSurfaceConstructions - gnd_surfs = const_set.defaultGroundContactSurfaceConstructions - ext_subsurfs = const_set.defaultExteriorSubSurfaceConstructions - int_subsurfs = const_set.defaultInteriorSubSurfaceConstructions - - # Can't handle incomplete construction sets - if ext_surfs.empty? || - int_surfs.empty? || - gnd_surfs.empty? || - ext_subsurfs.empty? || - int_subsurfs.empty? - - OpenStudio.logFree(OpenStudio::Error, 'openstudio.model.Space', "Default construction set #{const_set.name} is incomplete; constructions from this set will not be reported.") - next - end - - ext_surfs = ext_surfs.get - int_surfs = int_surfs.get - gnd_surfs = gnd_surfs.get - ext_subsurfs = ext_subsurfs.get - int_subsurfs = int_subsurfs.get - - case type - # Exterior Surfaces - when 'ExteriorWall', 'AtticWall' - constructions << ext_surfs.wallConstruction - when 'ExteriorFloor' - constructions << ext_surfs.floorConstruction - when 'ExteriorRoof', 'AtticRoof' - constructions << ext_surfs.roofCeilingConstruction - # Interior Surfaces - when 'InteriorWall', 'DemisingWall', 'InteriorPartition' - constructions << int_surfs.wallConstruction - when 'InteriorFloor', 'AtticFloor', 'DemisingFloor' - constructions << int_surfs.floorConstruction - when 'InteriorCeiling', 'DemisingRoof' - constructions << int_surfs.roofCeilingConstruction - # Ground Contact Surfaces - when 'GroundContactWall' - constructions << gnd_surfs.wallConstruction - when 'GroundContactFloor' - constructions << gnd_surfs.floorConstruction - when 'GroundContactRoof' - constructions << gnd_surfs.roofCeilingConstruction - # Exterior SubSurfaces - when 'ExteriorWindow' - constructions << ext_subsurfs.fixedWindowConstruction - constructions << ext_subsurfs.operableWindowConstruction - when 'ExteriorDoor' - constructions << ext_subsurfs.doorConstruction - when 'GlassDoor' - constructions << ext_subsurfs.glassDoorConstruction - when 'OverheadDoor' - constructions << ext_subsurfs.overheadDoorConstruction - when 'Skylight' - constructions << ext_subsurfs.skylightConstruction - when 'TubularDaylightDome' - constructions << ext_subsurfs.tubularDaylightDomeConstruction - when 'TubularDaylightDiffuser' - constructions << ext_subsurfs.tubularDaylightDiffuserConstruction - # Interior SubSurfaces - when 'InteriorWindow' - constructions << int_subsurfs.fixedWindowConstruction - constructions << int_subsurfs.operableWindowConstruction - when 'InteriorDoor' - constructions << int_subsurfs.doorConstruction - end - end - - # Hard-assigned surfaces - model.getSurfaces.sort.each do |surf| - next unless surf.outsideBoundaryCondition == boundary_condition - - surf_type = surf.surfaceType - if surf_type == 'Floor' || surf_type == 'Wall' - next unless type.include?(surf_type) - elsif surf_type == 'RoofCeiling' - next unless type.include?('Roof') || type.include?('Ceiling') - end - constructions << surf.construction - end - - # Hard-assigned subsurfaces - model.getSubSurfaces.sort.each do |surf| - next unless surf.outsideBoundaryCondition == boundary_condition - - surf_type = surf.subSurfaceType - if surf_type == 'FixedWindow' || surf_type == 'OperableWindow' - next unless type == 'ExteriorWindow' - elsif surf_type == 'Door' - next unless type.include?('Door') - else - next unless surf.subSurfaceType == type - end - constructions << surf.construction - end - - # Throw out the empty constructions - all_constructions = [] - constructions.uniq.each do |const| - next if const.empty? - - all_constructions << const.get - end - - # Only return the unique list (should already be uniq) - all_constructions = all_constructions.uniq - - # ConstructionBase can be sorted - 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. # # 90.1-2007, 90.1-2010, 90.1-2013 @@ -4492,11 +4107,11 @@ types_to_modify << ['Ground', 'GroundContactFloor', 'Unheated'] types_to_modify << ['Ground', 'GroundContactWall', 'Mass'] # Modify all constructions of each type types_to_modify.each do |boundary_cond, surf_type, const_type| - constructions = model_find_constructions(model, boundary_cond, surf_type) + constructions = OpenstudioStandards::Constructions.model_get_constructions(model, boundary_cond, surf_type) constructions.sort.each do |const| standards_info = const.standardsInformation standards_info.setIntendedSurfaceType(surf_type) standards_info.setStandardsConstructionType(const_type) @@ -4759,22 +4374,22 @@ # 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) + cooled = OpenstudioStandards::Space.space_cooled?(space) + heated = OpenstudioStandards::Space.space_heated?(space) cat = 'Unconditioned' # Unconditioned if !heated && !cooled cat = 'Unconditioned' # Heated-Only elsif heated && !cooled cat = 'Semiheated' # Heated and Cooled else - res = space_residential?(space) + res = OpenstudioStandards::Space.space_residential?(space) cat = if res 'ResConditioned' else 'NonResConditioned' end @@ -4917,11 +4532,11 @@ if red < 0.0 # surface with fenestration to its maximum but adjusted by door areas when need to add windows in surfaces no fenestration # turn negative to positive to get the correct adjustment factor. red = -red - surface_wwr = surface_get_wwr(surface) + surface_wwr = OpenstudioStandards::Geometry.surface_get_window_to_wall_ratio(surface) residual_fene += (0.9 - red * surface_wwr) * surface.grossArea end surface_adjust_fenestration_in_a_surface(surface, red, model) end @@ -4974,11 +4589,11 @@ end end # Determine the space category cat = 'NonRes' - if space_residential?(space) + if OpenstudioStandards::Space.space_residential?(space) cat = 'Res' end # if space.is_semiheated # cat = 'Semiheated' # end @@ -5026,11 +4641,11 @@ # Reduce the skylight area if any of the categories necessary model.getSpaces.sort.each do |space| # Determine the space category cat = 'NonRes' - if space_residential?(space) + if OpenstudioStandards::Space.space_residential?(space) cat = 'Res' end # if space.is_semiheated # cat = 'Semiheated' # end @@ -5061,11 +4676,11 @@ surface.subSurfaces.sort.each do |ss| next unless ss.subSurfaceType == 'Skylight' # Reduce the size of the skylight red = 1.0 - mult - sub_surface_reduce_area_by_percent_by_shrinking_toward_centroid(ss, red) + OpenstudioStandards::Geometry.sub_surface_reduce_area_by_percent_by_shrinking_toward_centroid(ss, red) end end end return true @@ -5207,34 +4822,10 @@ OpenStudio.logFree(OpenStudio::Info, 'openstudio.prototype.Model', "Set sizing factors to #{htg} for heating and #{clg} for cooling.") return true end - # 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 model [OpenStudio::Model::Model] OpenStudio model object - # @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) - - if (minz - z).abs < tolerance - OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Model', "The story with a min z value of #{minz.round(2)} is #{story.name}.") - return story - end - end - - story = OpenStudio::Model::BuildingStory.new(model) - story.setNominalZCoordinate(minz) - OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "No story with a min z value of #{minz.round(2)} m +/- #{tolerance} m was found, so a new story called #{story.name} was created.") - - return story - end - # Returns average daily hot water consumption by building type # recommendations from 2011 ASHRAE Handbook - HVAC Applications Table 7 section 50.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. @@ -5321,11 +4912,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 = model_standards_climate_zone(model) + climate_zone = OpenstudioStandards::Weather.model_get_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 @@ -5758,68 +5349,17 @@ # 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) + OpenstudioStandards::Geometry.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-2013-6A. - # institution: CEC, value: 3 becomes: CEC T24-CEC3. - # - # @param model [OpenStudio::Model::Model] OpenStudio model object - # @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-2013-#{cz.value}A" - else - "ASHRAE 169-2013-#{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] OpenStudio model object - # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A' - # @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? 'ASHRAE 169-2013-' - model.getClimateZones.setClimateZone('ASHRAE', climate_zone.gsub('ASHRAE 169-2013-', '')) - 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" # # @param model [OpenStudio::Model::Model] OpenStudio model object # @param surface_type [String] surface type @@ -5947,377 +5487,10 @@ end_size = model.objects.size OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "The model started with #{start_size} objects and finished with #{end_size} objects after removing unused resource objects.") return true end - # This method looks at occupancy profiles for the building as a whole and generates an hours of operation default - # schedule for the building. It also clears out any higher level hours of operation schedule assignments. - # Spaces are organized by res and non_res. Whichever of the two groups has higher design level of people is used for building hours of operation - # Resulting hours of operation can have as many rules as necessary to describe the operation. - # Each ScheduleDay should be an on/off schedule with only values of 0 and 1. There should not be more than one on/off cycle per day. - # In future this could create different hours of operation for residential vs. non-residential, by building type, story, or space type. - # However this measure is a stop gap to convert old generic schedules to parametric schedules. - # Future new schedules should be designed as paramtric from the start and would not need to run through this inference process - # - # @author David Goldwasser - # @param model [OpenStudio::Model::Model] OpenStudio model object - # @param fraction_of_daily_occ_range [Double] fraction above/below daily min range required to start and end hours of operation - # @param invert_res [Boolean] if true will reverse hours of operation for residential space types - # @param gen_occ_profile [Boolean] if true creates a merged occupancy schedule for diagnostic purposes. This schedule is added to the model but no specifically returned by this method - # @return [ScheduleRuleset] schedule that is assigned to the building as default hours of operation - def model_infer_hours_of_operation_building(model, fraction_of_daily_occ_range: 0.25, invert_res: true, gen_occ_profile: false) - # create an array of non-residential and residential spaces - res_spaces = [] - non_res_spaces = [] - res_people_design = 0 - non_res_people_design = 0 - model.getSpaces.sort.each do |space| - if space_residential?(space) - res_spaces << space - res_people_design += space.numberOfPeople * space.multiplier - else - non_res_spaces << space - non_res_people_design += space.numberOfPeople * space.multiplier - end - end - OpenStudio.logFree(OpenStudio::Info, 'openstudio.Standards.Model', "Model has design level of #{non_res_people_design} people in non residential spaces and #{res_people_design} people in residential spaces.") - - # create merged schedule for prevalent type (not used but can be generated for diagnostics) - if gen_occ_profile - res_prevalent = false - if res_people_design > non_res_people_design - occ_merged = spaces_get_occupancy_schedule(res_spaces, sch_name: 'Calculated Occupancy Fraction Residential Merged') - res_prevalent = true - else - occ_merged = spaces_get_occupancy_schedule(non_res_spaces, sch_name: 'Calculated Occupancy Fraction NonResidential Merged') - end - end - - # re-run spaces_get_occupancy_schedule with x above min occupancy to create on/off schedule - if res_people_design > non_res_people_design - hours_of_operation = spaces_get_occupancy_schedule(res_spaces, - sch_name: 'Building Hours of Operation Residential', - occupied_percentage_threshold: fraction_of_daily_occ_range, - threshold_calc_method: 'normalized_daily_range') - res_prevalent = true - else - hours_of_operation = spaces_get_occupancy_schedule(non_res_spaces, - sch_name: 'Building Hours of Operation NonResidential', - occupied_percentage_threshold: fraction_of_daily_occ_range, - threshold_calc_method: 'normalized_daily_range') - end - - # remove gaps resulting in multiple on off cycles for each rule in schedule so it will be valid hours of operation - profiles = [] - profiles << hours_of_operation.defaultDaySchedule - hours_of_operation.scheduleRules.each do |rule| - profiles << rule.daySchedule - end - profiles.sort.each do |profile| - times = profile.times - values = profile.values - next if times.size <= 3 # length of 1-3 should produce valid hours_of_operation profiles - - # Find the latest time where the value == 1 - latest_time = nil - times.zip(values).each do |time, value| - if value > 0 - latest_time = time - end - end - # Skip profiles that are zero all the time - next if latest_time.nil? - - # Calculate the duration from this point to midnight - wrap_dur_left_hr = 0 - if values.first == 0 && values.last == 0 - wrap_dur_left_hr = 24.0 - latest_time.totalHours - end - occ_gap_hash = {} - prev_time = 0 - prev_val = nil - times.each_with_index do |time, i| - next if time.totalHours == 0.0 # should not see this - next if values[i] == prev_val # check if two 0 until time next to each other - - if values[i] == 0 # only store vacant segments - if time.totalHours == 24 - occ_gap_hash[prev_time] = time.totalHours - prev_time + wrap_dur_left_hr - else - occ_gap_hash[prev_time] = time.totalHours - prev_time - end - end - prev_time = time.totalHours - prev_val = values[i] - end - profile.clearValues - max_occ_gap_start = occ_gap_hash.key(occ_gap_hash.values.max) - max_occ_gap_end_hr = max_occ_gap_start + occ_gap_hash[max_occ_gap_start] # can't add time and duration in hours - if max_occ_gap_end_hr > 24.0 then max_occ_gap_end_hr -= 24.0 end - - # time for gap start - target_start_hr = max_occ_gap_start.truncate - target_start_min = ((max_occ_gap_start - target_start_hr) * 60.0).truncate - max_occ_gap_start = OpenStudio::Time.new(0, target_start_hr, target_start_min, 0) - - # time for gap end - target_end_hr = max_occ_gap_end_hr.truncate - target_end_min = ((max_occ_gap_end_hr - target_end_hr) * 60.0).truncate - max_occ_gap_end = OpenStudio::Time.new(0, target_end_hr, target_end_min, 0) - - profile.addValue(max_occ_gap_start, 1) - profile.addValue(max_occ_gap_end, 0) - os_time_24 = OpenStudio::Time.new(0, 24, 0, 0) - if max_occ_gap_start > max_occ_gap_end - profile.addValue(os_time_24, 0) - else - profile.addValue(os_time_24, 1) - end - end - - # reverse 1 and 0 values for res_prevalent building - # currently spaces_get_occupancy_schedule doesn't use defaultDayProflie, so only inspecting rules for now. - if invert_res && res_prevalent - OpenStudio.logFree(OpenStudio::Info, 'openstudio.Standards.Model', 'Per argument passed in hours of operation are being inverted for buildings with more people in residential versus non-residential spaces.') - hours_of_operation.scheduleRules.each do |rule| - profile = rule.daySchedule - times = profile.times - values = profile.values - profile.clearValues - times.each_with_index do |time, i| - orig_val = values[i] - new_value = nil - if orig_val == 0 then new_value = 1 end - if orig_val == 1 then new_value = 0 end - profile.addValue(time, new_value) - end - end - end - - # set hours of operation for building level hours of operation - model.getDefaultScheduleSets.each(&:resetHoursofOperationSchedule) - if model.getBuilding.defaultScheduleSet.is_initialized - default_sch_set = model.getBuilding.defaultScheduleSet.get - else - default_sch_set = OpenStudio::Model::DefaultScheduleSet.new(model) - default_sch_set.setName('Building Default Schedule Set') - model.getBuilding.setDefaultScheduleSet(default_sch_set) - end - default_sch_set.setHoursofOperationSchedule(hours_of_operation) - - return hours_of_operation - end - - # This method users the hours of operation for a space and the existing ScheduleRuleset profiles to setup parametric schedule - # inputs. Inputs include one or more load profile formulas. Data is stored in model attributes for downstream - # application. This should impact all ScheduleRuleset objects in the model. Plant and Air loop hoours of operations - # should be traced back to a space or spaces. - # - # @author David Goldwasser - # @param model [OpenStudio::Model::Model] OpenStudio model object - # @param step_ramp_logic [String] type of step logic to use - # @param infer_hoo_for_non_assigned_objects [Boolean] attempt to get hoo for objects like swh with and exterior lighting - # @param gather_data_only [Boolean] false (stops method before changes made if true) - # @param hoo_var_method [String] accepts hours and fractional. Any other value value will result in hoo variables not being applied - # @return [Hash] schedule is key, value is hash of number of objects - def model_setup_parametric_schedules(model, step_ramp_logic: nil, infer_hoo_for_non_assigned_objects: true, gather_data_only: false, hoo_var_method: 'hours') - parametric_inputs = {} - default_sch_type = OpenStudio::Model::DefaultScheduleType.new('HoursofOperationSchedule') - # thermal zones, air loops, plant loops will require some logic if they refer to more than one hours of operaiton schedule. - # for initial use case while have same horus of operaiton so this can be pretty simple, but will have to re-visit it sometime - # possible solution A: choose hoo that contributes the largest fraction of floor area - # possible solution B: expand the hours of operation for a given day to include combined range of hoo objects - # whatever approach is used for gathering parametric inputs for existing ruleset schedules should also be used for model_apply_parametric_schedules - - # loop through spaces (trace hours of operation back to space) - gather_inputs_parametric_space_space_type_schedules(model.getSpaces, parametric_inputs, gather_data_only) - - # loop through space types (trace hours of operation back to space type). - gather_inputs_parametric_space_space_type_schedules(model.getSpaceTypes, parametric_inputs, gather_data_only) - - # loop through thermal zones (trace hours of operation back to spaces in thermal zone) - thermal_zone_hash = {} # key is zone and hash is hours of operation - model.getThermalZones.sort.each do |zone| - # identify hours of operation - hours_of_operation = spaces_hours_of_operation(zone.spaces) - thermal_zone_hash[zone] = hours_of_operation - # get thermostat setpoint schedules - if zone.thermostatSetpointDualSetpoint.is_initialized - thermostat = zone.thermostatSetpointDualSetpoint.get - if thermostat.heatingSetpointTemperatureSchedule.is_initialized && thermostat.heatingSetpointTemperatureSchedule.get.to_ScheduleRuleset.is_initialized - schedule = thermostat.heatingSetpointTemperatureSchedule.get.to_ScheduleRuleset.get - gather_inputs_parametric_schedules(schedule, thermostat, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: hoo_var_method) - end - if thermostat.coolingSetpointTemperatureSchedule.is_initialized && thermostat.coolingSetpointTemperatureSchedule.get.to_ScheduleRuleset.is_initialized - schedule = thermostat.coolingSetpointTemperatureSchedule.get.to_ScheduleRuleset.get - gather_inputs_parametric_schedules(schedule, thermostat, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: hoo_var_method) - end - end - end - - # loop through air loops (trace hours of operation back through spaces served by air loops) - air_loop_hash = {} # key is zone and hash is hours of operation - model.getAirLoopHVACs.sort.each do |air_loop| - # identify hours of operation - air_loop_spaces = [] - air_loop.thermalZones.sort.each do |zone| - air_loop_spaces += zone.spaces - air_loop_spaces += zone.spaces - end - hours_of_operation = spaces_hours_of_operation(air_loop_spaces) - air_loop_hash[air_loop] = hours_of_operation - if air_loop.availabilitySchedule.to_ScheduleRuleset.is_initialized - schedule = air_loop.availabilitySchedule.to_ScheduleRuleset.get - gather_inputs_parametric_schedules(schedule, air_loop, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: hoo_var_method) - end - avail_mgrs = air_loop.availabilityManagers - avail_mgrs.sort.each do |avail_mgr| - # @todo I'm finding availability mangers, but not any resources for them, even if I use OpenStudio::Model.getRecursiveChildren(avail_mgr) - resources = avail_mgr.resources - resources = OpenStudio::Model.getRecursiveResources(avail_mgr) - resources.sort.each do |resource| - if resource.to_ScheduleRuleset.is_initialized - schedule = resource.to_ScheduleRuleset.get - gather_inputs_parametric_schedules(schedule, avail_mgr, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: hoo_var_method) - end - end - end - end - - # look through all model HVAC components find scheduleRuleset objects, resources, that use them and zone or air loop for hours of operation - hvac_components = model.getHVACComponents - hvac_components.sort.each do |component| - # identify zone, or air loop it refers to, some may refer to plant loop, OA or other component - thermal_zone = nil - air_loop = nil - plant_loop = nil - schedules = [] - if component.to_ZoneHVACComponent.is_initialized && component.to_ZoneHVACComponent.get.thermalZone.is_initialized - thermal_zone = component.to_ZoneHVACComponent.get.thermalZone.get - end - if component.airLoopHVAC.is_initialized - air_loop = component.airLoopHVAC.get - end - if component.plantLoop.is_initialized - plant_loop = component.plantLoop.get - end - component.resources.sort.each do |resource| - if resource.to_ThermalZone.is_initialized - thermal_zone = resource.to_ThermalZone.get - elsif resource.to_ScheduleRuleset.is_initialized - schedules << resource.to_ScheduleRuleset.get - end - end - - # inspect resources for children of objects found in thermal zone or plant loop - # get objects like OA controllers and unitary object components - next if thermal_zone.nil? && air_loop.nil? - - children = OpenStudio::Model.getRecursiveChildren(component) - children.sort.each do |child| - child.resources.sort.each do |sub_resource| - if sub_resource.to_ScheduleRuleset.is_initialized - schedules << sub_resource.to_ScheduleRuleset.get - end - end - end - - # process schedules found for this component - schedules.sort.each do |schedule| - hours_of_operation = nil - if !thermal_zone.nil? - hours_of_operation = thermal_zone_hash[thermal_zone] - elsif !air_loop.nil? - hours_of_operation = air_loop_hash[air_loop] - elsif !plant_loop.nil? - OpenStudio.logFree(OpenStudio::Info, 'openstudio.model.Model', "#{schedule.name.get} is associated with plant loop, will not gather parametric inputs") - next - else - OpenStudio.logFree(OpenStudio::Warn, 'openstudio.model.Model', "Cannot identify where #{component.name.get} is in system. Will not gather parametric inputs for #{schedule.name.get}") - next - end - gather_inputs_parametric_schedules(schedule, component, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: hoo_var_method) - end - end - - # @todo Service Water Heating supply side (may or may not be associated with a space) - # @todo water use equipment definitions (temperature, sensible, latent) may be in multiple spaces, need to identify hoo, but typically constant schedules - - # water use equipment (flow rate fraction) - # @todo address common schedules used across multiple instances - model.getWaterUseEquipments.sort.each do |water_use_equipment| - if water_use_equipment.flowRateFractionSchedule.is_initialized && water_use_equipment.flowRateFractionSchedule.get.to_ScheduleRuleset.is_initialized - schedule = water_use_equipment.flowRateFractionSchedule.get.to_ScheduleRuleset.get - next if parametric_inputs.key?(schedule) - - opt_space = water_use_equipment.space - if opt_space.is_initialized - space = space.get - hours_of_operation = space_hours_of_operation(space) - gather_inputs_parametric_schedules(schedule, water_use_equipment, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: hoo_var_method) - else - hours_of_operation = spaces_hours_of_operation(model.getSpaces) - if !hours_of_operation.nil? - gather_inputs_parametric_schedules(schedule, water_use_equipment, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: hoo_var_method) - end - end - - end - end - # @todo Refrigeration (will be associated with thermal zone) - # @todo exterior lights (will be astronomical, but like AEDG's may have reduction later at night) - - return parametric_inputs - end - - # This method applies the hours of operation for a space and the load profile formulas in the overloaded ScheduleRulset - # objects to update time value pairs for ScheduleDay objects. Object type specific logic will be used to generate profiles - # for summer and winter design days. - # - # @note This measure will replace any prior chagnes made to ScheduleRule objects with new ScheduleRule values from - # profile formulas - # @author David Goldwasser - # @param model [OpenStudio::Model::Model] OpenStudio model object - # @param ramp_frequency [Double] ramp frequency in minutes. If nil method will match simulation timestep - # @param infer_hoo_for_non_assigned_objects [Boolean] # attempt to get hoo for objects like swh with and exterior lighting - # @param error_on_out_of_order [Boolean] true will error if applying formula creates out of order values - # @return [Array] of modified ScheduleRuleset objects - def model_apply_parametric_schedules(model, ramp_frequency: nil, infer_hoo_for_non_assigned_objects: true, error_on_out_of_order: true) - # get ramp frequency (fractional hour) from timestep - if ramp_frequency.nil? - steps_per_hour = if model.getSimulationControl.timestep.is_initialized - model.getSimulationControl.timestep.get.numberOfTimestepsPerHour - else - 6 # default OpenStudio timestep if none specified - end - ramp_frequency = 1.0 / steps_per_hour.to_f - end - - # Go through model and create parametric formulas for all schedules - parametric_inputs = model_setup_parametric_schedules(model, gather_data_only: true) - - parametric_schedules = [] - model.getScheduleRulesets.sort.each do |sch| - if !sch.hasAdditionalProperties || !sch.additionalProperties.hasFeature('param_sch_ver') - # for now don't look at schedules without targets, in future can alter these by looking at building level hours of operation - next if sch.directUseCount <= 0 # won't catch if used for space type load instance, but that space type isn't used - - # @todo address schedules that fall into this category, if they are used in the model - OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "For #{sch.sources.first.name}, #{sch.name} is not setup as parametric schedule. It has #{sch.sources.size} sources.") - next - end - - # apply parametric inputs - schedule_apply_parametric_inputs(sch, ramp_frequency, infer_hoo_for_non_assigned_objects, error_on_out_of_order, parametric_inputs) - - # add schedule to array - parametric_schedules << sch - end - - return parametric_schedules - end - private def model_apply_userdata_outdoor_air(model) return true end @@ -6603,465 +5776,29 @@ end end return model end - # pass array of space types or spaces - # - # @author David Goldwasser - # @param space_space_types [Array] array of spaces or space types - # @param parametric_inputs [Hash] - # @param gather_data_only [Boolean] - # @return [Hash] - def gather_inputs_parametric_space_space_type_schedules(space_space_types, parametric_inputs, gather_data_only) - space_space_types.each do |space_type| - # get hours of operation for space type once - next if space_type.class == 'OpenStudio::Model::SpaceTypes' && space_type.floorArea == 0 - - hours_of_operation = space_hours_of_operation(space_type) - if hours_of_operation.nil? - OpenStudio.logFree(OpenStudio::Warn, 'openstudio.Standards.Model', "Can't evaluate schedules for #{space_type.name}, doesn't have hours of operation.") - next - end - # loop through internal load instances - space_type.lights.each do |load_inst| - gather_inputs_parametric_load_inst_schedules(load_inst, parametric_inputs, hours_of_operation, gather_data_only) - end - space_type.luminaires.each do |load_inst| - gather_inputs_parametric_load_inst_schedules(load_inst, parametric_inputs, hours_of_operation, gather_data_only) - end - space_type.electricEquipment.each do |load_inst| - gather_inputs_parametric_load_inst_schedules(load_inst, parametric_inputs, hours_of_operation, gather_data_only) - end - space_type.gasEquipment.each do |load_inst| - gather_inputs_parametric_load_inst_schedules(load_inst, parametric_inputs, hours_of_operation, gather_data_only) - end - space_type.steamEquipment.each do |load_inst| - gather_inputs_parametric_load_inst_schedules(load_inst, parametric_inputs, hours_of_operation, gather_data_only) - end - space_type.otherEquipment.each do |load_inst| - gather_inputs_parametric_load_inst_schedules(load_inst, parametric_inputs, hours_of_operation, gather_data_only) - end - space_type.people.each do |load_inst| - gather_inputs_parametric_load_inst_schedules(load_inst, parametric_inputs, hours_of_operation, gather_data_only) - if load_inst.activityLevelSchedule.is_initialized && load_inst.activityLevelSchedule.get.to_ScheduleRuleset.is_initialized - act_sch = load_inst.activityLevelSchedule.get.to_ScheduleRuleset.get - gather_inputs_parametric_schedules(act_sch, load_inst, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: 'hours') - end - end - space_type.spaceInfiltrationDesignFlowRates.each do |load_inst| - gather_inputs_parametric_load_inst_schedules(load_inst, parametric_inputs, hours_of_operation, gather_data_only) - end - space_type.spaceInfiltrationEffectiveLeakageAreas.each do |load_inst| - gather_inputs_parametric_load_inst_schedules(load_inst, parametric_inputs, hours_of_operation, gather_data_only) - end - dsgn_spec_oa = space_type.designSpecificationOutdoorAir - if dsgn_spec_oa.is_initialized - gather_inputs_parametric_load_inst_schedules(dsgn_spec_oa.get, parametric_inputs, hours_of_operation, gather_data_only) - end - end - - return parametric_inputs - end - - # method to process load instance schedules for model_setup_parametric_schedules - # - # @author David Goldwasser - # @param load_inst [OpenStudio::Model::SpaceLoadInstance] - # @param parametric_inputs [Hash] - # @param hours_of_operation [Hash] - # @param gather_data_only [Boolean] - # @return [Hash] - def gather_inputs_parametric_load_inst_schedules(load_inst, parametric_inputs, hours_of_operation, gather_data_only) - if load_inst.class.to_s == 'OpenStudio::Model::People' - opt_sch = load_inst.numberofPeopleSchedule - elsif load_inst.class.to_s == 'OpenStudio::Model::DesignSpecificationOutdoorAir' - opt_sch = load_inst.outdoorAirFlowRateFractionSchedule - else - opt_sch = load_inst.schedule - end - if !opt_sch.is_initialized || !opt_sch.get.to_ScheduleRuleset.is_initialized - return nil - end - - gather_inputs_parametric_schedules(opt_sch.get.to_ScheduleRuleset.get, load_inst, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: 'hours') - - return parametric_inputs - end - - # method to process load instance schedules for model_setup_parametric_schedules - # - # @author David Goldwasser - # @param sch [OpenStudio::Model::Schedule] - # @param load_inst [OpenStudio::Model::SpaceLoadInstance] - # @param parametric_inputs [Hash] - # @param hours_of_operation [Hash] - # @param ramp [Boolean] - # @param min_ramp_dur_hr [Double] - # @param gather_data_only [Boolean] - # @param hoo_var_method [String] accepts hours and fractional. Any other value value will result in hoo variables not being applied - # @return [Hash] - def gather_inputs_parametric_schedules(sch, load_inst, parametric_inputs, hours_of_operation, ramp: true, min_ramp_dur_hr: 2.0, gather_data_only: false, hoo_var_method: 'hours') - if parametric_inputs.key?(sch) - if hours_of_operation != parametric_inputs[sch][:hoo_inputs] # don't warn if the hours of operation between old and new schedule are equivalent - OpenStudio.logFree(OpenStudio::Warn, 'openstudio.Standards.Model', "#{load_inst.name} uses #{sch.name} but parametric inputs have already been setup based on hours of operation for #{parametric_inputs[sch][:target].name}.") - return nil - end - end - - # gather and store data for scheduleRuleset - min_max = schedule_ruleset_annual_min_max_value(sch) - ruleset_hash = { floor: min_max['min'], ceiling: min_max['max'], target: load_inst, hoo_inputs: hours_of_operation } - parametric_inputs[sch] = ruleset_hash - - # stop here if only gathering information otherwise will continue and generate additional parametric properties for schedules and rules - if gather_data_only then return parametric_inputs end - - # set scheduleRuleset properties - props = sch.additionalProperties - props.setFeature('param_sch_ver', '0.0.1') # this is needed to see if formulas are in sync with version of standards that processes them also used to flag schedule as parametric - props.setFeature('param_sch_floor', min_max['min']) - props.setFeature('param_sch_ceiling', min_max['max']) - - # cleanup existing profiles - schedule_ruleset_cleanup_profiles(sch) - - # gather profiles - daily_flhs = [] # will be used to tag, min,medium,max operation for non typical operations - schedule_days = {} # key is day_schedule value is hours in day (used to tag profiles) - sch.scheduleRules.each do |rule| - schedule_days[rule.daySchedule] = rule.ruleIndex - daily_flhs << day_schedule_equivalent_full_load_hrs(rule.daySchedule) - end - schedule_days[sch.defaultDaySchedule] = -1 - daily_flhs << day_schedule_equivalent_full_load_hrs(sch.defaultDaySchedule) - - # get indices for current schedule - year_description = sch.model.yearDescription.get - year = year_description.assumedYear - year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, year) - year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, year) - indices_vector = sch.getActiveRuleIndices(year_start_date, year_end_date) - - # step through profiles and add additional properties to describe profiles - schedule_days.each_with_index do |(schedule_day, current_rule_index), i| - # loop through indices looking of rule in hoo that contains days in the rule - hoo_target_index = nil - days_used = [] - indices_vector.each_with_index do |profile_index, i| - if profile_index == current_rule_index then days_used << i + 1 end - end - # find days_used in hoo profiles that contains all days used from this profile - hoo_profile_match_hash = {} - best_fit_check = {} - hours_of_operation.each do |profile_index, value| - days_for_rule_not_in_hoo_profile = days_used - value[:days_used] - hoo_profile_match_hash[profile_index] = days_for_rule_not_in_hoo_profile - best_fit_check[profile_index] = days_for_rule_not_in_hoo_profile.size - if days_for_rule_not_in_hoo_profile.empty? - hoo_target_index = profile_index - end - end - # if schedule day days used can't be mapped to single hours of operation then do not use hoo variables, otherwise would have ot split rule and alter model - if hoo_target_index.nil? - hoo_start = nil - hoo_end = nil - occ = nil - vac = nil - # @todo issue warning when this happens on any profile that isn't a constant value - else - # get hours of operation for this specific profile - hoo_start = hours_of_operation[hoo_target_index][:hoo_start] - hoo_end = hours_of_operation[hoo_target_index][:hoo_end] - occ = hours_of_operation[hoo_target_index][:hoo_hours] - vac = 24.0 - hours_of_operation[hoo_target_index][:hoo_hours] - end - - props = schedule_day.additionalProperties - par_val_time_hash = {} # time is key, value is value in and optional value out as a one or two object array - times = schedule_day.times - values = schedule_day.values - values.each_with_index do |value, j| - # don't add value until 24 if it is the same as first value for non constant profiles - if values.size > 1 && j == values.size - 1 && value == values.first - next - end - - current_time = times[j].totalHours - # if step height goes floor to ceiling then do not ramp. - if !ramp || (values.uniq.size < 3) - # this will result in steps like old profiles, update to ramp in most cases - if j == values.size - 1 - par_val_time_hash[current_time] = [value, values.first] - else - par_val_time_hash[current_time] = [value, values[j + 1]] - end - else - if j == 0 - prev_time = times.last.totalHours - 24 # e.g. 24 would show as until 0 - else - prev_time = times[j - 1].totalHours - end - if j == values.size - 1 - next_time = times.first.totalHours + 24 # e.g. 6 would show as until 30 - next_value = values.first - - # do nothing if value is same as first value - if value == next_value - next - end - - else - next_time = times[j + 1].totalHours - next_value = values[j + 1] - end - # delta time is min min_ramp_dur_hr, half of previous dur, half of next dur - # todo - would be nice to change to 0.25 for vally less than 2 hours - multiplier = 0.5 - delta = [min_ramp_dur_hr, (current_time - prev_time) * multiplier, (next_time - current_time) * multiplier].min - # add value to left if not already added - if !par_val_time_hash.key?(current_time - delta) - time_left = current_time - delta - if time_left < 0.0 then time_left += 24.0 end - par_val_time_hash[time_left] = [value] - end - # add value to right - time_right = current_time + delta - if time_right > 24.0 then time_right -= 24.0 end - par_val_time_hash[time_right] = [next_value] - end - end - - # sort hash by keys - par_val_time_hash.sort.to_h - - # calculate estimated value (not including any secondary logic) - est_daily_flh = 0.0 - prev_time = par_val_time_hash.keys.max - 24.0 - prev_value = par_val_time_hash.values.last.last # last value in last optional pair of values - par_val_time_hash.sort.each do |time, value_array| - segment_length = time - prev_time - avg_value = (value_array.first + prev_value) * 0.5 - est_daily_flh += segment_length * avg_value - prev_time = time - prev_value = value_array.last - end - - # test expected value against estimated value - daily_flh = day_schedule_equivalent_full_load_hrs(schedule_day) - percent_change = ((daily_flh - est_daily_flh) / daily_flh) * 100.0 - if percent_change.abs > 0.05 - # @todo this estimation can have flaws. Fix or remove it, make sure to update for secondary logic (if we implement that here) - # post application checks compares against actual instead of estimated values - OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Model', "For day schedule #{schedule_day.name} in #{sch.name} there was a #{percent_change.round(4)}% change. Expected full load hours is #{daily_flh.round(4)}, but estimated value is #{est_daily_flh.round(4)}") - end - - raw_string = [] - par_val_time_hash.sort.each do |time, value_array| - # add in value variables - # not currently using range, only using min max for constant schedules or schedules with just two values - value_array_var = [] - value_array.each do |val| - if val == min_max['min'] && values.uniq.size < 3 - value_array_var << 'val_flr' - elsif val == min_max['max'] && values.uniq.size < 3 - value_array_var << 'val_clg' - else - value_array_var << val - end - end - - # add in hoo variables when matching profile found - if !hoo_start.nil? - - # identify which identifier (star,mid,end) time is closest to, which will impact formula structure - # includes code to identify delta for wrap around of 24 - formula_identifier = {} - start_delta_array = [hoo_start - time, hoo_start - time + 24, hoo_start - time - 24] - start_delta_array_abs = [(hoo_start - time).abs, (hoo_start - time + 24).abs, (hoo_start - time - 24).abs] - start_delta_h = start_delta_array[start_delta_array_abs.index(start_delta_array_abs.min)] - formula_identifier['start'] = start_delta_h - mid_calc = hoo_start + occ * 0.5 - mid_delta_array = [mid_calc - time, mid_calc - time + 24, mid_calc - time - 24] - mid_delta_array_abs = [(mid_calc - time).abs, (mid_calc - time + 24).abs, (mid_calc - time - 24).abs] - mid_delta_h = mid_delta_array[mid_delta_array_abs.index(mid_delta_array_abs.min)] - formula_identifier['mid'] = mid_delta_h - end_delta_array = [hoo_end - time, hoo_end - time + 24, hoo_end - time - 24] - end_delta_array_abs = [(hoo_end - time).abs, (hoo_end - time + 24).abs, (hoo_end - time - 24).abs] - end_delta_h = end_delta_array[end_delta_array_abs.index(end_delta_array_abs.min)] - formula_identifier['end'] = end_delta_h - - # need to store min absolute value to pick the best fit - formula_identifier_min_abs = {} - formula_identifier.each do |k, v| - formula_identifier_min_abs[k] = v.abs - end - - # pick from possible formula approaches for any datapoint where x is hour value - min_key = formula_identifier_min_abs.key(formula_identifier_min_abs.values.min) - min_value = formula_identifier[min_key] - - if hoo_var_method == 'hours' - # minimize x, which should be no greater than 12, see if rounding to 2 decimal places works - min_value = min_value.round(2) - if min_key == 'start' - if min_value == 0 - time = 'hoo_start' - elsif min_value < 0 - time = "hoo_start + #{min_value.abs}" - else # greater than 0 - time = "hoo_start - #{min_value}" - end - elsif min_key == 'mid' - if min_value == 0 - time = 'mid' - # converted to variable for simplicity but could also be described like this - # time = "hoo_start + occ * 0.5" - elsif min_value < 0 - time = "mid + #{min_value.abs}" - else # greater than 0 - time = "mid - #{min_value}" - end - else # min_key == "end" - if min_value == 0 - time = 'hoo_end' - elsif min_value < 0 - time = "hoo_end + #{min_value.abs}" - else # greater than 0 - time = "hoo_end - #{min_value}" - end - end - - elsif hoo_var_method == 'fractional' - - # minimize x(hour before converted to fraction), which should be no greater than 0.5 as fraction, see if rounding to 3 decimal places works - if occ > 0 - min_value_occ_fract = min_value.abs / occ - else - min_value_occ_fract = 0.0 - end - if vac > 0 - min_value_vac_fract = min_value.abs / vac - else - min_value_vac_fract = 0.0 - end - if min_key == 'start' - if min_value == 0 - time = 'hoo_start' - elsif min_value < 0 - time = "hoo_start + occ * #{min_value_occ_fract.round(3)}" - else # greater than 0 - time = "hoo_start - vac * #{min_value_vac_fract.round(3)}" - end - elsif min_key == 'mid' - # @todo see what is going wrong with after mid in formula - if min_value == 0 - time = 'mid' - # converted to variable for simplicity but could also be described like this - # time = "hoo_start + occ * 0.5" - elsif min_value < 0 - time = "mid + occ * #{min_value_occ_fract.round(3)}" - else # greater than 0 - time = "mid - occ * #{min_value_occ_fract.round(3)}" - end - else # min_key == "end" - if min_value == 0 - time = 'hoo_end' - elsif min_value < 0 - time = "hoo_end + vac * #{min_value_vac_fract.round(3)}" - else # greater than 0 - time = "hoo_end - occ * #{min_value_occ_fract.round(3)}" - end - end - - end - - end - - # populate string - if value_array_var.size == 1 - raw_string << "#{time} ~ #{value_array_var.first}" - else # should only have 1 or two values (value in and optional value out) - raw_string << "#{time} ~ #{value_array_var.first} ~ #{value_array_var.last}" - end - end - - # store profile formula with hoo and value variables - props.setFeature('param_day_profile', raw_string.join(' | ')) - - # @todo not used yet, but will add methods described below and others - # @todo lower infiltration based on air loop hours of operation if air loop has outdoor air object - # @todo lower lighting or plug loads based on occupancy at given time steps in a space - # @todo set elevator fraction based multiple factors such as trips, occupants per trip, and elevator type to determine floor consumption when not in use. - props.setFeature('param_day_secondary_logic', '') # secondary logic method such as occupancy impacting schedule values - props.setFeature('param_day_secondary_logic_arg_val', '') # optional argument used for some secondary logic applied to values - - # tag profile type - # may be useful for parametric changes to tag typical, medium, minimal, or same ones with off_peak prefix - # todo - I would like to use these same tags for hours of operation and have parametric tags then ignore the days of week and date range from the rule object - # tagging min/max makes sense in fractional schedules but not temperature schedules like thermostats (specifically cooling setpoints) - # todo - I think these tags should come from occpancy schedule for space(s) schedule. That way all schedules in a space will refer to same profile from hours of operation - # todo - add school specific logic hear or in post processing, currently default profile for school may not be most prevalent one - if current_rule_index == -1 - props.setFeature('param_day_tag', 'typical_operation') - elsif daily_flh == daily_flhs.min - props.setFeature('param_day_tag', 'minimal_operation') - elsif daily_flh == daily_flhs.max - props.setFeature('param_day_tag', 'maximum_operation') # normally this should not be used as typical should be the most active day - else - props.setFeature('param_day_tag', 'medium_operation') # not min max or typical - end - end - - return parametric_inputs - end - # Retrieves the lowest story in a model # # @param model [OpenStudio::Model::Model] OpenStudio model object # @return [OpenStudio::Model::BuildingStory] Lowest story included in the model def find_lowest_story(model) min_z_story = 1E+10 lowest_story = nil model.getSpaces.sort.each do |space| story = space.buildingStory.get lowest_story = story if lowest_story.nil? - space_min_z = building_story_minimum_z_value(story) + space_min_z = OpenstudioStandards::Geometry.building_story_get_minimum_height(story) if space_min_z < min_z_story min_z_story = space_min_z lowest_story = story end end return lowest_story end - # Utility function that returns the min and max value in a design day schedule. - # - # @todo move this to Standards.Schedule.rb - # @param schedule [OpenStudio::Model::Schedule] can be ScheduleCompact, ScheduleRuleset, ScheduleConstant - # @param type [String] 'heating' for winter design day, 'cooling' for summer design day - # @return [Hash] Hash has two keys, min and max. if failed, return 999.9 for min and max. - def search_min_max_value_from_design_day_schedule(schedule, type = 'winter') - if schedule.is_initialized - schedule = schedule.get - if schedule.to_ScheduleRuleset.is_initialized - schedule = schedule.to_ScheduleRuleset.get - setpoint_min_max = schedule_ruleset_design_day_min_max_value(schedule, type) - elsif schedule.to_ScheduleConstant.is_initialized - schedule = schedule.to_ScheduleConstant.get - # for constant schedule, there is only one value, so the annual should be equal to design condition. - setpoint_min_max = schedule_constant_annual_min_max_value(schedule) - elsif schedule.to_ScheduleCompact.is_initialized - schedule = schedule.to_ScheduleCompact.get - setpoint_min_max = schedule_compact_design_day_min_max_value(schedule, type) - end - return setpoint_min_max - end - OpenStudio.logFree(OpenStudio::Error, 'openstudio::standards::Schedule', 'Schedule is not exist, or wrong type of schedule (not Ruleset, Compact or Constant), or cannot found the design day schedules. Return 999.9 for min and max') - return { 'min' => 999.9, 'max' => 999.9 } - end - # Identifies non mechanically cooled ("nmc") systems, if applicable # # @param model [OpenStudio::Model::Model] OpenStudio model object # @return [Hash] Zone to nmc system type mapping def model_identify_non_mechanically_cooled_systems(model) @@ -7176,10 +5913,10 @@ # @return [Boolean] returns true if successful, false if not def model_identify_return_air_type(model) # air-loop based system model.getThermalZones.each do |zone| # Conditioning category won't include indirectly conditioned thermal zones - cond_cat = thermal_zone_conditioning_category(zone, model_standards_climate_zone(model)) + cond_cat = thermal_zone_conditioning_category(zone, OpenstudioStandards::Weather.model_get_climate_zone(model)) # Initialize the return air type return_air_type = nil # The thermal zone is conditioned by zonal system