diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c4b2bed9..ebcda882c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,19 @@ Classify the change according to the following categories: ### Deprecated ### Removed +## v0.32.4 +### Changed +- Consolidated PVWatts API calls to 1 call (previously 3 separate calls existed). API call occurs in `src/core/utils.jl/call_pvwatts_api()`. This function is called for PV in `src/core/production_factor.jl/get_production_factor(PV)` and for GHP in `src/core/scenario.jl`. If GHP and PV are evaluated together, the GHP PVWatts call for ambient temperature is also used to assign the pv.production_factor_series in Scenario.jl so that the PVWatts API does not get called again downstream in `get_production_factor(PV)`. +- In `src/core/utils.jl/call_pvwatts_api()`, updated NSRDB bounds used in PVWatts query (now includes southern New Zealand) +- Updated PV Watts version from v6 to v8. PVWatts V8 updates the weather data to 2020 TMY data from the NREL NSRDB for locations covered by the database. (The NSRDB weather data used in PVWatts V6 is from around 2015.) See other differences at https://developer.nrel.gov/docs/solar/pvwatts/. +- Made PV struct mutable: This allows for assigning pv.production_factor_series when calling PVWatts for GHP, to avoid a extra PVWatts calls later. +- Changed unit test expected values due to update to PVWatts v8, which slightly changed expected PV production factors. +- Changed **fuel_avail_gal** default to 1e9 for on-grid scenarios (same as off-grid) +### Fixed +- Issue with using a leap year with a URDB rate - the URDB rate was creating energy_rate of length 8784 instead of intended 8760 +- Don't double add adjustments to urdb rates with non-standard units +- Corrected `Generator` **installed_cost_per_kw** from 500 to 650 if **only_runs_during_grid_outage** is _true_ or 800 if _false_ + ## v0.32.3 ### Fixed - Calculate **num_battery_bins** default in `backup_reliability.jl` based on battery duration to prevent significant discretization error (and add test) diff --git a/Project.toml b/Project.toml index 156e8992a..2365fef8b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "REopt" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" authors = ["Nick Laws", "Hallie Dunham ", "Bill Becker ", "Bhavesh Rathod ", "Alex Zolan ", "Amanda Farthing "] -version = "0.32.3" +version = "0.32.4" [deps] ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3" diff --git a/src/core/generator.jl b/src/core/generator.jl index 31a9b7969..228febf06 100644 --- a/src/core/generator.jl +++ b/src/core/generator.jl @@ -30,19 +30,19 @@ """ `Generator` is an optional REopt input with the following keys and default values: ```julia + only_runs_during_grid_outage::Bool = true, existing_kw::Real = 0, min_kw::Real = 0, max_kw::Real = 1.0e6, - installed_cost_per_kw::Real = 500.0, + installed_cost_per_kw::Real = only_runs_during_grid_outage ? 650.0 : 800.0, om_cost_per_kw::Real = off_grid_flag ? 20.0 : 10.0, om_cost_per_kwh::Real = 0.0, fuel_cost_per_gallon::Real = 3.0, electric_efficiency_full_load::Real = 0.3233, electric_efficiency_half_load::Real = electric_efficiency_full_load, - fuel_avail_gal::Real = off_grid_flag ? 1.0e9 : 660.0, + fuel_avail_gal::Real = 1.0e9, fuel_higher_heating_value_kwh_per_gal::Real = 40.7, min_turn_down_fraction::Real = off_grid_flag ? 0.15 : 0.0, - only_runs_during_grid_outage::Bool = true, sells_energy_back_to_grid::Bool = false, can_net_meter::Bool = false, can_wholesale::Bool = false, @@ -125,19 +125,19 @@ struct Generator <: AbstractGenerator function Generator(; off_grid_flag::Bool = false, analysis_years::Int = 25, + only_runs_during_grid_outage::Bool = true, existing_kw::Real = 0, min_kw::Real = 0, max_kw::Real = 1.0e6, - installed_cost_per_kw::Real = 500.0, + installed_cost_per_kw::Real = only_runs_during_grid_outage ? 650.0 : 800.0, om_cost_per_kw::Real= off_grid_flag ? 20.0 : 10.0, om_cost_per_kwh::Real = 0.0, fuel_cost_per_gallon::Real = 3.0, electric_efficiency_full_load::Real = 0.3233, electric_efficiency_half_load::Real = electric_efficiency_full_load, - fuel_avail_gal::Real = off_grid_flag ? 1.0e9 : 660.0, + fuel_avail_gal::Real = 1.0e9, fuel_higher_heating_value_kwh_per_gal::Real = KWH_PER_GAL_DIESEL, min_turn_down_fraction::Real = off_grid_flag ? 0.15 : 0.0, - only_runs_during_grid_outage::Bool = true, sells_energy_back_to_grid::Bool = false, can_net_meter::Bool = false, can_wholesale::Bool = false, diff --git a/src/core/production_factor.jl b/src/core/production_factor.jl index dbb2c8f18..d1b0eedcc 100644 --- a/src/core/production_factor.jl +++ b/src/core/production_factor.jl @@ -35,44 +35,13 @@ function get_production_factor(pv::PV, latitude::Real, longitude::Real; timefram return pv.production_factor_series end - # Check if site is beyond the bounds of the NRSDB dataset. If so, use the international dataset. - dataset = "nsrdb" - if longitude < -179.5 || longitude > -21.0 || latitude < -21.5 || latitude > 60.0 - if longitude < 81.5 || longitude > 179.5 || latitude < -43.8 || latitude > 60.0 - if longitude < 67.0 || longitude > 81.5 || latitude < -43.8 || latitude > 38.0 - dataset = "intl" - end - end - end + watts, ambient_temp_celcius = call_pvwatts_api(latitude, longitude; tilt=pv.tilt, azimuth=pv.azimuth, module_type=pv.module_type, + array_type=pv.array_type, losses=round(pv.losses*100, digits=3), dc_ac_ratio=pv.dc_ac_ratio, + gcr=pv.gcr, inv_eff=pv.inv_eff*100, timeframe=timeframe, radius=pv.radius, + time_steps_per_hour=time_steps_per_hour) - url = string("https://developer.nrel.gov/api/pvwatts/v6.json", "?api_key=", nrel_developer_key, - "&lat=", latitude , "&lon=", longitude, "&tilt=", pv.tilt, - "&system_capacity=1", "&azimuth=", pv.azimuth, "&module_type=", pv.module_type, - "&array_type=", pv.array_type, "&losses=", round(pv.losses*100, digits=3), "&dc_ac_ratio=", pv.dc_ac_ratio, - "&gcr=", pv.gcr, "&inv_eff=", pv.inv_eff*100, "&timeframe=", timeframe, "&dataset=", dataset, - "&radius=", pv.radius - ) + return watts - try - @info "Querying PVWatts for production_factor with " pv.name - r = HTTP.get(url, keepalive=true, readtimeout=10) - @info "Response received from PVWatts" - response = JSON.parse(String(r.body)) - if r.status != 200 - throw(@error("Bad response from PVWatts: $(response["errors"])")) - end - @info "PVWatts success." - watts = collect(get(response["outputs"], "ac", []) / 1000) # scale to 1 kW system (* 1 kW / 1000 W) - if length(watts) != 8760 - throw(@error("PVWatts did not return a valid production factor. Got $watts")) - end - if time_steps_per_hour > 1 - watts = repeat(watts, inner=time_steps_per_hour) - end - return watts - catch e - throw(@error("Error occurred when calling PVWatts: $e")) - end end diff --git a/src/core/pv.jl b/src/core/pv.jl index 358077b59..9a071e6a5 100644 --- a/src/core/pv.jl +++ b/src/core/pv.jl @@ -33,12 +33,12 @@ array_type::Int=1, # PV Watts array type (0: Ground Mount Fixed (Open Rack); 1: Rooftop, Fixed; 2: Ground Mount 1-Axis Tracking; 3 : 1-Axis Backtracking; 4: Ground Mount, 2-Axis Tracking) tilt::Real= array_type == 1 ? 10 : abs(latitude), # tilt = 10 deg for rooftop systems, abs(lat) for ground-mount module_type::Int=0, # PV module type (0: Standard; 1: Premium; 2: Thin Film) - losses::Real=0.14, + losses::Real=0.14, # System losses azimuth::Real = latitude≥0 ? 180 : 0, # set azimuth to zero for southern hemisphere - gcr::Real=0.4, - radius::Int=0, - name::String="PV", - location::String="both", + gcr::Real=0.4, # Ground coverage ratio + radius::Int=0, # Radius, in miles, to use when searching for the closest climate data station. Use zero to use the closest station regardless of the distance + name::String="PV", # for use with multiple pvs + location::String="both", # one of ["roof", "ground", "both"] existing_kw::Real=0, min_kw::Real=0, max_kw::Real=1.0e9, @@ -82,7 +82,7 @@ If `azimuth` is not provided, then it is set to 180 if the site is in the northern hemisphere and 0 if in the southern hemisphere. """ -struct PV <: AbstractTech +mutable struct PV <: AbstractTech tilt array_type module_type @@ -151,7 +151,7 @@ struct PV <: AbstractTech acres_per_kw::Real=6e-3, inv_eff::Real=0.96, dc_ac_ratio::Real=1.2, - production_factor_series::Union{Nothing, Array{Real,1}} = nothing, + production_factor_series::Union{Nothing, Array{<:Real,1}} = nothing, federal_itc_fraction::Real = 0.3, federal_rebate_per_kw::Real = 0.0, state_ibi_fraction::Real = 0.0, diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 07e95a6da..904e07855 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -454,37 +454,27 @@ function Scenario(d::Dict; flex_hvac_from_json=false) number_of_ghpghx = length(d["GHP"]["ghpghx_inputs"]) end # Call PVWatts for hourly dry-bulb outdoor air temperature - ambient_temperature_f = [] + ambient_temp_degF = [] if !haskey(d["GHP"]["ghpghx_inputs"][1], "ambient_temperature_f") || isempty(d["GHP"]["ghpghx_inputs"][1]["ambient_temperature_f"]) - url = string("https://developer.nrel.gov/api/pvwatts/v6.json", "?api_key=", nrel_developer_key, - "&lat=", d["Site"]["latitude"] , "&lon=", d["Site"]["longitude"], "&tilt=", d["Site"]["latitude"], - "&system_capacity=1", "&azimuth=", 180, "&module_type=", 0, - "&array_type=", 0, "&losses=", 0.14, "&dc_ac_ratio=", 1.1, - "&gcr=", 0.4, "&inv_eff=", 99, "&timeframe=", "hourly", "&dataset=nsrdb", - "&radius=", 100) - try - @info "Querying PVWatts for ambient temperature" - r = HTTP.get(url) - response = JSON.parse(String(r.body)) - if r.status != 200 - throw(@error("Bad response from PVWatts: $(response["errors"])")) + # If PV is evaluated and we need to call PVWatts for ambient temperature, assign PV production factor here too with the same call + # By assigning pv.production_factor_series here, it will skip the PVWatts call in get_production_factor(PV) call from reopt_input.jl + if !isempty(pvs) + for pv in pvs + pv.production_factor_series, ambient_temp_celcius = call_pvwatts_api(site.latitude, site.longitude; tilt=pv.tilt, azimuth=pv.azimuth, module_type=pv.module_type, + array_type=pv.array_type, losses=round(pv.losses*100, digits=3), dc_ac_ratio=pv.dc_ac_ratio, + gcr=pv.gcr, inv_eff=pv.inv_eff*100, timeframe="hourly", radius=pv.radius, time_steps_per_hour=settings.time_steps_per_hour) end - @info "PVWatts success." - temp_c = get(response["outputs"], "tamb", []) - if length(temp_c) != 8760 || isempty(temp_c) - throw(@error("PVWatts did not return a valid temperature profile. Got $temp_c")) - end - ambient_temperature_f = temp_c * 1.8 .+ 32.0 - catch e - throw(@error("Error occurred when calling PVWatts: $e")) + else + pv_prodfactor, ambient_temp_celcius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) end + ambient_temp_degF = ambient_temp_celcius * 1.8 .+ 32.0 else - ambient_temperature_f = d["GHP"]["ghpghx_inputs"][1]["ambient_temperature_f"] + ambient_temp_degF = d["GHP"]["ghpghx_inputs"][1]["ambient_temperature_f"] end for i in 1:number_of_ghpghx ghpghx_inputs = d["GHP"]["ghpghx_inputs"][i] - d["GHP"]["ghpghx_inputs"][i]["ambient_temperature_f"] = ambient_temperature_f + d["GHP"]["ghpghx_inputs"][i]["ambient_temperature_f"] = ambient_temp_degF # Only SpaceHeating portion of Heating Load gets served by GHP, unless allowed by can_serve_dhw if get(ghpghx_inputs, "heating_thermal_load_mmbtu_per_hr", []) in [nothing, []] if haskey(d["GHP"], "can_serve_dhw") && d["GHP"]["can_serve_dhw"] diff --git a/src/core/site.jl b/src/core/site.jl index 2907ae879..6b42c2608 100644 --- a/src/core/site.jl +++ b/src/core/site.jl @@ -36,7 +36,7 @@ Inputs related to the physical location: longitude::Real, land_acres::Union{Real, Nothing} = nothing, # acres of land available for PV panels and/or Wind turbines. Constraint applied separately to PV and Wind, meaning the two technologies are assumed to be able to be co-located. roof_squarefeet::Union{Real, Nothing} = nothing, - min_resil_time_steps::Int=0, + min_resil_time_steps::Int=0, # The minimum number consecutive timesteps that load must be fully met once an outage begins. Only applies to multiple outage modeling using inputs outage_start_time_steps and outage_durations. mg_tech_sizes_equal_grid_sizes::Bool = true, node::Int = 1, CO2_emissions_reduction_min_fraction::Union{Float64, Nothing} = nothing, diff --git a/src/core/urdb.jl b/src/core/urdb.jl index 41cbbefe5..4c7c2a724 100644 --- a/src/core/urdb.jl +++ b/src/core/urdb.jl @@ -99,7 +99,7 @@ function URDBrate(urdb_response::Dict, year::Int; time_steps_per_hour=1) n_monthly_demand_tiers, monthly_demand_tier_limits, monthly_demand_rates, n_tou_demand_tiers, tou_demand_tier_limits, tou_demand_rates, tou_demand_ratchet_time_steps = parse_demand_rates(urdb_response, year, time_steps_per_hour=time_steps_per_hour) - + energy_rates, energy_tier_limits, n_energy_tiers, sell_rates = parse_urdb_energy_costs(urdb_response, year; time_steps_per_hour=time_steps_per_hour) @@ -270,6 +270,9 @@ function parse_urdb_energy_costs(d::Dict, year::Int; time_steps_per_hour=1, bigM for month in range(1, stop=12) n_days = daysinmonth(Date(string(year) * "-" * string(month))) + if month == 2 && isleapyear(year) + n_days -= 1 + end for day in range(1, stop=n_days) @@ -290,12 +293,10 @@ function parse_urdb_energy_costs(d::Dict, year::Int; time_steps_per_hour=1, bigM else tier_use = tier end - if non_kwh_units - rate = rate_average - else - rate = get(d["energyratestructure"][period][tier_use], "rate", 0) - end - total_rate = rate + get(d["energyratestructure"][period][tier_use], "adj", 0) + total_rate = non_kwh_units ? + rate_average : + (get(d["energyratestructure"][period][tier_use], "rate", 0) + + get(d["energyratestructure"][period][tier_use], "adj", 0)) sell = get(d["energyratestructure"][period][tier_use], "sell", 0) for step in range(1, stop=time_steps_per_hour) # repeat hourly rates intrahour diff --git a/src/core/utils.jl b/src/core/utils.jl index 067cd9852..650af9446 100644 --- a/src/core/utils.jl +++ b/src/core/utils.jl @@ -377,54 +377,57 @@ function generate_year_profile_hourly(year::Int64, consecutive_periods::Abstract end -function get_ambient_temperature(latitude::Real, longitude::Real; timeframe="hourly") - url = string("https://developer.nrel.gov/api/pvwatts/v6.json", "?api_key=", nrel_developer_key, - "&lat=", latitude , "&lon=", longitude, "&tilt=", latitude, - "&system_capacity=1", "&azimuth=", 180, "&module_type=", 0, - "&array_type=", 0, "&losses=", 14, - "&timeframe=", timeframe, "&dataset=nsrdb" - ) - - try - @info "Querying PVWatts for ambient temperature... " - r = HTTP.get(url) - response = JSON.parse(String(r.body)) - if r.status != 200 - throw(@error("Bad response from PVWatts: $(response["errors"])")) - end - @info "PVWatts success." - tamb = collect(get(response["outputs"], "tamb", [])) # Celcius - if length(tamb) != 8760 - throw(@error("PVWatts did not return a valid temperature. Got $tamb")) +""" + call_pvwatts_api(latitude::Real, longitude::Real; tilt=latitude, azimuth=180, module_type=0, array_type=1, + losses=14, dc_ac_ratio=1.2, gcr=0.4, inv_eff=96, timeframe="hourly", radius=0, time_steps_per_hour=1) +This calls the PVWatts API and returns both: + - PV production factor + - Ambient outdoor air dry bulb temperature profile [Celcius] +""" +function call_pvwatts_api(latitude::Real, longitude::Real; tilt=latitude, azimuth=180, module_type=0, array_type=1, + losses=14, dc_ac_ratio=1.2, gcr=0.4, inv_eff=96, timeframe="hourly", radius=0, time_steps_per_hour=1) + # Check if site is beyond the bounds of the NRSDB TMY dataset. If so, use the international dataset. + dataset = "nsrdb" + if longitude < -179.5 || longitude > -21.0 || latitude < -21.5 || latitude > 60.0 + if longitude < 81.5 || longitude > 179.5 || latitude < -60.0 || latitude > 60.0 + if longitude < 67.0 || latitude < -40.0 || latitude > 38.0 + dataset = "intl" + end end - return tamb - catch e - throw(@error("Error occurred when calling PVWatts: $e")) end -end - - -function get_pvwatts_prodfactor(latitude::Real, longitude::Real; timeframe="hourly") - url = string("https://developer.nrel.gov/api/pvwatts/v6.json", "?api_key=", nrel_developer_key, - "&lat=", latitude , "&lon=", longitude, "&tilt=", latitude, - "&system_capacity=1", "&azimuth=", 180, "&module_type=", 0, - "&array_type=", 0, "&losses=", 14, - "&timeframe=", timeframe, "&dataset=nsrdb" - ) + url = string("https://developer.nrel.gov/api/pvwatts/v8.json", "?api_key=", nrel_developer_key, + "&lat=", latitude , "&lon=", longitude, "&tilt=", tilt, + "&system_capacity=1", "&azimuth=", azimuth, "&module_type=", module_type, + "&array_type=", array_type, "&losses=", losses, "&dc_ac_ratio=", dc_ac_ratio, + "&gcr=", gcr, "&inv_eff=", inv_eff, "&timeframe=", timeframe, "&dataset=", dataset, + "&radius=", radius + ) try - @info "Querying PVWatts for production factor of 1 kW system with tilt set to latitude... " - r = HTTP.get(url) + @info "Querying PVWatts for production factor and ambient air temperature... " + r = HTTP.get(url, keepalive=true, readtimeout=10) response = JSON.parse(String(r.body)) if r.status != 200 throw(@error("Bad response from PVWatts: $(response["errors"])")) end @info "PVWatts success." + # Get both possible data of interest watts = collect(get(response["outputs"], "ac", []) / 1000) # scale to 1 kW system (* 1 kW / 1000 W) + tamb_celcius = collect(get(response["outputs"], "tamb", [])) # Celcius + # Validate outputs if length(watts) != 8760 throw(@error("PVWatts did not return a valid prodfactor. Got $watts")) end - return watts + # Validate tamb_celcius + if length(tamb_celcius) != 8760 + throw(@error("PVWatts did not return a valid temperature. Got $tamb_celcius")) + end + # Upsample or downsample based on model time_steps_per_hour + if time_steps_per_hour > 1 + watts = repeat(watts, inner=time_steps_per_hour) + tamb_celcius = repeat(tamb_celcius, inner=time_steps_per_hour) + end + return watts, tamb_celcius catch e throw(@error("Error occurred when calling PVWatts: $e")) end diff --git a/src/mpc/structs.jl b/src/mpc/structs.jl index c2a66e087..86450478e 100644 --- a/src/mpc/structs.jl +++ b/src/mpc/structs.jl @@ -283,7 +283,7 @@ function MPCGenerator(; fuel_cost_per_gallon::Real = 3.0, electric_efficiency_full_load::Real = 0.3233, electric_efficiency_half_load::Real = electric_efficiency_full_load, - fuel_avail_gal::Real = 660.0, + fuel_avail_gal::Real = 1.0e9, fuel_higher_heating_value_kwh_per_gal::Real = KWH_PER_GAL_DIESEL, min_turn_down_fraction::Real = 0.0, # TODO change this to non-zero value only_runs_during_grid_outage::Bool = true, @@ -310,7 +310,7 @@ struct MPCGenerator <: AbstractGenerator fuel_cost_per_gallon::Real = 3.0, electric_efficiency_full_load::Real = 0.3233, electric_efficiency_half_load::Real = electric_efficiency_full_load, - fuel_avail_gal::Real = 660.0, + fuel_avail_gal::Real = 1.0e9, fuel_higher_heating_value_kwh_per_gal::Real = KWH_PER_GAL_DIESEL, min_turn_down_fraction::Real = 0.0, # TODO change this to non-zero value only_runs_during_grid_outage::Bool = true, diff --git a/test/runtests.jl b/test/runtests.jl index 43040028f..ff2829d2c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -77,8 +77,8 @@ else # run HiGHS tests inputs = REoptInputs(s) results = run_reopt(model, inputs) - @test results["PV"]["size_kw"] ≈ 70.3084 atol=0.01 - @test results["Financial"]["lcc"] ≈ 430747.0 rtol=1e-5 # with levelization_factor hack the LCC is within 5e-5 of REopt API LCC + @test results["PV"]["size_kw"] ≈ 68.9323 atol=0.01 + @test results["Financial"]["lcc"] ≈ 432681.26 rtol=1e-5 # with levelization_factor hack the LCC is within 5e-5 of REopt API LCC @test all(x == 0.0 for x in results["PV"]["electric_to_load_series_kw"][1:744]) end @@ -98,9 +98,9 @@ else # run HiGHS tests r = run_reopt(model, "./scenarios/pv_storage.json") @test r["PV"]["size_kw"] ≈ 216.6667 atol=0.01 - @test r["Financial"]["lcc"] ≈ 1.240037e7 rtol=1e-5 - @test r["ElectricStorage"]["size_kw"] ≈ 55.9 atol=0.1 - @test r["ElectricStorage"]["size_kwh"] ≈ 78.9 atol=0.1 + @test r["Financial"]["lcc"] ≈ 1.2391786e7 rtol=1e-5 + @test r["ElectricStorage"]["size_kw"] ≈ 49.0 atol=0.1 + @test r["ElectricStorage"]["size_kwh"] ≈ 83.3 atol=0.1 end @testset "Outage with Generator" begin @@ -108,7 +108,7 @@ else # run HiGHS tests "output_flag" => false, "log_to_console" => false) ) results = run_reopt(model, "./scenarios/generator.json") - @test results["Generator"]["size_kw"] ≈ 8.13 atol=0.01 + @test results["Generator"]["size_kw"] ≈ 9.55 atol=0.01 @test (sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 1:9) + sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 13:8760)) == 0 p = REoptInputs("./scenarios/generator.json") @@ -464,7 +464,7 @@ else # run HiGHS tests @test reliability_results["unlimited_fuel_cumulative_survival_final_time_step"][1] ≈ 0.802997 atol=0.0001 @test reliability_results["cumulative_survival_final_time_step"][1] ≈ 0.802997 atol=0.0001 - @test reliability_results["mean_cumulative_survival_final_time_step"] ≈ 0.817088 atol=0.0001 + @test reliability_results["mean_cumulative_survival_final_time_step"] ≈ 0.817586 atol=0.0001 end # removed Wind test for two reasons diff --git a/test/scenarios/backup_reliability_reopt_inputs.json b/test/scenarios/backup_reliability_reopt_inputs.json index 8184026b6..2b1739d40 100644 --- a/test/scenarios/backup_reliability_reopt_inputs.json +++ b/test/scenarios/backup_reliability_reopt_inputs.json @@ -21,6 +21,8 @@ "federal_rebate_per_kw": 350.0 }, "Generator": { + "installed_cost_per_kw": 500.0, + "fuel_avail_gal": 660, "min_kw": 200, "max_kw": 200 }, diff --git a/test/scenarios/emissions.json b/test/scenarios/emissions.json index ff85084ce..8534728ca 100644 --- a/test/scenarios/emissions.json +++ b/test/scenarios/emissions.json @@ -41,6 +41,7 @@ "outage_durations": [10] }, "Generator": { + "installed_cost_per_kw": 500.0, "max_kw": 500.0, "fuel_avail_gal": 125.0, "min_turn_down_fraction": 0.0, diff --git a/test/scenarios/generator.json b/test/scenarios/generator.json index fe658e749..b6de9eddf 100644 --- a/test/scenarios/generator.json +++ b/test/scenarios/generator.json @@ -10,6 +10,7 @@ "outage_end_time_step": 12 }, "Generator": { + "installed_cost_per_kw": 500.0, "max_kw": 500.0, "fuel_avail_gal": 125.0, "min_turn_down_fraction": 0.0, diff --git a/test/scenarios/mpc.json b/test/scenarios/mpc.json index 2a69e5988..988a3d9b8 100644 --- a/test/scenarios/mpc.json +++ b/test/scenarios/mpc.json @@ -101,6 +101,7 @@ 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05] }, "Generator": { + "fuel_avail_gal": 660, "size_kw": 30, "only_runs_during_grid_outage": false }, diff --git a/test/scenarios/nogridcost_minresilhours.json b/test/scenarios/nogridcost_minresilhours.json index 34b2feb50..ba787ebaf 100644 --- a/test/scenarios/nogridcost_minresilhours.json +++ b/test/scenarios/nogridcost_minresilhours.json @@ -60,6 +60,8 @@ "critical_load_fraction": 1 }, "Generator": { + "fuel_avail_gal": 660, + "installed_cost_per_kw": 500.0 }, "Financial": { "value_of_lost_load_per_kwh": 0.001, diff --git a/test/scenarios/nogridcost_multiscenario.json b/test/scenarios/nogridcost_multiscenario.json index 90d312b05..01ec7ccad 100644 --- a/test/scenarios/nogridcost_multiscenario.json +++ b/test/scenarios/nogridcost_multiscenario.json @@ -53,6 +53,8 @@ "critical_load_fraction": 0.1 }, "Generator": { + "fuel_avail_gal": 660, + "installed_cost_per_kw": 500.0, "max_kw": 0.0 }, "Financial": { diff --git a/test/scenarios/outage.json b/test/scenarios/outage.json index 64adb56de..9e655c1bf 100644 --- a/test/scenarios/outage.json +++ b/test/scenarios/outage.json @@ -8,6 +8,8 @@ "outage_durations": [10] }, "Generator": { + "fuel_avail_gal": 660, + "installed_cost_per_kw": 500.0, "existing_kw": 0.0, "min_turn_down_fraction": 0.0, "only_runs_during_grid_outage": true, diff --git a/test/test_with_cplex.jl b/test/test_with_cplex.jl index fd3c2f658..50ffbf813 100644 --- a/test/test_with_cplex.jl +++ b/test/test_with_cplex.jl @@ -59,9 +59,9 @@ end results = run_reopt(model, "./scenarios/pv_storage.json") @test results["PV"]["size_kw"] ≈ 217 atol=1 - @test results["Financial"]["lcc"] ≈ 1.240037e7 rtol=1e-5 - @test results["ElectricStorage"]["size_kw"] ≈ 56 atol=1 - @test results["ElectricStorage"]["size_kwh"] ≈ 79 atol=1 + @test results["Financial"]["lcc"] ≈ 1.239151e7 rtol=1e-5 + @test results["ElectricStorage"]["size_kw"] ≈ 49 atol=1 + @test results["ElectricStorage"]["size_kwh"] ≈ 83 atol=1 end @@ -74,7 +74,7 @@ end @test value(m[:binMGTechUsed]["Generator"]) == 1 @test value(m[:binMGTechUsed]["PV"]) == 1 @test value(m[:binMGStorageUsed]) == 1 - @test results["Financial"]["lcc"] ≈ 7.19753998668e7 atol=5e4 + @test results["Financial"]["lcc"] ≈ 6.82164056207e7 atol=5e4 #= Scenario with $0/kWh value_of_lost_load_per_kwh, 12x169 hour outages, 1kW load/hour, and min_resil_time_steps = 168 @@ -98,7 +98,7 @@ end REoptInputs("./scenarios/monthly_rate.json"), ]; results = run_reopt(m, ps) - @test results[3]["Financial"]["lcc"] + results[10]["Financial"]["lcc"] ≈ 1.23887e7 + 437169.0 rtol=1e-5 + @test results[3]["Financial"]["lcc"] + results[10]["Financial"]["lcc"] ≈ 1.2830872235e7 rtol=1e-5 end diff --git a/test/test_with_xpress.jl b/test/test_with_xpress.jl index 3189e795c..7aaf7b9ad 100644 --- a/test/test_with_xpress.jl +++ b/test/test_with_xpress.jl @@ -256,7 +256,7 @@ end =# # Austin, TX -> existing_chiller and existing_boiler added with FlexibleHVAC - tamb = REopt.get_ambient_temperature(30.2672, -97.7431); + pf, tamb = REopt.call_pvwatts_api(30.2672, -97.7431); R = 0.00025 # K/kW C = 1e5 # kJ/K # the starting scenario has flat fuel and electricty costs @@ -428,11 +428,11 @@ end results = run_reopt([m1,m2], d) @test results["PV"]["size_kw"] ≈ 216.6667 atol=0.01 - @test results["PV"]["lcoe_per_kwh"] ≈ 0.0483 atol = 0.001 - @test results["Financial"]["lcc"] ≈ 1.240037e7 rtol=1e-5 + @test results["PV"]["lcoe_per_kwh"] ≈ 0.0468 atol = 0.001 + @test results["Financial"]["lcc"] ≈ 1.239179e7 rtol=1e-5 @test results["Financial"]["lcc_bau"] ≈ 12766397 rtol=1e-5 - @test results["ElectricStorage"]["size_kw"] ≈ 55.9 atol=0.1 - @test results["ElectricStorage"]["size_kwh"] ≈ 78.9 atol=0.1 + @test results["ElectricStorage"]["size_kw"] ≈ 49.02 atol=0.1 + @test results["ElectricStorage"]["size_kwh"] ≈ 83.3 atol=0.1 proforma_npv = REopt.npv(results["Financial"]["offtaker_annual_free_cashflows"] - results["Financial"]["offtaker_annual_free_cashflows_bau"], 0.081) @test results["Financial"]["npv"] ≈ proforma_npv rtol=0.0001 @@ -456,8 +456,8 @@ end # @test r["ElectricStorage"]["maintenance_cost"] ≈ 2972.66 atol=0.01 # the maintenance_cost comes out to 3004.39 on Actions, so we test the LCC since it should match @test r["Financial"]["lcc"] ≈ 1.240096e7 rtol=0.01 - @test last(value.(m[:SOH])) ≈ 63.129 rtol=0.01 - @test r["ElectricStorage"]["size_kwh"] ≈ 78.91 rtol=0.01 + @test last(value.(m[:SOH])) ≈ 66.633 rtol=0.01 + @test r["ElectricStorage"]["size_kwh"] ≈ 83.29 rtol=0.01 # test minimum_avg_soc_fraction d["ElectricStorage"]["minimum_avg_soc_fraction"] = 0.72 @@ -472,7 +472,7 @@ end m2 = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) p = REoptInputs("./scenarios/generator.json") results = run_reopt([m1,m2], p) - @test results["Generator"]["size_kw"] ≈ 8.13 atol=0.01 + @test results["Generator"]["size_kw"] ≈ 9.55 atol=0.01 @test (sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 1:9) + sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 13:8760)) == 0 @test results["ElectricLoad"]["bau_critical_load_met"] == false @@ -493,7 +493,7 @@ end @test value(m[:binMGTechUsed]["CHP"]) ≈ 1 @test value(m[:binMGTechUsed]["PV"]) ≈ 1 @test value(m[:binMGStorageUsed]) ≈ 1 - @test results["Financial"]["lcc"] ≈ 7.0176719775e7 atol=5e4 + @test results["Financial"]["lcc"] ≈ 6.82164056207e7 atol=5e4 #= Scenario with $0.001/kWh value_of_lost_load_per_kwh, 12x169 hour outages, 1kW load/hour, and min_resil_time_steps = 168 @@ -513,8 +513,8 @@ end # Scenario with generator, PV, electric storage m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) results = run_reopt(m, "./scenarios/outages_gen_pv_stor.json") - @test results["Outages"]["expected_outage_cost"] ≈ 4.800393567995261e6 atol=10 - @test results["Financial"]["lcc"] ≈ 8.9857671584e7 atol=100 + @test results["Outages"]["expected_outage_cost"] ≈ 3.54476923e6 atol=10 + @test results["Financial"]["lcc"] ≈ 8.6413594727e7 atol=100 # Scenario with generator, PV, wind, electric storage m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) @@ -522,8 +522,8 @@ end @test value(m[:binMGTechUsed]["Generator"]) ≈ 1 @test value(m[:binMGTechUsed]["PV"]) ≈ 1 @test value(m[:binMGTechUsed]["Wind"]) ≈ 1 - @test results["Outages"]["expected_outage_cost"] ≈ 50147.6 atol=1.0 - @test results["Financial"]["lcc"] ≈ 6.84048993e7 rtol=0.001 + @test results["Outages"]["expected_outage_cost"] ≈ 446899.75 atol=1.0 + @test results["Financial"]["lcc"] ≈ 6.71661825335e7 rtol=0.001 end @testset "Multiple Sites" begin @@ -533,7 +533,7 @@ end REoptInputs("./scenarios/monthly_rate.json"), ]; results = run_reopt(m, ps) - @test results[3]["Financial"]["lcc"] + results[10]["Financial"]["lcc"] ≈ 1.240037e7 + 437169.0 rtol=1e-5 + @test results[3]["Financial"]["lcc"] + results[10]["Financial"]["lcc"] ≈ 1.2830872235e7 rtol=1e-5 end @testset "MPC" begin @@ -707,11 +707,11 @@ end @test roof_east["size_kw"] ≈ 4 atol=0.1 @test ground_pv["lifecycle_om_cost_after_tax_bau"] ≈ 782.0 atol=0.1 @test roof_west["lifecycle_om_cost_after_tax_bau"] ≈ 782.0 atol=0.1 - @test ground_pv["annual_energy_produced_kwh_bau"] ≈ 8844.19 atol=0.1 - @test roof_west["annual_energy_produced_kwh_bau"] ≈ 7440.1 atol=0.1 - @test ground_pv["annual_energy_produced_kwh"] ≈ 26533.54 atol=0.1 - @test roof_west["annual_energy_produced_kwh"] ≈ 10416.52 atol=0.1 - @test roof_east["annual_energy_produced_kwh"] ≈ 6482.37 atol=0.1 + @test ground_pv["annual_energy_produced_kwh_bau"] ≈ 8933.09 atol=0.1 + @test roof_west["annual_energy_produced_kwh_bau"] ≈ 7656.11 atol=0.1 + @test ground_pv["annual_energy_produced_kwh"] ≈ 26799.26 atol=0.1 + @test roof_west["annual_energy_produced_kwh"] ≈ 10719.51 atol=0.1 + @test roof_east["annual_energy_produced_kwh"] ≈ 6685.95 atol=0.1 end @testset "Thermal Energy Storage + Absorption Chiller" begin @@ -1305,67 +1305,67 @@ end end if i == 1 - @test results["PV"]["size_kw"] ≈ 61.16 atol=1e-1 + @test results["PV"]["size_kw"] ≈ 60.12 atol=1e-1 @test results["ElectricStorage"]["size_kw"] ≈ 0.0 atol=1e-1 @test results["ElectricStorage"]["size_kwh"] ≈ 0.0 atol=1e-1 @test results["Generator"]["size_kw"] ≈ 21.52 atol=1e-1 - expected_npv = -70908 + expected_npv = -70009 @test (expected_npv - results["Financial"]["npv"])/expected_npv ≈ 0.0 atol=1e-2 @test results["Site"]["annual_renewable_electricity_kwh"] ≈ 76412.02 @test results["Site"]["renewable_electricity_fraction"] ≈ 0.8 - @test results["Site"]["renewable_electricity_fraction_bau"] ≈ 0.14495 atol=1e-4 + @test results["Site"]["renewable_electricity_fraction_bau"] ≈ 0.147576 atol=1e-4 @test results["Site"]["total_renewable_energy_fraction"] ≈ 0.8 - @test results["Site"]["total_renewable_energy_fraction_bau"] ≈ 0.14495 atol=1e-4 - @test results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] ≈ 0.61865 atol=1e-4 - @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ 283.5 atol=1 - @test results["Site"]["annual_emissions_tonnes_CO2"] ≈ 11.36 atol=1e-2 - @test results["Site"]["annual_emissions_tonnes_CO2_bau"] ≈ 32.16 atol=1e-2 - @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] ≈ 6.96 + @test results["Site"]["total_renewable_energy_fraction_bau"] ≈ 0.147576 atol=1e-4 + @test results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] ≈ 0.616639 atol=1e-4 + @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ 281.6 atol=1 + @test results["Site"]["annual_emissions_tonnes_CO2"] ≈ 11.38 atol=1e-2 + @test results["Site"]["annual_emissions_tonnes_CO2_bau"] ≈ 32.06 atol=1e-2 + @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] ≈ 7.04 @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 - @test results["Financial"]["lifecycle_emissions_cost_climate"] ≈ 7752.46 atol=1 - @test results["Financial"]["lifecycle_emissions_cost_climate_bau"] ≈ 20514.15 atol=1e-1 - @test results["Site"]["lifecycle_emissions_tonnes_CO2"] ≈ 217.19 - @test results["Site"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 569.53 - @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2"] ≈ 139.18 + @test results["Financial"]["lifecycle_emissions_cost_climate"] ≈ 7767.6 atol=1 + @test results["Financial"]["lifecycle_emissions_cost_climate_bau"] ≈ 20450.62 atol=1e-1 + @test results["Site"]["lifecycle_emissions_tonnes_CO2"] ≈ 217.63 + @test results["Site"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 567.77 + @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2"] ≈ 140.78 @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 - @test results["ElectricUtility"]["annual_emissions_tonnes_CO2"] ≈ 4.41 - @test results["ElectricUtility"]["annual_emissions_tonnes_CO2_bau"] ≈ 32.16 - @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2"] ≈ 78.01 - @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 569.53 + @test results["ElectricUtility"]["annual_emissions_tonnes_CO2"] ≈ 4.34 + @test results["ElectricUtility"]["annual_emissions_tonnes_CO2_bau"] ≈ 32.06 + @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2"] ≈ 76.86 + @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 567.77 elseif i == 2 #commented out values are results using same levelization factor as API - @test results["PV"]["size_kw"] ≈ 97.52 atol=1 - @test results["ElectricStorage"]["size_kw"] ≈ 20.27 atol=1 # 20.29 - @test results["ElectricStorage"]["size_kwh"] ≈ 159.05 atol=1 + @test results["PV"]["size_kw"] ≈ 106.13 atol=1 + @test results["ElectricStorage"]["size_kw"] ≈ 21.58 atol=1 # 20.29 + @test results["ElectricStorage"]["size_kwh"] ≈ 165.27 atol=1 @test !haskey(results, "Generator") # NPV @info results["Financial"]["npv"] - expected_npv = -249474.49 + expected_npv = -267404.54 @test (expected_npv - results["Financial"]["npv"])/expected_npv ≈ 0.0 atol=1e-3 # Renewable energy - @test results["Site"]["renewable_electricity_fraction"] ≈ 0.786193 atol=1e-3 - @test results["Site"]["annual_renewable_electricity_kwh"] ≈ 78619.3 atol=10 - @test results["Site"]["renewable_electricity_fraction_bau"] ≈ 0.1365 atol=1e-3 #0.1354 atol=1e-3 - @test results["Site"]["annual_renewable_electricity_kwh_bau"] ≈ 13650.39 atol=10 # 13542.62 atol=10 - @test results["Site"]["total_renewable_energy_fraction"] ≈ 0.786193 atol=1e-3 - @test results["Site"]["total_renewable_energy_fraction_bau"] ≈ 0.1365 atol=1e-3 # 0.1354 atol=1e-3 + @test results["Site"]["renewable_electricity_fraction"] ≈ 0.783298 atol=1e-3 + @test results["Site"]["annual_renewable_electricity_kwh"] ≈ 78329.85 atol=10 + @test results["Site"]["renewable_electricity_fraction_bau"] ≈ 0.132118 atol=1e-3 #0.1354 atol=1e-3 + @test results["Site"]["annual_renewable_electricity_kwh_bau"] ≈ 13211.78 atol=10 # 13542.62 atol=10 + @test results["Site"]["total_renewable_energy_fraction"] ≈ 0.783298 atol=1e-3 + @test results["Site"]["total_renewable_energy_fraction_bau"] ≈ 0.132118 atol=1e-3 # 0.1354 atol=1e-3 # CO2 emissions - totals ≈ from grid, from fuelburn, ER, $/tCO2 breakeven @test results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] ≈ 0.8 atol=1e-3 # 0.8 - @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ 351.24 atol=1e-1 + @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ 374.02125 atol=1e-1 @test results["Site"]["annual_emissions_tonnes_CO2"] ≈ 14.2 atol=1 @test results["Site"]["annual_emissions_tonnes_CO2_bau"] ≈ 70.99 atol=1 @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] ≈ 0.0 atol=1 # 0.0 @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 atol=1 # 0.0 - @test results["Financial"]["lifecycle_emissions_cost_climate"] ≈ 9056.43 atol=1 - @test results["Financial"]["lifecycle_emissions_cost_climate_bau"] ≈ 45282.17 atol=1 - @test results["Site"]["lifecycle_emissions_tonnes_CO2"] ≈ 251.43 atol=1 - @test results["Site"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 1257.16 atol=1 + @test results["Financial"]["lifecycle_emissions_cost_climate"] ≈ 9110.21 atol=1 + @test results["Financial"]["lifecycle_emissions_cost_climate_bau"] ≈ 45546.55 atol=1 + @test results["Site"]["lifecycle_emissions_tonnes_CO2"] ≈ 252.92 atol=1 + @test results["Site"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 1264.62 atol=1 @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2"] ≈ 0.0 atol=1 # 0.0 @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 atol=1 # 0.0 @test results["ElectricUtility"]["annual_emissions_tonnes_CO2"] ≈ 14.2 atol=1 @test results["ElectricUtility"]["annual_emissions_tonnes_CO2_bau"] ≈ 70.99 atol=1 - @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2"] ≈ 251.43 atol=1 - @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 1257.16 atol=1 + @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2"] ≈ 252.92 atol=1 + @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 1264.62 atol=1 #also test CO2 breakeven cost inputs["PV"]["min_kw"] = results["PV"]["size_kw"] - inputs["PV"]["existing_kw"] @@ -1674,4 +1674,4 @@ end @test "warnings" ∈ keys(r["Messages"]) @test length(r["Messages"]["errors"]) > 0 @test length(r["Messages"]["warnings"]) > 0 -end +end \ No newline at end of file