From 0d4a78717cbfaf09ebbd31afd13f355ffe5b5eee Mon Sep 17 00:00:00 2001 From: pypapus Date: Tue, 30 Jul 2024 15:09:13 -0600 Subject: [PATCH 01/29] Add API call for renewable energy fraction --- .gitignore | 1 + src/core/electric_utility.jl | 139 +++++++++++++++++++++++++++++++++-- src/core/reopt.jl | 1 + 3 files changed, 135 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 5a0d8a762..7abaf7641 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .vscode .DS_Store +gridRE_tests docs/build/ docs/Manifest.toml **.log \ No newline at end of file diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index aaa45e77f..ad13f7e2f 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -125,8 +125,8 @@ struct ElectricUtility outage_time_steps::Union{Nothing, UnitRange} scenarios::Union{Nothing, UnitRange} net_metering_limit_kw::Real - interconnection_limit_kw::Real - + interconnection_limit_kw::Real + clean_energy_fraction_series::Array{<:Real,1} # Utilities renewable energy fraction. function ElectricUtility(; @@ -156,6 +156,11 @@ struct ElectricUtility outage_probabilities::Array{<:Real,1} = isempty(outage_durations) ? Float64[] : [1/length(outage_durations) for p_i in 1:length(outage_durations)], outage_time_steps::Union{Nothing, UnitRange} = isempty(outage_durations) ? nothing : 1:maximum(outage_durations), scenarios::Union{Nothing, UnitRange} = isempty(outage_durations) ? nothing : 1:length(outage_durations), + + ### Grid Renewable Energy Fraction Inputs ### + # Utilities renewable energy fraction. Can be scalar or timeseries (aligned with time_steps_per_hour) + clean_energy_fraction_series::Union{Real, Array{<:Real, 1}} = Float64[], + cambium_cef_col::String = "cef_load", # Column name for clean energy fraction in Cambium database ### Grid Climate Emissions Inputs ### # Climate Option 1 (Default): Use levelized emissions data from NREL's Cambium database by specifying the following fields: @@ -195,6 +200,51 @@ struct ElectricUtility cambium_emissions_region = "NA - Cambium data not used for climate emissions" # will be overwritten if Cambium is used if !is_MPC + # Initialize clean energy fraction series + clean_energy_series_dict = Dict{String, Union{Nothing, Array{<:Real, 1}}}() + if typeof(clean_energy_fraction_series) <: Real # user provided scalar value + clean_energy_series_dict["cef"] = repeat([clean_energy_fraction_series], 8760*time_steps_per_hour) + elseif length(clean_energy_fraction_series) == 1 # user provided array of one value + clean_energy_series_dict["cef"] = repeat(clean_energy_fraction_series, 8760*time_steps_per_hour) + elseif length(clean_energy_fraction_series) / time_steps_per_hour ≈ 8760 # user provided array with correct length + clean_energy_series_dict["cef"] = clean_energy_fraction_series + elseif length(clean_energy_fraction_series) > 1 && !(length(clean_energy_fraction_series) / time_steps_per_hour ≈ 8760) # user provided array with incorrect length + if length(clean_energy_fraction_series) == 8760 + clean_energy_series_dict["cef"] = repeat(clean_energy_fraction_series, inner=time_steps_per_hour) + @warn("Clean energy fraction series has been adjusted to align with time_steps_per_hour of $(time_steps_per_hour).") + else + throw(@error("The provided ElectricUtility clean energy fraction series does not match the time_steps_per_hour.")) + end + else + # Retrieve clean energy fraction data if not user-provided + if cambium_start_year < 2023 || cambium_start_year > 2050 + @warn("The cambium_start_year must be between 2023 and 2050. Setting cambium_start_year to 2024.") + cambium_start_year = 2024 # Must update annually + end + try + clean_energy_response_dict = cambium_clean_energy_fraction_profile( + scenario = cambium_scenario, + location_type = cambium_location_type, + latitude = latitude, + longitude = longitude, + start_year = cambium_start_year, + lifetime = cambium_levelization_years, + metric_col = cambium_cef_col, + grid_level = cambium_grid_level, + time_steps_per_hour = time_steps_per_hour, + load_year = load_year, + emissions_year = 2017 # Cambium data starts on a Sunday + ) + clean_energy_series_dict["cef"] = clean_energy_response_dict["clean_energy_fraction_series"] + cambium_emissions_region = clean_energy_response_dict["location"] + catch + @warn("Could not look up Cambium renewable energy fraction profile from point ($(latitude), $(longitude)). + Location is likely outside contiguous US or something went wrong with the Cambium API request. Setting clean energy fraction to zero.") + clean_energy_series_dict["cef"] = zeros(Float64, 8760*time_steps_per_hour) + end + end + + # Get AVERT emissions region if avert_emissions_region == "" region_abbr, meters_to_region = avert_region_abbreviation(latitude, longitude) @@ -350,13 +400,12 @@ struct ElectricUtility outage_time_steps, scenarios, net_metering_limit_kw, - interconnection_limit_kw + interconnection_limit_kw, + is_MPC ? Float64[] : clean_energy_series_dict["cef"] ) end end - - """ Determine the AVERT region abberviation for a given lat/lon pair. 1. Checks to see if given point is in an AVERT region @@ -624,4 +673,82 @@ function align_emission_with_load_year(; load_year::Int, emissions_year::Int, em end return emissions_profile_adj -end \ No newline at end of file +end + +""" + cambium_clean_energy_fraction_profile(; scenario::String, + location_type::String, + latitude::Real, + longitude::Real, + start_year::Int, + lifetime::Int, + grid_level::String, + time_steps_per_hour::Int=1, + load_year::Int=2017, + emissions_year::Int=2017) +This function constructs an API request to the Cambium database to retrieve the clean energy fraction data. +""" + +function cambium_clean_energy_fraction_profile(; scenario::String, + location_type::String, + latitude::Real, + longitude::Real, + start_year::Int, + lifetime::Int, + metric_col::String, + grid_level::String, + time_steps_per_hour::Int=1, + load_year::Int=2017, + emissions_year::Int=2017) + + url = "https://scenarioviewer.nrel.gov/api/get-levelized/" # Cambium API endpoint + project_uuid = "82460f06-548c-4954-b2d9-b84ba92d63e2" # Cambium 2022 project UUID + + # Construct the payload for the API request + payload = Dict( + "project_uuid" => project_uuid, + "scenario" => scenario, + "location_type" => location_type, + "latitude" => string(round(latitude, digits=3)), + "longitude" => string(round(longitude, digits=3)), + "start_year" => string(start_year), + "lifetime" => string(lifetime), + "discount_rate" => "0.0", + "time_type" => "hourly", + "metric_col" => metric_col, # Metric for clean energy fraction + "smoothing_method" => "rolling", + "gwp" => "100yrAR6", + "grid_level" => grid_level, + "ems_mass_units" => "lb" + ) + + try + # Make the API request + r = HTTP.get(url; query=payload) + response = JSON.parse(String(r.body)) + output = response["message"] + clean_energy_fraction = output["values"] + + # Align day of week of clean energy fraction profile with load year + clean_energy_fraction = align_emission_with_load_year(load_year=load_year, emissions_year=emissions_year, emissions_profile=clean_energy_fraction) + + if time_steps_per_hour > 1 + clean_energy_fraction = repeat(clean_energy_fraction, inner=time_steps_per_hour) + end + + # Return the clean energy fraction data in a dictionary + response_dict = Dict{String, Any}( + "description" => "Hourly clean energy fraction for applicable Cambium location and location_type, adjusted to align with load year $(load_year).", + "units" => "Fraction of clean energy", + "location" => output["location"], + "metric_col" => output["metric_col"], + "clean_energy_fraction_series" => clean_energy_fraction + ) + return response_dict + catch + return Dict{String, Any}( + "error" => "Could not look up Cambium clean energy fraction profile from point ($(latitude), $(longitude)). Location is likely outside contiguous US or something went wrong with the Cambium API request." + ) + end +end + diff --git a/src/core/reopt.jl b/src/core/reopt.jl index e9344b703..7859ef85e 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -551,6 +551,7 @@ function run_reopt(m::JuMP.AbstractModel, p::REoptInputs; organize_pvs=true) results = reopt_results(m, p) time_elapsed = time() - tstart @info "Results processing took $(round(time_elapsed, digits=3)) seconds." + @info "REopt results have been processed." results["status"] = status results["solver_seconds"] = opt_time From 71acc5e82e6465ce7a1e5720b45ba6cdb537ee33 Mon Sep 17 00:00:00 2001 From: pypapus Date: Wed, 31 Jul 2024 01:24:07 -0600 Subject: [PATCH 02/29] Fixed cambium API call --- .gitignore | 1 - src/core/electric_utility.jl | 9 ++++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 7abaf7641..5a0d8a762 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .vscode .DS_Store -gridRE_tests docs/build/ docs/Manifest.toml **.log \ No newline at end of file diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index ad13f7e2f..48f55e2dc 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -633,7 +633,7 @@ function cambium_emissions_profile(; scenario::String, response = JSON.parse(String(r.body)) # contains response["status"] output = response["message"] co2_emissions = output["values"] ./ 1000 # [lb / MWh] --> [lb / kWh] - + # Align day of week of emissions and load profiles (Cambium data starts on Sundays so assuming emissions_year=2017) co2_emissions = align_emission_with_load_year(load_year=load_year,emissions_year=emissions_year,emissions_profile=co2_emissions) @@ -702,7 +702,7 @@ function cambium_clean_energy_fraction_profile(; scenario::String, emissions_year::Int=2017) url = "https://scenarioviewer.nrel.gov/api/get-levelized/" # Cambium API endpoint - project_uuid = "82460f06-548c-4954-b2d9-b84ba92d63e2" # Cambium 2022 project UUID + project_uuid = "82460f06-548c-4954-b2d9-b84ba92d63e2" # Cambium 2022 project UUID # Construct the payload for the API request payload = Dict( @@ -728,14 +728,14 @@ function cambium_clean_energy_fraction_profile(; scenario::String, response = JSON.parse(String(r.body)) output = response["message"] clean_energy_fraction = output["values"] + clean_energy_fraction = map(x -> Real(x), clean_energy_fraction) # Convert to Float64 # Align day of week of clean energy fraction profile with load year clean_energy_fraction = align_emission_with_load_year(load_year=load_year, emissions_year=emissions_year, emissions_profile=clean_energy_fraction) - if time_steps_per_hour > 1 clean_energy_fraction = repeat(clean_energy_fraction, inner=time_steps_per_hour) end - + # Return the clean energy fraction data in a dictionary response_dict = Dict{String, Any}( "description" => "Hourly clean energy fraction for applicable Cambium location and location_type, adjusted to align with load year $(load_year).", @@ -751,4 +751,3 @@ function cambium_clean_energy_fraction_profile(; scenario::String, ) end end - From eca3f12026b43b0419b7b3342eb37ee322257e7e Mon Sep 17 00:00:00 2001 From: pypapus Date: Wed, 31 Jul 2024 23:20:31 -0600 Subject: [PATCH 03/29] Clean energy fraction added to scenarios --- src/core/scenario.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index cf8197df2..8e9babeb8 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -129,7 +129,8 @@ function Scenario(d::Dict; flex_hvac_from_json=false) off_grid_flag=settings.off_grid_flag, time_steps_per_hour=settings.time_steps_per_hour, analysis_years=financial.analysis_years, - load_year=electric_load.year + load_year=electric_load.year, + clean_energy_fraction_series = Float64[] ) elseif !(settings.off_grid_flag) electric_utility = ElectricUtility(; latitude=site.latitude, longitude=site.longitude, @@ -140,7 +141,8 @@ function Scenario(d::Dict; flex_hvac_from_json=false) off_grid_flag=settings.off_grid_flag, time_steps_per_hour=settings.time_steps_per_hour, analysis_years=financial.analysis_years, - load_year=electric_load.year + load_year=electric_load.year, + clean_energy_fraction_series = Float64[] ) elseif settings.off_grid_flag if haskey(d, "ElectricUtility") @@ -154,7 +156,8 @@ function Scenario(d::Dict; flex_hvac_from_json=false) emissions_factor_series_lb_CO2_per_kwh = 0, emissions_factor_series_lb_NOx_per_kwh = 0, emissions_factor_series_lb_SO2_per_kwh = 0, - emissions_factor_series_lb_PM25_per_kwh = 0 + emissions_factor_series_lb_PM25_per_kwh = 0, + clean_energy_fraction_series = Float64[] ) end From 80d1d3ef8872414c951f29bc3a53279c296b4300 Mon Sep 17 00:00:00 2001 From: pypapus Date: Thu, 1 Aug 2024 01:22:43 -0600 Subject: [PATCH 04/29] Include profile of grid clean energy contribution in kW --- src/core/bau_inputs.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/core/bau_inputs.jl b/src/core/bau_inputs.jl index e854c1107..db3d1ccd2 100644 --- a/src/core/bau_inputs.jl +++ b/src/core/bau_inputs.jl @@ -162,6 +162,9 @@ function BAUInputs(p::REoptInputs) heating_loads_served_by_tes = Dict{String,Array{String,1}}() unavailability = get_unavailability_by_tech(p.s, techs, p.time_steps) + # Calculate clean energy contribution (kW) + calculate_clean_energy_contribution(p.s.electric_utility, p.s.electric_load) + REoptInputs( bau_scenario, techs, @@ -392,4 +395,10 @@ function bau_outage_check(critical_loads_kw::AbstractArray, pv_kw_series::Abstra end return true, length(critical_loads_kw), generator_fuel_use_gal +end + + +function calculate_clean_energy_contribution(electric_utility::ElectricUtility, electric_load::ElectricLoad) + clean_energy_contribution = electric_utility.clean_energy_fraction_series .* electric_load.loads_kw + return clean_energy_contribution end \ No newline at end of file From c095d4801eb72116b89dd519c3d724ff2b68c118 Mon Sep 17 00:00:00 2001 From: pypapus Date: Fri, 2 Aug 2024 15:29:07 -0600 Subject: [PATCH 05/29] Updated help text and added checks for user-provided values --- src/core/electric_utility.jl | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index 48f55e2dc..4fe3053c4 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -26,6 +26,10 @@ cambium_levelization_years::Int = analysis_years, # Expected lifetime or analysis period of the intervention being studied. Emissions will be averaged over this period. cambium_grid_level::String = "enduse", # Options: ["enduse", "busbar"]. Busbar refers to point where bulk generating stations connect to grid; enduse refers to point of consumption (includes distribution loss rate). + ### Grid Clean Energy Fraction Inputs ### + cambium_cef_col::String = "cef_load", Options = ["cef_load", "cef_gen"] # Cef_load refers to the proportion of electricity consumed (load) that in a region that comes from clean energy sources; cef_gen refers to the proportion of electricity generated in a region that comes from clean energy sources. + clean_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Utilities renewable energy fraction. Can be scalar or timeseries (aligned with time_steps_per_hour). + # Climate Option 2: Use CO2 emissions data from the EPA's AVERT based on the AVERT emissions region and specify annual percent decrease co2_from_avert::Bool = false, # Default is to use Cambium data for CO2 grid emissions. Set to `true` to instead use data from the EPA's AVERT database. @@ -82,6 +86,11 @@ !!! note "Climate and Health Emissions Modeling" Climate and health-related emissions from grid electricity come from two different data sources and have different REopt inputs as described below. + **Grid Clean Energy Fraction** + - For sites in the contiguous United States: + - Default clean energy fraction data comes from NREL's Cambium database (Current version: 2022) + - By default, REopt uses *clean energy fraction* for the region in which the site is located. + **Climate Emissions** - For sites in the contiguous United States: - Default climate-related emissions factors come from NREL's Cambium database (Current version: 2022) @@ -126,7 +135,7 @@ struct ElectricUtility scenarios::Union{Nothing, UnitRange} net_metering_limit_kw::Real interconnection_limit_kw::Real - clean_energy_fraction_series::Array{<:Real,1} # Utilities renewable energy fraction. + clean_energy_fraction_series::Union{Real,Array{<:Real,1}} # Utilities renewable energy fraction. function ElectricUtility(; @@ -203,13 +212,25 @@ struct ElectricUtility # Initialize clean energy fraction series clean_energy_series_dict = Dict{String, Union{Nothing, Array{<:Real, 1}}}() if typeof(clean_energy_fraction_series) <: Real # user provided scalar value + if clean_energy_fraction_series < 0 || clean_energy_fraction_series > 1 + throw(@error("The provided ElectricUtility clean energy fraction value must be between 0 and 1.")) + end clean_energy_series_dict["cef"] = repeat([clean_energy_fraction_series], 8760*time_steps_per_hour) elseif length(clean_energy_fraction_series) == 1 # user provided array of one value + if clean_energy_fraction_series[1] < 0 || clean_energy_fraction_series[1] > 1 + throw(@error("The provided ElectricUtility clean energy fraction value must be between 0 and 1.")) + end clean_energy_series_dict["cef"] = repeat(clean_energy_fraction_series, 8760*time_steps_per_hour) elseif length(clean_energy_fraction_series) / time_steps_per_hour ≈ 8760 # user provided array with correct length + if any(x -> x < 0 || x > 1, clean_energy_fraction_series) + throw(@error("All values in the provided ElectricUtility clean energy fraction series must be between 0 and 1.")) + end clean_energy_series_dict["cef"] = clean_energy_fraction_series elseif length(clean_energy_fraction_series) > 1 && !(length(clean_energy_fraction_series) / time_steps_per_hour ≈ 8760) # user provided array with incorrect length if length(clean_energy_fraction_series) == 8760 + if any(x -> x < 0 || x > 1, clean_energy_fraction_series) + throw(@error("All values in the provided ElectricUtility clean energy fraction series must be between 0 and 1.")) + end clean_energy_series_dict["cef"] = repeat(clean_energy_fraction_series, inner=time_steps_per_hour) @warn("Clean energy fraction series has been adjusted to align with time_steps_per_hour of $(time_steps_per_hour).") else From b67ff48ee077410760151f0576951cf950f76ca6 Mon Sep 17 00:00:00 2001 From: pypapus Date: Fri, 2 Aug 2024 15:32:10 -0600 Subject: [PATCH 06/29] Corrected off-grid flag in scenario.jl --- src/core/scenario.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 8e9babeb8..918f1dd26 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -130,7 +130,6 @@ function Scenario(d::Dict; flex_hvac_from_json=false) time_steps_per_hour=settings.time_steps_per_hour, analysis_years=financial.analysis_years, load_year=electric_load.year, - clean_energy_fraction_series = Float64[] ) elseif !(settings.off_grid_flag) electric_utility = ElectricUtility(; latitude=site.latitude, longitude=site.longitude, @@ -142,7 +141,6 @@ function Scenario(d::Dict; flex_hvac_from_json=false) time_steps_per_hour=settings.time_steps_per_hour, analysis_years=financial.analysis_years, load_year=electric_load.year, - clean_energy_fraction_series = Float64[] ) elseif settings.off_grid_flag if haskey(d, "ElectricUtility") @@ -157,7 +155,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) emissions_factor_series_lb_NOx_per_kwh = 0, emissions_factor_series_lb_SO2_per_kwh = 0, emissions_factor_series_lb_PM25_per_kwh = 0, - clean_energy_fraction_series = Float64[] + clean_energy_fraction_series = 0 ) end From 9a8f09f7185c0d35f7345f49404a5455a88d43db Mon Sep 17 00:00:00 2001 From: pypapus Date: Sun, 11 Aug 2024 21:31:45 -0600 Subject: [PATCH 07/29] Edited to remove cef calculation using BAU electric load --- src/core/bau_inputs.jl | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/core/bau_inputs.jl b/src/core/bau_inputs.jl index db3d1ccd2..e854c1107 100644 --- a/src/core/bau_inputs.jl +++ b/src/core/bau_inputs.jl @@ -162,9 +162,6 @@ function BAUInputs(p::REoptInputs) heating_loads_served_by_tes = Dict{String,Array{String,1}}() unavailability = get_unavailability_by_tech(p.s, techs, p.time_steps) - # Calculate clean energy contribution (kW) - calculate_clean_energy_contribution(p.s.electric_utility, p.s.electric_load) - REoptInputs( bau_scenario, techs, @@ -395,10 +392,4 @@ function bau_outage_check(critical_loads_kw::AbstractArray, pv_kw_series::Abstra end return true, length(critical_loads_kw), generator_fuel_use_gal -end - - -function calculate_clean_energy_contribution(electric_utility::ElectricUtility, electric_load::ElectricLoad) - clean_energy_contribution = electric_utility.clean_energy_fraction_series .* electric_load.loads_kw - return clean_energy_contribution end \ No newline at end of file From 379eeabed46ffdf591060635bf100a8a4bb03d13 Mon Sep 17 00:00:00 2001 From: pypapus Date: Mon, 12 Aug 2024 01:19:40 -0600 Subject: [PATCH 08/29] Added new "cef_constraints" to calculate clean energy fraction of the grid electricity serving the load and charging the batteries. --- src/REopt.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/REopt.jl b/src/REopt.jl index 15df7e434..0804f5415 100644 --- a/src/REopt.jl +++ b/src/REopt.jl @@ -159,6 +159,7 @@ include("constraints/ghp_constraints.jl") include("constraints/steam_turbine_constraints.jl") include("constraints/renewable_energy_constraints.jl") include("constraints/emissions_constraints.jl") +include("constraints/cef_constraints.jl") include("mpc/structs.jl") include("mpc/scenario.jl") From 26f5cecea382b12c6293be77442713bd5d6c5270 Mon Sep 17 00:00:00 2001 From: pypapus Date: Mon, 12 Aug 2024 01:23:42 -0600 Subject: [PATCH 09/29] Added cef (kWh) from the grid in accounting for renewable energy percentage of load --- src/constraints/renewable_energy_constraints.jl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/constraints/renewable_energy_constraints.jl b/src/constraints/renewable_energy_constraints.jl index 3a6211756..5df9d56e7 100644 --- a/src/constraints/renewable_energy_constraints.jl +++ b/src/constraints/renewable_energy_constraints.jl @@ -3,6 +3,7 @@ add_re_elec_constraints(m,p) Function to add minimum and/or maximum renewable electricity (as percentage of load) constraints, if specified by user. +Function modified to include cef fraction from the grid in accounting for renewable energy percentage of load. !!! note When a single outage is modeled (using outage_start_time_step), renewable electricity calculations account for operations during this outage (e.g., the critical load is used during time_steps_without_grid) @@ -50,7 +51,14 @@ function add_re_elec_calcs(m,p) # )) # end - m[:AnnualREEleckWh] = @expression(m,p.hours_per_time_step * ( + # Function modified to add clean energy fraction to in annual renewable energy calculations + + calc_clean_grid_kWh(m, p) + + m[:AnnualREEleckWh] = @expression(m, p.hours_per_time_step * ( + (p.s.electric_utility.include_grid_clean_energy_in_re ? + sum(m[:grid_clean_energy_series_kw][ts] for ts in p.time_steps) : 0) + # Include grid clean energy fraction conditionally + + # Clean energy fraction of grid electricity to load and battery sum(p.production_factor[t,ts] * p.levelization_factor[t] * m[:dvRatedProduction][t,ts] * p.tech_renewable_energy_fraction[t] for t in p.techs.elec, ts in p.time_steps ) - #total RE elec generation, excl steam turbine From 4cce89e7cffaa7900520929fb04ae0f8bfad0fd6 Mon Sep 17 00:00:00 2001 From: pypapus Date: Mon, 12 Aug 2024 01:26:35 -0600 Subject: [PATCH 10/29] Added new function to calculate the clean_energy_fraction (kWh) grid electricity serving the load and the batteries --- src/constraints/cef_constraints.jl | 62 ++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/constraints/cef_constraints.jl diff --git a/src/constraints/cef_constraints.jl b/src/constraints/cef_constraints.jl new file mode 100644 index 000000000..fb7d5d0ef --- /dev/null +++ b/src/constraints/cef_constraints.jl @@ -0,0 +1,62 @@ +# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. + +""" +calc_clean_grid_kWh(m, p) + +This function calculates the clean energy fraction of the electricity from the electric utility +by multiplying the electricity from the grid used to charge the batteries and the electricity from the grid directly serving the load +by the clean energy fraction series. + +Returns: +- clean_energy_fraction: The clean energy fraction of the grid electricity. +""" +function calc_clean_grid_kWh(m, p) + # Calculate the grid electricity used to charge the batteries and directly serve the load + m[:CleanGridToLoad], m[:CleanGridToBatt] = calc_grid_to_load(m, p) + + # Calculate the clean energy fraction from the electric utility + m[:grid_clean_energy_series_kw] = @expression(m, [ + ts in p.time_steps], (m[:CleanGridToLoad][ts] + m[:CleanGridToBatt][ts]) * p.s.electric_utility.clean_energy_fraction_series[ts] + ) +end + + +""" +calc_grid_to_load(m, p) + +This function calculates, for each timestep: +1. The electricity from the grid used to charge the batteries, accounting for battery losses. +2. The electricity from the grid directly serving the load. + +Returns: +- CleanGridToLoad: The electricity from the grid directly serving the load. +- CleanGridToBatt: The electricity from the grid used to charge the batteries, accounting for losses. +""" +function calc_grid_to_load(m, p) + if !isempty(p.s.storage.types.elec) + # Calculate the grid to load through the battery, accounting for the battery losses + m[:CleanGridToBatt] = @expression(m, [ + ts in p.time_steps], p.hours_per_time_step * sum( + m[:dvGridToStorage][b, ts] * p.s.storage.attr[b].charge_efficiency * p.s.storage.attr[b].discharge_efficiency + for b in p.s.storage.types.elec) + ) + else + m[:CleanGridToBatt] = zeros(length(p.time_steps)) + end + + # Validating battery efficiency + # m[:batteryEfficiency] = @expression(m, [ + # b in p.s.storage.types.elec], 1 - p.s.storage.attr[b].charge_efficiency * p.s.storage.attr[b].discharge_efficiency) + + # Calculate the grid serving load not through the battery + m[:CleanGridToLoad] = @expression(m, [ + ts in p.time_steps], p.hours_per_time_step * ( + sum(m[:dvGridPurchase][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) - + sum(m[:dvGridToStorage][b, ts] for b in p.s.storage.types.elec) + )) + + return m[:CleanGridToLoad], m[:CleanGridToBatt] +end + + + From 6500b40c874a0907a90f36d49a92d822944b015f Mon Sep 17 00:00:00 2001 From: pypapus Date: Mon, 12 Aug 2024 01:30:37 -0600 Subject: [PATCH 11/29] Added new input to include grid cef (kWh) in renewable energy percentage when true --- src/core/electric_utility.jl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index 4fe3053c4..88396c33d 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -135,7 +135,8 @@ struct ElectricUtility scenarios::Union{Nothing, UnitRange} net_metering_limit_kw::Real interconnection_limit_kw::Real - clean_energy_fraction_series::Union{Real,Array{<:Real,1}} # Utilities renewable energy fraction. + clean_energy_fraction_series::Array{<:Real,1} # Utilities renewable energy fraction. + include_grid_clean_energy_in_re::Bool function ElectricUtility(; @@ -158,6 +159,7 @@ struct ElectricUtility outage_start_time_step::Int=0, # for modeling a single outage, with critical load spliced into the baseline load ... outage_end_time_step::Int=0, # ... utiltity production_factor = 0 during the outage allow_simultaneous_export_import::Bool=true, # if true the site has two meters (in effect) + include_grid_clean_energy_in_re::Bool=true, # if true, the clean energy fraction of the grid electricity is included in the renewable electricity calculations # next 5 variables below used for minimax the expected outage cost, # with max taken over outage start time, expectation taken over outage duration outage_start_time_steps::Array{Int,1}=Int[], # we include in the minimization the maximum outage cost over outage start times @@ -264,6 +266,10 @@ struct ElectricUtility clean_energy_series_dict["cef"] = zeros(Float64, 8760*time_steps_per_hour) end end + + # save clean_energy_series_dict["cef"] as csv + # clean_energy_df = DataFrame(cef = clean_energy_series_dict["cef"]) + # CSV.write("clean_energy_fraction_series.csv", clean_energy_df) # Get AVERT emissions region @@ -422,7 +428,8 @@ struct ElectricUtility scenarios, net_metering_limit_kw, interconnection_limit_kw, - is_MPC ? Float64[] : clean_energy_series_dict["cef"] + is_MPC ? Float64[] : clean_energy_series_dict["cef"], + include_grid_clean_energy_in_re ) end end From 98d22b52e4202c557aa46f3985e87b0bdbe0f500 Mon Sep 17 00:00:00 2001 From: pypapus Date: Mon, 12 Aug 2024 01:32:01 -0600 Subject: [PATCH 12/29] Updated function adds grid clean energy kWh serving the load to results --- src/results/electric_utility.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/results/electric_utility.jl b/src/results/electric_utility.jl index d5b21f467..cafa3138e 100644 --- a/src/results/electric_utility.jl +++ b/src/results/electric_utility.jl @@ -44,6 +44,10 @@ function add_electric_utility_results(m::JuMP.AbstractModel, p::AbstractInputs, Year1UtilityEnergy = p.hours_per_time_step * sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for ts in p.time_steps, tier in 1:p.s.electric_tariff.n_energy_tiers) r["annual_energy_supplied_kwh"] = round(value(Year1UtilityEnergy), digits=2) + + calc_clean_grid_kWh(m, p) + + r["clean_grid_to_load_series_kw"] = round.(value.([m[:grid_clean_energy_series_kw][ts] for ts in p.time_steps]), digits=3) if !isempty(p.s.storage.types.elec) GridToLoad = (sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) From e3b0300bbe5dfa4bf5846936aba806e2c6759c59 Mon Sep 17 00:00:00 2001 From: pypapus Date: Thu, 15 Aug 2024 09:48:25 -0600 Subject: [PATCH 13/29] removed "hours_per_time_step" to use model time step. --- src/constraints/cef_constraints.jl | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/constraints/cef_constraints.jl b/src/constraints/cef_constraints.jl index fb7d5d0ef..8551f409b 100644 --- a/src/constraints/cef_constraints.jl +++ b/src/constraints/cef_constraints.jl @@ -32,11 +32,12 @@ Returns: - CleanGridToLoad: The electricity from the grid directly serving the load. - CleanGridToBatt: The electricity from the grid used to charge the batteries, accounting for losses. """ + function calc_grid_to_load(m, p) if !isempty(p.s.storage.types.elec) # Calculate the grid to load through the battery, accounting for the battery losses m[:CleanGridToBatt] = @expression(m, [ - ts in p.time_steps], p.hours_per_time_step * sum( + ts in p.time_steps], sum( m[:dvGridToStorage][b, ts] * p.s.storage.attr[b].charge_efficiency * p.s.storage.attr[b].discharge_efficiency for b in p.s.storage.types.elec) ) @@ -44,13 +45,9 @@ function calc_grid_to_load(m, p) m[:CleanGridToBatt] = zeros(length(p.time_steps)) end - # Validating battery efficiency - # m[:batteryEfficiency] = @expression(m, [ - # b in p.s.storage.types.elec], 1 - p.s.storage.attr[b].charge_efficiency * p.s.storage.attr[b].discharge_efficiency) - # Calculate the grid serving load not through the battery m[:CleanGridToLoad] = @expression(m, [ - ts in p.time_steps], p.hours_per_time_step * ( + ts in p.time_steps], ( sum(m[:dvGridPurchase][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) - sum(m[:dvGridToStorage][b, ts] for b in p.s.storage.types.elec) )) From 248481b60ee080865dc85b28a32fbedfce7cabf9 Mon Sep 17 00:00:00 2001 From: pypapus Date: Thu, 15 Aug 2024 10:04:01 -0600 Subject: [PATCH 14/29] Added "annual_clean_grid_to_load_kwh" in results --- src/results/electric_utility.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/results/electric_utility.jl b/src/results/electric_utility.jl index cafa3138e..09d1375fb 100644 --- a/src/results/electric_utility.jl +++ b/src/results/electric_utility.jl @@ -48,7 +48,9 @@ function add_electric_utility_results(m::JuMP.AbstractModel, p::AbstractInputs, calc_clean_grid_kWh(m, p) r["clean_grid_to_load_series_kw"] = round.(value.([m[:grid_clean_energy_series_kw][ts] for ts in p.time_steps]), digits=3) - + #sum to find the annual clean grid to load kWh + r["annual_clean_grid_to_load_kwh"] = round(sum(r["clean_grid_to_load_series_kw"]), digits=2) + if !isempty(p.s.storage.types.elec) GridToLoad = (sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) - sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) From fbf769100b3f62df663ef6a475db4e1bda96ccc7 Mon Sep 17 00:00:00 2001 From: pypapus Date: Fri, 16 Aug 2024 12:43:35 -0600 Subject: [PATCH 15/29] Combined cambium_emissions_profile() and cambium_clean_energy_fraction_profile() into one function --- src/core/electric_utility.jl | 138 ++++++++++------------------------- 1 file changed, 37 insertions(+), 101 deletions(-) diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index 88396c33d..901d77203 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -617,23 +617,25 @@ end emissions_year::Int=2017, grid_level::String) +This function constructs an API request to the Cambium database to retrieve either emissions data or clean energy fraction data depending on the `metric_col` provided. This function gets levelized grid CO2 or CO2e emission rate profiles (1-year time series) from the Cambium dataset. The returned profiles are adjusted for day of week alignment with the provided "load_year" (Cambium profiles always start on a Sunday.) This function is also used for the /cambium_emissions_profile endpoint in the REopt API, in particular for the webtool to display grid emissions defaults before running REopt. + """ -function cambium_emissions_profile(; scenario::String, - location_type::String, - latitude::Real, - longitude::Real, - start_year::Int, - lifetime::Int, - metric_col::String, - grid_level::String, - time_steps_per_hour::Int=1, - load_year::Int=2017, - emissions_year::Int=2017 - ) +function cambium_profile(; scenario::String, + location_type::String, + latitude::Real, + longitude::Real, + start_year::Int, + lifetime::Int, + metric_col::String, + grid_level::String, + time_steps_per_hour::Int=1, + load_year::Int=2017, + emissions_year::Int=2017 + ) url = "https://scenarioviewer.nrel.gov/api/get-levelized/" # Production project_uuid = "82460f06-548c-4954-b2d9-b84ba92d63e2" # Cambium 2022 @@ -660,29 +662,40 @@ function cambium_emissions_profile(; scenario::String, r = HTTP.get(url; query=payload) response = JSON.parse(String(r.body)) # contains response["status"] output = response["message"] - co2_emissions = output["values"] ./ 1000 # [lb / MWh] --> [lb / kWh] + data_series = output["values"] + # co2_emissions = output["values"] ./ 1000 # [lb / MWh] --> [lb / kWh] + + # Convert from [lb/MWh] to [lb/kWh] if the metric is emissions-related + if metric_col == "lrmer_co2e" + data_series = data_series ./ 1000 + end # Align day of week of emissions and load profiles (Cambium data starts on Sundays so assuming emissions_year=2017) - co2_emissions = align_emission_with_load_year(load_year=load_year,emissions_year=emissions_year,emissions_profile=co2_emissions) + data_series = align_emission_with_load_year(load_year=load_year, emissions_year=emissions_year, emissions_profile=data_series) if time_steps_per_hour > 1 - co2_emissions = repeat(co2_emissions, inner=time_steps_per_hour) + data_series = repeat(data_series, inner=time_steps_per_hour) end - + + description, units = if metric_col == "lrmer_co2e" + ("Hourly CO2 (or CO2e) grid emissions factors for applicable Cambium location and location_type, adjusted to align with load year $(load_year).", "Pounds emissions per kWh") + else + ("Hourly clean energy fraction for applicable Cambium location and location_type, adjusted to align with load year $(load_year).", "Fraction of clean energy") + end + response_dict = Dict{String, Any}( - "description" => "Hourly CO2 (or CO2e) grid emissions factors for applicable Cambium location and location_type, adjusted to align with load year $(load_year).", - "units" => "Pounds emissions per kWh", + "description" => description, + "units" => units, "location" => output["location"], "metric_col" => output["metric_col"], - "emissions_factor_series_lb_CO2_per_kwh" => co2_emissions + "data_series" => data_series ) return response_dict catch return Dict{String, Any}( - "error"=> - "Could not look up Cambium emissions profile from point ($(latitude), $(longitude)). - Location is likely outside contiguous US or something went wrong with the Cambium API request." - ) + "error" => "Could not look up Cambium profile from point ($(latitude), $(longitude)). + Location is likely outside contiguous US or something went wrong with the Cambium API request." + ) end end @@ -701,81 +714,4 @@ function align_emission_with_load_year(; load_year::Int, emissions_year::Int, em end return emissions_profile_adj -end - -""" - cambium_clean_energy_fraction_profile(; scenario::String, - location_type::String, - latitude::Real, - longitude::Real, - start_year::Int, - lifetime::Int, - grid_level::String, - time_steps_per_hour::Int=1, - load_year::Int=2017, - emissions_year::Int=2017) -This function constructs an API request to the Cambium database to retrieve the clean energy fraction data. -""" - -function cambium_clean_energy_fraction_profile(; scenario::String, - location_type::String, - latitude::Real, - longitude::Real, - start_year::Int, - lifetime::Int, - metric_col::String, - grid_level::String, - time_steps_per_hour::Int=1, - load_year::Int=2017, - emissions_year::Int=2017) - - url = "https://scenarioviewer.nrel.gov/api/get-levelized/" # Cambium API endpoint - project_uuid = "82460f06-548c-4954-b2d9-b84ba92d63e2" # Cambium 2022 project UUID - - # Construct the payload for the API request - payload = Dict( - "project_uuid" => project_uuid, - "scenario" => scenario, - "location_type" => location_type, - "latitude" => string(round(latitude, digits=3)), - "longitude" => string(round(longitude, digits=3)), - "start_year" => string(start_year), - "lifetime" => string(lifetime), - "discount_rate" => "0.0", - "time_type" => "hourly", - "metric_col" => metric_col, # Metric for clean energy fraction - "smoothing_method" => "rolling", - "gwp" => "100yrAR6", - "grid_level" => grid_level, - "ems_mass_units" => "lb" - ) - - try - # Make the API request - r = HTTP.get(url; query=payload) - response = JSON.parse(String(r.body)) - output = response["message"] - clean_energy_fraction = output["values"] - clean_energy_fraction = map(x -> Real(x), clean_energy_fraction) # Convert to Float64 - - # Align day of week of clean energy fraction profile with load year - clean_energy_fraction = align_emission_with_load_year(load_year=load_year, emissions_year=emissions_year, emissions_profile=clean_energy_fraction) - if time_steps_per_hour > 1 - clean_energy_fraction = repeat(clean_energy_fraction, inner=time_steps_per_hour) - end - - # Return the clean energy fraction data in a dictionary - response_dict = Dict{String, Any}( - "description" => "Hourly clean energy fraction for applicable Cambium location and location_type, adjusted to align with load year $(load_year).", - "units" => "Fraction of clean energy", - "location" => output["location"], - "metric_col" => output["metric_col"], - "clean_energy_fraction_series" => clean_energy_fraction - ) - return response_dict - catch - return Dict{String, Any}( - "error" => "Could not look up Cambium clean energy fraction profile from point ($(latitude), $(longitude)). Location is likely outside contiguous US or something went wrong with the Cambium API request." - ) - end -end +end \ No newline at end of file From 29518dcc5c4bf79d17be7cae648583d8bb94b40c Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 10 Sep 2024 08:57:51 -0600 Subject: [PATCH 16/29] Update electric_utility.jl --- src/core/electric_utility.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index 901d77203..e5a1a3f4e 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -647,7 +647,7 @@ function cambium_profile(; scenario::String, # "location" => "Colorado", # e.g., Contiguous United States, Colorado, Kansas, p33, p34 "latitude" => string(round(latitude, digits=3)), "longitude" => string(round(longitude, digits=3)), - "start_year" => string(start_year), # biennial from 2022-2050 (data year covers nominal year and years proceeding; e.g., 2040 values cover time range starting in 2036) + "start_year" => string(start_year), # biennial from 2022-2050 (data year covers nominal year and years proceeding; e.g., 2040 values cover time range starting in 2036) # The 2023 release has five-year time steps from 2025 through 2050 "lifetime" => string(lifetime), # Integer 1 or greater (Default 25 yrs) "discount_rate" => "0.0", # Zero = simple average (a pwf with discount rate gets applied to projected CO2 costs, but not quantity.) "time_type" => "hourly", # hourly or annual From 3a4bafd8c6e7a5d9a7ebd17f8e9250b4f3047a8e Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 5 Nov 2024 11:29:19 -0700 Subject: [PATCH 17/29] spelling --- src/core/electric_utility.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index d3f7eac72..54d444f01 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -19,7 +19,7 @@ ### Grid Climate Emissions Inputs ### # Climate Option 1 (Default): Use levelized emissions data from NREL's Cambium database by specifying the following fields: cambium_scenario::String = "Mid-case", # Cambium Scenario for evolution of electricity sector (see Cambium documentation for descriptions). - ## Options: ["Mid-case", "Mid-case with tax credit expiration", "Low renewable energy cost", "Low renewable energy cost with tax credit expiration", "High renewable energy cost", "High electrification", "Low natrual gas prices", "High natrual gas prices", "Mid-case with 95% decarbonization by 2050", "Mid-case with 100% decarbonization by 2035"] + ## Options: ["Mid-case", "Mid-case with tax credit expiration", "Low renewable energy cost", "Low renewable energy cost with tax credit expiration", "High renewable energy cost", "High electrification", "Low natural gas prices", "High natural gas prices", "Mid-case with 95% decarbonization by 2050", "Mid-case with 100% decarbonization by 2035"] cambium_location_type::String = "GEA Regions", # Geographic boundary at which emissions are calculated. Options: ["Nations", "GEA Regions", "States"] cambium_metric_col::String = "lrmer_co2e", # Emissions metric used. Default: "lrmer_co2e" - Long-run marginal emissions rate for CO2-equivalant, combined combustion and pre-combustion emissions rates. Options: See metric definitions and names in the Cambium documentation cambium_start_year::Int = 2024, # First year of operation of system. Emissions will be levelized starting in this year for the duration of cambium_levelization_years. # Options: any year 2023 through 2050. From dec6733340a6235fbd3c812ae1c3f1a17a29dcd7 Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 12 Nov 2024 11:10:37 -0700 Subject: [PATCH 18/29] change cambium_metric_col to cambium_co2_metric --- src/core/electric_utility.jl | 6 +++--- test/scenarios/chp_payback.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index 54d444f01..68bcb7974 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -21,7 +21,7 @@ cambium_scenario::String = "Mid-case", # Cambium Scenario for evolution of electricity sector (see Cambium documentation for descriptions). ## Options: ["Mid-case", "Mid-case with tax credit expiration", "Low renewable energy cost", "Low renewable energy cost with tax credit expiration", "High renewable energy cost", "High electrification", "Low natural gas prices", "High natural gas prices", "Mid-case with 95% decarbonization by 2050", "Mid-case with 100% decarbonization by 2035"] cambium_location_type::String = "GEA Regions", # Geographic boundary at which emissions are calculated. Options: ["Nations", "GEA Regions", "States"] - cambium_metric_col::String = "lrmer_co2e", # Emissions metric used. Default: "lrmer_co2e" - Long-run marginal emissions rate for CO2-equivalant, combined combustion and pre-combustion emissions rates. Options: See metric definitions and names in the Cambium documentation + cambium_co2_metric::String = "lrmer_co2e", # Emissions metric used. Default: "lrmer_co2e" - Long-run marginal emissions rate for CO2-equivalant, combined combustion and pre-combustion emissions rates. Options: See metric definitions and names in the Cambium documentation cambium_start_year::Int = 2024, # First year of operation of system. Emissions will be levelized starting in this year for the duration of cambium_levelization_years. # Options: any year 2023 through 2050. cambium_levelization_years::Int = analysis_years, # Expected lifetime or analysis period of the intervention being studied. Emissions will be averaged over this period. cambium_grid_level::String = "enduse", # Options: ["enduse", "busbar"]. Busbar refers to point where bulk generating stations connect to grid; enduse refers to point of consumption (includes distribution loss rate). @@ -178,7 +178,7 @@ struct ElectricUtility cambium_scenario::String = "Mid-case", # Cambium Scenario for evolution of electricity sector (see Cambium documentation for descriptions). ## Options: ["Mid-case", "Mid-case with tax credit expiration", "Low renewable energy cost", "Low renewable energy cost with tax credit expiration", "High renewable energy cost", "High electrification", "Low natrual gas prices", "High natrual gas prices", "Mid-case with 95% decarbonization by 2050", "Mid-case with 100% decarbonization by 2035"] cambium_location_type::String = "GEA Regions", # Geographic boundary at which emissions are calculated. Options: ["Nations", "GEA Regions", "States"] - cambium_metric_col::String = "lrmer_co2e", # Emissions metric. Default: "lrmer_co2e" - Long-run marginal emissions rate for CO2-equivalant, combined combustion and pre-combustion emissions rates. Options: See metric definitions and names in the Cambium documentation + cambium_co2_metric::String = "lrmer_co2e", # Emissions metric. Default: "lrmer_co2e" - Long-run marginal emissions rate for CO2-equivalant, combined combustion and pre-combustion emissions rates. Options: See metric definitions and names in the Cambium documentation cambium_start_year::Int = 2024, # First year of operation of system. # Options: any year now through 2050. cambium_levelization_years::Int = analysis_years, # Expected lifetime or analysis period of the intervention being studied. Emissions will be averaged over this period. cambium_grid_level::String = "enduse", # Busbar refers to point where bulk generating station connects to grid; enduse refers to point of consumption (includes distribution loss rate) @@ -332,7 +332,7 @@ struct ElectricUtility longitude = longitude, start_year = cambium_start_year, lifetime = cambium_levelization_years, - metric_col = cambium_metric_col, + metric_col = cambium_co2_metric, time_steps_per_hour = time_steps_per_hour, load_year = load_year, emissions_year = 2017, # because Cambium data always starts on a Sunday diff --git a/test/scenarios/chp_payback.json b/test/scenarios/chp_payback.json index ae8e05353..f20dc8696 100644 --- a/test/scenarios/chp_payback.json +++ b/test/scenarios/chp_payback.json @@ -47,7 +47,7 @@ "avert_emissions_region": "New England", "cambium_location_type": "GEA Regions", "cambium_levelization_years": 1, - "cambium_metric_col": "lrmer_co2e", + "cambium_co2_metric": "lrmer_co2e", "cambium_scenario": "Mid-case", "cambium_grid_level": "enduse" }, From 3916ba8b036e0c0125d14b351a6f7a4f1acb8f30 Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 12 Nov 2024 14:39:59 -0700 Subject: [PATCH 19/29] Change **cambium_emissions_region** to **cambium_region** and clean up markdown --- CHANGELOG.md | 5 ++ src/core/electric_utility.jl | 112 +++++++++++++++----------------- src/results/electric_utility.jl | 4 +- test/runtests.jl | 6 +- 4 files changed, 63 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14896e890..9483b7378 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,11 @@ Classify the change according to the following categories: ### Removed +## gridRE-dev +### Changed +- Changed name of ElectricUtility input **cambium_metric_col** to **cambium_co2_metric**, to distinguish between the CO2 and clean energy fraction metrics +- Changed name of ElectricUtility **cambium_emissions_region** to **cambium_region** + ## Develop ### Added - Battery residual value if choosing replacement strategy for degradation diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index 68bcb7974..f1f182e01 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -16,19 +16,17 @@ outage_durations::Array{Int,1}=Int[], # one-to-one with outage_probabilities, outage_durations can be a random variable outage_probabilities::Array{R,1} where R<:Real = [1.0], - ### Grid Climate Emissions Inputs ### - # Climate Option 1 (Default): Use levelized emissions data from NREL's Cambium database by specifying the following fields: + ### Cambium Emissions and Clean Energy Inputs ### cambium_scenario::String = "Mid-case", # Cambium Scenario for evolution of electricity sector (see Cambium documentation for descriptions). ## Options: ["Mid-case", "Mid-case with tax credit expiration", "Low renewable energy cost", "Low renewable energy cost with tax credit expiration", "High renewable energy cost", "High electrification", "Low natural gas prices", "High natural gas prices", "Mid-case with 95% decarbonization by 2050", "Mid-case with 100% decarbonization by 2035"] - cambium_location_type::String = "GEA Regions", # Geographic boundary at which emissions are calculated. Options: ["Nations", "GEA Regions", "States"] - cambium_co2_metric::String = "lrmer_co2e", # Emissions metric used. Default: "lrmer_co2e" - Long-run marginal emissions rate for CO2-equivalant, combined combustion and pre-combustion emissions rates. Options: See metric definitions and names in the Cambium documentation - cambium_start_year::Int = 2024, # First year of operation of system. Emissions will be levelized starting in this year for the duration of cambium_levelization_years. # Options: any year 2023 through 2050. - cambium_levelization_years::Int = analysis_years, # Expected lifetime or analysis period of the intervention being studied. Emissions will be averaged over this period. + cambium_location_type::String = "GEA Regions", # Geographic boundary at which emissions and clean energy fraction are calculated. Options: ["Nations", "GEA Regions", "States"] + cambium_start_year::Int = 2024, # First year of operation of system. Emissions and clean energy fraction will be levelized starting in this year for the duration of cambium_levelization_years. # Options: any year 2023 through 2050. + cambium_levelization_years::Int = analysis_years, # Expected lifetime or analysis period of the intervention being studied. Emissions and clean energy fraction will be averaged over this period. cambium_grid_level::String = "enduse", # Options: ["enduse", "busbar"]. Busbar refers to point where bulk generating stations connect to grid; enduse refers to point of consumption (includes distribution loss rate). - ### Grid Clean Energy Fraction Inputs ### - cambium_cef_col::String = "cef_load", Options = ["cef_load", "cef_gen"] # Cef_load refers to the proportion of electricity consumed (load) that in a region that comes from clean energy sources; cef_gen refers to the proportion of electricity generated in a region that comes from clean energy sources. - clean_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Utilities renewable energy fraction. Can be scalar or timeseries (aligned with time_steps_per_hour). + ### Grid Climate Emissions Inputs ### + # Climate Option 1 (Default): Use levelized emissions data from NREL's Cambium database by specifying the following fields: + cambium_co2_metric::String = "lrmer_co2e", # Emissions metric used. Default: "lrmer_co2e" - Long-run marginal emissions rate for CO2-equivalant, combined combustion and pre-combustion emissions rates. Options: See metric definitions and names in the Cambium documentation # Climate Option 2: Use CO2 emissions data from the EPA's AVERT based on the AVERT emissions region and specify annual percent decrease co2_from_avert::Bool = false, # Default is to use Cambium data for CO2 grid emissions. Set to `true` to instead use data from the EPA's AVERT database. @@ -52,6 +50,11 @@ emissions_factor_NOx_decrease_fraction::Real = EMISSIONS_DECREASE_DEFAULTS["NOx"], emissions_factor_SO2_decrease_fraction::Real = EMISSIONS_DECREASE_DEFAULTS["SO2"], emissions_factor_PM25_decrease_fraction::Real = EMISSIONS_DECREASE_DEFAULTS["PM25"] + + ### Grid Clean Energy Fraction Inputs ### + cambium_cef_metric::String = "cef_load", # Options = ["cef_load", "cef_gen"] # cef_load is the fraction of generation that is clean, for the generation that is allocated to a region’s end-use load; cef_gen is the fraction of generation that is clean within a region + clean_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Utilities renewable energy fraction. Can be scalar or timeseries (aligned with time_steps_per_hour). + include_grid_clean_energy_in_re::Bool=true, # if true, the clean energy fraction of the grid electricity is included in the site's renewable electricity calculations ``` !!! note "Outage modeling" @@ -83,38 +86,39 @@ This constructor is intended to be used with latitude/longitude arguments provided for the non-MPC case and without latitude/longitude arguments provided for the MPC case. -!!! note "Climate and Health Emissions Modeling" +!!! note "Climate and Health Emissions and Grid Clean Energy Modeling" Climate and health-related emissions from grid electricity come from two different data sources and have different REopt inputs as described below. - **Grid Clean Energy Fraction** - - For sites in the contiguous United States: - - Default clean energy fraction data comes from NREL's Cambium database (Current version: 2022) - - By default, REopt uses *clean energy fraction* for the region in which the site is located. - **Climate Emissions** - - For sites in the contiguous United States: + - For sites in the contiguous United States (CONUS): - Default climate-related emissions factors come from NREL's Cambium database (Current version: 2022) - By default, REopt uses *levelized long-run marginal emission rates for CO2-equivalent (CO2e) emissions* for the region in which the site is located. By default, the emissions rates are levelized over the analysis period (e.g., from 2024 through 2048 for a 25-year analysis) - The inputs to the Cambium API request can be modified by the user based on emissions accounting needs (e.g., can change "lifetime" to 1 to analyze a single year's emissions) - Note for analysis periods extending beyond 2050: Values beyond 2050 are estimated with the 2050 values. Analysts are advised to use caution when selecting values that place significant weight on 2050 (e.g., greater than 50%) - Users can alternatively choose to use emissions factors from the EPA's AVERT by setting `co2_from_avert` to `true` - - For Alaska and HI: Grid CO2e emissions rates for AK and HI come from the eGRID database. These are single values repeated throughout the year. The default annual emissions_factor_CO2_decrease_fraction will be applied to this rate to account for future greening of the grid. - - For sites outside of the United States: We currently do not have default grid emissions rates for sites outside of the U.S. For these sites, users must supply custom emissions factor series (e.g., emissions_factor_series_lb_CO2_per_kwh) and projected emissions decreases (e.g., emissions_factor_CO2_decrease_fraction). + - For Alaska and HI: Grid CO2e emissions rates come from the eGRID database. These are single values repeated throughout the year. The default annual `emissions_factor_CO2_decrease_fraction` will be applied to this rate to account for future greening of the grid. + - For sites outside of the United States: REopt does not have default grid emissions rates for sites outside of the U.S. For these sites, users must supply custom emissions factor series (`emissions_factor_series_lb_CO2_per_kwh`) and projected emissions decreases (`emissions_factor_CO2_decrease_fraction`). **Health Emissions** - - For sites in the contiguous United States: health-related emissions factors (PM2.5, SO2, and NOx) come from the EPA's AVERT database. + - For sites in CONUS: health-related emissions factors (PM2.5, SO2, and NOx) come from the EPA's AVERT database. + - For Alaska and HI: Grid health emissions rates come from the eGRID database. These are single values repeated throughout the year. The default annual `emissions_factor_XXX_decrease_fraction` will be applied to this rate to account for future greening of the grid. - The default `avert_emissions_region` input is determined by the site's latitude and longitude. Alternatively, you may input the desired AVERT `avert_emissions_region`, which must be one of: - ["California", "Central", "Florida", "Mid-Atlantic", "Midwest", "Carolinas", "New England", - "Northwest", "New York", "Rocky Mountains", "Southeast", "Southwest", "Tennessee", "Texas", - "Alaska", "Hawaii (except Oahu)", "Hawaii (Oahu)"] + ["California", "Central", "Florida", "Mid-Atlantic", "Midwest", "Carolinas", "New England","Northwest", "New York", "Rocky Mountains", "Southeast", "Southwest", "Tennessee", "Texas", "Alaska", "Hawaii (except Oahu)", "Hawaii (Oahu)"] + - For sites outside of the United States: REopt does not have default grid emissions rates for sites outside of the U.S. For these sites, users must supply custom emissions factor series (e.g., `emissions_factor_series_lb_NOx_per_kwh`) and projected emissions decreases (e.g., `emissions_factor_NOx_decrease_fraction`). + + **Grid Clean Energy Fraction** + - For sites in CONUS: + - Default clean energy fraction data comes from NREL's Cambium database (Current version: 2022) + - By default, REopt uses *clean energy fraction* for the region in which the site is located. + - For sites outside of CONUS: REopt does not have default grid clean energy fraction data. Users must supply a custom `clean_energy_fraction_series` """ struct ElectricUtility avert_emissions_region::String # AVERT emissions region distance_to_avert_emissions_region_meters::Real - cambium_emissions_region::String # Determined by location (lat long) and cambium_location_type + cambium_region::String # Determined by location (lat long) and cambium_location_type emissions_factor_series_lb_CO2_per_kwh::Array{<:Real,1} emissions_factor_series_lb_NOx_per_kwh::Array{<:Real,1} emissions_factor_series_lb_SO2_per_kwh::Array{<:Real,1} @@ -135,7 +139,7 @@ struct ElectricUtility scenarios::Union{Nothing, UnitRange} net_metering_limit_kw::Real interconnection_limit_kw::Real - clean_energy_fraction_series::Array{<:Real,1} # Utilities renewable energy fraction. + clean_energy_fraction_series::Array{<:Real,1} # Utility renewable energy fraction. include_grid_clean_energy_in_re::Bool function ElectricUtility(; @@ -156,10 +160,11 @@ struct ElectricUtility # Inputs for ElectricUtility net_metering_limit_kw::Real = 0, # Upper limit on the total capacity of technologies that can participate in net metering agreement. interconnection_limit_kw::Real = 1.0e9, + allow_simultaneous_export_import::Bool=true, # if true the site has two meters (in effect) + outage_start_time_step::Int=0, # for modeling a single outage, with critical load spliced into the baseline load ... outage_end_time_step::Int=0, # ... utility production_factor = 0 during the outage - allow_simultaneous_export_import::Bool=true, # if true the site has two meters (in effect) - include_grid_clean_energy_in_re::Bool=true, # if true, the clean energy fraction of the grid electricity is included in the renewable electricity calculations + # next 5 variables below used for minimax the expected outage cost, # with max taken over outage start time, expectation taken over outage duration outage_start_time_steps::Array{Int,1}=Int[], # we include in the minimization the maximum outage cost over outage start times @@ -168,47 +173,36 @@ struct ElectricUtility outage_time_steps::Union{Nothing, UnitRange} = isempty(outage_durations) ? nothing : 1:maximum(outage_durations), scenarios::Union{Nothing, UnitRange} = isempty(outage_durations) ? nothing : 1:length(outage_durations), - ### Grid Renewable Energy Fraction Inputs ### - # Utilities renewable energy fraction. Can be scalar or timeseries (aligned with time_steps_per_hour) - clean_energy_fraction_series::Union{Real, Array{<:Real, 1}} = Float64[], - cambium_cef_col::String = "cef_load", # Column name for clean energy fraction in Cambium database - + ### Cambium Emissions and Clean Energy Inputs ### + cambium_scenario::String = "Mid-case", # Cambium Scenario for evolution of electricity sector (see Cambium documentation for descriptions). + cambium_location_type::String = "GEA Regions", # Geographic boundary at which emissions and clean energy fraction are calculated. Options: ["Nations", "GEA Regions", "States"] + cambium_start_year::Int = 2024, # First year of operation of system. Emissions and clean energy fraction will be levelized starting in this year for the duration of cambium_levelization_years. # Options: any year 2023 through 2050. + cambium_levelization_years::Int = analysis_years, # Expected lifetime or analysis period of the intervention being studied. Emissions and clean energy fraction will be averaged over this period. + cambium_grid_level::String = "enduse", # Options: ["enduse", "busbar"]. Busbar refers to point where bulk generating stations connect to grid; enduse refers to point of consumption (includes distribution loss rate). + ### Grid Climate Emissions Inputs ### - # Climate Option 1 (Default): Use levelized emissions data from NREL's Cambium database by specifying the following fields: - cambium_scenario::String = "Mid-case", # Cambium Scenario for evolution of electricity sector (see Cambium documentation for descriptions). - ## Options: ["Mid-case", "Mid-case with tax credit expiration", "Low renewable energy cost", "Low renewable energy cost with tax credit expiration", "High renewable energy cost", "High electrification", "Low natrual gas prices", "High natrual gas prices", "Mid-case with 95% decarbonization by 2050", "Mid-case with 100% decarbonization by 2035"] - cambium_location_type::String = "GEA Regions", # Geographic boundary at which emissions are calculated. Options: ["Nations", "GEA Regions", "States"] - cambium_co2_metric::String = "lrmer_co2e", # Emissions metric. Default: "lrmer_co2e" - Long-run marginal emissions rate for CO2-equivalant, combined combustion and pre-combustion emissions rates. Options: See metric definitions and names in the Cambium documentation - cambium_start_year::Int = 2024, # First year of operation of system. # Options: any year now through 2050. - cambium_levelization_years::Int = analysis_years, # Expected lifetime or analysis period of the intervention being studied. Emissions will be averaged over this period. - cambium_grid_level::String = "enduse", # Busbar refers to point where bulk generating station connects to grid; enduse refers to point of consumption (includes distribution loss rate) - - # Climate Option 2: Use CO2 emissions data from the EPA's AVERT based on the AVERT emissions region and specify annual percent decrease + cambium_co2_metric::String = "lrmer_co2e", # Emissions metric used. Default: "lrmer_co2e" - Long-run marginal emissions rate for CO2-equivalant, combined combustion and pre-combustion emissions rates. Options: See metric definitions and names in the Cambium documentation co2_from_avert::Bool = false, # Default is to use Cambium data for CO2 grid emissions. Set to `true` to instead use data from the EPA's AVERT database. - - # Climate Option 3: Provide your own custom emissions factors for CO2 and specify annual percent decrease - emissions_factor_series_lb_CO2_per_kwh::Union{Real,Array{<:Real,1}} = Float64[], # Custom CO2 emissions profile. Can be scalar or timeseries (aligned with time_steps_per_hour) - - # Used with Climate Options 2 or 3: Annual percent decrease in CO2 emissions factors + emissions_factor_series_lb_CO2_per_kwh::Union{Real,Array{<:Real,1}} = Float64[], # Custom CO2 emissions profile. Can be scalar or timeseries (aligned with time_steps_per_hour). Ensure emissions year aligns with load year. emissions_factor_CO2_decrease_fraction::Union{Nothing, Real} = co2_from_avert || length(emissions_factor_series_lb_CO2_per_kwh) > 0 ? EMISSIONS_DECREASE_DEFAULTS["CO2e"] : nothing , # Annual percent decrease in the total annual CO2 emissions rate of the grid. A negative value indicates an annual increase. ### Grid Health Emissions Inputs ### - # Health Option 1 (Default): Use health emissions data from the EPA's AVERT based on the AVERT emissions region and specify annual percent decrease avert_emissions_region::String = "", # AVERT emissions region. Default is based on location, or can be overriden by providing region here. - - # Health Option 2: Provide your own custom emissions factors for health emissions and specify annual percent decrease: - emissions_factor_series_lb_NOx_per_kwh::Union{Real,Array{<:Real,1}} = Float64[], # Custom NOx emissions profile. Can be scalar or timeseries (aligned with time_steps_per_hour) - emissions_factor_series_lb_SO2_per_kwh::Union{Real,Array{<:Real,1}} = Float64[], # Custom SO2 emissions profile. Can be scalar or timeseries (aligned with time_steps_per_hour) - emissions_factor_series_lb_PM25_per_kwh::Union{Real,Array{<:Real,1}} = Float64[], # Custom PM2.5 emissions profile. Can be scalar or timeseries (aligned with time_steps_per_hour) - - # Used with Health Options 1 or 2: Annual percent decrease in health emissions factors: + emissions_factor_series_lb_NOx_per_kwh::Union{Real,Array{<:Real,1}} = Float64[], # Custom NOx emissions profile. Can be scalar or timeseries (aligned with time_steps_per_hour). Ensure emissions year aligns with load year. + emissions_factor_series_lb_SO2_per_kwh::Union{Real,Array{<:Real,1}} = Float64[], # Custom SO2 emissions profile. Can be scalar or timeseries (aligned with time_steps_per_hour). Ensure emissions year aligns with load year. + emissions_factor_series_lb_PM25_per_kwh::Union{Real,Array{<:Real,1}} = Float64[], # Custom PM2.5 emissions profile. Can be scalar or timeseries (aligned with time_steps_per_hour). Ensure emissions year aligns with load year. emissions_factor_NOx_decrease_fraction::Real = EMISSIONS_DECREASE_DEFAULTS["NOx"], emissions_factor_SO2_decrease_fraction::Real = EMISSIONS_DECREASE_DEFAULTS["SO2"], emissions_factor_PM25_decrease_fraction::Real = EMISSIONS_DECREASE_DEFAULTS["PM25"], + + ### Grid Clean Energy Fraction Inputs ### + cambium_cef_metric::String = "cef_load", # Options = ["cef_load", "cef_gen"] # cef_load is the fraction of generation that is clean, for the generation that is allocated to a region’s end-use load; cef_gen is the fraction of generation that is clean within a region + clean_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Utilities renewable energy fraction. Can be scalar or timeseries (aligned with time_steps_per_hour). + include_grid_clean_energy_in_re::Bool=true # if true, the clean energy fraction of the grid electricity is included in the site's renewable electricity calculations ) is_MPC = isnothing(latitude) || isnothing(longitude) - cambium_emissions_region = "NA - Cambium data not used for climate emissions" # will be overwritten if Cambium is used + cambium_region = "NA - Cambium data not used for climate emissions" # will be overwritten if Cambium is used if !is_MPC # Initialize clean energy fraction series @@ -252,14 +246,14 @@ struct ElectricUtility longitude = longitude, start_year = cambium_start_year, lifetime = cambium_levelization_years, - metric_col = cambium_cef_col, + metric_col = cambium_cef_metric, grid_level = cambium_grid_level, time_steps_per_hour = time_steps_per_hour, load_year = load_year, emissions_year = 2017 # Cambium data starts on a Sunday ) clean_energy_series_dict["cef"] = clean_energy_response_dict["clean_energy_fraction_series"] - cambium_emissions_region = clean_energy_response_dict["location"] + cambium_region = clean_energy_response_dict["location"] catch @warn("Could not look up Cambium renewable energy fraction profile from point ($(latitude), $(longitude)). Location is likely outside contiguous US or something went wrong with the Cambium API request. Setting clean energy fraction to zero.") @@ -339,7 +333,7 @@ struct ElectricUtility grid_level = cambium_grid_level ) emissions_series_dict[ekey] = cambium_response_dict["emissions_factor_series_lb_CO2_per_kwh"] - cambium_emissions_region = cambium_response_dict["location"] + cambium_region = cambium_response_dict["location"] catch @warn("Could not look up Cambium emissions profile from point ($(latitude), $(longitude)). Location is likely outside contiguous US or something went wrong with the Cambium API request. Setting CO2 emissions to zero.") @@ -409,7 +403,7 @@ struct ElectricUtility new( is_MPC ? "" : avert_emissions_region, is_MPC || isnothing(meters_to_region) ? typemax(Int64) : meters_to_region, - cambium_emissions_region, + cambium_region, is_MPC ? Float64[] : emissions_series_dict["CO2"], is_MPC ? Float64[] : emissions_series_dict["NOx"], is_MPC ? Float64[] : emissions_series_dict["SO2"], diff --git a/src/results/electric_utility.jl b/src/results/electric_utility.jl index 373f122bd..d2d61813a 100644 --- a/src/results/electric_utility.jl +++ b/src/results/electric_utility.jl @@ -14,7 +14,7 @@ - `lifecycle_emissions_tonnes_PM25` # Total tons of PM2.5 emissions associated with the site's grid-purchased electricity over the analysis period. If include_exported_elec_emissions_in_total is False, this value only reflects grid purchaes. Otherwise, it accounts for emissions offset from any export to the grid. - `avert_emissions_region` # EPA AVERT region of the site. Used for health-related emissions from grid electricity (populated if default emissions values are used) and climate emissions if "co2_from_avert" is set to true. - `distance_to_avert_emissions_region_meters` # Distance in meters from the site to the nearest AVERT emissions region. -- `cambium_emissions_region` # NREL Cambium region of the site. Used for climate-related emissions from grid electricity (populated only if default (Cambium) climate emissions values are used) +- `cambium_region` # NREL Cambium region of the site. Used for climate-related emissions from grid electricity (populated only if default (Cambium) climate emissions values are used) !!! note "'Series' and 'Annual' energy and emissions outputs are average annual" REopt performs load balances using average annual production values for technologies that include degradation. @@ -78,7 +78,7 @@ function add_electric_utility_results(m::JuMP.AbstractModel, p::AbstractInputs, r["avert_emissions_region"] = p.s.electric_utility.avert_emissions_region r["distance_to_avert_emissions_region_meters"] = p.s.electric_utility.distance_to_avert_emissions_region_meters - r["cambium_emissions_region"] = p.s.electric_utility.cambium_emissions_region + r["cambium_region"] = p.s.electric_utility.cambium_region end d["ElectricUtility"] = r diff --git a/test/runtests.jl b/test/runtests.jl index 0d355fae0..7c13a7701 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2179,7 +2179,7 @@ else # run HiGHS tests @test scen.electric_utility.avert_emissions_region == "Rocky Mountains" @test scen.electric_utility.distance_to_avert_emissions_region_meters ≈ 0 atol=1e-5 - @test scen.electric_utility.cambium_emissions_region == "RMPAc" + @test scen.electric_utility.cambium_region == "RMPAc" @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) / 8760 ≈ 0.394608 rtol=1e-3 @test scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh[1] ≈ 0.677942 rtol=1e-4 # Should start on Friday @test scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh[8760] ≈ 0.6598207198 rtol=1e-5 # Should end on Friday @@ -2197,7 +2197,7 @@ else # run HiGHS tests @test scen.electric_utility.avert_emissions_region == "Alaska" @test scen.electric_utility.distance_to_avert_emissions_region_meters ≈ 0 atol=1e-5 - @test scen.electric_utility.cambium_emissions_region == "NA - Cambium data not used for climate emissions" + @test scen.electric_utility.cambium_region == "NA - Cambium data not used for climate emissions" @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) / 8760 ≈ 1.29199999 rtol=1e-3 # check that data from eGRID (AVERT data file) is used @test scen.electric_utility.emissions_factor_CO2_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["CO2e"] # should get updated to this value @test scen.electric_utility.emissions_factor_SO2_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["SO2"] # should be 2.163% for AVERT data @@ -2212,7 +2212,7 @@ else # run HiGHS tests @test scen.electric_utility.avert_emissions_region == "" @test scen.electric_utility.distance_to_avert_emissions_region_meters ≈ 5.521032136418236e6 atol=1.0 - @test scen.electric_utility.cambium_emissions_region == "NA - Cambium data not used for climate emissions" + @test scen.electric_utility.cambium_region == "NA - Cambium data not used for climate emissions" @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) ≈ 0 @test sum(scen.electric_utility.emissions_factor_series_lb_NOx_per_kwh) ≈ 0 @test sum(scen.electric_utility.emissions_factor_series_lb_SO2_per_kwh) ≈ 0 From 37de923298f5e6e8a01870bcc6b230077ca589d7 Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 13 Nov 2024 14:05:18 -0700 Subject: [PATCH 20/29] Update electric_utility.jl --- src/core/electric_utility.jl | 177 +++++++++++------------------------ 1 file changed, 57 insertions(+), 120 deletions(-) diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index f1f182e01..cc0bbc00e 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -20,7 +20,7 @@ cambium_scenario::String = "Mid-case", # Cambium Scenario for evolution of electricity sector (see Cambium documentation for descriptions). ## Options: ["Mid-case", "Mid-case with tax credit expiration", "Low renewable energy cost", "Low renewable energy cost with tax credit expiration", "High renewable energy cost", "High electrification", "Low natural gas prices", "High natural gas prices", "Mid-case with 95% decarbonization by 2050", "Mid-case with 100% decarbonization by 2035"] cambium_location_type::String = "GEA Regions", # Geographic boundary at which emissions and clean energy fraction are calculated. Options: ["Nations", "GEA Regions", "States"] - cambium_start_year::Int = 2024, # First year of operation of system. Emissions and clean energy fraction will be levelized starting in this year for the duration of cambium_levelization_years. # Options: any year 2023 through 2050. + cambium_start_year::Int = 2025, # First year of operation of system. Emissions and clean energy fraction will be levelized starting in this year for the duration of cambium_levelization_years. # Options: any year 2023 through 2050. # TODO: update options with Cambium 2023 cambium_levelization_years::Int = analysis_years, # Expected lifetime or analysis period of the intervention being studied. Emissions and clean energy fraction will be averaged over this period. cambium_grid_level::String = "enduse", # Options: ["enduse", "busbar"]. Busbar refers to point where bulk generating stations connect to grid; enduse refers to point of consumption (includes distribution loss rate). @@ -176,7 +176,7 @@ struct ElectricUtility ### Cambium Emissions and Clean Energy Inputs ### cambium_scenario::String = "Mid-case", # Cambium Scenario for evolution of electricity sector (see Cambium documentation for descriptions). cambium_location_type::String = "GEA Regions", # Geographic boundary at which emissions and clean energy fraction are calculated. Options: ["Nations", "GEA Regions", "States"] - cambium_start_year::Int = 2024, # First year of operation of system. Emissions and clean energy fraction will be levelized starting in this year for the duration of cambium_levelization_years. # Options: any year 2023 through 2050. + cambium_start_year::Int = 2025, # First year of operation of system. Emissions and clean energy fraction will be levelized starting in this year for the duration of cambium_levelization_years. # Options: any year 2023 through 2050. cambium_levelization_years::Int = analysis_years, # Expected lifetime or analysis period of the intervention being studied. Emissions and clean energy fraction will be averaged over this period. cambium_grid_level::String = "enduse", # Options: ["enduse", "busbar"]. Busbar refers to point where bulk generating stations connect to grid; enduse refers to point of consumption (includes distribution loss rate). @@ -202,69 +202,17 @@ struct ElectricUtility ) is_MPC = isnothing(latitude) || isnothing(longitude) - cambium_region = "NA - Cambium data not used for climate emissions" # will be overwritten if Cambium is used + cambium_region = "NA - Cambium data not used" # will be overwritten if Cambium is used if !is_MPC - # Initialize clean energy fraction series - clean_energy_series_dict = Dict{String, Union{Nothing, Array{<:Real, 1}}}() - if typeof(clean_energy_fraction_series) <: Real # user provided scalar value - if clean_energy_fraction_series < 0 || clean_energy_fraction_series > 1 - throw(@error("The provided ElectricUtility clean energy fraction value must be between 0 and 1.")) - end - clean_energy_series_dict["cef"] = repeat([clean_energy_fraction_series], 8760*time_steps_per_hour) - elseif length(clean_energy_fraction_series) == 1 # user provided array of one value - if clean_energy_fraction_series[1] < 0 || clean_energy_fraction_series[1] > 1 - throw(@error("The provided ElectricUtility clean energy fraction value must be between 0 and 1.")) - end - clean_energy_series_dict["cef"] = repeat(clean_energy_fraction_series, 8760*time_steps_per_hour) - elseif length(clean_energy_fraction_series) / time_steps_per_hour ≈ 8760 # user provided array with correct length - if any(x -> x < 0 || x > 1, clean_energy_fraction_series) - throw(@error("All values in the provided ElectricUtility clean energy fraction series must be between 0 and 1.")) - end - clean_energy_series_dict["cef"] = clean_energy_fraction_series - elseif length(clean_energy_fraction_series) > 1 && !(length(clean_energy_fraction_series) / time_steps_per_hour ≈ 8760) # user provided array with incorrect length - if length(clean_energy_fraction_series) == 8760 - if any(x -> x < 0 || x > 1, clean_energy_fraction_series) - throw(@error("All values in the provided ElectricUtility clean energy fraction series must be between 0 and 1.")) - end - clean_energy_series_dict["cef"] = repeat(clean_energy_fraction_series, inner=time_steps_per_hour) - @warn("Clean energy fraction series has been adjusted to align with time_steps_per_hour of $(time_steps_per_hour).") - else - throw(@error("The provided ElectricUtility clean energy fraction series does not match the time_steps_per_hour.")) - end - else - # Retrieve clean energy fraction data if not user-provided - if cambium_start_year < 2023 || cambium_start_year > 2050 - @warn("The cambium_start_year must be between 2023 and 2050. Setting cambium_start_year to 2024.") - cambium_start_year = 2024 # Must update annually - end - try - clean_energy_response_dict = cambium_clean_energy_fraction_profile( - scenario = cambium_scenario, - location_type = cambium_location_type, - latitude = latitude, - longitude = longitude, - start_year = cambium_start_year, - lifetime = cambium_levelization_years, - metric_col = cambium_cef_metric, - grid_level = cambium_grid_level, - time_steps_per_hour = time_steps_per_hour, - load_year = load_year, - emissions_year = 2017 # Cambium data starts on a Sunday - ) - clean_energy_series_dict["cef"] = clean_energy_response_dict["clean_energy_fraction_series"] - cambium_region = clean_energy_response_dict["location"] - catch - @warn("Could not look up Cambium renewable energy fraction profile from point ($(latitude), $(longitude)). - Location is likely outside contiguous US or something went wrong with the Cambium API request. Setting clean energy fraction to zero.") - clean_energy_series_dict["cef"] = zeros(Float64, 8760*time_steps_per_hour) - end + # Check some inputs + if any(x -> x < 0 || x > 1, clean_energy_fraction_series) + throw(@error("All values in the provided ElectricUtility clean_energy_fraction_series must be between 0 and 1.")) + end + if cambium_start_year < 2023 || cambium_start_year > 2050 # TODO: update? + cambium_start_year = 2025 # Must update annually + @warn("The cambium_start_year must be between 2023 and 2050. Setting cambium_start_year to $(cambium_start_year).") end - - # save clean_energy_series_dict["cef"] as csv - # clean_energy_df = DataFrame(cef = clean_energy_series_dict["cef"]) - # CSV.write("clean_energy_fraction_series.csv", clean_energy_df) - # Get AVERT emissions region if avert_emissions_region == "" @@ -289,60 +237,61 @@ struct ElectricUtility emissions_factor_CO2_decrease_fraction = 0.0 # For Cambium data and if not user-provided end - # Get all grid emissions series - emissions_series_dict = Dict{String, Union{Nothing,Array{<:Real,1}}}() + # Get all grid emissions series and clean energy fraction series + emissions_and_cef_series_dict = Dict{String, Union{Nothing,Array{<:Real,1}}}() for (eseries, ekey) in [ (emissions_factor_series_lb_CO2_per_kwh, "CO2"), (emissions_factor_series_lb_NOx_per_kwh, "NOx"), (emissions_factor_series_lb_SO2_per_kwh, "SO2"), - (emissions_factor_series_lb_PM25_per_kwh, "PM25") + (emissions_factor_series_lb_PM25_per_kwh, "PM25"), + (clean_energy_fraction_series, "clean_energy_fraction_series") ] if off_grid_flag # no grid emissions for off-grid - emissions_series_dict[ekey] = zeros(Float64, 8760*time_steps_per_hour) + emissions_and_cef_series_dict[ekey] = zeros(Float64, 8760*time_steps_per_hour) elseif typeof(eseries) <: Real # user provided scalar value - emissions_series_dict[ekey] = repeat([eseries], 8760*time_steps_per_hour) + emissions_and_cef_series_dict[ekey] = repeat([eseries], 8760*time_steps_per_hour) elseif length(eseries) == 1 # user provided array of one value - emissions_series_dict[ekey] = repeat(eseries, 8760*time_steps_per_hour) + emissions_and_cef_series_dict[ekey] = repeat(eseries, 8760*time_steps_per_hour) elseif length(eseries) / time_steps_per_hour ≈ 8760 # user provided array with correct length - emissions_series_dict[ekey] = eseries + emissions_and_cef_series_dict[ekey] = eseries elseif length(eseries) > 1 && !(length(eseries) / time_steps_per_hour ≈ 8760) # user provided array with incorrect length if length(eseries) == 8760 - emissions_series_dict[ekey] = repeat(eseries,inner=time_steps_per_hour) - @warn("Emissions series for $(ekey) has been adjusted to align with time_steps_per_hour of $(time_steps_per_hour).") + emissions_and_cef_series_dict[ekey] = repeat(eseries,inner=time_steps_per_hour) + @warn("The ElectricUtility emissions or clean enery fraction series for $(ekey) has been adjusted to align with time_steps_per_hour of $(time_steps_per_hour).") else - throw(@error("The provided ElectricUtility emissions factor series for $(ekey) does not match the time_steps_per_hour.")) + throw(@error("The provided ElectricUtility emissions or clean enery fraction series for $(ekey) does not match the time_steps_per_hour.")) end - else # if not user-provided, get emissions factors from AVERT and/or Cambium - if ekey == "CO2" && co2_from_avert == false # Use Cambium for CO2 - if cambium_start_year < 2023 || cambium_start_year > 2050 - @warn("The cambium_start_year must be between 2023 and 2050. Setting to cambium_start_year to 2024.") - cambium_start_year = 2024 # Must update annually - end + else # if not user-provided, get emissions or cef factors from Cambium and/or AVERT + if ekey == "CO2" && co2_from_avert == false || ekey == "clean_energy_fraction_series" # Use Cambium for CO2 or clean energy factors try - cambium_response_dict = cambium_emissions_profile( # Adjusted for day of week alignment with load and time_steps_per_hour + cambium_response_dict = cambium_profile( # Adjusted for day of week alignment with load and time_steps_per_hour scenario = cambium_scenario, location_type = cambium_location_type, latitude = latitude, longitude = longitude, start_year = cambium_start_year, lifetime = cambium_levelization_years, - metric_col = cambium_co2_metric, + metric_col = ekey == "CO2" ? cambium_co2_metric : cambium_cef_metric, time_steps_per_hour = time_steps_per_hour, load_year = load_year, - emissions_year = 2017, # because Cambium data always starts on a Sunday + profile_year = 2017, # because Cambium data always starts on a Sunday grid_level = cambium_grid_level ) - emissions_series_dict[ekey] = cambium_response_dict["emissions_factor_series_lb_CO2_per_kwh"] + emissions_and_cef_series_dict[ekey] = cambium_response_dict["data_series"] cambium_region = cambium_response_dict["location"] + + # save clean_energy_series_dict["cef"] as csv + # clean_energy_df = DataFrame(cef = clean_energy_series_dict["cef"]) + # CSV.write("clean_energy_fraction_series.csv", clean_energy_df) catch - @warn("Could not look up Cambium emissions profile from point ($(latitude), $(longitude)). - Location is likely outside contiguous US or something went wrong with the Cambium API request. Setting CO2 emissions to zero.") - emissions_series_dict[ekey] = zeros(Float64, 8760*time_steps_per_hour) + @warn("Could not look up Cambium $(ekey) profile for location ($(latitude), $(longitude)). + Location is likely outside contiguous US or something went wrong with the Cambium API request. Setting ElectricUtility $(ekey) factors to zero.") + emissions_and_cef_series_dict[ekey] = zeros(Float64, 8760*time_steps_per_hour) end else # otherwise use AVERT if !isnothing(region_abbr) avert_data_year = 2022 # Must update when AVERT data are updated - emissions_series_dict[ekey] = avert_emissions_profiles( + emissions_and_cef_series_dict[ekey] = avert_emissions_profiles( avert_region_abbr = region_abbr, latitude = latitude, longitude = longitude, @@ -351,13 +300,12 @@ struct ElectricUtility avert_data_year = avert_data_year )["emissions_factor_series_lb_"*ekey*"_per_kwh"] else - emissions_series_dict[ekey] = zeros(Float64, 8760*time_steps_per_hour) # Warnings will happen in avert_emissions_profiles + emissions_and_cef_series_dict[ekey] = zeros(Float64, 8760*time_steps_per_hour) # Warnings will happen in avert_emissions_profiles end end - # Handle missing emissions inputs (due to failed lookup and not provided by user) - if isnothing(emissions_series_dict[ekey]) - @warn "Cannot find hourly $(ekey) emissions for region $(region_abbr). Setting emissions to zero." + if isnothing(emissions_and_cef_series_dict[ekey]) + @warn "Cannot find hourly ElectricUtility $(ekey) series for region $(region_abbr). Setting this input to zero." if ekey == "CO2" && (!isnothing(CO2_emissions_reduction_min_fraction) || !isnothing(CO2_emissions_reduction_max_fraction) || @@ -368,7 +316,7 @@ struct ElectricUtility throw(@error("To include health costs in the objective function, you must either enter custom health grid emissions factors or a site location within the contiguous U.S.")) end - emissions_series_dict[ekey] = zeros(8760*time_steps_per_hour) + emissions_and_cef_series_dict[ekey] = zeros(8760*time_steps_per_hour) end end end @@ -404,10 +352,10 @@ struct ElectricUtility is_MPC ? "" : avert_emissions_region, is_MPC || isnothing(meters_to_region) ? typemax(Int64) : meters_to_region, cambium_region, - is_MPC ? Float64[] : emissions_series_dict["CO2"], - is_MPC ? Float64[] : emissions_series_dict["NOx"], - is_MPC ? Float64[] : emissions_series_dict["SO2"], - is_MPC ? Float64[] : emissions_series_dict["PM25"], + is_MPC ? Float64[] : emissions_and_cef_series_dict["CO2"], + is_MPC ? Float64[] : emissions_and_cef_series_dict["NOx"], + is_MPC ? Float64[] : emissions_and_cef_series_dict["SO2"], + is_MPC ? Float64[] : emissions_and_cef_series_dict["PM25"], emissions_factor_CO2_decrease_fraction, emissions_factor_NOx_decrease_fraction, emissions_factor_SO2_decrease_fraction, @@ -422,7 +370,7 @@ struct ElectricUtility scenarios, net_metering_limit_kw, interconnection_limit_kw, - is_MPC ? Float64[] : clean_energy_series_dict["cef"], + is_MPC ? Float64[] : emissions_and_cef_series_dict["clean_energy_fraction_series"], include_grid_clean_energy_in_re ) end @@ -588,7 +536,7 @@ function avert_emissions_profiles(; avert_region_abbr::String="", latitude::Real # Find col index for region. Row 1 does not contain AVERT data so skip that. emissions_profile_unadjusted = round.(avert_df[2:end,findfirst(x -> x == avert_region_abbr, avert_df[1,:])], digits=6) # Adjust for day of week alignment with load - ef_profile_adjusted = align_emission_with_load_year(load_year=load_year, emissions_year=avert_data_year, emissions_profile=emissions_profile_unadjusted) + ef_profile_adjusted = align_profile_with_load_year(load_year=load_year, profile_year=avert_data_year, emissions_profile=emissions_profile_unadjusted) # Adjust for non-hourly timesteps if time_steps_per_hour > 1 ef_profile_adjusted = repeat(ef_profile_adjusted,inner=time_steps_per_hour) @@ -599,7 +547,7 @@ function avert_emissions_profiles(; avert_region_abbr::String="", latitude::Real end """ - cambium_emissions_profiles(; scenario::String, + cambium_profile(; scenario::String, location_type::String, latitude::Real, longitude::Real, @@ -608,14 +556,14 @@ end metric_col::String, time_steps_per_hour::Int=1, load_year::Int=2017, - emissions_year::Int=2017, + profile_year::Int=2017, grid_level::String) This function constructs an API request to the Cambium database to retrieve either emissions data or clean energy fraction data depending on the `metric_col` provided. This function gets levelized grid CO2 or CO2e emission rate profiles (1-year time series) from the Cambium dataset. The returned profiles are adjusted for day of week alignment with the provided "load_year" (Cambium profiles always start on a Sunday.) -This function is also used for the /cambium_emissions_profile endpoint in the REopt API, in particular for the webtool to display grid emissions defaults before running REopt. +This function is also used for the /cambium_profile endpoint in the REopt API, in particular for the webtool to display grid emissions defaults before running REopt. """ function cambium_profile(; scenario::String, @@ -628,7 +576,7 @@ function cambium_profile(; scenario::String, grid_level::String, time_steps_per_hour::Int=1, load_year::Int=2017, - emissions_year::Int=2017 + profile_year::Int=2017 ) url = "https://scenarioviewer.nrel.gov/api/get-levelized/" # Production @@ -656,30 +604,19 @@ function cambium_profile(; scenario::String, r = HTTP.get(url; query=payload) response = JSON.parse(String(r.body)) # contains response["status"] output = response["message"] - data_series = output["values"] - # co2_emissions = output["values"] ./ 1000 # [lb / MWh] --> [lb / kWh] - # Convert from [lb/MWh] to [lb/kWh] if the metric is emissions-related - if metric_col == "lrmer_co2e" - data_series = data_series ./ 1000 - end - - # Align day of week of emissions and load profiles (Cambium data starts on Sundays so assuming emissions_year=2017) - data_series = align_emission_with_load_year(load_year=load_year, emissions_year=emissions_year, emissions_profile=data_series) + data_series = occursin("co2", metric_col) ? output["values"] ./ 1000 : output["values"] + + # Align day of week of emissions or clean energy and load profiles (Cambium data starts on Sundays so assuming profile_year=2017) + data_series = align_profile_with_load_year(load_year=load_year, profile_year=profile_year, emissions_profile=data_series) if time_steps_per_hour > 1 data_series = repeat(data_series, inner=time_steps_per_hour) end - - description, units = if metric_col == "lrmer_co2e" - ("Hourly CO2 (or CO2e) grid emissions factors for applicable Cambium location and location_type, adjusted to align with load year $(load_year).", "Pounds emissions per kWh") - else - ("Hourly clean energy fraction for applicable Cambium location and location_type, adjusted to align with load year $(load_year).", "Fraction of clean energy") - end response_dict = Dict{String, Any}( - "description" => description, - "units" => units, + "description" => "Hourly CO2 (or CO2e) grid emissions factors or clean energy fraction for applicable Cambium location and location_type, adjusted to align with load year $(load_year).", + "units" => occursin("co2", metric_col) ? "Pounds emissions per kWh" : "Fraction of clean energy", "location" => output["location"], "metric_col" => output["metric_col"], "data_series" => data_series @@ -693,9 +630,9 @@ function cambium_profile(; scenario::String, end end -function align_emission_with_load_year(; load_year::Int, emissions_year::Int, emissions_profile::Array{<:Real,1}) +function align_profile_with_load_year(; load_year::Int, profile_year::Int, emissions_profile::Array{<:Real,1}) - ef_start_day = dayofweek(Date(emissions_year,1,1)) # Monday = 1; Sunday = 7 + ef_start_day = dayofweek(Date(profile_year,1,1)) # Monday = 1; Sunday = 7 load_start_day = dayofweek(Date(load_year,1,1)) if ef_start_day == load_start_day From ff29a783a53fa462cf0d3f4f4996b7557c8341b7 Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 13 Nov 2024 14:13:29 -0700 Subject: [PATCH 21/29] emissions_profile to profile_data --- src/core/electric_utility.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index cc0bbc00e..99cb294c2 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -536,7 +536,7 @@ function avert_emissions_profiles(; avert_region_abbr::String="", latitude::Real # Find col index for region. Row 1 does not contain AVERT data so skip that. emissions_profile_unadjusted = round.(avert_df[2:end,findfirst(x -> x == avert_region_abbr, avert_df[1,:])], digits=6) # Adjust for day of week alignment with load - ef_profile_adjusted = align_profile_with_load_year(load_year=load_year, profile_year=avert_data_year, emissions_profile=emissions_profile_unadjusted) + ef_profile_adjusted = align_profile_with_load_year(load_year=load_year, profile_year=avert_data_year, profile_data=emissions_profile_unadjusted) # Adjust for non-hourly timesteps if time_steps_per_hour > 1 ef_profile_adjusted = repeat(ef_profile_adjusted,inner=time_steps_per_hour) @@ -608,7 +608,7 @@ function cambium_profile(; scenario::String, data_series = occursin("co2", metric_col) ? output["values"] ./ 1000 : output["values"] # Align day of week of emissions or clean energy and load profiles (Cambium data starts on Sundays so assuming profile_year=2017) - data_series = align_profile_with_load_year(load_year=load_year, profile_year=profile_year, emissions_profile=data_series) + data_series = align_profile_with_load_year(load_year=load_year, profile_year=profile_year, profile_data=data_series) if time_steps_per_hour > 1 data_series = repeat(data_series, inner=time_steps_per_hour) @@ -630,19 +630,19 @@ function cambium_profile(; scenario::String, end end -function align_profile_with_load_year(; load_year::Int, profile_year::Int, emissions_profile::Array{<:Real,1}) +function align_profile_with_load_year(; load_year::Int, profile_year::Int, profile_data::Array{<:Real,1}) ef_start_day = dayofweek(Date(profile_year,1,1)) # Monday = 1; Sunday = 7 load_start_day = dayofweek(Date(load_year,1,1)) if ef_start_day == load_start_day - emissions_profile_adj = emissions_profile + profile_data_adj = profile_data else # Example: Emissions year = 2017; ef_start_day = 7 (Sunday). Load year = 2021; load_start_day = 5 (Fri) cut_days = 7+(load_start_day-ef_start_day) # Ex: = 7+(5-7) = 5 --> cut Sun, Mon, Tues, Wed, Thurs - wrap_ts = emissions_profile[25:24+24*cut_days] # Ex: = emissions_profile[25:144] wrap Mon-Fri to end - emissions_profile_adj = append!(emissions_profile[24*cut_days+1:end],wrap_ts) # Ex: now starts on Fri and end Fri to align with 2021 cal + wrap_ts = profile_data[25:24+24*cut_days] # Ex: = profile_data[25:144] wrap Mon-Fri to end + profile_data_adj = append!(emissions_profile[24*cut_days+1:end],wrap_ts) # Ex: now starts on Fri and end Fri to align with 2021 cal end - return emissions_profile_adj + return profile_data_adj end \ No newline at end of file From cee81765ebcb1af3b7f539cd00f6a607cd626509 Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 13 Nov 2024 14:24:18 -0700 Subject: [PATCH 22/29] reorganize constraints --- src/REopt.jl | 1 - src/constraints/cef_constraints.jl | 59 ------------------- .../renewable_energy_constraints.jl | 57 +++++++++++++++++- src/core/electric_utility.jl | 14 ++--- 4 files changed, 63 insertions(+), 68 deletions(-) delete mode 100644 src/constraints/cef_constraints.jl diff --git a/src/REopt.jl b/src/REopt.jl index 7598c1f5f..5ff756e2f 100644 --- a/src/REopt.jl +++ b/src/REopt.jl @@ -162,7 +162,6 @@ include("constraints/ghp_constraints.jl") include("constraints/steam_turbine_constraints.jl") include("constraints/renewable_energy_constraints.jl") include("constraints/emissions_constraints.jl") -include("constraints/cef_constraints.jl") include("mpc/structs.jl") include("mpc/scenario.jl") diff --git a/src/constraints/cef_constraints.jl b/src/constraints/cef_constraints.jl deleted file mode 100644 index 8551f409b..000000000 --- a/src/constraints/cef_constraints.jl +++ /dev/null @@ -1,59 +0,0 @@ -# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. - -""" -calc_clean_grid_kWh(m, p) - -This function calculates the clean energy fraction of the electricity from the electric utility -by multiplying the electricity from the grid used to charge the batteries and the electricity from the grid directly serving the load -by the clean energy fraction series. - -Returns: -- clean_energy_fraction: The clean energy fraction of the grid electricity. -""" -function calc_clean_grid_kWh(m, p) - # Calculate the grid electricity used to charge the batteries and directly serve the load - m[:CleanGridToLoad], m[:CleanGridToBatt] = calc_grid_to_load(m, p) - - # Calculate the clean energy fraction from the electric utility - m[:grid_clean_energy_series_kw] = @expression(m, [ - ts in p.time_steps], (m[:CleanGridToLoad][ts] + m[:CleanGridToBatt][ts]) * p.s.electric_utility.clean_energy_fraction_series[ts] - ) -end - - -""" -calc_grid_to_load(m, p) - -This function calculates, for each timestep: -1. The electricity from the grid used to charge the batteries, accounting for battery losses. -2. The electricity from the grid directly serving the load. - -Returns: -- CleanGridToLoad: The electricity from the grid directly serving the load. -- CleanGridToBatt: The electricity from the grid used to charge the batteries, accounting for losses. -""" - -function calc_grid_to_load(m, p) - if !isempty(p.s.storage.types.elec) - # Calculate the grid to load through the battery, accounting for the battery losses - m[:CleanGridToBatt] = @expression(m, [ - ts in p.time_steps], sum( - m[:dvGridToStorage][b, ts] * p.s.storage.attr[b].charge_efficiency * p.s.storage.attr[b].discharge_efficiency - for b in p.s.storage.types.elec) - ) - else - m[:CleanGridToBatt] = zeros(length(p.time_steps)) - end - - # Calculate the grid serving load not through the battery - m[:CleanGridToLoad] = @expression(m, [ - ts in p.time_steps], ( - sum(m[:dvGridPurchase][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) - - sum(m[:dvGridToStorage][b, ts] for b in p.s.storage.types.elec) - )) - - return m[:CleanGridToLoad], m[:CleanGridToBatt] -end - - - diff --git a/src/constraints/renewable_energy_constraints.jl b/src/constraints/renewable_energy_constraints.jl index 5df9d56e7..2aeb87eca 100644 --- a/src/constraints/renewable_energy_constraints.jl +++ b/src/constraints/renewable_energy_constraints.jl @@ -56,7 +56,7 @@ function add_re_elec_calcs(m,p) calc_clean_grid_kWh(m, p) m[:AnnualREEleckWh] = @expression(m, p.hours_per_time_step * ( - (p.s.electric_utility.include_grid_clean_energy_in_re ? + (p.s.electric_utility.include_grid_clean_energy_in_total ? sum(m[:grid_clean_energy_series_kw][ts] for ts in p.time_steps) : 0) + # Include grid clean energy fraction conditionally + # Clean energy fraction of grid electricity to load and battery sum(p.production_factor[t,ts] * p.levelization_factor[t] * m[:dvRatedProduction][t,ts] * @@ -88,4 +88,59 @@ function add_re_elec_calcs(m,p) ) ) nothing +end + +""" +calc_clean_grid_kWh(m, p) + +This function calculates the clean energy fraction of the electricity from the electric utility +by multiplying the electricity from the grid used to charge the batteries and the electricity from the grid directly serving the load +by the clean energy fraction series. + +Returns: +- clean_energy_fraction: The clean energy fraction of the grid electricity. +""" +function calc_clean_grid_kWh(m, p) + # Calculate the grid electricity used to charge the batteries and directly serve the load + m[:CleanGridToLoad], m[:CleanGridToBatt] = calc_grid_to_load(m, p) + + # Calculate the clean energy fraction from the electric utility + m[:grid_clean_energy_series_kw] = @expression(m, [ + ts in p.time_steps], (m[:CleanGridToLoad][ts] + m[:CleanGridToBatt][ts]) * p.s.electric_utility.clean_energy_fraction_series[ts] + ) +end + + +""" +calc_grid_to_load(m, p) + +This function calculates, for each timestep: +1. The electricity from the grid used to charge the batteries, accounting for battery losses. +2. The electricity from the grid directly serving the load. + +Returns: +- CleanGridToLoad: The electricity from the grid directly serving the load. +- CleanGridToBatt: The electricity from the grid used to charge the batteries, accounting for losses. +""" + +function calc_grid_to_load(m, p) + if !isempty(p.s.storage.types.elec) + # Calculate the grid to load through the battery, accounting for the battery losses + m[:CleanGridToBatt] = @expression(m, [ + ts in p.time_steps], sum( + m[:dvGridToStorage][b, ts] * p.s.storage.attr[b].charge_efficiency * p.s.storage.attr[b].discharge_efficiency + for b in p.s.storage.types.elec) + ) + else + m[:CleanGridToBatt] = zeros(length(p.time_steps)) + end + + # Calculate the grid serving load not through the battery + m[:CleanGridToLoad] = @expression(m, [ + ts in p.time_steps], ( + sum(m[:dvGridPurchase][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) - + sum(m[:dvGridToStorage][b, ts] for b in p.s.storage.types.elec) + )) + + return m[:CleanGridToLoad], m[:CleanGridToBatt] end \ No newline at end of file diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index 99cb294c2..87941b499 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -54,7 +54,7 @@ ### Grid Clean Energy Fraction Inputs ### cambium_cef_metric::String = "cef_load", # Options = ["cef_load", "cef_gen"] # cef_load is the fraction of generation that is clean, for the generation that is allocated to a region’s end-use load; cef_gen is the fraction of generation that is clean within a region clean_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Utilities renewable energy fraction. Can be scalar or timeseries (aligned with time_steps_per_hour). - include_grid_clean_energy_in_re::Bool=true, # if true, the clean energy fraction of the grid electricity is included in the site's renewable electricity calculations + include_grid_clean_energy_in_total::Bool=true, # if true, the clean energy fraction of the grid electricity is included in the site's total renewable electricity calculations ``` !!! note "Outage modeling" @@ -140,7 +140,7 @@ struct ElectricUtility net_metering_limit_kw::Real interconnection_limit_kw::Real clean_energy_fraction_series::Array{<:Real,1} # Utility renewable energy fraction. - include_grid_clean_energy_in_re::Bool + include_grid_clean_energy_in_total::Bool function ElectricUtility(; @@ -198,7 +198,7 @@ struct ElectricUtility ### Grid Clean Energy Fraction Inputs ### cambium_cef_metric::String = "cef_load", # Options = ["cef_load", "cef_gen"] # cef_load is the fraction of generation that is clean, for the generation that is allocated to a region’s end-use load; cef_gen is the fraction of generation that is clean within a region clean_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Utilities renewable energy fraction. Can be scalar or timeseries (aligned with time_steps_per_hour). - include_grid_clean_energy_in_re::Bool=true # if true, the clean energy fraction of the grid electricity is included in the site's renewable electricity calculations + include_grid_clean_energy_in_total::Bool=true # if true, the clean energy fraction of the grid electricity is included in the site's renewable electricity calculations ) is_MPC = isnothing(latitude) || isnothing(longitude) @@ -371,7 +371,7 @@ struct ElectricUtility net_metering_limit_kw, interconnection_limit_kw, is_MPC ? Float64[] : emissions_and_cef_series_dict["clean_energy_fraction_series"], - include_grid_clean_energy_in_re + include_grid_clean_energy_in_total ) end end @@ -559,11 +559,11 @@ end profile_year::Int=2017, grid_level::String) -This function constructs an API request to the Cambium database to retrieve either emissions data or clean energy fraction data depending on the `metric_col` provided. -This function gets levelized grid CO2 or CO2e emission rate profiles (1-year time series) from the Cambium dataset. +This function constructs an API request to the Cambium database to retrieve either emissions data or clean energy fraction data depending on the `metric_col` provided. +The data will be averaged on an hourly basis over the "lifetime" provided. The returned profiles are adjusted for day of week alignment with the provided "load_year" (Cambium profiles always start on a Sunday.) -This function is also used for the /cambium_profile endpoint in the REopt API, in particular for the webtool to display grid emissions defaults before running REopt. +This function is also used for the /cambium_profile endpoint in the REopt API, in particular for the webtool to display grid emissions data. """ function cambium_profile(; scenario::String, From 8571808a904e6e341ee1b30b304263d171d0b989 Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 13 Nov 2024 14:26:17 -0700 Subject: [PATCH 23/29] minor edits --- src/core/reopt.jl | 1 - src/core/scenario.jl | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 41879dce2..662d249cd 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -580,7 +580,6 @@ function run_reopt(m::JuMP.AbstractModel, p::REoptInputs; organize_pvs=true) results = reopt_results(m, p) time_elapsed = time() - tstart @info "Results processing took $(round(time_elapsed, digits=3)) seconds." - @info "REopt results have been processed." results["status"] = status results["solver_seconds"] = opt_time diff --git a/src/core/scenario.jl b/src/core/scenario.jl index d588a0320..b3fb51a62 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -137,7 +137,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) off_grid_flag=settings.off_grid_flag, time_steps_per_hour=settings.time_steps_per_hour, analysis_years=financial.analysis_years, - load_year=electric_load.year, + load_year=electric_load.year ) elseif !(settings.off_grid_flag) electric_utility = ElectricUtility(; latitude=site.latitude, longitude=site.longitude, @@ -148,7 +148,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) off_grid_flag=settings.off_grid_flag, time_steps_per_hour=settings.time_steps_per_hour, analysis_years=financial.analysis_years, - load_year=electric_load.year, + load_year=electric_load.year ) elseif settings.off_grid_flag if haskey(d, "ElectricUtility") From 60cdeae501d9aaa4fb7cfe5758d27f582f4d0d53 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 14 Nov 2024 10:30:27 -0700 Subject: [PATCH 24/29] simplify constraints --- CHANGELOG.md | 8 +- .../renewable_energy_constraints.jl | 92 +++++-------------- src/core/electric_utility.jl | 12 +-- src/core/site.jl | 7 +- src/results/site.jl | 6 +- 5 files changed, 41 insertions(+), 84 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9483b7378..9039de9e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,9 +27,15 @@ Classify the change according to the following categories: ## gridRE-dev +### Added +- Added ElectricUtility inputs to account for the clean or renewable energy fraction of grid-purchased electricity: + - **cambium_cef_metric** to utilize clean energy data from NREL's Cambium database + - **clean_energy_fraction_series** to supply a custom grid clean energy scalar or series +- Added Site input to allow user to choose whether to include grid RE in min max constraints: **include_grid_renewable_electricity_in_min_max_constraints** ### Changed - Changed name of ElectricUtility input **cambium_metric_col** to **cambium_co2_metric**, to distinguish between the CO2 and clean energy fraction metrics -- Changed name of ElectricUtility **cambium_emissions_region** to **cambium_region** +- Changed name of ElectricUtility **cambium_emissions_region** to **cambium_region** +- Changed name of function (also available as endpoint through REopt API) from **cambium_emissions_profile** to **cambium_profile** ## Develop ### Added diff --git a/src/constraints/renewable_energy_constraints.jl b/src/constraints/renewable_energy_constraints.jl index 2aeb87eca..c7066786b 100644 --- a/src/constraints/renewable_energy_constraints.jl +++ b/src/constraints/renewable_energy_constraints.jl @@ -3,7 +3,7 @@ add_re_elec_constraints(m,p) Function to add minimum and/or maximum renewable electricity (as percentage of load) constraints, if specified by user. -Function modified to include cef fraction from the grid in accounting for renewable energy percentage of load. +Includes renewable energy from grid if specified by user. !!! note When a single outage is modeled (using outage_start_time_step), renewable electricity calculations account for operations during this outage (e.g., the critical load is used during time_steps_without_grid) @@ -12,14 +12,17 @@ Function modified to include cef fraction from the grid in accounting for renewa #Renewable electricity constraints function add_re_elec_constraints(m,p) if !isnothing(p.s.site.renewable_electricity_min_fraction) - @constraint(m, MinREElecCon, m[:AnnualREEleckWh] >= p.s.site.renewable_electricity_min_fraction*m[:AnnualEleckWh]) + @constraint(m, MinREElecCon, m[:AnnualOnsiteREEleckWh] + + include_grid_renewable_electricity_in_min_max_constraints * m[:AnnualGridREEleckWh] + >= p.s.site.renewable_electricity_min_fraction*m[:AnnualEleckWh]) end if !isnothing(p.s.site.renewable_electricity_max_fraction) - @constraint(m, MaxREElecCon, m[:AnnualREEleckWh] <= p.s.site.renewable_electricity_max_fraction*m[:AnnualEleckWh]) + @constraint(m, MaxREElecCon, m[:AnnualOnsiteREEleckWh] + + include_grid_renewable_electricity_in_min_max_constraints * m[:AnnualGridREEleckWh] + <= p.s.site.renewable_electricity_max_fraction*m[:AnnualEleckWh]) end end - """ add_re_elec_calcs(m,p) @@ -35,7 +38,7 @@ function add_re_elec_calcs(m,p) # TODO: When steam turbine implemented, uncomment code below, replacing p.TechCanSupplySteamTurbine, p.STElecOutToThermInRatio with new names # # Steam turbine RE elec calculations # if isempty(p.steam) - # SteamTurbineAnnualREEleckWh = 0 + # SteamTurbineAnnualOnsiteREEleckWh = 0 # else # # Note: SteamTurbine's input p.tech_renewable_energy_fraction = 0 because it is actually a decision variable dependent on fraction of steam generated by RE fuel # SteamTurbinePercentREEstimate = @expression(m, @@ -43,7 +46,7 @@ function add_re_elec_calcs(m,p) # ) # # Note: Steam turbine battery losses, curtailment, and exported RE terms are only accurate if all techs that can supply ST # # have equal RE%, otherwise it is an approximation because the general equation is non linear. - # SteamTurbineAnnualREEleckWh = @expression(m,p.hours_per_time_step * ( + # SteamTurbineAnnualOnsiteREEleckWh = @expression(m,p.hours_per_time_step * ( # p.STElecOutToThermInRatio * sum(m[:dvThermalToSteamTurbine][tst,ts]*p.tech_renewable_energy_fraction[tst] for ts in p.time_steps, tst in p.TechCanSupplySteamTurbine) # plus steam turbine RE generation # - sum(m[:dvProductionToStorage][b,t,ts] * SteamTurbinePercentREEstimate * (1-p.s.storage.attr[b].charge_efficiency*p.s.storage.attr[b].discharge_efficiency) for t in p.steam, b in p.s.storage.types.elec, ts in p.time_steps) # minus battery storage losses from RE from steam turbine # - sum(m[:dvCurtail][t,ts] * SteamTurbinePercentREEstimate for t in p.steam, ts in p.time_steps) # minus curtailment. @@ -51,14 +54,7 @@ function add_re_elec_calcs(m,p) # )) # end - # Function modified to add clean energy fraction to in annual renewable energy calculations - - calc_clean_grid_kWh(m, p) - - m[:AnnualREEleckWh] = @expression(m, p.hours_per_time_step * ( - (p.s.electric_utility.include_grid_clean_energy_in_total ? - sum(m[:grid_clean_energy_series_kw][ts] for ts in p.time_steps) : 0) + # Include grid clean energy fraction conditionally - + # Clean energy fraction of grid electricity to load and battery + m[:AnnualOnsiteREEleckWh] = @expression(m, p.hours_per_time_step * ( sum(p.production_factor[t,ts] * p.levelization_factor[t] * m[:dvRatedProduction][t,ts] * p.tech_renewable_energy_fraction[t] for t in p.techs.elec, ts in p.time_steps ) - #total RE elec generation, excl steam turbine @@ -72,75 +68,29 @@ function add_re_elec_calcs(m,p) for t in p.techs.elec, u in p.export_bins_by_tech[t], ts in p.time_steps ) # minus exported RE, if RE accounting method = 0. ) - # + SteamTurbineAnnualREEleckWh # SteamTurbine RE Elec, already adjusted for p.hours_per_time_step + # + SteamTurbineAnnualOnsiteREEleckWh # SteamTurbine RE Elec, already adjusted for p.hours_per_time_step ) # Note: if battery ends up being allowed to discharge to grid, need to make sure only RE that is being consumed onsite is counted so battery doesn't become a back door for RE to grid. # Note: calculations currently do not ascribe any renewable energy attribute to grid-purchased electricity + m[:AnnualGridREEleckWh] = @expression(m, p.hours_per_time_step * ( + sum(m[:dvGridPurchase][ts, tier] - m[:dvGridToStorage][b, ts] * + (1 - p.s.storage.attr[b].charge_efficiency * p.s.storage.attr[b].discharge_efficiency) * + p.s.electric_utility.clean_energy_fraction_series[ts] + for ts in p.time_steps, tier in 1:p.s.electric_tariff.n_energy_tiers, b in p.s.storage.types.elec + ) # clean energy from grid, minus battery efficiency losses + ) + ) + m[:AnnualEleckWh] = @expression(m,p.hours_per_time_step * ( # input electric load sum(p.s.electric_load.loads_kw[ts] for ts in p.time_steps_with_grid) + sum(p.s.electric_load.critical_loads_kw[ts] for ts in p.time_steps_without_grid) - # tech electric loads + # tech electric loads #TODO: Uncomment? # + sum(m[:dvCoolingProduction][t,ts] for t in p.ElectricChillers, ts in p.time_steps )/ p.ElectricChillerCOP # electric chiller elec load # + sum(m[:dvCoolingProduction][t,ts] for t in p.AbsorptionChillers, ts in p.time_steps )/ p.AbsorptionChillerElecCOP # absorportion chiller elec load # + sum(p.GHPElectricConsumed[g,ts] * m[:binGHP][g] for g in p.GHPOptions, ts in p.time_steps) # GHP elec load ) ) nothing -end - -""" -calc_clean_grid_kWh(m, p) - -This function calculates the clean energy fraction of the electricity from the electric utility -by multiplying the electricity from the grid used to charge the batteries and the electricity from the grid directly serving the load -by the clean energy fraction series. - -Returns: -- clean_energy_fraction: The clean energy fraction of the grid electricity. -""" -function calc_clean_grid_kWh(m, p) - # Calculate the grid electricity used to charge the batteries and directly serve the load - m[:CleanGridToLoad], m[:CleanGridToBatt] = calc_grid_to_load(m, p) - - # Calculate the clean energy fraction from the electric utility - m[:grid_clean_energy_series_kw] = @expression(m, [ - ts in p.time_steps], (m[:CleanGridToLoad][ts] + m[:CleanGridToBatt][ts]) * p.s.electric_utility.clean_energy_fraction_series[ts] - ) -end - - -""" -calc_grid_to_load(m, p) - -This function calculates, for each timestep: -1. The electricity from the grid used to charge the batteries, accounting for battery losses. -2. The electricity from the grid directly serving the load. - -Returns: -- CleanGridToLoad: The electricity from the grid directly serving the load. -- CleanGridToBatt: The electricity from the grid used to charge the batteries, accounting for losses. -""" - -function calc_grid_to_load(m, p) - if !isempty(p.s.storage.types.elec) - # Calculate the grid to load through the battery, accounting for the battery losses - m[:CleanGridToBatt] = @expression(m, [ - ts in p.time_steps], sum( - m[:dvGridToStorage][b, ts] * p.s.storage.attr[b].charge_efficiency * p.s.storage.attr[b].discharge_efficiency - for b in p.s.storage.types.elec) - ) - else - m[:CleanGridToBatt] = zeros(length(p.time_steps)) - end - - # Calculate the grid serving load not through the battery - m[:CleanGridToLoad] = @expression(m, [ - ts in p.time_steps], ( - sum(m[:dvGridPurchase][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) - - sum(m[:dvGridToStorage][b, ts] for b in p.s.storage.types.elec) - )) - - return m[:CleanGridToLoad], m[:CleanGridToBatt] end \ No newline at end of file diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index 87941b499..ea4fc6911 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -53,8 +53,7 @@ ### Grid Clean Energy Fraction Inputs ### cambium_cef_metric::String = "cef_load", # Options = ["cef_load", "cef_gen"] # cef_load is the fraction of generation that is clean, for the generation that is allocated to a region’s end-use load; cef_gen is the fraction of generation that is clean within a region - clean_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Utilities renewable energy fraction. Can be scalar or timeseries (aligned with time_steps_per_hour). - include_grid_clean_energy_in_total::Bool=true, # if true, the clean energy fraction of the grid electricity is included in the site's total renewable electricity calculations + clean_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Utilities renewable energy fraction. Can be scalar or timeseries (aligned with time_steps_per_hour) ``` !!! note "Outage modeling" @@ -139,8 +138,7 @@ struct ElectricUtility scenarios::Union{Nothing, UnitRange} net_metering_limit_kw::Real interconnection_limit_kw::Real - clean_energy_fraction_series::Array{<:Real,1} # Utility renewable energy fraction. - include_grid_clean_energy_in_total::Bool + clean_energy_fraction_series::Array{<:Real,1} # fraction of grid electricity that is clean or renewable function ElectricUtility(; @@ -197,8 +195,7 @@ struct ElectricUtility ### Grid Clean Energy Fraction Inputs ### cambium_cef_metric::String = "cef_load", # Options = ["cef_load", "cef_gen"] # cef_load is the fraction of generation that is clean, for the generation that is allocated to a region’s end-use load; cef_gen is the fraction of generation that is clean within a region - clean_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Utilities renewable energy fraction. Can be scalar or timeseries (aligned with time_steps_per_hour). - include_grid_clean_energy_in_total::Bool=true # if true, the clean energy fraction of the grid electricity is included in the site's renewable electricity calculations + clean_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Utilities renewable energy fraction. Can be scalar or timeseries (aligned with time_steps_per_hour) ) is_MPC = isnothing(latitude) || isnothing(longitude) @@ -370,8 +367,7 @@ struct ElectricUtility scenarios, net_metering_limit_kw, interconnection_limit_kw, - is_MPC ? Float64[] : emissions_and_cef_series_dict["clean_energy_fraction_series"], - include_grid_clean_energy_in_total + is_MPC ? Float64[] : emissions_and_cef_series_dict["clean_energy_fraction_series"] ) end end diff --git a/src/core/site.jl b/src/core/site.jl index 0ecb42752..998e4124a 100644 --- a/src/core/site.jl +++ b/src/core/site.jl @@ -17,6 +17,7 @@ Inputs related to the physical location: bau_grid_emissions_lb_CO2_per_year::Union{Float64, Nothing} = nothing, renewable_electricity_min_fraction::Real = 0.0, renewable_electricity_max_fraction::Union{Float64, Nothing} = nothing, + include_grid_renewable_electricity_in_min_max_constraints::Bool = false, include_exported_elec_emissions_in_total::Bool = true, include_exported_renewable_electricity_in_total::Bool = true, outdoor_air_temperature_degF::Union{Nothing, Array{<:Real,1}} = nothing, @@ -37,8 +38,10 @@ mutable struct Site bau_grid_emissions_lb_CO2_per_year renewable_electricity_min_fraction renewable_electricity_max_fraction + include_grid_renewable_electricity_in_min_max_constraints include_exported_elec_emissions_in_total include_exported_renewable_electricity_in_total + include_grid_renewable_electricity_in_total outdoor_air_temperature_degF node # TODO validate that multinode Sites do not share node numbers? Or just raise warning function Site(; @@ -54,8 +57,10 @@ mutable struct Site bau_grid_emissions_lb_CO2_per_year::Union{Float64, Nothing} = nothing, renewable_electricity_min_fraction::Union{Float64, Nothing} = nothing, renewable_electricity_max_fraction::Union{Float64, Nothing} = nothing, + include_grid_renewable_electricity_in_min_max_constraints::Bool = false, include_exported_elec_emissions_in_total::Bool = true, include_exported_renewable_electricity_in_total::Bool = true, + include_grid_renewable_electricity_in_total::Bool = false, outdoor_air_temperature_degF::Union{Nothing, Array{<:Real,1}} = nothing, node::Int = 1, ) @@ -79,7 +84,7 @@ mutable struct Site mg_tech_sizes_equal_grid_sizes, CO2_emissions_reduction_min_fraction, CO2_emissions_reduction_max_fraction, bau_emissions_lb_CO2_per_year, bau_grid_emissions_lb_CO2_per_year, renewable_electricity_min_fraction, - renewable_electricity_max_fraction, include_exported_elec_emissions_in_total, + renewable_electricity_max_fraction, include_grid_renewable_electricity_in_min_max_constraints, include_exported_elec_emissions_in_total, include_exported_renewable_electricity_in_total, outdoor_air_temperature_degF, node) end end \ No newline at end of file diff --git a/src/results/site.jl b/src/results/site.jl index bacece662..2ef460e28 100644 --- a/src/results/site.jl +++ b/src/results/site.jl @@ -42,8 +42,8 @@ function add_site_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() # renewable elec - r["annual_renewable_electricity_kwh"] = round(value(m[:AnnualREEleckWh]), digits=2) - r["renewable_electricity_fraction"] = round(value(m[:AnnualREEleckWh])/value(m[:AnnualEleckWh]), digits=6) + r["annual_renewable_electricity_kwh"] = round(value(m[:AnnualOnsiteREEleckWh]), digits=2) + r["renewable_electricity_fraction"] = round(value(m[:AnnualOnsiteREEleckWh])/value(m[:AnnualEleckWh]), digits=6) # total renewable energy add_re_tot_calcs(m,p) @@ -138,7 +138,7 @@ function add_re_tot_calcs(m::JuMP.AbstractModel, p::REoptInputs) # - AnnualSteamToSteamTurbine # minus steam going to SteamTurbine; already adjusted by p.hours_per_time_step ) end - m[:AnnualRETotkWh] = @expression(m, m[:AnnualREEleckWh] + AnnualREHeatkWh) + m[:AnnualRETotkWh] = @expression(m, m[:AnnualOnsiteREEleckWh] + AnnualREHeatkWh) m[:AnnualTotkWh] = @expression(m, m[:AnnualEleckWh] + AnnualHeatkWh) nothing end \ No newline at end of file From 75967683b4887df20ab08b6f084146d95431894f Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 14 Nov 2024 10:35:43 -0700 Subject: [PATCH 25/29] Update renewable_energy_constraints.jl --- src/constraints/renewable_energy_constraints.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/constraints/renewable_energy_constraints.jl b/src/constraints/renewable_energy_constraints.jl index c7066786b..0e1e4ddf7 100644 --- a/src/constraints/renewable_energy_constraints.jl +++ b/src/constraints/renewable_energy_constraints.jl @@ -38,7 +38,7 @@ function add_re_elec_calcs(m,p) # TODO: When steam turbine implemented, uncomment code below, replacing p.TechCanSupplySteamTurbine, p.STElecOutToThermInRatio with new names # # Steam turbine RE elec calculations # if isempty(p.steam) - # SteamTurbineAnnualOnsiteREEleckWh = 0 + # SteamTurbineAnnualREEleckWh = 0 # else # # Note: SteamTurbine's input p.tech_renewable_energy_fraction = 0 because it is actually a decision variable dependent on fraction of steam generated by RE fuel # SteamTurbinePercentREEstimate = @expression(m, @@ -46,7 +46,7 @@ function add_re_elec_calcs(m,p) # ) # # Note: Steam turbine battery losses, curtailment, and exported RE terms are only accurate if all techs that can supply ST # # have equal RE%, otherwise it is an approximation because the general equation is non linear. - # SteamTurbineAnnualOnsiteREEleckWh = @expression(m,p.hours_per_time_step * ( + # SteamTurbineAnnualREEleckWh = @expression(m,p.hours_per_time_step * ( # p.STElecOutToThermInRatio * sum(m[:dvThermalToSteamTurbine][tst,ts]*p.tech_renewable_energy_fraction[tst] for ts in p.time_steps, tst in p.TechCanSupplySteamTurbine) # plus steam turbine RE generation # - sum(m[:dvProductionToStorage][b,t,ts] * SteamTurbinePercentREEstimate * (1-p.s.storage.attr[b].charge_efficiency*p.s.storage.attr[b].discharge_efficiency) for t in p.steam, b in p.s.storage.types.elec, ts in p.time_steps) # minus battery storage losses from RE from steam turbine # - sum(m[:dvCurtail][t,ts] * SteamTurbinePercentREEstimate for t in p.steam, ts in p.time_steps) # minus curtailment. @@ -68,7 +68,7 @@ function add_re_elec_calcs(m,p) for t in p.techs.elec, u in p.export_bins_by_tech[t], ts in p.time_steps ) # minus exported RE, if RE accounting method = 0. ) - # + SteamTurbineAnnualOnsiteREEleckWh # SteamTurbine RE Elec, already adjusted for p.hours_per_time_step + # + SteamTurbineAnnualREEleckWh # SteamTurbine RE Elec, already adjusted for p.hours_per_time_step ) # Note: if battery ends up being allowed to discharge to grid, need to make sure only RE that is being consumed onsite is counted so battery doesn't become a back door for RE to grid. # Note: calculations currently do not ascribe any renewable energy attribute to grid-purchased electricity From 434f9287b4f3988e40b8f7dfdfd3fcb50528240f Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 14 Nov 2024 10:59:24 -0700 Subject: [PATCH 26/29] Update site.jl --- src/core/site.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/site.jl b/src/core/site.jl index 998e4124a..651f39891 100644 --- a/src/core/site.jl +++ b/src/core/site.jl @@ -41,7 +41,6 @@ mutable struct Site include_grid_renewable_electricity_in_min_max_constraints include_exported_elec_emissions_in_total include_exported_renewable_electricity_in_total - include_grid_renewable_electricity_in_total outdoor_air_temperature_degF node # TODO validate that multinode Sites do not share node numbers? Or just raise warning function Site(; @@ -60,7 +59,6 @@ mutable struct Site include_grid_renewable_electricity_in_min_max_constraints::Bool = false, include_exported_elec_emissions_in_total::Bool = true, include_exported_renewable_electricity_in_total::Bool = true, - include_grid_renewable_electricity_in_total::Bool = false, outdoor_air_temperature_degF::Union{Nothing, Array{<:Real,1}} = nothing, node::Int = 1, ) From 3b7a93abcd03e1dae37f45158078da26162d64eb Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 14 Nov 2024 12:39:15 -0700 Subject: [PATCH 27/29] update outputs --- .../renewable_energy_constraints.jl | 2 +- src/core/electric_utility.jl | 20 +++++++++---------- src/core/scenario.jl | 2 +- src/results/electric_utility.jl | 9 +++------ src/results/results.jl | 6 +++--- src/results/site.jl | 14 ++++++------- test/runtests.jl | 20 +++++++++---------- .../erp_gens_batt_pv_wind_reopt_results.json | 6 +++--- test/test_with_xpress.jl | 20 +++++++++---------- 9 files changed, 48 insertions(+), 51 deletions(-) diff --git a/src/constraints/renewable_energy_constraints.jl b/src/constraints/renewable_energy_constraints.jl index 0e1e4ddf7..251a4f81e 100644 --- a/src/constraints/renewable_energy_constraints.jl +++ b/src/constraints/renewable_energy_constraints.jl @@ -76,7 +76,7 @@ function add_re_elec_calcs(m,p) m[:AnnualGridREEleckWh] = @expression(m, p.hours_per_time_step * ( sum(m[:dvGridPurchase][ts, tier] - m[:dvGridToStorage][b, ts] * (1 - p.s.storage.attr[b].charge_efficiency * p.s.storage.attr[b].discharge_efficiency) * - p.s.electric_utility.clean_energy_fraction_series[ts] + p.s.electric_utility.renewable_energy_fraction_series[ts] for ts in p.time_steps, tier in 1:p.s.electric_tariff.n_energy_tiers, b in p.s.storage.types.elec ) # clean energy from grid, minus battery efficiency losses ) diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index ea4fc6911..4aa7de355 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -53,7 +53,7 @@ ### Grid Clean Energy Fraction Inputs ### cambium_cef_metric::String = "cef_load", # Options = ["cef_load", "cef_gen"] # cef_load is the fraction of generation that is clean, for the generation that is allocated to a region’s end-use load; cef_gen is the fraction of generation that is clean within a region - clean_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Utilities renewable energy fraction. Can be scalar or timeseries (aligned with time_steps_per_hour) + renewable_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Fraction of energy supplied by the grid that is renewable. Can be scalar or timeseries (aligned with time_steps_per_hour) ``` !!! note "Outage modeling" @@ -111,7 +111,7 @@ - For sites in CONUS: - Default clean energy fraction data comes from NREL's Cambium database (Current version: 2022) - By default, REopt uses *clean energy fraction* for the region in which the site is located. - - For sites outside of CONUS: REopt does not have default grid clean energy fraction data. Users must supply a custom `clean_energy_fraction_series` + - For sites outside of CONUS: REopt does not have default grid clean energy fraction data. Users must supply a custom `renewable_energy_fraction_series` """ struct ElectricUtility @@ -138,7 +138,7 @@ struct ElectricUtility scenarios::Union{Nothing, UnitRange} net_metering_limit_kw::Real interconnection_limit_kw::Real - clean_energy_fraction_series::Array{<:Real,1} # fraction of grid electricity that is clean or renewable + renewable_energy_fraction_series::Array{<:Real,1} # fraction of grid electricity that is clean or renewable function ElectricUtility(; @@ -195,7 +195,7 @@ struct ElectricUtility ### Grid Clean Energy Fraction Inputs ### cambium_cef_metric::String = "cef_load", # Options = ["cef_load", "cef_gen"] # cef_load is the fraction of generation that is clean, for the generation that is allocated to a region’s end-use load; cef_gen is the fraction of generation that is clean within a region - clean_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Utilities renewable energy fraction. Can be scalar or timeseries (aligned with time_steps_per_hour) + renewable_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Utilities renewable energy fraction. Can be scalar or timeseries (aligned with time_steps_per_hour) ) is_MPC = isnothing(latitude) || isnothing(longitude) @@ -203,8 +203,8 @@ struct ElectricUtility if !is_MPC # Check some inputs - if any(x -> x < 0 || x > 1, clean_energy_fraction_series) - throw(@error("All values in the provided ElectricUtility clean_energy_fraction_series must be between 0 and 1.")) + if any(x -> x < 0 || x > 1, renewable_energy_fraction_series) + throw(@error("All values in the provided ElectricUtility renewable_energy_fraction_series must be between 0 and 1.")) end if cambium_start_year < 2023 || cambium_start_year > 2050 # TODO: update? cambium_start_year = 2025 # Must update annually @@ -241,7 +241,7 @@ struct ElectricUtility (emissions_factor_series_lb_NOx_per_kwh, "NOx"), (emissions_factor_series_lb_SO2_per_kwh, "SO2"), (emissions_factor_series_lb_PM25_per_kwh, "PM25"), - (clean_energy_fraction_series, "clean_energy_fraction_series") + (renewable_energy_fraction_series, "renewable_energy_fraction_series") ] if off_grid_flag # no grid emissions for off-grid emissions_and_cef_series_dict[ekey] = zeros(Float64, 8760*time_steps_per_hour) @@ -259,7 +259,7 @@ struct ElectricUtility throw(@error("The provided ElectricUtility emissions or clean enery fraction series for $(ekey) does not match the time_steps_per_hour.")) end else # if not user-provided, get emissions or cef factors from Cambium and/or AVERT - if ekey == "CO2" && co2_from_avert == false || ekey == "clean_energy_fraction_series" # Use Cambium for CO2 or clean energy factors + if ekey == "CO2" && co2_from_avert == false || ekey == "renewable_energy_fraction_series" # Use Cambium for CO2 or clean energy factors try cambium_response_dict = cambium_profile( # Adjusted for day of week alignment with load and time_steps_per_hour scenario = cambium_scenario, @@ -279,7 +279,7 @@ struct ElectricUtility # save clean_energy_series_dict["cef"] as csv # clean_energy_df = DataFrame(cef = clean_energy_series_dict["cef"]) - # CSV.write("clean_energy_fraction_series.csv", clean_energy_df) + # CSV.write("renewable_energy_fraction_series.csv", clean_energy_df) catch @warn("Could not look up Cambium $(ekey) profile for location ($(latitude), $(longitude)). Location is likely outside contiguous US or something went wrong with the Cambium API request. Setting ElectricUtility $(ekey) factors to zero.") @@ -367,7 +367,7 @@ struct ElectricUtility scenarios, net_metering_limit_kw, interconnection_limit_kw, - is_MPC ? Float64[] : emissions_and_cef_series_dict["clean_energy_fraction_series"] + is_MPC ? Float64[] : emissions_and_cef_series_dict["renewable_energy_fraction_series"] ) end end diff --git a/src/core/scenario.jl b/src/core/scenario.jl index b3fb51a62..163d66a85 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -163,7 +163,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) emissions_factor_series_lb_NOx_per_kwh = 0, emissions_factor_series_lb_SO2_per_kwh = 0, emissions_factor_series_lb_PM25_per_kwh = 0, - clean_energy_fraction_series = 0 + renewable_energy_fraction_series = 0 ) end diff --git a/src/results/electric_utility.jl b/src/results/electric_utility.jl index d2d61813a..b99401fec 100644 --- a/src/results/electric_utility.jl +++ b/src/results/electric_utility.jl @@ -4,6 +4,7 @@ - `annual_energy_supplied_kwh` # Total energy supplied from the grid in an average year. - `electric_to_load_series_kw` # Vector of power drawn from the grid to serve load. - `electric_to_storage_series_kw` # Vector of power drawn from the grid to charge the battery. +- `annual_renewable_electricity_supplied_kwh` # Total renewable electricity supplied from the grid in an average year. - `annual_emissions_tonnes_CO2` # Average annual total tons of CO2 emissions associated with the site's grid-purchased electricity. If include_exported_elec_emissions_in_total is False, this value only reflects grid purchases. Otherwise, it accounts for emissions offset from any export to the grid. - `annual_emissions_tonnes_NOx` # Average annual total tons of NOx emissions associated with the site's grid-purchased electricity. If include_exported_elec_emissions_in_total is False, this value only reflects grid purchases. Otherwise, it accounts for emissions offset from any export to the grid. - `annual_emissions_tonnes_SO2` # Average annual total tons of SO2 emissions associated with the site's grid-purchased electricity. If include_exported_elec_emissions_in_total is False, this value only reflects grid purchases. Otherwise, it accounts for emissions offset from any export to the grid. @@ -36,7 +37,7 @@ function add_electric_utility_results(m::JuMP.AbstractModel, p::AbstractInputs, if :WHL in p.s.electric_tariff.export_bins if abs(sum(value.(m[Symbol("WHL_benefit"*_n)])) - 10*sum([ld*rate for (ld,rate) in zip(p.s.electric_load.loads_kw, p.s.electric_tariff.export_rates[:WHL])]) / value(m[Symbol("WHL_benefit"*_n)])) <= 1e-3 @warn """Wholesale benefit is at the maximum allowable by the model; the problem is likely unbounded without this - limit in place. Check the inputs to ensure that there are practical limits for max system sizes and that + limit in place. Check the inputs to ensure that there are practical limits for max system sizes and that the wholesale and retail electricity rates are accurate.""" end end @@ -45,11 +46,7 @@ function add_electric_utility_results(m::JuMP.AbstractModel, p::AbstractInputs, for ts in p.time_steps, tier in 1:p.s.electric_tariff.n_energy_tiers) r["annual_energy_supplied_kwh"] = round(value(Year1UtilityEnergy), digits=2) - calc_clean_grid_kWh(m, p) - - r["clean_grid_to_load_series_kw"] = round.(value.([m[:grid_clean_energy_series_kw][ts] for ts in p.time_steps]), digits=3) - #sum to find the annual clean grid to load kWh - r["annual_clean_grid_to_load_kwh"] = round(sum(r["clean_grid_to_load_series_kw"]), digits=2) + r["annual_renewable_electricity_supplied_kwh"] = round(value(m[:AnnualGridREEleckWh]), digits=2) if !isempty(p.s.storage.types.elec) GridToLoad = (sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) diff --git a/src/results/results.jl b/src/results/results.jl index 9b79d13a8..4cbb1c472 100644 --- a/src/results/results.jl +++ b/src/results/results.jl @@ -175,9 +175,9 @@ function combine_results(p::REoptInputs, bau::Dict, opt::Dict, bau_scenario::BAU ("ExistingBoiler", "annual_fuel_consumption_mmbtu"), ("ExistingChiller", "annual_thermal_production_tonhour"), ("ExistingChiller", "annual_electric_consumption_kwh"), - ("Site", "annual_renewable_electricity_kwh"), - ("Site", "renewable_electricity_fraction"), - ("Site", "total_renewable_energy_fraction"), + ("Site", "annual_onsite_renewable_electricity_kwh"), + ("Site", "onsite_renewable_electricity_fraction_of_elec_load"), + ("Site", "onsite_renewable_energy_fraction_of_elec_and_thermal_load"), ("Site", "annual_emissions_tonnes_CO2"), ("Site", "annual_emissions_tonnes_NOx"), ("Site", "annual_emissions_tonnes_SO2"), diff --git a/src/results/site.jl b/src/results/site.jl index 2ef460e28..14d79ff2b 100644 --- a/src/results/site.jl +++ b/src/results/site.jl @@ -5,9 +5,9 @@ Adds the Site results to the dictionary passed back from `run_reopt` using the solved model `m` and the `REoptInputs`. Site results: -- `annual_renewable_electricity_kwh` -- `renewable_electricity_fraction` -- `total_renewable_energy_fraction` +- `annual_onsite_renewable_electricity_kwh` +- `onsite_renewable_electricity_fraction_of_elec_load` +- `onsite_renewable_energy_fraction_of_elec_and_thermal_load` - `annual_emissions_tonnes_CO2` # Average annual total tons of emissions associated with the site's grid-purchased electricity and on-site fuel consumption. - `annual_emissions_tonnes_NOx` # Average annual total tons of emissions associated with the site's grid-purchased electricity and on-site fuel consumption. - `annual_emissions_tonnes_SO2` # Average annual total tons of emissions associated with the site's grid-purchased electricity and on-site fuel consumption. @@ -42,12 +42,12 @@ function add_site_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() # renewable elec - r["annual_renewable_electricity_kwh"] = round(value(m[:AnnualOnsiteREEleckWh]), digits=2) - r["renewable_electricity_fraction"] = round(value(m[:AnnualOnsiteREEleckWh])/value(m[:AnnualEleckWh]), digits=6) + r["annual_onsite_renewable_electricity_kwh"] = round(value(m[:AnnualOnsiteREEleckWh]), digits=2) + r["onsite_renewable_electricity_fraction_of_elec_load"] = round(value(m[:AnnualOnsiteREEleckWh])/value(m[:AnnualEleckWh]), digits=6) # total renewable energy add_re_tot_calcs(m,p) - r["total_renewable_energy_fraction"] = round(value(m[:AnnualRETotkWh])/value(m[:AnnualTotkWh]), digits=6) + r["onsite_renewable_energy_fraction_of_elec_and_thermal_load"] = round(value(m[:AnnualOnsiteRETotkWh])/value(m[:AnnualTotkWh]), digits=6) # Lifecycle emissions results at Site level if !isnothing(p.s.site.bau_emissions_lb_CO2_per_year) @@ -138,7 +138,7 @@ function add_re_tot_calcs(m::JuMP.AbstractModel, p::REoptInputs) # - AnnualSteamToSteamTurbine # minus steam going to SteamTurbine; already adjusted by p.hours_per_time_step ) end - m[:AnnualRETotkWh] = @expression(m, m[:AnnualOnsiteREEleckWh] + AnnualREHeatkWh) + m[:AnnualOnsiteRETotkWh] = @expression(m, m[:AnnualOnsiteREEleckWh] + AnnualREHeatkWh) m[:AnnualTotkWh] = @expression(m, m[:AnnualEleckWh] + AnnualHeatkWh) nothing end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 7c13a7701..1f5aab156 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2283,8 +2283,8 @@ else # run HiGHS tests @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"] ≈ 9.13 atol=1e-1 - @test results["Site"]["total_renewable_energy_fraction"] ≈ 0.8 - @test results["Site"]["total_renewable_energy_fraction_bau"] ≈ 0.148375 atol=1e-4 + @test results["Site"]["onsite_renewable_energy_fraction_of_elec_and_thermal_load"] ≈ 0.8 + @test results["Site"]["onsite_renewable_energy_fraction_of_elec_and_thermal_load_bau"] ≈ 0.148375 atol=1e-4 @test results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] ≈ 0.57403012 atol=1e-4 @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ 332.4 atol=1 @test results["Site"]["annual_emissions_tonnes_CO2"] ≈ 11.85 atol=1e-2 @@ -2303,10 +2303,10 @@ else # run HiGHS tests @test results["ElectricStorage"]["size_kwh"] ≈ 170.94 atol=1 @test !haskey(results, "Generator") # Renewable energy - @test results["Site"]["renewable_electricity_fraction"] ≈ 0.78586 atol=1e-3 - @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"] ≈ 13308.5 atol=10 # 13542.62 atol=10 - @test results["Site"]["total_renewable_energy_fraction_bau"] ≈ 0.132118 atol=1e-3 # 0.1354 atol=1e-3 + @test results["Site"]["onsite_renewable_electricity_fraction_of_elec_load"] ≈ 0.78586 atol=1e-3 + @test results["Site"]["onsite_renewable_electricity_fraction_of_elec_load_bau"] ≈ 0.132118 atol=1e-3 #0.1354 atol=1e-3 + @test results["Site"]["annual_onsite_renewable_electricity_kwh_bau"] ≈ 13308.5 atol=10 # 13542.62 atol=10 + @test results["Site"]["onsite_renewable_energy_fraction_of_elec_and_thermal_load_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"] ≈ 491.5 atol=1e-1 @@ -2354,12 +2354,12 @@ else # run HiGHS tests @test results["Site"]["lifecycle_emissions_tonnes_NOx"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_NOx"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_NOx"] atol=0.1 @test results["Site"]["lifecycle_emissions_tonnes_SO2"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_SO2"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_SO2"] atol=1e-2 @test results["Site"]["lifecycle_emissions_tonnes_PM25"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_PM25"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_PM25"] atol=1.5e-2 - @test results["Site"]["annual_renewable_electricity_kwh"] ≈ results["PV"]["annual_energy_produced_kwh"] + inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_electric_production_kwh"] atol=1 - @test results["Site"]["renewable_electricity_fraction"] ≈ results["Site"]["annual_renewable_electricity_kwh"] / results["ElectricLoad"]["annual_calculated_kwh"] atol=1e-6#0.044285 atol=1e-4 + @test results["Site"]["annual_onsite_renewable_electricity_kwh"] ≈ results["PV"]["annual_energy_produced_kwh"] + inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_electric_production_kwh"] atol=1 + @test results["Site"]["onsite_renewable_electricity_fraction_of_elec_load"] ≈ results["Site"]["annual_onsite_renewable_electricity_kwh"] / results["ElectricLoad"]["annual_calculated_kwh"] atol=1e-6#0.044285 atol=1e-4 KWH_PER_MMBTU = 293.07107 - annual_RE_kwh = inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_thermal_production_mmbtu"] * KWH_PER_MMBTU + results["Site"]["annual_renewable_electricity_kwh"] + annual_RE_kwh = inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_thermal_production_mmbtu"] * KWH_PER_MMBTU + results["Site"]["annual_onsite_renewable_electricity_kwh"] annual_heat_kwh = (results["CHP"]["annual_thermal_production_mmbtu"] + results["ExistingBoiler"]["annual_thermal_production_mmbtu"]) * KWH_PER_MMBTU - @test results["Site"]["total_renewable_energy_fraction"] ≈ annual_RE_kwh / (annual_heat_kwh + results["ElectricLoad"]["annual_calculated_kwh"]) atol=1e-6 + @test results["Site"]["onsite_renewable_energy_fraction_of_elec_and_thermal_load"] ≈ annual_RE_kwh / (annual_heat_kwh + results["ElectricLoad"]["annual_calculated_kwh"]) atol=1e-6 end end end diff --git a/test/scenarios/erp_gens_batt_pv_wind_reopt_results.json b/test/scenarios/erp_gens_batt_pv_wind_reopt_results.json index 2eecaedfe..401b8b19a 100644 --- a/test/scenarios/erp_gens_batt_pv_wind_reopt_results.json +++ b/test/scenarios/erp_gens_batt_pv_wind_reopt_results.json @@ -10,13 +10,13 @@ "annual_emissions_tonnes_PM25": 0.02, "lifecycle_emissions_tonnes_NOx": 0.46, "annual_emissions_from_fuelburn_tonnes_CO2": 0.0, - "total_renewable_energy_fraction": 0.271611, + "onsite_renewable_energy_fraction_of_elec_and_thermal_load": 0.271611, "annual_emissions_from_fuelburn_tonnes_SO2": 0.0, "lifecycle_emissions_from_fuelburn_tonnes_SO2": 0.0, - "renewable_electricity_fraction": 0.271611, + "onsite_renewable_electricity_fraction_of_elec_load": 0.271611, "lifecycle_emissions_from_fuelburn_tonnes_CO2": 0.0, "lifecycle_emissions_from_fuelburn_tonnes_NOx": 0.0, - "annual_renewable_electricity_kwh": 271611.29, + "annual_onsite_renewable_electricity_kwh": 271611.29, "annual_emissions_tonnes_CO2": 359.92, "lifecycle_emissions_tonnes_CO2": 6373.62 }, diff --git a/test/test_with_xpress.jl b/test/test_with_xpress.jl index c368d88b4..feebbc2f4 100644 --- a/test/test_with_xpress.jl +++ b/test/test_with_xpress.jl @@ -1449,8 +1449,8 @@ end @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 - @test results["Site"]["total_renewable_energy_fraction"] ≈ 0.8 - @test results["Site"]["total_renewable_energy_fraction_bau"] ≈ 0.147576 atol=1e-4 + @test results["Site"]["onsite_renewable_energy_fraction_of_elec_and_thermal_load"] ≈ 0.8 + @test results["Site"]["onsite_renewable_energy_fraction_of_elec_and_thermal_load_bau"] ≈ 0.147576 atol=1e-4 @test results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] ≈ 0.58694032 atol=1e-4 @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ 355.8 atol=1 @test results["Site"]["annual_emissions_tonnes_CO2"] ≈ 11.64 atol=1e-2 @@ -1469,10 +1469,10 @@ end @test results["ElectricStorage"]["size_kwh"] ≈ 166.29 atol=1 @test !haskey(results, "Generator") # Renewable energy - @test results["Site"]["renewable_electricity_fraction"] ≈ 0.78586 atol=1e-3 - @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_bau"] ≈ 0.132118 atol=1e-3 # 0.1354 atol=1e-3 + @test results["Site"]["onsite_renewable_electricity_fraction_of_elec_load"] ≈ 0.78586 atol=1e-3 + @test results["Site"]["onsite_renewable_electricity_fraction_of_elec_load_bau"] ≈ 0.132118 atol=1e-3 #0.1354 atol=1e-3 + @test results["Site"]["annual_onsite_renewable_electricity_kwh_bau"] ≈ 13211.78 atol=10 # 13542.62 atol=10 + @test results["Site"]["onsite_renewable_energy_fraction_of_elec_and_thermal_load_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"] ≈ 460.7 atol=1e-1 @@ -1520,12 +1520,12 @@ end @test results["Site"]["lifecycle_emissions_tonnes_NOx"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_NOx"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_NOx"] atol=0.1 @test results["Site"]["lifecycle_emissions_tonnes_SO2"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_SO2"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_SO2"] atol=1e-2 @test results["Site"]["lifecycle_emissions_tonnes_PM25"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_PM25"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_PM25"] atol=1.5e-2 - @test results["Site"]["annual_renewable_electricity_kwh"] ≈ results["PV"]["annual_energy_produced_kwh"] + inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_electric_production_kwh"] atol=1 - @test results["Site"]["renewable_electricity_fraction"] ≈ results["Site"]["annual_renewable_electricity_kwh"] / results["ElectricLoad"]["annual_calculated_kwh"] atol=1e-6#0.044285 atol=1e-4 + @test results["Site"]["annual_onsite_renewable_electricity_kwh"] ≈ results["PV"]["annual_energy_produced_kwh"] + inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_electric_production_kwh"] atol=1 + @test results["Site"]["onsite_renewable_electricity_fraction_of_elec_load"] ≈ results["Site"]["annual_onsite_renewable_electricity_kwh"] / results["ElectricLoad"]["annual_calculated_kwh"] atol=1e-6#0.044285 atol=1e-4 KWH_PER_MMBTU = 293.07107 - annual_RE_kwh = inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_thermal_production_mmbtu"] * KWH_PER_MMBTU + results["Site"]["annual_renewable_electricity_kwh"] + annual_RE_kwh = inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_thermal_production_mmbtu"] * KWH_PER_MMBTU + results["Site"]["annual_onsite_renewable_electricity_kwh"] annual_heat_kwh = (results["CHP"]["annual_thermal_production_mmbtu"] + results["ExistingBoiler"]["annual_thermal_production_mmbtu"]) * KWH_PER_MMBTU - @test results["Site"]["total_renewable_energy_fraction"] ≈ annual_RE_kwh / (annual_heat_kwh + results["ElectricLoad"]["annual_calculated_kwh"]) atol=1e-6 + @test results["Site"]["onsite_renewable_energy_fraction_of_elec_and_thermal_load"] ≈ annual_RE_kwh / (annual_heat_kwh + results["ElectricLoad"]["annual_calculated_kwh"]) atol=1e-6 end end end From f2bb4483c2428655bee252389f1f28e304384729 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 14 Nov 2024 12:54:46 -0700 Subject: [PATCH 28/29] add outputs --- src/results/results.jl | 3 +++ src/results/site.jl | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/results/results.jl b/src/results/results.jl index 4cbb1c472..e6ff65fdf 100644 --- a/src/results/results.jl +++ b/src/results/results.jl @@ -150,6 +150,7 @@ function combine_results(p::REoptInputs, bau::Dict, opt::Dict, bau_scenario::BAU ("ElectricTariff", "lifecycle_coincident_peak_cost_after_tax"), ("ElectricUtility", "electric_to_load_series_kw"), ("ElectricUtility", "annual_energy_supplied_kwh"), + ("ElectricUtility","annual_renewable_electricity_supplied_kwh"), ("ElectricUtility", "annual_emissions_tonnes_CO2"), ("ElectricUtility", "annual_emissions_tonnes_NOx"), ("ElectricUtility", "annual_emissions_tonnes_SO2"), @@ -178,6 +179,8 @@ function combine_results(p::REoptInputs, bau::Dict, opt::Dict, bau_scenario::BAU ("Site", "annual_onsite_renewable_electricity_kwh"), ("Site", "onsite_renewable_electricity_fraction_of_elec_load"), ("Site", "onsite_renewable_energy_fraction_of_elec_and_thermal_load"), + ("Site", "onsite_and_grid_renewable_electricity_fraction_of_elec_load"), + ("Site", "onsite_and_grid_renewable_energy_fraction_of_elec_and_thermal_load"), ("Site", "annual_emissions_tonnes_CO2"), ("Site", "annual_emissions_tonnes_NOx"), ("Site", "annual_emissions_tonnes_SO2"), diff --git a/src/results/site.jl b/src/results/site.jl index 14d79ff2b..93df8ed3f 100644 --- a/src/results/site.jl +++ b/src/results/site.jl @@ -5,9 +5,11 @@ Adds the Site results to the dictionary passed back from `run_reopt` using the solved model `m` and the `REoptInputs`. Site results: -- `annual_onsite_renewable_electricity_kwh` +- `annual_onsite_renewable_electricity_kwh` # renewable electricity from on-site renewable electricity-generating technologies (including fuel-burning technologies) - `onsite_renewable_electricity_fraction_of_elec_load` - `onsite_renewable_energy_fraction_of_elec_and_thermal_load` +- `onsite_and_grid_renewable_electricity_fraction_of_elec_load` +- `onsite_and_grid_renewable_energy_fraction_of_elec_and_thermal_load` - `annual_emissions_tonnes_CO2` # Average annual total tons of emissions associated with the site's grid-purchased electricity and on-site fuel consumption. - `annual_emissions_tonnes_NOx` # Average annual total tons of emissions associated with the site's grid-purchased electricity and on-site fuel consumption. - `annual_emissions_tonnes_SO2` # Average annual total tons of emissions associated with the site's grid-purchased electricity and on-site fuel consumption. @@ -43,11 +45,13 @@ function add_site_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") # renewable elec r["annual_onsite_renewable_electricity_kwh"] = round(value(m[:AnnualOnsiteREEleckWh]), digits=2) - r["onsite_renewable_electricity_fraction_of_elec_load"] = round(value(m[:AnnualOnsiteREEleckWh])/value(m[:AnnualEleckWh]), digits=6) + r["onsite_renewable_electricity_fraction_of_elec_load"] = round(value(m[:AnnualOnsiteREEleckWh])/value(m[:AnnualEleckWh]), digits=4) + r["onsite_and_grid_renewable_electricity_fraction_of_elec_load"] = round((value(m[:AnnualOnsiteREEleckWh]) + value(m[:AnnualGridREEleckWh])) /value(m[:AnnualEleckWh]), digits=4) # total renewable energy add_re_tot_calcs(m,p) - r["onsite_renewable_energy_fraction_of_elec_and_thermal_load"] = round(value(m[:AnnualOnsiteRETotkWh])/value(m[:AnnualTotkWh]), digits=6) + r["onsite_renewable_energy_fraction_of_elec_and_thermal_load"] = round(value(m[:AnnualOnsiteRETotkWh])/value(m[:AnnualTotkWh]), digits=4) + r["onsite_and_grid_renewable_energy_fraction_of_elec_and_thermal_load"] = round((value(m[:AnnualOnsiteRETotkWh]) + value(m[:AnnualGridREEleckWh]))/value(m[:AnnualTotkWh]), digits=4) # Lifecycle emissions results at Site level if !isnothing(p.s.site.bau_emissions_lb_CO2_per_year) From a058fed559f5ae7d6acd0bf4a87ee8abbcac6199 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 14 Nov 2024 16:17:48 -0700 Subject: [PATCH 29/29] fix to align_profile_with_load_year --- src/core/electric_utility.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index 4aa7de355..18ff21a0a 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -637,7 +637,7 @@ function align_profile_with_load_year(; load_year::Int, profile_year::Int, profi # Example: Emissions year = 2017; ef_start_day = 7 (Sunday). Load year = 2021; load_start_day = 5 (Fri) cut_days = 7+(load_start_day-ef_start_day) # Ex: = 7+(5-7) = 5 --> cut Sun, Mon, Tues, Wed, Thurs wrap_ts = profile_data[25:24+24*cut_days] # Ex: = profile_data[25:144] wrap Mon-Fri to end - profile_data_adj = append!(emissions_profile[24*cut_days+1:end],wrap_ts) # Ex: now starts on Fri and end Fri to align with 2021 cal + profile_data_adj = append!(profile_data[24*cut_days+1:end],wrap_ts) # Ex: now starts on Fri and end Fri to align with 2021 cal end return profile_data_adj