# ********************************************************************* # * Copyright (c) 2008-2015, Natural Resources Canada # * All rights reserved. # * # * This library is free software; you can redistribute it and/or # * modify it under the terms of the GNU Lesser General Public # * License as published by the Free Software Foundation; either # * version 2.1 of the License, or (at your option) any later version. # * # * This library is distributed in the hope that it will be useful, # * but WITHOUT ANY WARRANTY; without even the implied warranty of # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # * Lesser General Public License for more details. # * # * You should have received a copy of the GNU Lesser General Public # * License along with this library; if not, write to the Free Software # * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # **********************************************************************/ require "#{File.dirname(__FILE__)}/btap" require 'fileutils' require 'csv' require 'rubygems' module BTAP module FileIO #Test Constructions Module if __FILE__ == $0 require 'test/unit' class FileIOTests < Test::Unit::TestCase def setup end def test_load_save_DOE_file() BTAP::FileIO::load_e_quest(BTAP::OS_RUBY_PATH + '\Resources\eQuest_3.64\4StoreyBuilding.inp') end end end # Get the name of the model. # @author Phylroy A. Lopez # @return [String] the name of the model. def self.get_name(model) unless model.building.get.getAttribute("name").empty? return model.building.get.getAttribute("name").get.valueAsString else return "" end end # @author Phylroy A. Lopez # Get the name of the model. # @author Phylroy A. Lopez # @return [String] the name of the model. def self.set_name(model,name) unless model.building.empty? model.building.get.setName(name) end end # @author Phylroy A. Lopez # Get the name of the model. # @author Phylroy A. Lopez # @return [String] the name of the model. def self.set_sql_file(model,sql_path) model.setSqlFile(OpenStudio::Path.new( sql_path) ) end #@author Phylroy A. Lopez # Get the filepath of all files with extention # @param folder [String} the path to the folder to be scanned. # @param ext [String] the file extension name, ex ".epw" def self.get_find_files_from_folder_by_extension(folder, ext) Dir.glob("#{folder}/**/*#{ext}") end def self.delete_files_in_folder_by_extention(folder,ext) BTAP::FileIO::get_find_files_from_folder_by_extension(folder, ext).each do |file| FileUtils.rm(file) puts "#{file} deleted." end end def self.find_file_in_folder_by_filename(folder,filename) Dir.glob("#{folder}/**/*#{filename}") end def self.fix_url_to_path(url_string) if url_string =~/\/([a-zA-Z]:.*)/ return $1 else return url_string end end # This method loads an Openstudio file into the model. # @author Phylroy A. Lopez # @param filepath [String] path to the OSM file. # @param name [String] optional model name to be set to model. # @return [OpenStudio::Model::Model] an OpenStudio model object. def self.load_idf(filepath, name = "") #load file unless File.exist?(filepath) raise 'File does not exist: ' + filepath.to_s end puts "loading file #{filepath}..." model_path = OpenStudio::Path.new(filepath.to_s) #Upgrade version if required. version_translator = OpenStudio::OSVersion::VersionTranslator.new model = OpenStudio::EnergyPlus::loadAndTranslateIdf(model_path) version_translator.errors.each {|error| puts "Error: #{error.logMessage}\n\n"} version_translator.warnings.each {|warning| puts "Warning: #{warning.logMessage}\n\n"} #If model did not load correctly. if model.empty? raise 'something went wrong' end model = model.get if name != "" self.set_name(model,name) end puts "File #{filepath} loaded." return model end def self.replace_model(model,new_model,runner = nil) # pull original weather file object over weather_file = new_model.getOptionalWeatherFile if not weather_file.empty? weather_file.get.remove BTAP::runner_register("Info", "Removed alternate model's weather file object.",runner) end original_weather_file = model.getOptionalWeatherFile if not original_weather_file.empty? original_weather_file.get.clone(new_model) end # pull original design days over new_model.getDesignDays.each { |designDay| designDay.remove } model.getDesignDays.each { |designDay| designDay.clone(new_model) } # swap underlying data in model with underlying data in new_model # remove existing objects from model handles = OpenStudio::UUIDVector.new model.objects.each do |obj| handles << obj.handle end model.removeObjects(handles) # add new file to empty model model.addObjects( new_model.toIdfFile.objects ) BTAP::runner_register("Info", "Model name is now #{model.building.get.name}.", runner) end # This method loads an Openstudio file into the model. # @author Phylroy A. Lopez # @param filepath [String] path to the OSM file. # @param name [String] optional model name to be set to model. # @return [OpenStudio::Model::Model] an OpenStudio model object. def self.load_osm(filepath, name = "") #load file unless File.exist?(filepath) raise 'File does not exist: ' + filepath.to_s end puts "loading file #{filepath}..." model_path = OpenStudio::Path.new(filepath.to_s) #Upgrade version if required. version_translator = OpenStudio::OSVersion::VersionTranslator.new model = version_translator.loadModel(model_path) version_translator.errors.each {|error| puts "Error: #{error.logMessage}\n\n"} version_translator.warnings.each {|warning| puts "Warning: #{warning.logMessage}\n\n"} #If model did not load correctly. if model.empty? raise "could not load #{filepath}" end model = model.get if name != "" and not name.nil? self.set_name(model,name) end puts "File #{filepath} loaded." return model end # This method loads an *Quest file into the model. # @author Phylroy A. Lopez # @param filepath [String] path to the OSM file. # @return [OpenStudio::Model::Model] an OpenStudio model object. def self.load_e_quest(filepath) #load file unless File.exist?(filepath) raise 'File does not exist: ' + filepath.to_s end puts "loading equest file #{filepath}. This will only convert geometry." #Create an instancse of a DOE model doe_model = BTAP::EQuest::DOEBuilding.new() #Load the inp data into the DOE model. doe_model.load_inp(filepath) #Convert the model to a OSM format. model = doe_model.create_openstudio_model_new() return model end #This method will inject OSM objects from a OSM file/library into the current # model. # @author Phylroy A. Lopez # @param filepath [String] path to the OSM library file. # @return [OpenStudio::Model::Model] an OpenStudio model object (self reference). def self.inject_osm_file(model, filepath) osm_data = BTAP::FileIO::load_osm(filepath) model.addObjects(osm_data.objects); return model end # This method will return a deep copy of the model. # Simply because I don't trust the clone method yet. # @author Phylroy A. Lopez # @return [OpenStudio::Model::Model] a copy of the OpenStudio model object. def self.deep_copy(model,bool = true) return model.clone(bool).to_Model # model.save(OpenStudio::Path.new("deep_copy.osm")) # model_copy = self.load_osm("deep_copy.osm") # File.delete("deep_copy.osm") # return model_copy end # This method will save the model to an osm file. # @author Phylroy A. Lopez # @param model # @param filename The full path to save to. # @return [OpenStudio::Model::Model] a copy of the OpenStudio model object. def self.save_osm(model,filename) FileUtils.mkdir_p(File.dirname(filename)) File.delete(filename) if File.exist?(filename) model.save(OpenStudio::Path.new(filename)) puts "File #{filename} saved." end # This method will translate to an E+ IDF format and save the model to an idf file. # @author Phylroy A. Lopez # @param model # @param filename The full path to save to. # @return [OpenStudio::Model::Model] a copy of the OpenStudio model object. def self.save_idf(model,filename) OpenStudio::EnergyPlus::ForwardTranslator.new().translateModel(model).toIdfFile().save(OpenStudio::Path.new(filename),true) end # This method will recursively translate all IDFs in a folder to OSMs, and save them to the OSM_-No_Space_Types folder # @author Brendan Coughlin # @param filepath The directory that holds the IDFs - usually DOEArchetypes\Original # @return nil def self.convert_idf_to_osm(filepath) Find.find(filepath) { |file| if file[-4..-1] == ".idf" model = FileIO.load_idf(file) # this is a bit ugly but it works properly when called on a recursive folder structure FileIO.save_osm(model, (File.expand_path("..\\OSM-No_Space_Types\\", filepath) << "\\" << Pathname.new(file).basename.to_s)[0..-5]) puts # empty line break end } end def self.get_timestep_data(osm_file,sql_file,variable_name_array, env_period = nil, hourly_time_step = nil ) column_data = get_timeseries_arrays(sql, env_period, hourly_time_step, "Boiler Fan Coil Part Load Ratio") end def self.convert_all_eso_to_csv(in_folder,out_folder) list_of_csv_files = Array.new FileUtils.mkdir_p(out_folder) osmfiles = BTAP::FileIO::get_find_files_from_folder_by_extension(in_folder,".eso") osmfiles.each do |eso_file_path| #Run ESO Vars command must be run in folder. root_folder = Dir.getwd() puts File.dirname(eso_file_path) Dir.chdir(File.dirname(eso_file_path)) if File.exist?("eplustbl.htm") File.open("dummy.rvi", 'w') {|f| f.write("") } system("#{BTAP::SimManager::ProcessManager::find_read_vars_eso()} dummy.rvi unlimited") #get name of run from html file. runname = "" f = File.open("eplustbl.htm") f.each_line do |line| if line =~ /

Building: (.*)<\/b><\/p>/ puts "Found name: #{$1}" runname = $1 break end end f.close #copy files over with distinct names puts "copy hourly results to #{out_folder}/#{runname}_eplusout.csv" FileUtils.cp("eplusout.csv","#{out_folder}/#{runname}_eplusout.csv") puts "copy html results to #{out_folder}/#{runname}_eplustbl.htm" FileUtils.cp("eplustbl.htm","#{out_folder}/#{runname}_eplustbl.htm") puts "copy sql results to #{out_folder}/#{runname}_eplusout.sql" FileUtils.cp("eplusout.sql","#{out_folder}/#{runname}_eplusout.sql") list_of_csv_files << "#{out_folder}/#{runname}_eplusout.csv" end Dir.chdir(root_folder) end return list_of_csv_files end # This method will read a CSV file and return rows as hashes based on the selection given. # @author Phylroy Lopez # @param file The path to the csv file. # @param searchHash # @return matches A Array of rows that match the searchHash. The row is a Hash itself. def self.csv_look_up_rows(file, searchHash) options = { :headers => true, :converters => :numeric } table = CSV.read( file, options ) # we'll save the matches here matches = nil # save a copy of the headers matches = table.find_all do |row| row match = true searchHash.keys.each do |key| match = match && ( row[key] == searchHash[key] ) end match end return matches end def self.csv_look_up_unique_row(file, searchHash) #Load Vintage database information. matches = BTAP::FileIO::csv_look_up_rows(file, searchHash) raise( "Error: CSV lookup found more than one row that met criteria #{searchHash} in #{@file} ") if matches.size() > 1 raise( "Error: CSV lookup found no rows that met criteria #{searchHash} in #{@file}") if matches.size() < 1 return matches[0] end # This method will read a CSV file and return the unique values in a given column header. # @author Phylroy Lopez # @param file The path to the csv file. # @param colHeader The header name in teh csv file. # @return matches A Array of rows that match the searchHash. The row is a Hash itself. def self.csv_look_up_unique_col_data(file, colHeader) column_data = Array.new CSV.foreach( file, :headers => true ) do |row| column_data << row[colHeader] # For each row, give me the cell that is under the colHeader column end return column_data.sort!.uniq end def self.sum_row_headers(row,headers) total = 0.0 headers.each { |header| total = total + row[header] } return total end def self.terminus_hourly_output(csv_file) puts "Starting Terminus output processing." puts "reading #{csv_file} being processed" #reads csv file into memory. original = CSV.read(csv_file, { :headers => true, #This flag tell the parser that there are headers. :converters => :numeric #This tell it to convert string data into numeric when possible. } ) puts "done reading #{csv_file} being processed" # We are going to collect the header names that fit a pattern. But first we need to # create array containers to save the header name. In ruby we can use the string header names # as the array index. #Create arrays to store the header names for each type. waterheater_gas_rate_headers = Array.new() waterheater_electric_rate_headers = Array.new() waterheater_heating_rate_headers = Array.new() cooling_coil_electric_power_headers = Array.new() cooling_coil_total_cooling_rate_headers = Array.new() heating_coil_air_heating_rate_headers = Array.new() heating_coil_gas_rate_headers = Array.new() plant_supply_heating_demand_rate_headers = Array.new() facility_total_electrical_demand_headers = Array.new() boiler_gas_rate_headers = Array.new() time_index = Array.new() boiler_gas_rate_headers = Array.new() heating_coil_electric_power_headers = Array.new() #remove rows 2-169 (or 1-168 in computer array terms) original = self.remove_rows_from_csv_table(0,72,original) #Scan the CSV file to file all the headers that match the pattern. This will go through all the headers and find # any header that matches our regular expression if a match is made, the header name is stuffed into the string array. original.headers.each do |header| stripped_header = header.strip waterheater_electric_rate_headers << header if stripped_header =~/^.*:Water Heater Electric Power \[W\]\(Hourly\)$/ waterheater_gas_rate_headers << header if stripped_header =~/^.*:Water Heater Gas Rate \[W\]\(Hourly\)$/ waterheater_heating_rate_headers << header if stripped_header =~/^.*:Water Heater Heating Rate \[W\]\(Hourly\)$/ cooling_coil_electric_power_headers << header if stripped_header =~/^.*:Cooling Coil Electric Power \[W\]\(Hourly\)$/ cooling_coil_total_cooling_rate_headers << header if stripped_header =~/^.*:Cooling Coil Total Cooling Rate \[W\]\(Hourly\)$/ heating_coil_air_heating_rate_headers << header if stripped_header =~/^.*:Heating Coil Air Heating Rate \[W\]\(Hourly\)$/ heating_coil_gas_rate_headers << header if stripped_header =~/^.*:Heating Coil Gas Rate \[W\]\(Hourly\)$/ heating_coil_electric_power_headers << header if stripped_header =~/^.*:Heating Coil Electric Power \[W\]\(Hourly\)$/ plant_supply_heating_demand_rate_headers << header if stripped_header =~/^(?!SWH PLANT LOOP).*:Plant Supply Side Heating Demand Rate \[W\]\(Hourly\)$/ facility_total_electrical_demand_headers << header if stripped_header =~/^.*:Facility Total Electric Demand Power \[W\]\(Hourly\)$/ boiler_gas_rate_headers << header if stripped_header =~/^.*:Boiler Gas Rate \[W\]\(Hourly\)/ end #Debug printout stuff. Make sure the output it captures the headers you want otherwise modify the regex above puts waterheater_gas_rate_headers puts waterheater_electric_rate_headers puts waterheater_heating_rate_headers puts cooling_coil_electric_power_headers puts cooling_coil_total_cooling_rate_headers puts heating_coil_air_heating_rate_headers puts heating_coil_gas_rate_headers puts plant_supply_heating_demand_rate_headers puts facility_total_electrical_demand_headers puts boiler_gas_rate_headers puts heating_coil_electric_power_headers #open up a new file to save the file to..Note: This will fail it the file is open in EXCEL. CSV.open("#{csv_file}.terminus_hourly.csv", 'w') do |csv| #Create header row for new terminus hourly file. csv << [ "Date/Time", "water_heater_gas_rate_total", "water_heater_electric_rate_total", "water_heater_heating_rate_total", "cooling_coil_electric_power_total", "cooling_coil_total_cooling_rate_total", "heating_coil_air_heating_rate_total", "heating_coil_gas_rate_total", "heating_coil_electric_power_total", "plant_supply_heating_demand_rate_total", "facility_total_electrical_demand_total", "boiler_gas_rate_total" ] original.each do |row| # We are now writing data to the new csv file. This is where we can manipulate the data, row by row. # sum the headers collected above and store in specific *_total variables. # This is done via a small function self.sum_row_headers. There may only be a single # header collected.. That is fine. It is better to be flexible than hardcode anything. water_heater_gas_rate_total = self.sum_row_headers(row,waterheater_gas_rate_headers) water_heater_electric_rate_total = self.sum_row_headers(row,waterheater_electric_rate_headers) water_heater_heating_rate_total = self.sum_row_headers(row,waterheater_heating_rate_headers) cooling_coil_electric_power_total = self.sum_row_headers(row, cooling_coil_electric_power_headers) cooling_coil_total_cooling_rate_total = self.sum_row_headers(row, cooling_coil_total_cooling_rate_headers) heating_coil_air_heating_rate_total = self.sum_row_headers(row, heating_coil_air_heating_rate_headers) heating_coil_gas_rate_total = self.sum_row_headers(row, heating_coil_gas_rate_headers) heating_coil_electric_power_total = self.sum_row_headers(row, heating_coil_electric_power_headers) plant_supply_heating_demand_rate_total = self.sum_row_headers(row, plant_supply_heating_demand_rate_headers) facility_total_electrical_demand_total = self.sum_row_headers(row, facility_total_electrical_demand_headers) boiler_gas_rate_headers_total = self.sum_row_headers(row, boiler_gas_rate_headers) #Write the data out. Should match header row as above. csv << [ row["Date/Time"], #Time index is hardcoded because every file will have a "Date/Time" column header. water_heater_gas_rate_total, water_heater_electric_rate_total, water_heater_heating_rate_total, cooling_coil_electric_power_total, cooling_coil_total_cooling_rate_total, heating_coil_air_heating_rate_total, heating_coil_gas_rate_total, heating_coil_electric_power_total, plant_supply_heating_demand_rate_total, facility_total_electrical_demand_total, boiler_gas_rate_headers_total ] end end puts "Ending Terminus output processing." end def self.remove_rows_from_csv_table(start_index,stop_index,table) total_rows_to_remove = stop_index - start_index (0..total_rows_to_remove-1).each do |counter| table.delete(start_index) end return table end #load a model into OS & version translates, exiting and erroring if a problem is found def safe_load_model(model_path_string) model_path = OpenStudio::Path.new(model_path_string) if OpenStudio::exists(model_path) versionTranslator = OpenStudio::OSVersion::VersionTranslator.new model = versionTranslator.loadModel(model_path) if model.empty? raise "Version translation failed for #{model_path_string}" else model = model.get end else raise "#{model_path_string} couldn't be found" end return model end #load a sql file, exiting and erroring if a problem is found def safe_load_sql(sql_path_string) sql_path = OpenStudio::Path.new(sql_path_string) if OpenStudio::exists(sql_path) sql = OpenStudio::SqlFile.new(sql_path) else puts "#{sql_path} couldn't be found" exit end return sql end #function to wrap debug == true puts def debug_puts(puts_text) if Debug_Mode == true puts "#{puts_text}" end end def get_timeseries_arrays(openstudio_sql_file, timestep, variable_name_array, regex_name_filter = /.*/, env_period = nil) returnArray = Array.new() variable_name_array.each do |variable_name| possible_key_values = openstudio_sql_file.availableKeyValues(env_period,timestep,variable_name) possible_variable_names = openstudio_sql_file.availableVariableNames(env_period,timestep).include?(variable_name) if not possible_variable_names.nil? and possible_variable_names.include?(variable_name) and not possible_key_values.nil? possible_key_values.get.each do |key_value| unless regex_name_filter.match(key_value).nil? returnArray << get_timeseries_array(openstudio_sql_file, timestep, variable_name, key_value) end end end return returnArray end end #gets a time series data vector from the sql file and puts the values into a standard array of numbers def get_timeseries_array(openstudio_sql_file, timestep, variable_name, key_value) zone_time_step = "Zone Timestep" hourly_time_step = "Hourly" hvac_time_step = "HVAC System Timestep" timestep = hourly_time_step env_period = openstudio_sql_file.availableEnvPeriods[0] #puts openstudio_sql_file.class #puts env_period.class #puts timestep.class #puts variable_name.class #puts key_value.class key_value = key_value.upcase #upper cases the key_value b/c it is always uppercased in the sql file. #timestep = timestep.capitalize #capitalize the timestep b/c it is always capitalized in the sql file #timestep = timestep.split(" ").each{|word| word.capitalize!}.join(" ") #returns an array of all keyValues matching the variable name, envPeriod, and reportingFrequency #we'll use this to check if the query will work before we send it. puts "*#{env_period}*#{timestep}*#{variable_name}" time_series_array = [] puts env_period.class if env_period.nil? puts "here" time_series_array = [nil] return time_series_array end possible_env_periods = openstudio_sql_file.availableEnvPeriods() if possible_env_periods.nil? time_series_array = [nil] return time_series_array end possible_timesteps = openstudio_sql_file.availableReportingFrequencies(env_period) if possible_timesteps.nil? time_series_array = [nil] return time_series_array end possible_variable_names = openstudio_sql_file.availableVariableNames(env_period,timestep) if possible_variable_names.nil? time_series_array = [nil] return time_series_array end possible_key_values = openstudio_sql_file.availableKeyValues(env_period,timestep,variable_name) if possible_key_values.nil? time_series_array = [nil] return time_series_array end if possible_key_values.include? key_value and possible_variable_names.include? variable_name and possible_env_periods.include? env_period and possible_timesteps.include? timestep #the query is valid time_series = openstudio_sql_file.timeSeries(env_period, timestep, variable_name, key_value) if time_series #checks to see if time_series exists time_series = time_series.get.values debug_puts " #{key_value} time series length = #{time_series.size}" for i in 0..(time_series.size - 1) #puts "#{i.to_s} -- #{time_series[i]}" time_series_array << time_series[i] end end else #do this if the query is not valid. The comments might help troubleshoot. time_series_array = [nil] debug_puts "***The pieces below do NOT make a valid query***" debug_puts " *#{key_value}* - this key value might not exist for the variable you are looking for" debug_puts " *#{timestep}* - this value should be Hourly, Monthly, Zone Timestep, HVAC System Timestep, etc" debug_puts " *#{variable_name}* - every word should be capitalized EG: Refrigeration System Total Compressor Electric Energy " debug_puts " *#{env_period}* - you can get an array of all the valid env periods by using the sql_file.availableEnvPeriods() method " debug_puts " Possible key values: #{possible_key_values}" debug_puts " Possible Variable Names: #{possible_variable_names}" debug_puts " Possible run periods: #{possible_env_periods}" debug_puts " Possible timesteps: #{possible_timesteps}" end return time_series_array end #gets the average of the numbers in an array def non_zero_array_average(arr) debug_puts "average of the entire array = #{arr.inject{ |sum, el| sum + el }.to_f / arr.size}" arr.delete(0) debug_puts "average of the non-zero numbers in the array = #{arr.inject{ |sum, el| sum + el }.to_f / arr.size}" return arr.inject{ |sum, el| sum + el }.to_f / arr.size end #method for converting from IP to SI if you know the strings of the input and the output def ip_to_si(number, ip_unit_string, si_unit_string) ip_unit = OpenStudio::createUnit(ip_unit_string, "IP".to_UnitSystem).get si_unit = OpenStudio::createUnit(si_unit_string, "SI".to_UnitSystem).get #puts "#{ip_unit} --> #{si_unit}" ip_quantity = OpenStudio::Quantity.new(number, ip_unit) si_quantity = OpenStudio::convert(ip_quantity, si_unit).get #puts "#{ip_quantity} = #{si_quantity}" return si_quantity.value end # This is a simple example which uses rubyzip to # recursively generate a zip file from the contents of # a specified directory. The directory itself is not # included in the archive, rather just its contents. # # Usage: # require /path/to/the/ZipFileGenerator/Class # directoryToZip = "/tmp/input" # outputFile = "/tmp/out.zip" # zf = BTAP::FileIO::ZipFileGenerator.new(directoryToZip, outputFile) # zf.write() # # class ZipFileGenerator # # Initialize with the directory to zip and the location of the output archive. # def initialize(input_dir, output_file) # @input_dir = input_dir # @output_file = output_file # self.write() # end # # # Zip the input directory. # def write # entries = Dir.entries(@input_dir) - %w(. ..) # # ::Zip::File.open(@output_file, ::Zip::File::CREATE) do |io| # write_entries entries, '', io # end # end # # private # # # A helper method to make the recursion work. # def write_entries(entries, path, io) # entries.each do |e| # zip_file_path = path == '' ? e : File.join(path, e) # disk_file_path = File.join(@input_dir, zip_file_path) # puts "Deflating #{disk_file_path}" # # if File.directory? disk_file_path # recursively_deflate_directory(disk_file_path, io, zip_file_path) # else # put_into_archive(disk_file_path, io, zip_file_path) # end # end # end # # def recursively_deflate_directory(disk_file_path, io, zip_file_path) # io.mkdir zip_file_path # subdir = Dir.entries(disk_file_path) - %w(. ..) # write_entries subdir, zip_file_path, io # end # # def put_into_archive(disk_file_path, io, zip_file_path) # io.get_output_stream(zip_file_path) do |f| # f.puts(File.open(disk_file_path, 'rb').read) # end # end # end # end #FileIO end #BTAP