lib/openstudio/analysis/formulation.rb in openstudio-analysis-1.2.0 vs lib/openstudio/analysis/formulation.rb in openstudio-analysis-1.3.0

- old
+ new

@@ -1,7 +1,7 @@ # ******************************************************************************* -# OpenStudio(R), Copyright (c) 2008-2021, Alliance for Sustainable Energy, LLC. +# OpenStudio(R), Copyright (c) 2008-2023, Alliance for Sustainable Energy, LLC. # All rights reserved. # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # (1) Redistributions of source code must retain the above copyright notice, @@ -56,27 +56,29 @@ attr_reader :analysis_type attr_reader :outputs attr_accessor :display_name attr_accessor :workflow attr_accessor :algorithm + attr_accessor :osw_path # the attributes below are used for packaging data into the analysis zip file attr_reader :weather_files attr_reader :seed_models attr_reader :worker_inits attr_reader :worker_finalizes attr_reader :libraries + attr_reader :server_scripts # Create an instance of the OpenStudio::Analysis::Formulation # # @param display_name [String] Display name of the project. # @return [Object] An OpenStudio::Analysis::Formulation object def initialize(display_name) @display_name = display_name @analysis_type = nil @outputs = [] - + @workflow = OpenStudio::Analysis::Workflow.new # Initialize child objects (expect workflow) @weather_file = WeatherFile.new @seed_model = SeedModel.new @algorithm = OpenStudio::Analysis::AlgorithmAttributes.new @@ -84,24 +86,26 @@ @weather_files = SupportFiles.new @seed_models = SupportFiles.new @worker_inits = SupportFiles.new @worker_finalizes = SupportFiles.new @libraries = SupportFiles.new - # @initialization_scripts = SupportFiles.new + @server_scripts = ServerScripts.new end - - # Initialize or return the current workflow object - # - # @return [Object] An OpenStudio::Analysis::Workflow object - def workflow - @workflow ||= OpenStudio::Analysis::Workflow.new - end - + # Define the type of analysis which is going to be running # # @param name [String] Name of the algorithm/analysis. (e.g. rgenoud, lhs, single_run) - attr_writer :analysis_type + # allowed values are ANALYSIS_TYPES = ['spea_nrel', 'rgenoud', 'nsga_nrel', 'lhs', 'preflight', + # 'morris', 'sobol', 'doe', 'fast99', 'ga', 'gaisl', + # 'single_run', 'repeat_run', 'batch_run'] + def analysis_type=(value) + if OpenStudio::Analysis::AlgorithmAttributes::ANALYSIS_TYPES.include?(value) + @analysis_type = value + else + raise "Invalid analysis type. Allowed types: #{OpenStudio::Analysis::AlgorithmAttributes::ANALYSIS_TYPES}" + end + end # Path to the seed model # # @param path [String] Path to the seed model. This should be relative. def seed_model=(file) @@ -131,33 +135,47 @@ # @option output_hash [Integer] :objective_function_index Index of the objective function. Default: nil # @option output_hash [Float] :objective_function_target Target for the objective function to reach (if defined). Default: nil # @option output_hash [Float] :scaling_factor How to scale the objective function(s). Default: nil # @option output_hash [Integer] :objective_function_group If grouping objective functions, then group ID. Default: nil def add_output(output_hash) - output_hash = { - units: '', - objective_function: false, - objective_function_index: nil, - objective_function_target: nil, - objective_function_group: nil, - scaling_factor: nil - }.merge(output_hash) - - # Check if the name is already been added. Note that if the name is added again, it will not update any of - # the fields - exist = @outputs.select { |o| o[:name] == output_hash[:name] } - if exist.empty? + # Check if the name is already been added. + exist = @outputs.find_index { |o| o[:name] == output_hash[:name] } + # if so, update the fields but keep objective_function_index the same + if exist + original = @outputs[exist] + if original[:objective_function] && !output_hash[:objective_function] + return @outputs + end + output = original.merge(output_hash) + output[:objective_function_index] = original[:objective_function_index] + @outputs[exist] = output + else + output = { + units: '', + objective_function: false, + objective_function_index: nil, + objective_function_target: nil, + #set default to nil or 1 if objective_function is true and this is not set + objective_function_group: (output_hash[:objective_function] ? 1 : nil), + scaling_factor: nil, + #set default to false or true if objective_function is true and this is not set + visualize: (output_hash[:objective_function] ? true : false), + metadata_id: nil, + export: true, + }.merge(output_hash) + #set display_name default to be name if its not set + output[:display_name] = output_hash[:display_name] ? output_hash[:display_name] : output_hash[:name] + #set display_name_short default to be display_name if its not set, this can be null if :display_name not set + output[:display_name_short] = output_hash[:display_name_short] ? output_hash[:display_name_short] : output_hash[:display_name] # if the variable is an objective_function, then increment and # assign and objective function index - if output_hash[:objective_function] + if output[:objective_function] values = @outputs.select { |o| o[:objective_function] } - output_hash[:objective_function_index] = values.size # size is already +1 - else - output_hash[:objective_function] = false + output[:objective_function_index] = values.size end - @outputs << output_hash + @outputs << output end @outputs end @@ -213,10 +231,20 @@ # log: could not find weather file warn 'Could not resolve a valid weather file. Check paths to weather files' end h[:analysis][:file_format_version] = version + h[:analysis][:cli_debug] = "--debug" + h[:analysis][:cli_verbose] = "--verbose" + h[:analysis][:run_workflow_timeout] = 28800 + h[:analysis][:upload_results_timeout] = 28800 + h[:analysis][:initialize_worker_timeout] = 28800 + #-BLB I dont think this does anything. server_scripts are run if they are in + #the /scripts/analysis or /scripts/data_point directories + #but nothing is ever checked in the OSA. + # + h[:analysis][:server_scripts] = {} # This is a hack right now, but after the initial hash is created go back and add in the objective functions # to the the algorithm as defined in the output_variables list ofs = @outputs.map { |i| i[:name] if i[:objective_function] }.compact if h[:analysis][:problem][:algorithm] @@ -319,13 +347,213 @@ FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename) save_analysis_zip(filename) end + + + def save_osa_zip(filename) + filename += '.zip' if File.extname(filename) == '' + FileUtils.mkdir_p File.dirname(filename) unless Dir.exist? File.dirname(filename) + + save_analysis_zip_osa(filename) + end + + # convert an OSW to an OSA + # osw_filename is the full path to the OSW file + # assumes the associated files and directories are in the same location + # /example.osw + # /measures + # /seeds + # /weather + # + def convert_osw(osw_filename) + #load OSW so we can loop over [:steps] + if File.exist? osw_filename #will this work for both rel and abs paths? + osw = JSON.parse(File.read(osw_filename), symbolize_names: true) + @osw_path = File.expand_path(osw_filename) + else + raise "Could not find workflow file #{osw_filename}" + end + + #set the weather and seed files if set in OSW + self.weather_file = osw[:weather_file] ? osw[:weather_file] : nil + self.seed_model = osw[:seed_file] ? osw[:seed_file] : nil + + #set analysis_type default to Single_Run + self.analysis_type = 'single_run' + + #loop over OSW 'steps' and map over measures + #there is no name/display name in the OSW. Just measure directory name + #read measure.XML from directory to get name / display name + #increment name by +_1 if there are duplicates + #add measure + #change default args to osw arg values + + osw[:steps].each do |step| + #get measure directory + measure_dir = step[:measure_dir_name] + measure_name = measure_dir.split("measures/").last + #get XML + measure_dir_abs_path = File.join(File.dirname(File.expand_path(osw_filename)),measure_dir) + xml = parse_measure_xml(File.join(measure_dir_abs_path, '/measure.xml')) + #add check for previous names _+1 + count = 1 + name = xml[:name] + display_name = xml[:display_name] + loop do + measure = @workflow.find_measure(name) + break if measure.nil? + + count += 1 + name = "#{xml[:name]}_#{count}" + display_name = "#{xml[:display_name]} #{count}" + end + #Add Measure to workflow + @workflow.add_measure_from_path(name, display_name, measure_dir_abs_path) #this forces to an absolute path which seems constent with PAT + #@workflow.add_measure_from_path(name, display_name, measure_dir) #this uses the path in the OSW which could be relative + + #Change the default argument values to the osw values + #1. find measure in @workflow + m = @workflow.find_measure(name) + #2. loop thru osw args + step[:arguments].each do |k,v| + #check if argument is in measure, otherwise setting argument_value will crash + raise "OSW arg: #{k} is not in Measure: #{name}" if m.arguments.find_all { |a| a[:name] == k.to_s }.empty? + #set measure arg to match osw arg + m.argument_value(k.to_s, v) + end + + end + end + private + # New format for OSAs. Package up the seed, weather files, and measures + # filename is the name of the file to be saved. ex: analysis.zip + # it will parse the OSA and zip up all the files defined in the workflow + def save_analysis_zip_osa(filename) + def add_directory_to_zip_osa(zipfile, local_directory, relative_zip_directory) + puts "Add Directory #{local_directory}" + Dir[File.join(local_directory.to_s, '**', '**')].each do |file| + puts "Adding File #{file}" + + zipfile.add(file.sub(local_directory, relative_zip_directory), file) + end + zipfile + end + #delete file if exists + FileUtils.rm_f(filename) if File.exist?(filename) + #get the full path to the OSW, since all Files/Dirs should be in same directory as the OSW + puts "osw_path: #{@osw_path}" + osw_full_path = File.dirname(File.expand_path(@osw_path)) + puts "osw_full_path: #{osw_full_path}" + + Zip::File.open(filename, create: true) do |zf| + ## Weather files + puts 'Adding Support Files: Weather' + #check if weather file exists. use abs path. remove leading ./ from @weather_file path if there. + #check if path is already absolute + if File.exists?(@weather_file[:file]) + puts " Adding #{@weather_file[:file]}" + zf.add("weather/#{File.basename(@weather_file[:file])}", @weather_file[:file]) + #make absolute path and check for file + elsif File.exists?(File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))) + puts " Adding #{File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))}" + zf.add("weather/#{File.basename(@weather_file[:file])}", File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))) + else + raise "Weather file does not exist at: #{File.join(osw_full_path,@weather_file[:file].sub(/^\.\//, ''))}" + end + + ## Seed files + puts 'Adding Support Files: Seed Models' + #check if weather file exists. use abs path. remove leading ./ from @seed_model path if there. + #check if path is already absolute + if File.exists?(@seed_model[:file]) + puts " Adding #{@seed_model[:file]}" + zf.add("seeds/#{File.basename(@seed_model[:file])}", @seed_model[:file]) + #make absolute path and check for file + elsif File.exists?(File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))) + puts " Adding #{File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))}" + zf.add("seeds/#{File.basename(@seed_model[:file])}", File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))) + else + raise "Seed file does not exist at: #{File.join(osw_full_path,@seed_model[:file].sub(/^\.\//, ''))}" + end + + puts 'Adding Support Files: Libraries' + @libraries.each do |lib| + raise "Libraries must specify their 'library_name' as metadata which becomes the directory upon zip" unless lib[:metadata][:library_name] + + if File.directory? lib[:file] + Dir[File.join(lib[:file], '**', '**')].each do |file| + puts " Adding #{file}" + zf.add(file.sub(lib[:file], "lib/#{lib[:metadata][:library_name]}"), file) + end + else + # just add the file to the zip + puts " Adding #{lib[:file]}" + zf.add(lib[:file], "lib/#{File.basename(lib[:file])}", lib[:file]) + end + end + + puts 'Adding Support Files: Server Scripts' + @server_scripts.each_with_index do |f, index| + if f[:init_or_final] == 'finalization' + file_name = 'finalization.sh' + else + file_name = 'initialization.sh' + end + if f[:server_or_data_point] == 'analysis' + new_name = "scripts/analysis/#{file_name}" + else + new_name = "scripts/data_point/#{file_name}" + end + puts " Adding #{f[:file]} as #{new_name}" + zf.add(new_name, f[:file]) + + if f[:arguments] + arg_file = "#{(new_name.sub(/\.sh\z/, ''))}.args" + puts " Adding arguments as #{arg_file}" + file = Tempfile.new('arg') + file.write(f[:arguments]) + zf.add(arg_file, file) + file.close + end + end + + ## Measures + puts 'Adding Measures' + added_measures = [] + # The list of the measures should always be there, but make sure they are uniq + @workflow.each do |measure| + measure_dir_to_add = measure.measure_definition_directory_local + + next if added_measures.include? measure_dir_to_add + + puts " Adding #{File.basename(measure_dir_to_add)}" + Dir[File.join(measure_dir_to_add, '**')].each do |file| + if File.directory?(file) + if File.basename(file) == 'resources' || File.basename(file) == 'lib' + #remove leading ./ from measure_definition_directory path if there. + add_directory_to_zip_osa(zf, file, "#{measure.measure_definition_directory.sub(/^\.\//, '')}/#{File.basename(file)}") + end + else + puts " Adding File #{file}" + #remove leading ./ from measure.measure_definition_directory string with regex .sub(/^\.\//, '') + zip_path_for_measures = file.sub(measure_dir_to_add, measure.measure_definition_directory.sub(/^\.\//, '')) + #puts " zip_path_for_measures: #{zip_path_for_measures}" + zf.add(zip_path_for_measures, file) + end + end + + added_measures << measure_dir_to_add + end + end + end + + #keep legacy function # Package up the seed, weather files, and measures def save_analysis_zip(filename) def add_directory_to_zip(zipfile, local_directory, relative_zip_directory) # puts "Add Directory #{local_directory}" Dir[File.join(local_directory.to_s, '**', '**')].each do |file| @@ -397,20 +625,20 @@ puts 'Adding Support Files: Worker Finalization Scripts' @worker_finalizes.each_with_index do |f, index| ordered_file_name = "#{index.to_s.rjust(2, '0')}_#{File.basename(f[:file])}" puts " Adding #{f[:file]} as #{ordered_file_name}" - zf.add(f[:file].sub(f[:file], "./scripts/worker_finalization/#{ordered_file_name}"), f[:file]) + zf.add(f[:file].sub(f[:file], "scripts/worker_finalization/#{ordered_file_name}"), f[:file]) if f[:metadata][:args] arg_file = "#{File.basename(ordered_file_name, '.*')}.args" file = Tempfile.new('arg') file.write(f[:metadata][:args]) - zf.add("./scripts/worker_finalization/#{arg_file}", file) + zf.add("scripts/worker_finalization/#{arg_file}", file) file.close end end - + ## Measures puts 'Adding Measures' added_measures = [] # The list of the measures should always be there, but make sure they are uniq @workflow.each do |measure|