diff --git a/.gitignore b/.gitignore index a05d9de1b..db6e0e7aa 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ example_files/python_deps/Miniconda* example_files/python_deps/python_config.json example_files/python_deps/python example_files/python_deps/python-3.10 + + +reopt_project \ No newline at end of file diff --git a/Gemfile b/Gemfile index c2ead26ef..b1260f51e 100644 --- a/Gemfile +++ b/Gemfile @@ -48,7 +48,7 @@ allow_local = ENV['FAVOR_LOCAL_GEMS'] # if allow_local && File.exist?('../urbanopt-reporting-gem') # gem 'urbanopt-reporting', path: '../urbanopt-reporting-gem' # elsif allow_local -gem 'urbanopt-reporting', github: 'URBANopt/urbanopt-reporting-gem', branch: 'develop' +# gem 'urbanopt-reporting', github: 'URBANopt/urbanopt-reporting-gem', branch: 'develop' # end # if allow_local && File.exist?('../urbanopt-rnm-us-gem') diff --git a/example_files/Gemfile b/example_files/Gemfile index bb9771c5c..4ab1d9a17 100644 --- a/example_files/Gemfile +++ b/example_files/Gemfile @@ -84,7 +84,7 @@ gem 'openstudio-geb', '~> 0.7.0' # elsif allow_local # gem 'urbanopt-geojson', github: 'URBANopt/urbanopt-geojson-gem', branch: 'develop' # else -gem 'urbanopt-geojson', '~> 1.1.0' +gem 'urbanopt-geojson', '~> 1.2.0' # end # NEVER put SCENARIO-GEM in this file...it will make all simulations fail due to the sqlite dependency @@ -93,7 +93,7 @@ gem 'urbanopt-geojson', '~> 1.1.0' # if allow_local && File.exist?('../urbanopt-reporting-gem') # gem 'urbanopt-reporting', path: '../../urbanopt-reporting-gem' # elsif allow_local -gem 'urbanopt-reporting', github: 'URBANopt/urbanopt-reporting-gem', branch: 'develop' +# gem 'urbanopt-reporting', github: 'URBANopt/urbanopt-reporting-gem', branch: 'develop' # else -#gem 'urbanopt-reporting', '~> 1.1.0' +gem 'urbanopt-reporting', '~> 1.2.0' # end diff --git a/example_files/reopt/base_assumptions.json b/example_files/reopt/base_assumptions.json index c7a48e75a..bc1694b26 100644 --- a/example_files/reopt/base_assumptions.json +++ b/example_files/reopt/base_assumptions.json @@ -29,7 +29,7 @@ "ElectricLoad": { "year": 2017, "critical_loads_kw_is_net": false, - "critical_load_fraction": 0, + "critical_load_fraction": 1.0, "loads_kw_is_net": true }, "PV": { @@ -78,14 +78,14 @@ "fuel_intercept_gal_per_hr": 0.0125, "generator_only_runs_during_grid_outage": true, "state_rebate_per_kw": 0.0, - "installed_cost_per_kw": 2500.0, + "installed_cost_per_kw": 650.0, "utility_ibi_max": 0.0, "fuel_avail_gal": 0.0, "min_turn_down_fraction": 0.3, "production_incentive_max_kw": 0.0, "utility_ibi_fraction": 0.0, "state_ibi_max": 0.0, - "fuel_cost_per_gallon": 20.0, + "fuel_cost_per_gallon": 2.25, "fuel_slope_gal_per_kwh": 0.068, "utility_rebate_max": 0.0, "macrs_option_years": 0, diff --git a/example_files/reopt/erp_assumptions.json b/example_files/reopt/erp_assumptions.json new file mode 100644 index 000000000..7dc9fa937 --- /dev/null +++ b/example_files/reopt/erp_assumptions.json @@ -0,0 +1,5 @@ +{ + "Outage":{ + "max_outage_duration": 24 + } +} \ No newline at end of file diff --git a/example_files/reopt/multiPV_assumptions.json b/example_files/reopt/multiPV_assumptions.json index 99c6ee977..601413070 100644 --- a/example_files/reopt/multiPV_assumptions.json +++ b/example_files/reopt/multiPV_assumptions.json @@ -29,7 +29,7 @@ "ElectricLoad": { "year": 2017, "critical_loads_kw_is_net": false, - "critical_load_fraction": 0, + "critical_load_fraction": 1.0, "loads_kw_is_net": true }, "PV": [ @@ -116,14 +116,14 @@ "fuel_intercept_gal_per_hr": 0.0125, "generator_only_runs_during_grid_outage": true, "state_rebate_per_kw": 0.0, - "installed_cost_per_kw": 2500.0, + "installed_cost_per_kw": 650.0, "utility_ibi_max": 0.0, "fuel_avail_gal": 0.0, "min_turn_down_fraction": 0.3, "production_incentive_max_kw": 0.0, "utility_ibi_fraction": 0.0, "state_ibi_max": 0.0, - "fuel_cost_per_gallon": 20.0, + "fuel_cost_per_gallon": 2.25, "fuel_slope_gal_per_kwh": 0.068, "utility_rebate_max": 0.0, "macrs_option_years": 0, diff --git a/example_files/reopt/multiPV_assumptions_ERP.json b/example_files/reopt/multiPV_assumptions_ERP.json new file mode 100644 index 000000000..5f3f03542 --- /dev/null +++ b/example_files/reopt/multiPV_assumptions_ERP.json @@ -0,0 +1,166 @@ +{ + "Settings": { + "timeout_seconds": 295, + "time_steps_per_hour": 1, + "off_grid_flag": false + }, + "Site": { + "roof_squarefeet": null, + "renewable_electricity_min_fraction": 0, + "renewable_electricity_max_fraction": 1 + }, + "Financial": { + "elec_cost_escalation_rate_fraction": 0.026, + "offtaker_discount_rate_fraction": 0.081, + "value_of_lost_load_per_kwh": 100.0, + "analysis_years": 20, + "microgrid_upgrade_cost_fraction": 0.3, + "offtaker_tax_rate_fraction": 0.26, + "om_cost_escalation_rate_fraction": 0.025 + }, + "ElectricTariff": { + "add_monthly_rates_to_urdb_rate": false, + "urdb_label": "63d2f36655296095a0092a6e" + }, + "ElectricUtility": { + "interconnection_limit_kw": 100000000.0, + "net_metering_limit_kw": 0.0, + "outage_start_time_steps": [1000], + "outage_durations": [24] + }, + "ElectricLoad": { + "year": 2017, + "critical_loads_kw_is_net": false, + "critical_load_fraction": 1.0, + "loads_kw_is_net": true + }, + "PV": [ + { + "name": "Roof - South Face", + "location":"roof", + "production_incentive_years": 1.0, + "macrs_bonus_fraction": 0.0, + "max_kw": 1000000000.0, + "production_incentive_max_benefit": 1000000000.0, + "radius": 0.0, + "state_ibi_fraction": 0.0, + "utility_rebate_max": 10000000000.0, + "installed_cost_per_kw": 2000.0, + "utility_ibi_max": 10000000000.0, + "tilt": 10.0, + "federal_rebate_per_kw": 0.0, + "gcr": 0.4, + "production_incentive_max_kw": 1000000000.0, + "utility_ibi_fraction": 0.0, + "state_ibi_max": 10000000000.0, + "state_rebate_per_kw": 0.0, + "macrs_option_years": 5, + "state_rebate_max": 10000000000.0, + "dc_ac_ratio": 1.1, + "federal_itc_fraction": 0.3, + "production_incentive_per_kwh": 0.0, + "module_type": 0, + "array_type": 1, + "existing_kw": 0.0, + "om_cost_per_kw": 16.0, + "utility_rebate_per_kw": 0.0, + "min_kw": 0.0, + "losses": 0.14, + "macrs_itc_reduction": 0.5, + "degradation_fraction": 0.005, + "inv_eff": 0.96, + "azimuth": 180.0 + }, + { + "name": "Groundmount", + "location":"ground", + "production_incentive_years": 1.0, + "macrs_bonus_fraction": 0.0, + "max_kw": 1000000000.0, + "production_incentive_max_benefit": 1000000000.0, + "radius": 0.0, + "state_ibi_fraction": 0.0, + "utility_rebate_max": 10000000000.0, + "installed_cost_per_kw": 2000.0, + "utility_ibi_max": 10000000000.0, + "tilt": 10.0, + "federal_rebate_per_kw": 0.0, + "gcr": 0.4, + "production_incentive_max_kw": 1000000000.0, + "utility_ibi_fraction": 0.0, + "state_ibi_max": 10000000000.0, + "state_rebate_per_kw": 0.0, + "macrs_option_years": 5, + "state_rebate_max": 10000000000.0, + "dc_ac_ratio": 1.1, + "federal_itc_fraction": 0.3, + "production_incentive_per_kwh": 0.0, + "module_type": 0, + "array_type": 1, + "existing_kw": 0.0, + "om_cost_per_kw": 16.0, + "utility_rebate_per_kw": 0.0, + "min_kw": 0.0, + "losses": 0.14, + "macrs_itc_reduction": 0.5, + "degradation_fraction": 0.005, + "inv_eff": 0.96, + "azimuth": 180.0 + } + ], + "Generator": { + "production_incentive_years": 0.0, + "macrs_bonus_fraction": 0.0, + "om_cost_per_kwh": 20.0, + "max_kw": 1000000000.0, + "production_incentive_max_benefit": 0.0, + "state_ibi_fraction": 0.0, + "fuel_intercept_gal_per_hr": 0.0125, + "generator_only_runs_during_grid_outage": true, + "state_rebate_per_kw": 0.0, + "installed_cost_per_kw": 650, + "utility_ibi_max": 0.0, + "fuel_avail_gal": 0.0, + "min_turn_down_fraction": 0.3, + "production_incentive_max_kw": 0.0, + "utility_ibi_fraction": 0.0, + "state_ibi_max": 0.0, + "fuel_cost_per_gallon": 2.25, + "fuel_slope_gal_per_kwh": 0.068, + "utility_rebate_max": 0.0, + "macrs_option_years": 0, + "state_rebate_max": 0.0, + "federal_itc_fraction": 0.0, + "existing_kw": 0.0, + "production_incentive_per_kwh": 0.0, + "om_cost_per_kw": 50.0, + "utility_rebate_per_kw": 0.0, + "min_kw": 0.0, + "macrs_itc_reduction": 0.0, + "federal_rebate_per_kw": 0.0, + "generator_sells_energy_back_to_grid": false + }, + "ElectricStorage": { + "max_kwh": 1000000.0, + "rectifier_efficiency_fraction": 0.96, + "total_itc_fraction": 0.0, + "min_kw": 0.0, + "max_kw": 1000000.0, + "replace_cost_per_kw": 460.0, + "replace_cost_per_kwh": 230.0, + "min_kwh": 0.0, + "installed_cost_per_kw": 1000.0, + "total_rebate_per_kw": 0, + "installed_cost_per_kwh": 500.0, + "inverter_efficiency_fraction": 0.96, + "macrs_itc_reduction": 0.5, + "can_grid_charge": true, + "macrs_bonus_fraction": 0.0, + "battery_replacement_year": 10, + "macrs_option_years": 7, + "internal_efficiency_fraction": 0.975, + "soc_min_fraction": 0.2, + "soc_init_fraction": 0.5, + "inverter_replacement_year": 10 + } +} diff --git a/lib/uo_cli.rb b/lib/uo_cli.rb index 1e8466d55..64ff03f6c 100755 --- a/lib/uo_cli.rb +++ b/lib/uo_cli.rb @@ -143,6 +143,10 @@ def opt_create "Specify the existing ScenarioFile that you want to extend with REopt functionality\n" \ "Example: uo create --reopt-scenario-file baseline_scenario.csv\n", type: String, short: :r + opt :reopt_erp_scenario_file, "\nCreate a ScenarioFile that includes a column defining the REopt ERP assumptions file to include outage planning as part of REopt Sizing.\n" \ + "Specify the existing ScenarioFile that you want to extend with REopt functionality\n" \ + "Example: uo create --reopt-erp-scenario-file baseline_scenario.csv\n", type: String, short: :x + opt :reopt_scenario_cost_file, "\nCreate a ScenarioFile that includes a column defining the REopt assumptions file and columns with capital costs\n" \ "Specify the existing ScenarioFile that you want to extend with REopt cost analysis functionality\n" \ "Example: uo create --reopt-scenario-cost-file baseline_scenario.csv\n", type: String, short: :R @@ -307,8 +311,8 @@ def opt_process opt :reopt_feature, "\nOptimize for each building individually with REopt\n" \ 'Example: uo process --reopt-feature', short: :e - opt :reopt_resilience, "\nInclude resilience reporting in REopt optimization\n" \ - 'Example: uo process --reopt-scenario --reopt-resilience', short: :p + opt :reopt_backup_power, "\nInclude output survivability reporting in REopt optimization\n" \ + 'Example: uo process --reopt-scenario --reopt-backup-power or --reopt-feature --reopt-backup-power', short: :p opt :reopt_keep_existing, "\nKeep existing reopt feature optimizations instead of rerunning them to avoid rate limit issues.\n" \ 'Example: uo process --reopt-feature --reopt-keep-existing', short: :k @@ -319,6 +323,11 @@ def opt_process opt :reopt_scenario_assumptions_file, "\nPath to the scenario REopt assumptions JSON file you want to use. Use with the --reopt-scenario post-processor.\n" \ 'If not specified, the reopt/multiPV_assumptions.json file will be used', type: String, short: :a + opt :reopt_erp_assumptions_file, "\nPath to the scenario REopt ERP assumptions JSON file you want to use.\n" \ + "This includes DER sizing and outage duration for REopt ERP capability.\n" \ + "Use with the --reopt-scenario --reopt-backup-power --reopt-erp-assumptions-file post-processor or --reopt-feature --reopt-backup-power --reopt-erp-assumptions-file post-processor\n" \ + 'If not specified, the reopt/erp_assumptions.json file will be used', type: String, short: :b + opt :scenario, "\nSelect which scenario to optimize", default: 'baseline_scenario.csv', required: true, short: :s opt :feature, "\nSelect which FeatureFile to use", default: 'example_project.json', required: true, short: :f @@ -607,7 +616,7 @@ def self.create_reopt_scenario_cost_file(existing_scenario_file) # read the newly created REopt scenario file reopt_scenario_file = File.join(existing_path, "REopt_cost_#{existing_name}") table = CSV.read(reopt_scenario_file, headers: true, col_sep: ',') - + # add additional capital cost columns to it table.each do |row| row['Total Capital Costs ($)'] = 100 @@ -621,6 +630,35 @@ def self.create_reopt_scenario_cost_file(existing_scenario_file) end end + # Write new ScenarioFile with REopt column for ERP functionality + # params \ + # +existing_scenario_file+:: _string_ - Name of existing ScenarioFile + def self.create_reopt_erp_scenario_file(existing_scenario_file) + existing_path, existing_name = File.split(File.expand_path(existing_scenario_file)) + # make reopt folder (if it does not exist) + unless Dir.exist?(File.join(existing_path, 'reopt')) + Dir.mkdir File.join(existing_path, 'reopt') + # copy reopt files from cli examples + $LOAD_PATH.each do |path_item| + if path_item.to_s.end_with?('example_files') + reopt_files = File.join(path_item, 'reopt') + Pathname.new(reopt_files).children.each { |reopt_file| FileUtils.cp(reopt_file, File.join(existing_path, 'reopt')) } + end + end + end + + table = CSV.read(existing_scenario_file, headers: true, col_sep: ',') + # Add another column, row by row: + table.each do |row| + row['REopt Assumptions'] = 'multiPV_assumptions_ERP.json' + end + # write new file (name it REopt + existing scenario name) + CSV.open(File.join(existing_path, "REopt_ERP_#{existing_name}"), 'w') do |f| + f << table.headers + table.each { |row| f << row } + end + end + # Change num_parallel in runner.conf to set number of cores to use when running simulations # This function is called during project_dir creation/updating so users aren't surprised if they look at the config file def self.use_num_parallel(project_dir) @@ -1308,9 +1346,16 @@ def self.install_python_dependencies puts "\nDone" end + # Create REopt ScenarioFile for ERP capability from existing sceanrio + if @opthash.command == 'create' && @opthash.subopts[:reopt_erp_scenario_file] + puts "\nCreating ScenarioFile with REopt ERP functionality, extending from #{@opthash.subopts[:reopt_erp_scenario_file]}..." + create_reopt_erp_scenario_file(@opthash.subopts[:reopt_erp_scenario_file]) + puts "\nDone" + end + # Create REopt ScenarioFile with capital costs from existing if @opthash.command == 'create' && @opthash.subopts[:reopt_scenario_cost_file] - puts "\nCreating ScenarioFile with REopt functionality, extending from #{@opthash.subopts[:reopt_scenario_cost_file]}..." + puts "\nCreating ScenarioFile with REopt capital cost functionality, extending from #{@opthash.subopts[:reopt_scenario_cost_file]}..." create_reopt_scenario_cost_file(@opthash.subopts[:reopt_scenario_cost_file]) puts "\nDone" end @@ -1319,6 +1364,7 @@ def self.install_python_dependencies if @opthash.command == 'create' && @opthash.subopts[:scenario_file].nil? && @opthash.subopts[:reopt_scenario_file].nil? && + @opthash.subopts[:reopt_erp_scenario_file].nil? && @opthash.subopts[:reopt_scenario_cost_file].nil? && @opthash.subopts[:project_folder].nil? abort("\nNo options provided for the `create` command. Did you forget a flag? Perhaps `-p`? See `uo create --help` for all options\n") @@ -1624,22 +1670,38 @@ def self.install_python_dependencies results << { process_type: 'disco', status: 'failed', timestamp: Time.now.strftime('%Y-%m-%dT%k:%M:%S.%L') } abort("\nNo DISCO results available in folder '#{opendss_folder}'\n") end - elsif (@opthash.subopts[:reopt_scenario] == true) || (@opthash.subopts[:reopt_feature] == true) || (@opthash.subopts[:reopt_resilience] == true) - if @opthash.subopts[:reopt_resilience] == true - abort('The REopt API is now using open-source optimization solvers; you may experience longer solve times and' \ - ' timeout errors, especially for evaluations with net metering, resilience, and/or 3+ technologies. ' \ - 'We will support resilience calculations with the REopt API in a future release.') + elsif (@opthash.subopts[:reopt_scenario] == true) || (@opthash.subopts[:reopt_feature] == true) || (@opthash.subopts[:reopt_backup_power] == true) + # --- REOPT Scenarios --- + + # Configure ERP Assumptions + if @opthash.subopts[:reopt_backup_power] == true + ## Read the erp_assumptions file if provided + # This file ensures outage duration is provided for running back up power analysis. Outage duration corresponds to multi pv assumption outage hours. + if @opthash.subopts[:reopt_erp_assumptions_file] + erp_assumptions_file = File.expand_path(@opthash.subopts[:reopt_erp_assumptions_file]).to_s + puts "\nUsing ERP assumptions file: #{erp_assumptions_file}\n" + else + # use default, read from the REopt folder in the URBANopt project + reopt_folder = File.join(@root_dir, 'reopt') + erp_assumptions_file = File.join(reopt_folder, 'erp_assumptions.json') + puts "\nUsing default ERP assumptions file: #{erp_assumptions_file}\n" + end + else + erp_assumptions_file = nil end + # Configure Reopt General Assumptions scenario_base = default_post_processor.scenario_base # see if reopt-scenario-assumptions-file was passed in, otherwise use the default scenario_assumptions = scenario_base.scenario_reopt_assumptions_file + puts "Using default scenario assumptions file: #{scenario_assumptions}\n" if @opthash.subopts[:reopt_scenario] == true && @opthash.subopts[:reopt_scenario_assumptions_file] scenario_assumptions = File.expand_path(@opthash.subopts[:reopt_scenario_assumptions_file]).to_s + puts scenario_assumptions end - puts "\nRunning the REopt Scenario post-processor with scenario assumptions file: #{scenario_assumptions}\n" + puts "\nRunning the REopt post-processor with scenario assumptions file: #{scenario_assumptions}\n" # Add community photovoltaic if present in the Feature File community_photovoltaic = [] feature_file = JSON.parse(File.read(File.expand_path(@opthash.subopts[:feature])), symbolize_names: true) @@ -1650,14 +1712,18 @@ def self.install_python_dependencies rescue StandardError => e puts "\nERROR: #{e.message}" end - # Retrieve capital costs from scenario file if present + + # Configure Capital Costs Processing (retrieve from scenario CSV if they exist) scenario_file = CSV.read(File.expand_path(@opthash.subopts[:scenario]), headers: true, header_converters: :symbol) - assumptions_hash = JSON.parse(File.read(File.expand_path(scenario_assumptions)), symbolize_names: true) # column headers converted to symbols required_columns = [:total_capital_costs, :capital_cost_per_floor_area_sqft] if (scenario_file.headers & required_columns).any? # assume cost analysis if either column is present puts "\nINFO: Capital cost data found in ScenarioFile. Preparing wind capital costs for REopt Analysis...\n" + + # retrieve assumptions hash for modifications + assumptions_hash = JSON.parse(File.read(File.expand_path(scenario_assumptions)), symbolize_names: true) + # check if both columns are present or just one has_total_costs = scenario_file.headers.include?(:total_capital_costs) has_cost_per_sqft = scenario_file.headers.include?(:capital_cost_per_floor_area_sqft) @@ -1711,36 +1777,46 @@ def self.install_python_dependencies assumptions_hash[:Wind][:macrs_bonus_fraction] = 0 assumptions_hash[:Wind][:federal_itc_fraction] = 0 assumptions_hash[:Wind][:production_factor_series] = Array.new(8760, 0) - end - - # Check if the fuel cost has been overridden in the assumptions file - if assumptions_hash[:ExistingBoiler] && assumptions_hash[:ExistingBoiler][:fuel_cost_per_mmbtu] - if assumptions_hash[:ExistingBoiler][:fuel_cost_per_mmbtu] == 100 - puts "WARNING: The 'fuel_cost_per_mmbtu' under 'ExistingBoiler' is still set to the default value of 100. Please update this value with a realistic fuel cost." + + # for cost calculations only: + # Check if the fuel cost has been overridden in the assumptions file + if assumptions_hash[:ExistingBoiler] && assumptions_hash[:ExistingBoiler][:fuel_cost_per_mmbtu] + if assumptions_hash[:ExistingBoiler][:fuel_cost_per_mmbtu] == 100 + puts "WARNING: The 'fuel_cost_per_mmbtu' under 'ExistingBoiler' is still set to the default value of 100. Please update this value with a realistic fuel cost." + else + puts "INFO: The 'fuel_cost_per_mmbtu' under 'ExistingBoiler' has been overridden with a value of #{assumptions_hash[:ExistingBoiler][:fuel_cost_per_mmbtu]}." + end else - puts "INFO: The 'fuel_cost_per_mmbtu' under 'ExistingBoiler' has been overridden with a value of #{assumptions_hash[:ExistingBoiler][:fuel_cost_per_mmbtu]}." + # There is no existing boiler fuel cost. Warn user. + puts "WARNING: There is no 'ExistingBoiler.fuel_cost_per_mmbtu' value in the assumptions file." end - else - # There is no existing boiler fuel cost. Warn user. - puts "WARNING: There is no 'ExistingBoiler.fuel_cost_per_mmbtu' value in the assumptions file." - end - # Write assumptions hash to file since REoptPostProcessor reads from file - temp_assumptions_file = File.join(@root_dir, 'run', @scenario_name.downcase, 'temp_reopt_scenario_assumptions.json') - File.open(temp_assumptions_file, 'w') { |f| f.write JSON.pretty_generate(assumptions_hash) } + # Write assumptions hash to file since REoptPostProcessor reads from file + cost_assumptions_file = File.join(@root_dir, 'run', @scenario_name.downcase, 'reopt_cost_scenario_assumptions.json') + File.open(cost_assumptions_file, 'w') { |f| f.write JSON.pretty_generate(assumptions_hash) } + + # Overwrite old scenario assumptions with new assumptions for post processing below + scenario_assumptions = cost_assumptions_file + end + + # Now actually Run REopt postprocessor + # initialize reopt post processor reopt_post_processor = URBANopt::REopt::REoptPostProcessor.new( scenario_report, - temp_assumptions_file, + scenario_assumptions, scenario_base.reopt_feature_assumptions, - DEVELOPER_NREL_KEY, false + DEVELOPER_NREL_KEY, false, + erp_assumptions_file ) + if @opthash.subopts[:reopt_scenario] == true puts "\nPost-processing entire scenario with REopt\n" scenario_report_scenario = reopt_post_processor.run_scenario_report( scenario_report: scenario_report, save_name: 'scenario_optimization', - run_resilience: @opthash.subopts[:reopt_resilience], - community_photovoltaic: community_photovoltaic + run_resilience: @opthash.subopts[:reopt_backup_power], + community_photovoltaic: community_photovoltaic, + erp_assumptions_file: erp_assumptions_file ) results << { process_type: 'reopt_scenario', status: 'Complete', timestamp: Time.now.strftime('%Y-%m-%dT%k:%M:%S.%L') } puts "\nDone\n" @@ -1759,9 +1835,10 @@ def self.install_python_dependencies scenario_report: scenario_report, save_names_feature_reports: ['feature_optimization'] * scenario_report.feature_reports.length, save_name_scenario_report: 'feature_optimization', - run_resilience: @opthash.subopts[:reopt_resilience], + run_resilience: @opthash.subopts[:reopt_backup_power], keep_existing_output: @opthash.subopts[:reopt_keep_existing], - groundmount_photovoltaic: groundmount_photovoltaic + groundmount_photovoltaic: groundmount_photovoltaic, + erp_assumptions_file: erp_assumptions_file ) results << { process_type: 'reopt_feature', status: 'Complete', timestamp: Time.now.strftime('%Y-%m-%dT%k:%M:%S.%L') } puts "\nDone\n" diff --git a/spec/spec_files/REopt_scenario_ERP.csv b/spec/spec_files/REopt_scenario_ERP.csv new file mode 100644 index 000000000..4faf8948c --- /dev/null +++ b/spec/spec_files/REopt_scenario_ERP.csv @@ -0,0 +1,3 @@ +Feature Id,Feature Name,Mapper Class,REopt Assumptions +2,Restaurant 1,URBANopt::Scenario::BaselineMapper,multiPV_assumptions_ERP.json +5,District Office 1,URBANopt::Scenario::BaselineMapper,multiPV_assumptions_ERP.json \ No newline at end of file diff --git a/spec/spec_files/reopt/base_assumptions.json b/spec/spec_files/reopt/base_assumptions.json index 5185d4511..3aa53bd25 100644 --- a/spec/spec_files/reopt/base_assumptions.json +++ b/spec/spec_files/reopt/base_assumptions.json @@ -29,7 +29,7 @@ "ElectricLoad": { "year": 2017, "critical_loads_kw_is_net": false, - "critical_load_fraction": 0, + "critical_load_fraction": 1.0, "loads_kw_is_net": true }, "PV": { @@ -78,14 +78,14 @@ "fuel_intercept_gal_per_hr": 0.0125, "generator_only_runs_during_grid_outage": true, "state_rebate_per_kw": 0.0, - "installed_cost_per_kw": 2500.0, + "installed_cost_per_kw": 650.0, "utility_ibi_max": 0.0, "fuel_avail_gal": 0.0, "min_turn_down_fraction": 0.3, "production_incentive_max_kw": 0.0, "utility_ibi_fraction": 0.0, "state_ibi_max": 0.0, - "fuel_cost_per_gallon": 20.0, + "fuel_cost_per_gallon": 2.25, "fuel_slope_gal_per_kwh": 0.068, "utility_rebate_max": 0.0, "macrs_option_years": 0, diff --git a/spec/spec_files/reopt/erp_assumptions.json b/spec/spec_files/reopt/erp_assumptions.json new file mode 100644 index 000000000..7dc9fa937 --- /dev/null +++ b/spec/spec_files/reopt/erp_assumptions.json @@ -0,0 +1,5 @@ +{ + "Outage":{ + "max_outage_duration": 24 + } +} \ No newline at end of file diff --git a/spec/spec_files/reopt/multiPV_assumptions.json b/spec/spec_files/reopt/multiPV_assumptions.json index 138c0efd1..c1437ef8f 100644 --- a/spec/spec_files/reopt/multiPV_assumptions.json +++ b/spec/spec_files/reopt/multiPV_assumptions.json @@ -29,7 +29,7 @@ "ElectricLoad": { "year": 2017, "critical_loads_kw_is_net": false, - "critical_load_fraction": 0, + "critical_load_fraction": 1.0, "loads_kw_is_net": true }, "PV": [ @@ -116,14 +116,14 @@ "fuel_intercept_gal_per_hr": 0.0125, "generator_only_runs_during_grid_outage": true, "state_rebate_per_kw": 0.0, - "installed_cost_per_kw": 2500.0, + "installed_cost_per_kw": 650.0, "utility_ibi_max": 0.0, "fuel_avail_gal": 0.0, "min_turn_down_fraction": 0.3, "production_incentive_max_kw": 0.0, "utility_ibi_fraction": 0.0, "state_ibi_max": 0.0, - "fuel_cost_per_gallon": 20.0, + "fuel_cost_per_gallon": 2.25, "fuel_slope_gal_per_kwh": 0.068, "utility_rebate_max": 0.0, "macrs_option_years": 0, diff --git a/spec/spec_files/reopt/multiPV_assumptions_ERP.json b/spec/spec_files/reopt/multiPV_assumptions_ERP.json new file mode 100644 index 000000000..5f3f03542 --- /dev/null +++ b/spec/spec_files/reopt/multiPV_assumptions_ERP.json @@ -0,0 +1,166 @@ +{ + "Settings": { + "timeout_seconds": 295, + "time_steps_per_hour": 1, + "off_grid_flag": false + }, + "Site": { + "roof_squarefeet": null, + "renewable_electricity_min_fraction": 0, + "renewable_electricity_max_fraction": 1 + }, + "Financial": { + "elec_cost_escalation_rate_fraction": 0.026, + "offtaker_discount_rate_fraction": 0.081, + "value_of_lost_load_per_kwh": 100.0, + "analysis_years": 20, + "microgrid_upgrade_cost_fraction": 0.3, + "offtaker_tax_rate_fraction": 0.26, + "om_cost_escalation_rate_fraction": 0.025 + }, + "ElectricTariff": { + "add_monthly_rates_to_urdb_rate": false, + "urdb_label": "63d2f36655296095a0092a6e" + }, + "ElectricUtility": { + "interconnection_limit_kw": 100000000.0, + "net_metering_limit_kw": 0.0, + "outage_start_time_steps": [1000], + "outage_durations": [24] + }, + "ElectricLoad": { + "year": 2017, + "critical_loads_kw_is_net": false, + "critical_load_fraction": 1.0, + "loads_kw_is_net": true + }, + "PV": [ + { + "name": "Roof - South Face", + "location":"roof", + "production_incentive_years": 1.0, + "macrs_bonus_fraction": 0.0, + "max_kw": 1000000000.0, + "production_incentive_max_benefit": 1000000000.0, + "radius": 0.0, + "state_ibi_fraction": 0.0, + "utility_rebate_max": 10000000000.0, + "installed_cost_per_kw": 2000.0, + "utility_ibi_max": 10000000000.0, + "tilt": 10.0, + "federal_rebate_per_kw": 0.0, + "gcr": 0.4, + "production_incentive_max_kw": 1000000000.0, + "utility_ibi_fraction": 0.0, + "state_ibi_max": 10000000000.0, + "state_rebate_per_kw": 0.0, + "macrs_option_years": 5, + "state_rebate_max": 10000000000.0, + "dc_ac_ratio": 1.1, + "federal_itc_fraction": 0.3, + "production_incentive_per_kwh": 0.0, + "module_type": 0, + "array_type": 1, + "existing_kw": 0.0, + "om_cost_per_kw": 16.0, + "utility_rebate_per_kw": 0.0, + "min_kw": 0.0, + "losses": 0.14, + "macrs_itc_reduction": 0.5, + "degradation_fraction": 0.005, + "inv_eff": 0.96, + "azimuth": 180.0 + }, + { + "name": "Groundmount", + "location":"ground", + "production_incentive_years": 1.0, + "macrs_bonus_fraction": 0.0, + "max_kw": 1000000000.0, + "production_incentive_max_benefit": 1000000000.0, + "radius": 0.0, + "state_ibi_fraction": 0.0, + "utility_rebate_max": 10000000000.0, + "installed_cost_per_kw": 2000.0, + "utility_ibi_max": 10000000000.0, + "tilt": 10.0, + "federal_rebate_per_kw": 0.0, + "gcr": 0.4, + "production_incentive_max_kw": 1000000000.0, + "utility_ibi_fraction": 0.0, + "state_ibi_max": 10000000000.0, + "state_rebate_per_kw": 0.0, + "macrs_option_years": 5, + "state_rebate_max": 10000000000.0, + "dc_ac_ratio": 1.1, + "federal_itc_fraction": 0.3, + "production_incentive_per_kwh": 0.0, + "module_type": 0, + "array_type": 1, + "existing_kw": 0.0, + "om_cost_per_kw": 16.0, + "utility_rebate_per_kw": 0.0, + "min_kw": 0.0, + "losses": 0.14, + "macrs_itc_reduction": 0.5, + "degradation_fraction": 0.005, + "inv_eff": 0.96, + "azimuth": 180.0 + } + ], + "Generator": { + "production_incentive_years": 0.0, + "macrs_bonus_fraction": 0.0, + "om_cost_per_kwh": 20.0, + "max_kw": 1000000000.0, + "production_incentive_max_benefit": 0.0, + "state_ibi_fraction": 0.0, + "fuel_intercept_gal_per_hr": 0.0125, + "generator_only_runs_during_grid_outage": true, + "state_rebate_per_kw": 0.0, + "installed_cost_per_kw": 650, + "utility_ibi_max": 0.0, + "fuel_avail_gal": 0.0, + "min_turn_down_fraction": 0.3, + "production_incentive_max_kw": 0.0, + "utility_ibi_fraction": 0.0, + "state_ibi_max": 0.0, + "fuel_cost_per_gallon": 2.25, + "fuel_slope_gal_per_kwh": 0.068, + "utility_rebate_max": 0.0, + "macrs_option_years": 0, + "state_rebate_max": 0.0, + "federal_itc_fraction": 0.0, + "existing_kw": 0.0, + "production_incentive_per_kwh": 0.0, + "om_cost_per_kw": 50.0, + "utility_rebate_per_kw": 0.0, + "min_kw": 0.0, + "macrs_itc_reduction": 0.0, + "federal_rebate_per_kw": 0.0, + "generator_sells_energy_back_to_grid": false + }, + "ElectricStorage": { + "max_kwh": 1000000.0, + "rectifier_efficiency_fraction": 0.96, + "total_itc_fraction": 0.0, + "min_kw": 0.0, + "max_kw": 1000000.0, + "replace_cost_per_kw": 460.0, + "replace_cost_per_kwh": 230.0, + "min_kwh": 0.0, + "installed_cost_per_kw": 1000.0, + "total_rebate_per_kw": 0, + "installed_cost_per_kwh": 500.0, + "inverter_efficiency_fraction": 0.96, + "macrs_itc_reduction": 0.5, + "can_grid_charge": true, + "macrs_bonus_fraction": 0.0, + "battery_replacement_year": 10, + "macrs_option_years": 7, + "internal_efficiency_fraction": 0.975, + "soc_min_fraction": 0.2, + "soc_init_fraction": 0.5, + "inverter_replacement_year": 10 + } +} diff --git a/spec/uo_cli_spec.rb b/spec/uo_cli_spec.rb index b236509b4..9802dd472 100644 --- a/spec/uo_cli_spec.rb +++ b/spec/uo_cli_spec.rb @@ -22,6 +22,7 @@ test_scenario_res = test_directory_res / 'two_building_res.csv' test_scenario_res_hpxml = test_directory_res_hpxml / 'two_building_res_hpxml.csv' test_scenario_reopt = test_directory_pv / 'REopt_scenario.csv' + test_scenario_reopt_erp = test_directory_pv / 'REopt_scenario_ERP.csv' test_scenario_reopt_cost = test_directory_pv / 'REopt_cost_baseline_scenario.csv' test_scenario_elec = test_directory_elec / 'electrical_scenario.csv' test_scenario_ev = test_directory / 'two_building_ev_scenario.csv' @@ -734,6 +735,20 @@ def select_measures(test_dir, measure_name_list, workflow = 'base_workflow.osw', expect((test_directory_pv / 'run' / 'reopt_scenario' / '3' / 'finished.job').exist?).to be false end + it 'runs a PV scenario with ERP when called with reopt', :electric do + system("cp #{spec_dir / 'spec_files' / 'REopt_scenario_ERP.csv'} #{test_scenario_reopt_erp}") + # Copy in reopt folder + system("cp -R #{spec_dir / 'spec_files' / 'reopt'} #{test_directory_pv / 'reopt'}") + system("#{call_cli} run --scenario #{test_scenario_reopt_erp} --feature #{test_feature_pv}") + expect((test_directory_pv / 'reopt').exist?).to be true + expect((test_directory_pv / 'reopt' / 'base_assumptions.json').exist?).to be true + expect((test_directory_pv / 'reopt' / 'multiPV_assumptions_ERP.json').exist?).to be true + expect((test_directory_pv / 'run' / 'reopt_scenario_erp' / '5' / 'finished.job').exist?).to be true + expect((test_directory_pv / 'run' / 'reopt_scenario_erp' / '2' / 'finished.job').exist?).to be true + expect((test_directory_pv / 'run' / 'reopt_scenario_erp' / '3' / 'finished.job').exist?).to be false + end + + it 'successfully gets results from the opendss cli', :electric do # This test requires the 'runs an electrical network scenario' be run first system("#{call_cli} process --default --scenario #{test_scenario_elec} --feature #{test_feature_elec}") @@ -759,6 +774,17 @@ def select_measures(test_dir, measure_name_list, workflow = 'base_workflow.osw', expect((test_directory_pv / 'run' / 'scenario_comparison.html').exist?).to be true end + it 'reopt post-processes an ERP scenario', :electric do + # This test requires the 'runs a PV scenario with ERP when called with reopt' be run first + system("#{call_cli} process --reopt-scenario --scenario #{test_scenario_reopt_erp} --feature #{test_feature_pv}") + expect((test_directory_pv / 'run' / 'reopt_scenario_erp' / 'scenario_optimization.json').exist?).to be true + expect((test_directory_pv / 'run' / 'reopt_scenario_erp' / 'process_status.json').exist?).to be true + expect((test_directory_pv / 'run' / 'reopt_scenario_erp' / 'reopt'/ 'scenario_report_reopt_scenario_erp_reopt_run.json').exist?).to be true + # and visualize + system("#{call_cli} visualize --feature #{test_feature_pv}") + expect((test_directory_pv / 'run' / 'scenario_comparison.html').exist?).to be true + end + it 'reopt post-processes a scenario with specified scenario assumptions file', :electric do # This test requires the 'runs a PV scenario when called with reopt' be run first expect { system("#{call_cli} process --reopt-scenario -a #{test_reopt_scenario_assumptions_file} --scenario #{test_scenario_reopt} --feature #{test_feature_pv}") } @@ -768,13 +794,22 @@ def select_measures(test_dir, measure_name_list, workflow = 'base_workflow.osw', expect((test_directory_pv / 'run' / 'reopt_scenario' / 'process_status.json').exist?).to be true end - it 'reopt post-processes a scenario with resilience reporting', :electric do - skip('Resilience processing is not yet implemented with REopt v3') - # This test requires the 'runs a PV scenario when called with reopt' be run first - system("#{call_cli} process --reopt-scenario --reopt-resilience --scenario #{test_scenario_reopt} --feature #{test_feature_pv}") - expect((test_directory_pv / 'run' / 'reopt_scenario' / 'scenario_optimization.json').exist?).to be true - expect((test_directory_pv / 'run' / 'reopt_scenario' / 'process_status.json').exist?).to be true - # path_to_resilience_report_file = test_directory_pv / 'run' / 'reopt_scenario' / 'reopt' / 'scenario_report_reopt_scenario_reopt_run_resilience.json' + it 'reopt post-processes a scenario with erp reporting', :electric do + # This test requires the 'runs a PV scenario when called with reopt erp' be run first + system("#{call_cli} process --reopt-scenario --reopt-backup-power --scenario #{test_scenario_reopt_erp} --feature #{test_feature_pv}") + expect((test_directory_pv / 'run' / 'reopt_scenario_erp' / 'scenario_optimization.json').exist?).to be true + expect((test_directory_pv / 'run' / 'reopt_scenario_erp' / 'process_status.json').exist?).to be true + path_to_resilience_report_file = test_directory_pv / 'run' / 'reopt_scenario_erp' / 'reopt' / 'scenario_report_reopt_scenario_erp_reopt_run_resilience.json' + expect((path_to_resilience_report_file).exist?).to be true + end + + it 'reopt post-processes a feature with erp reporting', :electric do + # This test requires the 'runs a PV scenario when called with reopt erp' be run first + system("#{call_cli} process --reopt-feature --reopt-backup-power --scenario #{test_scenario_reopt_erp} --feature #{test_feature_pv}") + expect((test_directory_pv / 'run' / 'reopt_scenario_erp' / 'feature_optimization.json').exist?).to be true + expect((test_directory_pv / 'run' / 'reopt_scenario_erp' / 'process_status.json').exist?).to be true + path_to_resilience_report_file = test_directory_pv / 'run' / 'reopt_scenario_erp' / '2' / 'reopt' / 'feature_report_2_reopt_run_resilience.json' + expect((path_to_resilience_report_file).exist?).to be true end it 'reopt post-processes each feature and visualize', :electric do diff --git a/uo_cli.gemspec b/uo_cli.gemspec index 378976156..b6d6d6a60 100644 --- a/uo_cli.gemspec +++ b/uo_cli.gemspec @@ -34,11 +34,11 @@ Gem::Specification.new do |spec| # use specific versions of urbanopt and openstudio dependencies while under heavy development spec.add_runtime_dependency 'optimist', '~> 3.2' - spec.add_runtime_dependency 'urbanopt-geojson', '~> 1.1.0' - spec.add_runtime_dependency 'urbanopt-reopt', '~> 1.1.0' - spec.add_runtime_dependency 'urbanopt-reporting', '~> 1.1.0' - spec.add_runtime_dependency 'urbanopt-rnm-us', '~> 1.1.0' - spec.add_runtime_dependency 'urbanopt-scenario', '~> 1.1.0' + spec.add_runtime_dependency 'urbanopt-geojson', '~> 1.2.0' + spec.add_runtime_dependency 'urbanopt-reopt', '~> 1.2.0' + spec.add_runtime_dependency 'urbanopt-reporting', '~> 1.2.0' + spec.add_runtime_dependency 'urbanopt-rnm-us', '~> 1.2.0' + spec.add_runtime_dependency 'urbanopt-scenario', '~> 1.2.0' spec.add_development_dependency 'rake', '~> 13.0' spec.add_development_dependency 'rspec', '~> 3.13'