Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Grid Renewable Energy Fraction #426

Draft
wants to merge 20 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0d4a787
Add API call for renewable energy fraction
pypapus Jul 30, 2024
71acc5e
Fixed cambium API call
pypapus Jul 31, 2024
eca3f12
Clean energy fraction added to scenarios
pypapus Aug 1, 2024
80d1d3e
Include profile of grid clean energy contribution in kW
pypapus Aug 1, 2024
c095d48
Updated help text and added checks for user-provided values
pypapus Aug 2, 2024
b67ff48
Corrected off-grid flag in scenario.jl
pypapus Aug 2, 2024
9a8f09f
Edited to remove cef calculation using BAU electric load
pypapus Aug 12, 2024
379eeab
Added new "cef_constraints" to calculate clean energy fraction of the…
pypapus Aug 12, 2024
26f5cec
Added cef (kWh) from the grid in accounting for renewable energy perc…
pypapus Aug 12, 2024
4cce89e
Added new function to calculate the clean_energy_fraction (kWh) grid …
pypapus Aug 12, 2024
6500b40
Added new input to include grid cef (kWh) in renewable energy percent…
pypapus Aug 12, 2024
98d22b5
Updated function adds grid clean energy kWh serving the load to results
pypapus Aug 12, 2024
c3c4d90
Merge branch 'develop' into gridRE-dev
adfarth Aug 12, 2024
e3b0300
removed "hours_per_time_step" to use model time step.
pypapus Aug 15, 2024
248481b
Added "annual_clean_grid_to_load_kwh" in results
pypapus Aug 15, 2024
1d77ec8
Merge branch 'gridRE-dev' of https://github.com/NREL/REopt.jl into gr…
pypapus Aug 15, 2024
fbf7691
Combined cambium_emissions_profile() and cambium_clean_energy_fractio…
pypapus Aug 16, 2024
29518dc
Update electric_utility.jl
adfarth Sep 10, 2024
132f982
Merge branch 'develop' into gridRE-dev
adfarth Nov 4, 2024
3a4bafd
spelling
adfarth Nov 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.vscode
.DS_Store
gridRE_tests
adfarth marked this conversation as resolved.
Show resolved Hide resolved
docs/build/
docs/Manifest.toml
**.log
139 changes: 133 additions & 6 deletions src/core/electric_utility.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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(;

Expand Down Expand Up @@ -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[],
pypapus marked this conversation as resolved.
Show resolved Hide resolved
cambium_cef_col::String = "cef_load", # Column name for clean energy fraction in Cambium database
pypapus marked this conversation as resolved.
Show resolved Hide resolved

### Grid Climate Emissions Inputs ###
# Climate Option 1 (Default): Use levelized emissions data from NREL's Cambium database by specifying the following fields:
Expand Down Expand Up @@ -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
Copy link
Collaborator Author

@adfarth adfarth Aug 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you'll want to add the off_grid_flag check here as well (same as done for the grid emissions)

EDIT: Actually, I think you can avoid doing this if you make the change suggested in "scenario.jl" below

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Off-grid flag to be added to "scenario.jl"

Copy link
Collaborator Author

@adfarth adfarth Aug 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low priority, but at some point we'll probably want a check that user-provided values are between 0-1 (inclusive), and throw an error if they are not

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added checks for user provided values

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,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pypapus could you check if changing the grid_level changes the values returned for the cef_load metric?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the grid level from "enduse" to "busbar" does not change the value of the cef_load.

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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -624,4 +673,82 @@ function align_emission_with_load_year(; load_year::Int, emissions_year::Int, em
end

return emissions_profile_adj
end
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

1 change: 1 addition & 0 deletions src/core/reopt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can remove this :)

results["status"] = status
results["solver_seconds"] = opt_time

Expand Down
Loading