#!/usr/bin/ ruby #********************************************************************************* # URBANopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC, and other # contributors. All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # # Redistributions of source code must retain the above copyright notice, this list # of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright notice, this # list of conditions and the following disclaimer in the documentation and/or other # materials provided with the distribution. # # Neither the name of the copyright holder nor the names of its contributors may be # used to endorse or promote products derived from this software without specific # prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED # OF THE POSSIBILITY OF SUCH DAMAGE. #********************************************************************************* require "uo_cli/version" require "optparse" require "urbanopt/geojson" require "urbanopt/scenario" require "csv" require "json" require "openssl" module URBANopt module CLI # Set up user interface @user_input = {} the_parser = OptionParser.new do |opts| opts.banner = "Usage: uo [-pmradsfiv]\n" + "\n" + "URBANopt CLI\n" + "First create a project folder with -p, then run additional commands as desired\n" + "Additional config options can be set with the 'runner.conf' file inside your new project folder" opts.separator "" opts.on("-p", "--project_folder ",String, "Create project directory named in your current folder\n" + " You must be insde the project directory you just created for all following commands to work") do |folder| @user_input[:project_folder] = folder end opts.on("-m", "--make_scenario", String, "Create ScenarioCSV files for each MapperFile using the Feature file path. Must specify -f argument\n" + " Example: uo -m -f example_project.json\n" + " Or, Create Scenario CSV for each MapperFile for a single Feature from Feature File. Must specify -f and -i argument\n" + " Example: uo -m -f example_project.json -i 1") do @user_input[:make_scenario_from] = "Create scenario files from FeatureFiles or for single Feature according to the MapperFiles in the 'mappers' directory" # This text does not get displayed to the user end opts.on("-r", "--run", String, "Run simulations. Must specify -s & -f arguments\n" + " Example: uo -r -s baseline_scenario.csv -f example_project.json") do @user_input[:run_scenario] = "Run simulations" # This text does not get displayed to the user end opts.on("-a", "--aggregate", String, "Aggregate individual feature results to scenario-level results. Must specify -s & -f arguments\n" + " Example: uo -a -s baseline_scenario.csv -f example_project.json") do @user_input[:aggregate] = "Aggregate all features to a whole Scenario" # This text does not get displayed to the user end opts.on("-d", "--delete_scenario", String, "Delete results from scenario. Must specify -s argument\n" + " Example: uo -d -s baseline_scenario.csv") do @user_input[:delete_scenario] = "Delete scenario results that were created from " # This text does not get displayed to the user end opts.on("-s", "--scenario_file ", String, "Specify (ScenarioCSV file path). Used as input for other commands") do |scenario| @user_input[:scenario] = scenario end opts.on("-f", "--feature_file ", String, "Specify (Feature file path). Used as input for other commands") do |feature| @user_input[:feature] = feature end opts.on("-i", "--feature_id ", Integer, "Specify (Feature ID). Used as input for other commands") do |feature_id| @user_input[:feature_id] = feature_id end opts.on("-v", "--version", "Show CLI version and exit") do @user_input[:version_request] = VERSION end end begin the_parser.parse! rescue OptionParser::InvalidOption => e puts e end # Simulate energy usage for each Feature or for single feature as defined by ScenarioCSV\ # params\ # +scenario+:: _string_ Path to csv file that defines the scenario\ # +feature_file_path+:: _string_ Path to Feature File used to describe set of features in the district # # FIXME: This only works when scenario_file and feature_file are in the project root directory # This works when called with filename (from inside project directory) and with absolute filepaths # Also, feels a little weird that now I'm only using instance variables and not passing anything to this function. I guess it's ok? def self.run_func root_dir = File.dirname(File.absolute_path(@user_input[:scenario])) scenario_basename = File.basename(File.absolute_path(@user_input[:scenario])) name = File.basename(scenario_basename, File.extname(scenario_basename)) run_dir = File.join(root_dir, 'run', name.downcase) if @feature_id feature_run_dir = File.join(run_dir,@feature_id) # If run folder for feature exists, remove it if File.exist?(feature_run_dir) FileUtils.rm_rf(feature_run_dir) end end csv_file = File.join(root_dir, scenario_basename) featurefile = File.join(root_dir, @feature_name) mapper_files_dir = File.join(root_dir, "mappers") num_header_rows = 1 feature_file = URBANopt::GeoJSON::GeoFile.from_file(featurefile) scenario_output = URBANopt::Scenario::ScenarioCSV.new(name, root_dir, run_dir, feature_file, mapper_files_dir, csv_file, num_header_rows) return scenario_output end # Create a scenario csv file from a FeatureFile # params\ # +feature_file_path+:: _string_ Path to a FeatureFile def self.create_scenario_csv_file(feature_file_path, feature_id) feature_file_json = JSON.parse(File.read(feature_file_path), :symbolize_names => true) Dir["#{@feature_path}/mappers/*.rb"].each do |mapper_file| mapper_path, mapper_name = File.split(mapper_file) mapper_name = mapper_name.split('.')[0] unless feature_id == 'SKIP' scenario_file_name = "#{mapper_name.downcase}_scenario-#{feature_id}.csv" else scenario_file_name = "#{mapper_name.downcase}_scenario.csv" end CSV.open(File.join(@feature_path, scenario_file_name), "wb", :write_headers => true, :headers => ["Feature Id","Feature Name","Mapper Class"]) do |csv| feature_file_json[:features].each do |feature| if feature_id == 'SKIP' # ensure that feature is a building if feature[:properties][:type] == "Building" csv << [feature[:properties][:id], feature[:properties][:name], "URBANopt::Scenario::#{mapper_name}Mapper"] end elsif feature_id == feature[:properties][:id].to_i csv << [feature[:properties][:id], feature[:properties][:name], "URBANopt::Scenario::#{mapper_name}Mapper"] elsif # If Feature ID specified does not exist in the Feature File raise error unless feature_file_json[:features].any? {|hash| hash[:properties][:id].include?(feature_id.to_s)} abort("\nYou must provide Feature ID from FeatureFile!\n---\n\n") end end end end end end # Create project folder # params\ # +dir_name+:: _string_ Name of new project folder # # Folder gets created in the current working directory # Includes weather for UO's example location, a base workflow file, and mapper files to show a baseline and a high-efficiency option. def self.create_project_folder(dir_name) if Dir.exist?(dir_name) abort("ERROR: there is already a directory here named #{dir_name}... aborting") else puts "CREATING URBANopt project directory: #{dir_name}" Dir.mkdir dir_name Dir.mkdir File.join(dir_name, 'mappers') Dir.mkdir File.join(dir_name, 'weather') Dir.mkdir File.join(dir_name, 'osm_building') mappers_dir_abs_path = File.absolute_path(File.join(dir_name, 'mappers/')) weather_dir_abs_path = File.absolute_path(File.join(dir_name, 'weather/')) osm_dir_abs_path = File.absolute_path(File.join(dir_name, 'osm_building/')) # FIXME: When residential hpxml flow is implemented (https://github.com/urbanopt/urbanopt-example-geojson-project/pull/24 gets merged) these files will change config_file = "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/runner.conf" example_feature_file = "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/example_project.json" example_gem_file = "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/Gemfile" remote_mapper_files = [ "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/mappers/base_workflow.osw", "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/mappers/Baseline.rb", "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/mappers/HighEfficiency.rb", ] remote_weather_files = [ "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/weather/USA_NY_Buffalo-Greater.Buffalo.Intl.AP.725280_TMY3.epw", "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/weather/USA_NY_Buffalo-Greater.Buffalo.Intl.AP.725280_TMY3.ddy", "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/weather/USA_NY_Buffalo-Greater.Buffalo.Intl.AP.725280_TMY3.stat", ] osm_files = [ "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/osm_building/7.osm", "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/osm_building/8.osm", "https://raw.githubusercontent.com/urbanopt/urbanopt-cli/master/example_files/osm_building/9.osm" ] # Download files to user's local machine remote_mapper_files.each do |mapper_file| mapper_path, mapper_name = File.split(mapper_file) mapper_download = open(mapper_file, {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE}) IO.copy_stream(mapper_download, File.join(mappers_dir_abs_path, mapper_name)) end remote_weather_files.each do |weather_file| weather_path, weather_name = File.split(weather_file) weather_download = open(weather_file, {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE}) IO.copy_stream(weather_download, File.join(weather_dir_abs_path, weather_name)) end osm_files.each do |osm_file| osm_path, osm_name = File.split(osm_file) osm_download = open(osm_file, {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE}) IO.copy_stream(osm_download, File.join(osm_dir_abs_path, osm_name)) end gem_path, gem_name = File.split(example_gem_file) example_gem_download = open(example_gem_file, {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE}) IO.copy_stream(example_gem_download, File.join(dir_name, gem_name)) feature_path, feature_name = File.split(example_feature_file) example_feature_download = open(example_feature_file, {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE}) IO.copy_stream(example_feature_download, File.join(dir_name, feature_name)) config_path, config_name = File.split(config_file) config_download = open(config_file, {ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE}) IO.copy_stream(config_download, File.join(dir_name, config_name)) end end # Perform CLI actions if @user_input[:project_folder] create_project_folder(@user_input[:project_folder]) puts "\nAn example FeatureFile is included: 'example_project.json'. You may place your own FeatureFile alongside the example." puts "Weather data is provided for the example FeatureFile. Additional weather data files may be downloaded from energyplus.net/weather for free" puts "If you use additional weather files, ensure they are added to the 'weather' directory. You will need to configure your mapper file and your osw file to use the desired weather file" puts "Next, move inside your new folder ('cd ') and create ScenarioFiles using this CLI: 'uo -m -f '" end if @user_input[:make_scenario_from] if @user_input[:feature].nil? abort("\nYou must provide the '-f' flag and a valid path to a FeatureFile!\n---\n\n") end @feature_path, @feature_name = File.split(@user_input[:feature]) if @user_input[:feature_id] puts "\nBuilding sample ScenarioFiles, assigning mapper classes to Feature ID #{@user_input[:feature_id]}..." create_scenario_csv_file(@user_input[:feature], @user_input[:feature_id]) puts "Done" else puts "\nBuilding sample ScenarioFiles, assigning mapper classes to each feature from #{@feature_name}..." # Skip Feature ID argument if not present create_scenario_csv_file(@user_input[:feature], 'SKIP') puts "Done" end end if @user_input[:run_scenario] if @user_input[:scenario].nil? abort("\nYou must provide '-s' flag and a valid path to a ScenarioFile!\n---\n\n") end if @user_input[:feature].nil? abort("\nYou must provide '-f' flag and a valid path to a FeatureFile!\n---\n\n") end if @user_input[:scenario].include? "-" @scenario_folder = "#{@user_input[:scenario].split(/\W+/)[0].capitalize}" @feature_id = "#{@user_input[:scenario].split(/\W+/)[1]}" else @scenario_folder = "#{@user_input[:scenario].split('.')[0].capitalize}" end @feature_path, @feature_name = File.split(@user_input[:feature]) puts "\nSimulating features of '#{@feature_name}' as directed by '#{@user_input[:scenario]}'...\n\n" scenario_runner = URBANopt::Scenario::ScenarioRunnerOSW.new scenario_runner.run(run_func()) puts "Done" end if @user_input[:aggregate] if @user_input[:scenario].nil? abort("\nYou must provide '-s' flag and a valid path to a ScenarioFile!\n---\n\n") end if @user_input[:feature].nil? abort("\nYou must provide '-f' flag and a valid path to a FeatureFile!\n---\n\n") end @scenario_folder = "#{@user_input[:scenario].split('.')[0].capitalize}" @scenario_path, @scenario_name = File.split(@user_input[:scenario]) @feature_path, @feature_name = File.split(@user_input[:feature]) puts "\nAggregating results across all features of #{@feature_name} according to '#{@scenario_name}'...\n" scenario_result = URBANopt::Scenario::ScenarioDefaultPostProcessor.new(run_func()).run scenario_result.save puts "Done" end if @user_input[:delete_scenario] if @user_input[:scenario].nil? abort("\nYou must provide '-s' flag and a valid path to a ScenarioFile!\n---\n\n") end @scenario_path, @scenario_name = File.split(@user_input[:scenario]) scenario_name = @scenario_name.split('.')[0] scenario_path = File.absolute_path(@scenario_path) scenario_results_dir = File.join(scenario_path, 'run', scenario_name) puts "\nDeleting previous results from '#{@scenario_name}'..." FileUtils.rm_rf(scenario_results_dir) puts "Done" end if @user_input[:version_request] puts "URBANopt CLI version: #{@user_input[:version_request]}" end end # End module CLI end # End module Urbanopt