diff --git a/CHANGELOG.md b/CHANGELOG.md index 14896e890..15597e7dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,27 @@ Classify the change according to the following categories: ### Removed +## gridRE-dev +### Added +- Added the following inputs to account for the clean or renewable energy fraction of grid-purchased electricity: + - ElectricUtility **cambium_cef_metric** to utilize clean energy data from NREL's Cambium database + - **renewable_energy_fraction_series** to supply a custom grid clean or renewable energy scalar or series + - Site **include_grid_renewable_electricity_in_min_max_constraints** - to allow user to choose whether to include grid RE in min max constraints +- Added the following outputs: + - ElectricUtility **annual_renewable_electricity_supplied_kwh** + - Site **onsite_and_grid_renewable_electricity_fraction_of_elec_load** + - Site **onsite_and_grid_renewable_energy_fraction_of_elec_and_thermal_load** +### Changed +- Changed name of the following inputs: + - ElectricUtility input **cambium_metric_col** changed to **cambium_co2_metric** +- Changed name of the following outputs: + - ElectricUtility **cambium_emissions_region** changed to **cambium_region** + - Site **annual_renewable_electricity_kwh** changed to **annual_onsite_renewable_electricity_kwh** + - Site **renewable_electricity_fraction** changed to **onsite_renewable_electricity_fraction_of_elec_load** + - Site **total_renewable_energy_fraction** changed to **onsite_renewable_energy_fraction_of_elec_and_thermal_load** +- Changed name of function (also available as endpoint through REopt API) from **cambium_emissions_profile** to **cambium_profile** + + ## Develop ### Added - Battery residual value if choosing replacement strategy for degradation diff --git a/src/constraints/renewable_energy_constraints.jl b/src/constraints/renewable_energy_constraints.jl index 3a6211756..b0fe9452a 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. +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) @@ -11,14 +12,17 @@ Function to add minimum and/or maximum renewable electricity (as percentage of l #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) @@ -50,7 +54,7 @@ function add_re_elec_calcs(m,p) # )) # end - m[:AnnualREEleckWh] = @expression(m,p.hours_per_time_step * ( + 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 @@ -69,11 +73,21 @@ function add_re_elec_calcs(m,p) # 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] * 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) # renewable energy from grid + - sum(m[:dvGridToStorage][b, ts] * p.s.electric_utility.renewable_energy_fraction_series[ts] * + (1 - p.s.storage.attr[b].charge_efficiency * p.s.storage.attr[b].discharge_efficiency) + for ts in p.time_steps, b in p.s.storage.types.elec + ) # minus battery efficiency losses from grid charging storage (assumes all that is charged is discharged) + ) + ) + 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 diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index 1fa2c8404..82d1d68fc 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -16,16 +16,18 @@ 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 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 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. + ## 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 = 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). + ### 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. @@ -48,6 +50,10 @@ 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 + 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" @@ -79,33 +85,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. **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 `renewable_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} @@ -125,8 +137,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 + renewable_energy_fraction_series::Array{<:Real,1} # fraction of grid electricity that is clean or renewable function ElectricUtility(; @@ -146,9 +158,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) + # 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 @@ -156,45 +170,47 @@ 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 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_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_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 - 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) + ### 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 = 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). - # Used with Climate Options 2 or 3: Annual percent decrease in CO2 emissions factors + ### Grid Climate Emissions Inputs ### + 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. + 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 + 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) - 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" # will be overwritten if Cambium is used if !is_MPC + # Check some inputs + 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 + @warn("The cambium_start_year must be between 2023 and 2050. Setting cambium_start_year to $(cambium_start_year).") + end + # Get AVERT emissions region if avert_emissions_region == "" region_abbr, meters_to_region = avert_region_abbreviation(latitude, longitude) @@ -218,60 +234,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"), + (renewable_energy_fraction_series, "renewable_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 == "renewable_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_metric_col, + 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"] - cambium_emissions_region = cambium_response_dict["location"] + 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("renewable_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, @@ -280,13 +297,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) || @@ -297,7 +313,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 @@ -332,11 +348,11 @@ struct ElectricUtility new( is_MPC ? "" : avert_emissions_region, is_MPC || isnothing(meters_to_region) ? typemax(Int64) : meters_to_region, - cambium_emissions_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"], + cambium_region, + 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, @@ -350,13 +366,12 @@ struct ElectricUtility outage_time_steps, scenarios, net_metering_limit_kw, - interconnection_limit_kw + interconnection_limit_kw, + is_MPC ? Float64[] : emissions_and_cef_series_dict["renewable_energy_fraction_series"] ) 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 @@ -517,7 +532,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, 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) @@ -528,35 +543,38 @@ 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, 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, - grid_level::String) + profile_year::Int=2017, + ) -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_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 data. + """ -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, + profile_year::Int=2017 + ) url = "https://scenarioviewer.nrel.gov/api/get-levelized/" # Production project_uuid = "82460f06-548c-4954-b2d9-b84ba92d63e2" # Cambium 2022 @@ -568,7 +586,7 @@ function cambium_emissions_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 @@ -583,45 +601,44 @@ 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] - - # 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) + # Convert from [lb/MWh] to [lb/kWh] if the metric is emissions-related + data_series = occursin("co2", metric_col) ? output["values"] ./ 1000 : convert(Array{Float64,1}, 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, profile_data=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 - + 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" => "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"], - "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 -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, profile_data::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 - 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!(profile_data[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 diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 621781f53..163d66a85 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -162,7 +162,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, + renewable_energy_fraction_series = 0 ) end diff --git a/src/core/site.jl b/src/core/site.jl index 0ecb42752..651f39891 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,6 +38,7 @@ 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 outdoor_air_temperature_degF @@ -54,6 +56,7 @@ 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, outdoor_air_temperature_degF::Union{Nothing, Array{<:Real,1}} = nothing, @@ -79,7 +82,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/electric_utility.jl b/src/results/electric_utility.jl index 88819ab41..e06c887fb 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. @@ -14,7 +15,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. @@ -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 @@ -44,8 +45,8 @@ 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) - - if !isempty(p.s.storage.types.elec) + + 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) for ts in p.time_steps) @@ -60,7 +61,7 @@ function add_electric_utility_results(m::JuMP.AbstractModel, p::AbstractInputs, r["electric_to_load_series_kw"] = round.(value.(GridToLoad), digits=3) r["electric_to_storage_series_kw"] = round.(value.(GridToBatt), digits=3) - if _n=="" #only output emissions results if not a multinode model + if _n=="" #only output emissions and RE results if not a multinode model r["lifecycle_emissions_tonnes_CO2"] = round(value(m[:yr1_emissions_from_elec_grid_net_if_selected_lbs_CO2]*TONNE_PER_LB*p.pwf_grid_emissions["CO2"]), digits=2) r["lifecycle_emissions_tonnes_NOx"] = round(value(m[:yr1_emissions_from_elec_grid_net_if_selected_lbs_NOx]*TONNE_PER_LB*p.pwf_grid_emissions["NOx"]), digits=2) r["lifecycle_emissions_tonnes_SO2"] = round(value(m[:yr1_emissions_from_elec_grid_net_if_selected_lbs_SO2]*TONNE_PER_LB*p.pwf_grid_emissions["SO2"]), digits=2) @@ -72,7 +73,9 @@ 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 + + r["annual_renewable_electricity_supplied_kwh"] = round(value(m[:AnnualGridREEleckWh]), digits=2) end d["ElectricUtility"] = r diff --git a/src/results/financial.jl b/src/results/financial.jl index 9f40459fc..31b035adf 100644 --- a/src/results/financial.jl +++ b/src/results/financial.jl @@ -356,7 +356,7 @@ function get_depreciation_schedule(p::REoptInputs, tech::Union{AbstractTech,Abst federal_itc_fraction = 0.0 try - federal_itc_fraction = tech.federal_itc_fraction + federal_itc_fraction = tech.federal_itc_fraction # TODO: also check for total_itc_fraction as storage does not use federal_itc_fraction? catch @warn "Did not find $(tech).federal_itc_fraction so using 0.0 in calculation of depreciation_schedule." end diff --git a/src/results/results.jl b/src/results/results.jl index 9b79d13a8..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"), @@ -175,9 +176,11 @@ 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", "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 bacece662..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_renewable_electricity_kwh` -- `renewable_electricity_fraction` -- `total_renewable_energy_fraction` +- `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. @@ -42,12 +44,14 @@ 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_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=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["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=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) @@ -138,7 +142,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[: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 0d355fae0..8742e89cc 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" @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" @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 @@ -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,16 +2354,31 @@ 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 + @testset "Renewable Energy from Grid" begin + inputs = JSON.parsefile("./scenarios/re_emissions_elec_only.json") # PV, Generator, ElectricStorage + + s = Scenario(inputs) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + + bessloss = 0.96*0.975^0.5*0.96*0.975^0.5 + grid2load = results["ElectricUtility"]["electric_to_load_series_kw"] + grid2bess = results["ElectricUtility"]["electric_to_storage_series_kw"] + gridRE = sum(grid2load + grid2bess - (grid2bess*(1-bessloss)) .* s.electric_utility.renewable_energy_fraction_series) + + @test results["ElectricUtility"]["annual_renewable_electricity_supplied_kwh"] ≈ gridRE atol=1e-2 + end + @testset "Back pressure steam turbine" begin """ Validation to ensure that: 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" }, 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