From 83ff665bb354e1751708923349c1e0cd6ad5aaad Mon Sep 17 00:00:00 2001 From: Weili Xu Date: Fri, 4 Oct 2024 14:03:59 -0700 Subject: [PATCH 1/2] include ruleset functions --- .../standards/Standards.Model.rb | 20 +- .../utilities/ruleset_check.rb | 231 +++++++++--------- 2 files changed, 132 insertions(+), 119 deletions(-) diff --git a/lib/openstudio-standards/standards/Standards.Model.rb b/lib/openstudio-standards/standards/Standards.Model.rb index 9aa7de687..c444779fc 100644 --- a/lib/openstudio-standards/standards/Standards.Model.rb +++ b/lib/openstudio-standards/standards/Standards.Model.rb @@ -102,6 +102,9 @@ def model_create_prm_any_baseline_building(user_model, building_type, climate_zo # Check proposed model unmet load hours if unmet_load_hours_check + # Set proposed model export data in json format + OpenStudioStandards::RulesetChecking.export_json_output(proposed_model) + # 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 = OpenstudioStandards::SqlFile.model_get_annual_occupied_unmet_hours(proposed_model) @@ -128,8 +131,17 @@ def model_create_prm_any_baseline_building(user_model, building_type, climate_zo proposed_model.save(OpenStudio::Path.new("#{sizing_run_dir}/proposed_final.osm"), true) forward_translator = OpenStudio::EnergyPlus::ForwardTranslator.new idf = forward_translator.translateModel(proposed_model) + + proposed_model.getSpaces.sort.each do |space| + space_cond_type = space_conditioning_category(space) + next if space_cond_type == 'Unconditioned' + OpenStudioStandards::RulesetChecking.tag_spaces(idf, space) + end idf_path = OpenStudio::Path.new("#{sizing_run_dir}/proposed_final.idf") idf.save(idf_path, true) + + # export to epjson + OpenStudioStandards::RulesetChecking.export_epjson(proposed_model, sizing_run_dir, 'proposed_final') end # Define different orientation from original orientation @@ -494,15 +506,21 @@ def model_create_prm_any_baseline_building(user_model, building_type, climate_zo # @todo: turn off self shading # Set Solar Distribution to MinimalShadowing... problem is when you also have detached shading such as surrounding buildings etc # It won't be taken into account, while it should: only self shading from the building itself should be turned off but to my knowledge there isn't a way to do this in E+ - model_status = degs > 0 ? "baseline_final_#{degs}" : 'baseline_final' + OpenStudioStandards::RulesetChecking.export_json_output(model) model.save(OpenStudio::Path.new("#{sizing_run_dir}/#{model_status}.osm"), true) # Translate to IDF and save for debugging forward_translator = OpenStudio::EnergyPlus::ForwardTranslator.new idf = forward_translator.translateModel(model) + model.getSpaces.sort.each do |space| + space_cond_type = space_conditioning_category(space) + next if space_cond_type == 'Unconditioned' + OpenStudioStandards::RulesetChecking.tag_spaces(idf, space) + end idf_path = OpenStudio::Path.new("#{sizing_run_dir}/#{model_status}.idf") idf.save(idf_path, true) + OpenStudioStandards::RulesetChecking.export_epjson(model, sizing_run_dir, "#{model_status}") # Check unmet load hours if unmet_load_hours_check diff --git a/lib/openstudio-standards/utilities/ruleset_check.rb b/lib/openstudio-standards/utilities/ruleset_check.rb index 6b8c75252..3df14e09f 100644 --- a/lib/openstudio-standards/utilities/ruleset_check.rb +++ b/lib/openstudio-standards/utilities/ruleset_check.rb @@ -1,5 +1,88 @@ module OpenStudioStandards module RulesetChecking + # building type hash map + BUILDING_MAP = { + 'All others' => { + 'lighting' => 'OFFICE_OPEN_PLAN', + 'ventilation' => 'OFFICE_BUILDINGS_OFFICE_SPACE', + 'shw' => 'ALL_OTHERS' + }, + 'Grocery store' => { + 'lighting' => 'SALES_AREA', + 'ventilation' => 'RETAIL_SUPERMARKET', + 'shw' => 'ALL_OTHERS' + }, + 'Healthcare (outpatient)' => { + 'lighting' => 'HEALTHCARE_FACILITY_PATIENT_ROOM', + 'ventilation' => 'OUTPATIENT_HEALTH_CARE_FACILITIES_URGENT_CARE_EXAMINATION_ROOM', + 'shw' => 'HEALTH_CARE_CLINIC' + }, + 'Hospital' => { + 'lighting' => 'HEALTHCARE_FACILITY_PATIENT_ROOM', + 'ventilation' => 'OUTPATIENT_HEALTH_CARE_FACILITIES_GENERAL_EXAMINATION_ROOM', + 'shw' => 'HOSPITAL_AND_OUTPATIENT_SURGERY' + }, + 'Hotel/motel <= 75 rooms' => { + 'lighting' => 'GUEST_ROOM', + 'ventilation' => 'HOTELS_MOTELS_RESORTS_DORMITORIES_BEDROOM_LIVING_ROOM', + 'shw' => 'HOTEL' + }, + 'Hotel/motel > 75 rooms' => { + 'lighting' => 'GUEST_ROOM', + 'ventilation' => 'HOTELS_MOTELS_RESORTS_DORMITORIES_BEDROOM_LIVING_ROOM', + 'shw' => 'HOTEL' + }, + 'Office 5,000 to 50,000 sq ft' => { + 'lighting' => 'OFFICE_OPEN_PLAN', + 'ventilation' => 'OFFICE_BUILDINGS_OFFICE_SPACE', + 'shw' => 'OFFICE' + }, + 'Office <= 5,000 sq ft' => { + 'lighting' => 'OFFICE_OPEN_PLAN', + 'ventilation' => 'OFFICE_BUILDINGS_OFFICE_SPACE', + 'shw' => 'OFFICE' + }, + 'Office > 50,000 sq ft' => { + 'lighting' => 'OFFICE_OPEN_PLAN', + 'ventilation' => 'OFFICE_BUILDINGS_OFFICE_SPACE', + 'shw' => 'OFFICE' + }, + 'Restaurant (full service)' => { + 'lighting' => 'DINING_AREA_ALL_OTHERS', + 'ventilation' => 'FOOD_AND_BEVERAGE_SERVICE_RESTAURANT_DINING_ROOMS', + 'shw' => 'DINING_FAMILY' + }, + 'Restaurant (quick service)' => { + 'lighting' => 'DINING_AREA_CAFETERIA_OR_FAST_FOOD_DINING', + 'ventilation' => 'FOOD_AND_BEVERAGE_SERVICE_CAFETERIA_FAST_FOOD_DINING', + 'shw' => 'DINING_BAR_LOUNGE_LEISURE' + }, + 'Retail (stand alone)' => { + 'lighting' => 'RETAIL_FACILITIES_DRESSING_FITTING_ROOM', + 'ventilation' => 'RETAIL_SALES_EXCEPT_OTHER_SPECIFIC_RETAIL', + 'shw' => 'shw_type_retail_stand_alone' + }, + 'Retail (strip mall)' => { + 'lighting' => 'RETAIL_FACILITIES_MALL_CONCOURSE', + 'ventilation' => 'RETAIL_MALL_COMMON_AREAS', + 'shw' => 'RETAIL' + }, + 'School (primary)' => { + 'lighting' => 'CLASSROOM_LECTURE_HALL_TRAINING_ROOM_SCHOOL', + 'ventilation' => 'EDUCATIONAL_FACILITIES_CLASSROOMS_AGES_5_TO_8', + 'shw' => 'SCHOOL_UNIVERSITY' + }, + 'School (secondary and university)' => { + 'lighting' => 'CLASSROOM_LECTURE_HALL_TRAINING_ROOM_SCHOOL', + 'ventilation' => 'EDUCATIONAL_FACILITIES_CLASSROOMS_AGE_9_PLUS', + 'shw' => 'SCHOOL_UNIVERSITY' + }, + 'Warehouse (nonrefrigerated)' => { + 'lighting' => 'WAREHOUSE_STORAGE_AREA_MEDIUM_TO_BULKY_PALLETIZED_ITEMS', + 'ventilation' => 'MISCELLANEOUS_SPACES_WAREHOUSES', + 'shw' => 'WAREHOUSE' + } + } # Export OpenStudio model to epJson file. # # @param model [OpenStudio::Model::Model] @@ -11,7 +94,7 @@ def self.export_epjson(model, save_dir, model_name) workspace = forward_translator.translateModel(model) workspace_epjson_str = OpenStudio::EPJSON::toJSONString(workspace) begin - output_path = "#{save_dir}/#{model_name}.json" + output_path = "#{save_dir}/#{model_name}.epjson" epjson_model = File.open(output_path, 'w') epjson_model.puts(workspace_epjson_str) rescue StandardError => e @@ -28,108 +111,21 @@ def self.export_epjson(model, save_dir, model_name) # Which requires the import of IDF model and OpenStudio model # # @param idf_model [OpenStudio::IDF] - # @param model [OpenStudio::Model::Model] + # @param space [OpenStudio::Model::Space] # @return [Boolean] - def self.tag_spaces(idf_model, model) - building_map = { - 'All others' => { - 'lighting' => 'OFFICE_OPEN_PLAN', - 'ventilation' => 'OFFICE_BUILDINGS_OFFICE_SPACE', - 'shw' => 'ALL_OTHERS' - }, - 'Grocery store' => { - 'lighting' => 'SALES_AREA', - 'ventilation' => 'RETAIL_SUPERMARKET', - 'shw' => 'ALL_OTHERS' - }, - 'Healthcare (outpatient)' => { - 'lighting' => 'HEALTHCARE_FACILITY_PATIENT_ROOM', - 'ventilation' => 'OUTPATIENT_HEALTH_CARE_FACILITIES_URGENT_CARE_EXAMINATION_ROOM', - 'shw' => 'HEALTH_CARE_CLINIC' - }, - 'Hospital' => { - 'lighting' => 'HEALTHCARE_FACILITY_PATIENT_ROOM', - 'ventilation' => 'OUTPATIENT_HEALTH_CARE_FACILITIES_GENERAL_EXAMINATION_ROOM', - 'shw' => 'HOSPITAL_AND_OUTPATIENT_SURGERY' - }, - 'Hotel/motel <= 75 rooms' => { - 'lighting' => 'GUEST_ROOM', - 'ventilation' => 'HOTELS_MOTELS_RESORTS_DORMITORIES_BEDROOM_LIVING_ROOM', - 'shw' => 'HOTEL' - }, - 'Hotel/motel > 75 rooms' => { - 'lighting' => 'GUEST_ROOM', - 'ventilation' => 'HOTELS_MOTELS_RESORTS_DORMITORIES_BEDROOM_LIVING_ROOM', - 'shw' => 'HOTEL' - }, - 'Office 5,000 to 50,000 sq ft' => { - 'lighting' => 'OFFICE_OPEN_PLAN', - 'ventilation' => 'OFFICE_BUILDINGS_OFFICE_SPACE', - 'shw' => 'OFFICE' - }, - 'Office <= 5,000 sq ft' => { - 'lighting' => 'OFFICE_OPEN_PLAN', - 'ventilation' => 'OFFICE_BUILDINGS_OFFICE_SPACE', - 'shw' => 'OFFICE' - }, - 'Office > 50,000 sq ft' => { - 'lighting' => 'OFFICE_OPEN_PLAN', - 'ventilation' => 'OFFICE_BUILDINGS_OFFICE_SPACE', - 'shw' => 'OFFICE' - }, - 'Restaurant (full service)' => { - 'lighting' => 'DINING_AREA_ALL_OTHERS', - 'ventilation' => 'FOOD_AND_BEVERAGE_SERVICE_RESTAURANT_DINING_ROOMS', - 'shw' => 'DINING_FAMILY' - }, - 'Restaurant (quick service)' => { - 'lighting' => 'DINING_AREA_CAFETERIA_OR_FAST_FOOD_DINING', - 'ventilation' => 'FOOD_AND_BEVERAGE_SERVICE_CAFETERIA_FAST_FOOD_DINING', - 'shw' => 'DINING_BAR_LOUNGE_LEISURE' - }, - 'Retail (stand alone)' => { - 'lighting' => 'RETAIL_FACILITIES_DRESSING_FITTING_ROOM', - 'ventilation' => 'RETAIL_SALES_EXCEPT_OTHER_SPECIFIC_RETAIL', - 'shw' => 'shw_type_retail_stand_alone' - }, - 'Retail (strip mall)' => { - 'lighting' => 'RETAIL_FACILITIES_MALL_CONCOURSE', - 'ventilation' => 'RETAIL_MALL_COMMON_AREAS', - 'shw' => 'RETAIL' - }, - 'School (primary)' => { - 'lighting' => 'CLASSROOM_LECTURE_HALL_TRAINING_ROOM_SCHOOL', - 'ventilation' => 'EDUCATIONAL_FACILITIES_CLASSROOMS_AGES_5_TO_8', - 'shw' => 'SCHOOL_UNIVERSITY' - }, - 'School (secondary and university)' => { - 'lighting' => 'CLASSROOM_LECTURE_HALL_TRAINING_ROOM_SCHOOL', - 'ventilation' => 'EDUCATIONAL_FACILITIES_CLASSROOMS_AGE_9_PLUS', - 'shw' => 'SCHOOL_UNIVERSITY' - }, - 'Warehouse (nonrefrigerated)' => { - 'lighting' => 'WAREHOUSE_STORAGE_AREA_MEDIUM_TO_BULKY_PALLETIZED_ITEMS', - 'ventilation' => 'MISCELLANEOUS_SPACES_WAREHOUSES', - 'shw' => 'WAREHOUSE' - } - } - model.getSpaces.sort.each do |space| - space_cond_type = space_conditioning_category(space) - next if space_cond_type == 'Unconditioned' - - # all spaces from PRMs shall have the additional properties of building_type_for_wwr. - # In this implementation, we will use this data to populate the rest of space types. - bldg_type_wwr = get_additional_property_as_string(space, 'building_type_for_wwr', 'All others') - bldg_types = building_map[bldg_type_wwr] - idf_space = idf_model.getObjectsByName(space.name.to_s)[0] - space_tag_index = find_index_of_string(idf_space, 'Space Type') - tag_1_index = space_tag_index + 1 - tag_2_index = space_tag_index + 2 + def self.tag_spaces(idf_model, space) + # all spaces from PRMs shall have the additional properties of building_type_for_wwr. + # In this implementation, we will use this data to populate the rest of space types. + bldg_type_wwr = get_additional_property_as_string(space, 'building_type_for_wwr', 'All others') + bldg_types = BUILDING_MAP[bldg_type_wwr] + idf_space = idf_model.getObjectsByName(space.name.to_s)[0] + space_tag_index = OpenStudioStandards::RulesetChecking::find_index_of_string(idf_space, 'Space Type') + tag_1_index = space_tag_index + 1 + tag_2_index = space_tag_index + 2 - idf_space.setString(space_tag_index, bldg_types['lighting']) - idf_space.setString(tag_1_index, bldg_types['ventilation']) - idf_space.setString(tag_2_index, bldg_types['shw']) - end + idf_space.setString(space_tag_index, bldg_types['lighting']) + idf_space.setString(tag_1_index, bldg_types['ventilation']) + idf_space.setString(tag_2_index, bldg_types['shw']) end # Revise the output variables to prepare data export for RPD generation @@ -170,28 +166,27 @@ def self.export_json_output(model) output_variable.setReportingFrequency('hourly') output_variable.setKeyValue('*') end - end - # Find the index of the E+ object which has a commented line with the search string - # (e.g. find the index of "!- Fan Inlet Node Name") - # @param idf_object - # @param search_string str - # @return index if found - def self.find_index_of_string(idf_object, search_string) + # Find the index of the E+ object which has a commented line with the search string + # (e.g. find the index of "!- Fan Inlet Node Name") + # @param idf_object + # @param search_string str + # @return index if found + def self.find_index_of_string(idf_object, search_string) - split_object = idf_object.to_s.split("\n") + split_object = idf_object.to_s.split("\n") - index_counter = 0 + index_counter = 0 - split_object.each do |line| - if line.include? search_string - # subtract 1 because the first line is the object type, not a index - return index_counter - 1 - else - index_counter += 1 + split_object.each do |line| + if line.include? search_string + # subtract 1 because the first line is the object type, not a index + return index_counter - 1 + else + index_counter += 1 + end end + raise "Search string: '#{search_string}' not found in #{idf_object.to_s}" end - - raise "Search string: '#{search_string}' not found in #{idf_object.to_s}" end end From 987d92652363d1d5477a06a29c2027f2457dbe41 Mon Sep 17 00:00:00 2001 From: Weili Xu Date: Tue, 15 Oct 2024 09:48:57 -0700 Subject: [PATCH 2/2] Fix bugs for space objects that have no space type field. --- .../utilities/ruleset_check.rb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/openstudio-standards/utilities/ruleset_check.rb b/lib/openstudio-standards/utilities/ruleset_check.rb index 3df14e09f..32a091d11 100644 --- a/lib/openstudio-standards/utilities/ruleset_check.rb +++ b/lib/openstudio-standards/utilities/ruleset_check.rb @@ -167,7 +167,19 @@ def self.export_json_output(model) output_variable.setKeyValue('*') end + # Count the number of lines of a idf object and return the last index + # (e.g. find the index of "!- Fan Inlet Node Name") + # @param idf_object + # @param search_string str + # @return index if found + def self.find_last_index_of_idf_object(idf_object) + split_object = idf_object.to_s.split("\n") + # remove the object title. + split_object.length - 1 + end + # Find the index of the E+ object which has a commented line with the search string + # If the index is not found, this function returns the last index of the object + 1 # (e.g. find the index of "!- Fan Inlet Node Name") # @param idf_object # @param search_string str @@ -186,7 +198,7 @@ def self.find_index_of_string(idf_object, search_string) index_counter += 1 end end - raise "Search string: '#{search_string}' not found in #{idf_object.to_s}" + find_last_index_of_idf_object(idf_object) end end end