lib/openstudio-standards/btap/fileio.rb in openstudio-standards-0.2.9 vs lib/openstudio-standards/btap/fileio.rb in openstudio-standards-0.2.10.rc1
- old
+ new
@@ -841,9 +841,358 @@
end
return diffs
end
+ # This method converts an idf object to a hash
+ #
+ # @author Padmassun Rajakareyar
+ # @param obj [Openstudio::ModelObject]
+ # @return new_obj_hash [Hash] idf object converted to hash.
+ # @example
+ # Converts the following IDF (openstudio::ModelObject) to
+ # ==================================================================
+ # OS:Material,
+ # {8adb3faa-8e6a-48e3-bd73-ba6a02154b02}, !- Handle
+ # 1/2IN Gypsum, !- Name
+ # Smooth, !- Roughness
+ # 0.0127, !- Thickness {m}
+ # 0.16, !- Conductivity {W/m-K}
+ # 784.9, !- Density {kg/m3}
+ # 830.000000000001, !- Specific Heat {J/kg-K}
+ # 0.9, !- Thermal Absorptance
+ # 0.4, !- Solar Absorptance
+ # 0.4; !- Visible Absorptance
+ # ===================================================================
+ #
+ # ===================================================================
+ # {
+ # "Handle": "{8adb3faa-8e6a-48e3-bd73-ba6a02154b02}",
+ # "Name": "1/2IN Gypsum",
+ # "Roughness": "Smooth",
+ # "Thickness {m}": "0.0127",
+ # "Conductivity {W/m-K}": "0.16",
+ # "Density {kg/m3}": "784.9",
+ # "Specific Heat {J/kg-K}": "830.000000000001",
+ # "Thermal Absorptance": "0.9",
+ # "Solar Absorptance": "0.4",
+ # "Visible Absorptance": "0.4"
+ # },
+ # ===================================================================
+ def self.idf_to_h(obj)
+ # split idf object by line
+ obj_string = obj.to_s.split("\n")
+ new_obj_hash = {}
+
+ # itterate through each line and split the value and field
+ # and assign it to the hash
+ obj_string.each_with_index {|line,i|
+ next if i == 0
+ line.gsub!(/(\,|\;)/, '') # remove commas and semi-colons
+ line.strip! # remove whitespace at the end and the beginning of the string
+ v,k = line.split(/\s*\!\-\s+/) # split the line into at the string '!-' including the spaces before and after
+ new_obj_hash[k] = v
+ }
+ new_obj_hash
+ end
+
+ # This method uses idf_to_h(obj) method, but deletes the fields named 'Handle' and 'Name'
+ #
+ # @author Padmassun Rajakareyar
+ # @param obj [Openstudio::ModelObject]
+ # @return new_obj_hash [Hash] idf object converted to hash.
+ # @example
+ # Converts the following IDF (openstudio::ModelObject) to
+ # ==================================================================
+ # OS:Material,
+ # {8adb3faa-8e6a-48e3-bd73-ba6a02154b02}, !- Handle
+ # 1/2IN Gypsum, !- Name
+ # Smooth, !- Roughness
+ # 0.0127, !- Thickness {m}
+ # 0.16, !- Conductivity {W/m-K}
+ # 784.9, !- Density {kg/m3}
+ # 830.000000000001, !- Specific Heat {J/kg-K}
+ # 0.9, !- Thermal Absorptance
+ # 0.4, !- Solar Absorptance
+ # 0.4; !- Visible Absorptance
+ # ===================================================================
+ #
+ # ===================================================================
+ # {
+ # "Roughness": "Smooth",
+ # "Thickness {m}": "0.0127",
+ # "Conductivity {W/m-K}": "0.16",
+ # "Density {kg/m3}": "784.9",
+ # "Specific Heat {J/kg-K}": "830.000000000001",
+ # "Thermal Absorptance": "0.9",
+ # "Solar Absorptance": "0.4",
+ # "Visible Absorptance": "0.4"
+ # },
+ # ===================================================================
+ def self.idf_to_h_clean(obj)
+ # converts the idf object to hash
+ idf_hash = idf_to_h(obj)
+
+ # remove the field named `Handle` and `Name` from the idf_hash
+ idf_hash.delete("Handle") if idf_hash.key?("Handle")
+ idf_hash.delete("Handle".to_sym) if idf_hash.key?("Handle".to_sym)
+
+ idf_hash.delete("Name") if idf_hash.key?("Name")
+ idf_hash.delete("Name".to_sym) if idf_hash.key?("Name".to_sym)
+
+ # Loop through idf_hash and delete any field that matched the UUID regex
+ # idf_hash.each {|k,v|
+ # idf_hash.delete(k) if /^\{[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\}$/.match(v)
+ # }
+ # idf_hash
+ end
+
+ # This method will group similar objects under a key that has all the
+ # values of an IDF object which excludes Handles and Names.
+ # NOTE: The objexts grouped should have the fields in the same order.
+ # If not, then it would not be consired as a duplicate.
+ #
+ # @author Padmassun Rajakareyar
+ # @param obj_array [Array] An array of idf_hash. Each idf_hash was created by idf_to_h_clean or idf_to_h method
+ # @return grouped_objs [Hash] grouped idf_hash based on values within the obj_array.
+ # @example Output:
+ # "[\"Smooth\", \"0.216479986995276\", \"0.9\", \"0.7\", \"0.8\"]": [
+ # {
+ # "Handle": "{a7c45cf6-166d-48a6-9750-efb5c3386d91}",
+ # "Name": "Typical Carpet Pad",
+ # "Roughness": "Smooth",
+ # "Thermal Resistance {m2-K/W}": "0.216479986995276",
+ # "Thermal Absorptance": "0.9",
+ # "Solar Absorptance": "0.7",
+ # "Visible Absorptance": "0.8"
+ # },
+ # {
+ # "Handle": "{9873fb84-bfaf-459d-8b70-2c3f5722bda9}",
+ # "Name": "Typical Carpet Pad 1",
+ # "Roughness": "Smooth",
+ # "Thermal Resistance {m2-K/W}": "0.216479986995276",
+ # "Thermal Absorptance": "0.9",
+ # "Solar Absorptance": "0.7",
+ # "Visible Absorptance": "0.8"
+ # },
+ # {
+ # "Handle": "{63a12315-1de4-453a-b154-e6e6e9871be2}",
+ # "Name": "Typical Carpet Pad 4",
+ # "Roughness": "Smooth",
+ # "Thermal Resistance {m2-K/W}": "0.216479986995276",
+ # "Thermal Absorptance": "0.9",
+ # "Solar Absorptance": "0.7",
+ # "Visible Absorptance": "0.8"
+ # }
+ # ],
+ def self.group_similar_objects(obj_array)
+ # group [objects] by values except Handles and Name
+ grouped_objs = obj_array.group_by{ |idf_hash|
+ out = []
+ # skip Handle and Name keys
+ #
+ # ideally the `keys` of the `idf_hash` should be sorted,
+ # but I'll leave it alone for now
+ idf_hash.each {|key, val|
+ next if key == "Handle"
+ next if key == "Name"
+ out << val # push all the values into an array. This becomes the key of the hash that contains the duplicate objects
+ }
+ out
+ }
+
+ # Sort the grouped [objects] by Name.
+ # This is doen such that the first object will always have the smaller name
+ grouped_objs.each {|key, dup_array|
+ dup_array.sort_by{|idf_hash|
+ # puts idf_hash
+ idf_hash['Name'] # Sort by Name
+ }
+ }
+ return grouped_objs
+ end
+
+ # Replace the UUID of the duplicate material, unless it contains ", !- Handle"
+ # This is done so after the model has been written to the disk, It can be read and
+ # the duplicate materials can be removed safely.
+ #
+ # @author Padmassun Rajakareyar
+ # @param model [Openstudio::Model]
+ # @param grouped_objs [Hash] Output provided by group_similar_objects()
+ # @return model_string [String] An Openstudio::Model object converted to a string
+ def self.replace_duplicate_obj_handles(model, grouped_objs)
+ model_string = model.to_s # convert the OS:Model into a String
+ grouped_objs.each {|key, dup_array|
+ dup_array.each_with_index {|idf_hash, index |
+ next if index == 0 # skipping index 0, because it has the shortest name and considered as the original
+
+ # givn that the idf_hash['Handle'] => '{8c88931b-e19d-479b-ac71-138d18c97cc9}'
+ # The following regex matches "{8c88931b-e19d-479b-ac71-138d18c97cc9}" in the following line
+ # {8c88931b-e19d-479b-ac71-138d18c97cc9}, !- Layer 1
+ #
+ # but matches nothing if the line has the keyword '!- Handle' in it e.g.
+ # {8c88931b-e19d-479b-ac71-138d18c97cc9}, !- Handle
+ replace_regex = idf_hash['Handle'].to_s.gsub('{', '\{'). # escape brackets
+ gsub('}', '\}'). # escape brackets
+ gsub('-', '\-') + # escape dashes
+ '(?!.*?\!\-(\s)*Handle)' # making sure the matched handle is not part of the line that contains the substring '!- Handle'
+ replace_regex = Regexp.new(replace_regex)
+ # p replace_regex
+
+ # replace duplicate handles with the handle found at index 0
+ model_string.gsub!(replace_regex, dup_array[0]['Handle'])# {|match| puts match; dup_array[0]['Handle']}
+ }
+ }
+ return model_string
+ end
+
+ # This method gets the model string, writes it in a temporary location, reads it, and
+ # converts it to an OpenStudio Model using a VersionTranslater
+ #
+ # @author Padmassun Rajakareyar
+ # @param model_string [String] An Openstudio::Model object converted to a string
+ # @return model [Openstudio::Model]
+ def self.get_OS_Model_from_string(model_string)
+ require 'securerandom'
+ require 'fileutils'
+ # make temerorary directory called `temp` to store the osm file
+ FileUtils.mkdir_p(File.join('.', 'temp'))
+ # Using SecureRandom to generate the UUID to keep the osm file unique
+ temp_filename =File.join('.', 'temp',SecureRandom.uuid.to_s + '.osm')
+ # write the model string as a file
+ File.open(temp_filename, 'w') { |file| file.write(model_string) }
+ # use a VersionTranslator to read the osm model
+ translator = OpenStudio::OSVersion::VersionTranslator.new
+ path = OpenStudio::Path.new(temp_filename)
+ # read the model
+ model = translator.loadModel(path)
+ model = model.get
+ # remove the temporary model
+ FileUtils.rm(temp_filename)
+ return model
+ end
+
+ # Eleminates duplicate ModelObjects of the given ModelObject type,
+ # based on the values of each fields within the ModelObject
+ #
+ # @author Padmassun Rajakareyar
+ # @param model [Openstudio::Model]
+ # @param model_obj_type [String] ModelObject type e.g. "OS:Material"
+ # @return new_model [Openstudio::Model] Returns new model (OS:Model) if the changes were made.
+ # or else returns the old model if no changes were made.
+ def self.eleminate_duplicate_objs(model, model_obj_type) # = "OS:Material")
+ model_objs_json = {}
+
+ # convert each of the ModelObjectas a hash for easy parsing
+ model.getModelObjects.sort.each {|obj|
+ # hsh = idf_to_h_clean(obj) # stores the idf converted to hash, without UUIDs and Name field
+
+ # create a hash containing all the model objects sorted by the name of the model object.
+ # e.g the `OS:Construction` ModelObject type will e placed within the `OS:Construction` key, and
+ # each ModelObject has been converted to an idf_hash and pushed into the array with the appropriate key.
+ (model_objs_json[obj.iddObject.name.to_s] ||= []) << idf_to_h(obj) # unless hsh.empty?
+ }
+
+ # isolate a single ModelObject type specified by the model_obj_type variable
+ mat_array = model_objs_json[model_obj_type]
+ if mat_array.nil? # return the old model if model_obj_type is not found
+ puts "Skipping because ModelObject of type [#{model_obj_type}] was not found"
+ return model
+ end
+ # group duplicates
+ grouped_objs = group_similar_objects(mat_array)
+ # replace handles of duplicate objects
+ model_string = replace_duplicate_obj_handles(model, grouped_objs)
+ # write the model string to a file and read it as a model
+ new_model = get_OS_Model_from_string(model_string)
+
+ # Now loop through each of the grouped objects. skip the first one, and get ModelObjects (from new osm file)
+ # by name and safely remove all the duplicates
+ grouped_objs.each {|key, dup_array|
+ dup_array.each_with_index {|object, index |
+ next if index == 0
+ unless object.key?('Name') # if the idf_hash does not have a key called Name, Skip it.
+ puts "Skipping ModelObject of type [#{model_obj_type}] With data [#{object.inspect}] does not have a field called 'Name'"
+ next
+ end
+ name = object['Name']
+ # puts "object: [#{object}]"
+ # puts "object['Name']: [#{object['Name']}]"
+ # get the object to delete by name
+ obj_to_delete = new_model.getModelObjectByName(name)
+ if obj_to_delete.empty? # check if the object to be deleted is initialized (or present within the new model)
+ puts "ModelObject of type [#{model_obj_type}] with name [#{object['Name']}] does not exist in new model"
+ else
+ puts "ModelObject of type [#{model_obj_type}] with name [#{object['Name']}] was deleted"
+ obj_to_delete = obj_to_delete.get # get the modelObject if it is initialized
+ obj_to_delete.remove # remove object form the model
+ end
+ }
+ }
+ # File.open('./models/after.osm', 'w') { |file| file.write(new_model.to_s) }
+
+
+ # File.open("./models/grp_#{model_obj_type}.json", 'w') { |file| file.write(JSON.pretty_generate(grouped_objs)) }
+
+ return new_model
+ # File.open('./models/after.osm', 'w') { |file| file.write(model_string) }
+ # puts("\n\n\n" + "=="*10 + "\n\n")
+ # puts JSON.pretty_generate(grouped_mats)
+ end
+
+ # Eleminates duplicate Materials and Construction Objects
+ # The list of ModelObjects that are removed are:
+ # "OS:Material",
+ # "OS:Material:NoMass"
+ # "OS:WindowMaterial:SimpleGlazingSystem"
+ # "OS:WindowMaterial:Glazing"
+ # "OS:WindowMaterial:Gas"
+ # "OS:StandardsInformation:Material"
+ # "OS:Construction"
+ # "OS:DefaultSurfaceConstructions"
+ # "OS:DefaultSubSurfaceConstructions"
+ # "OS:DefaultConstructionSet"
+ # "OS:StandardsInformation:Construction"
+ #
+ # @author Padmassun Rajakareyar
+ # @param model [Openstudio::Model]
+ # @return new_model [Openstudio::Model] Returns new model after removing the
+ # duplicate ModelObjects atated abioe.
+ def self.remove_duplicate_materials_and_constructions(model)
+ old_number_of_objects = model.getModelObjects.length
+ new_model = model
+ # eleminate dplicate Material objects
+ obj_types = [
+ "OS:Material",
+ "OS:Material:NoMass",
+ "OS:WindowMaterial:SimpleGlazingSystem",
+ "OS:WindowMaterial:Glazing",
+ "OS:WindowMaterial:Gas",
+ "OS:StandardsInformation:Material"
+ ]
+
+ obj_types.each {|model_obj_type|
+ new_model = eleminate_duplicate_objs(new_model, model_obj_type)
+ }
+
+ # eleminate dplicate Construction objects
+ obj_types = [
+ "OS:Construction",
+ "OS:DefaultSurfaceConstructions",
+ "OS:DefaultSubSurfaceConstructions",
+ "OS:DefaultConstructionSet",
+ "OS:StandardsInformation:Construction",
+ ]
+ obj_types.each {|model_obj_type|
+ new_model = eleminate_duplicate_objs(new_model, model_obj_type)
+ }
+
+ new_number_of_objects = new_model.getModelObjects.length
+
+ puts "Number of objects removed: #{old_number_of_objects - new_number_of_objects}"
+ return new_model
+ end
+
end #FileIO
\ No newline at end of file