Skip to content

Commit

Permalink
Merge pull request #401 from NREL/fix-net-metering-and-demand-charges
Browse files Browse the repository at this point in the history
bug fixes for max net metering benefit, multi-period and multi-tier demand charges
  • Loading branch information
zolanaj authored Jun 11, 2024
2 parents 8ac9ea8 + ccfcb51 commit ea92b7c
Show file tree
Hide file tree
Showing 6 changed files with 1,391 additions and 116 deletions.
16 changes: 10 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ Classify the change according to the following categories:
### Deprecated
### Removed

## Develop - 2024-06-05
### Fixed
- Increased the big-M bound on maximum net metering benefit to prevent artificially low export benefits
- Fixed a bug in which tier limits did not load correctly when the number of tiers vary by period in the inputs
- Set a limit for demand and energy tier maxes to avoid errors returned by HiGHS due to numerical limits
- Index utility rate demand and energy tier limits on month and/or ratchet in addition to tier. This allows for the inclusion of multi-tiered energy and demand rates in which the rates may vary by month or ratchet, whereas previously only the maximum tier limit was used.


## v0.47.1
### Fixed
- Type issue with `CoolingLoad` monthly energy input
Expand All @@ -40,17 +48,13 @@ Classify the change according to the following categories:
- Refactored various functions to ensure **ProcessHeatLoad** is processed correctly in line with other heating loads.
- When the URDB response `energyratestructure` has a "unit" value that is not "kWh", throw an error instead of averaging rates in each energy tier.
- Refactored heating flow constraints to be in ./src/constraints/thermal_tech_constraints.jl instead of its previous separate locations in the storage and turbine constraints.
- Changed default Financial **owner_tax_rate_fraction** and **offtaker_tax_rate_fraction** from 0.257 to 0.26 to align with API and user manual defaults.
### Fixed
- Updated the PV result **lifecycle_om_cost_after_tax** to account for the third-party factor for third-party ownership analyses.
- Convert `max_electric_load_kw` to _Float64_ before passing to function `get_chp_defaults_prime_mover_size_class`
- Fixed a bug in which excess heat from one heating technology resulted in waste heat from another technology.
- Modified thermal waste heat constraints for heating technologies to avoid errors in waste heat results tracking.

## v0.46.2
### Changed
- When the URDB response `energyratestructure` has a "unit" value that is not "kWh", throw an error instead of averaging rates in each energy tier.
- Changed default Financial **owner_tax_rate_fraction** and **offtaker_tax_rate_fraction** from 0.257 to 0.26 to align with API and user manual defaults.

## v0.46.1
### Changed
- Updated the GHP testset .json `./test/scenarios/ghp_inputs.json` to include a nominal HotThermalStorage and ColdThermalStorage system.
Expand Down Expand Up @@ -182,7 +186,7 @@ Classify the change according to the following categories:
## v0.37.5
### Fixed
- Fixed AVERT emissions profiles for NOx. Were previously the same as the SO2 profiles. AVERT emissions profiles are currently generated from AVERT v3.2 https://www.epa.gov/avert/download-avert. See REopt User Manual for more information.
- Fix setting of equal demand tiers in scrub_urdb_demand_tiers!, which was previously causing an error.
- Fix setting of equal demand tiers in `scrub_urdb_demand_tiers`, now renamed `scrub_urdb_tiers`.
- When calling REopt.jl from a python environment using PyJulia and PyCall, some urdb_response fields get converted from a list-of-lists to a matrix type, when REopt.jl expects an array type. This fix adds checks on the type for two urdb_response fields and converts them to an array if needed.
- Update the outages dispatch results to align with CHP availability during outages

Expand Down
16 changes: 8 additions & 8 deletions src/constraints/electric_utility_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function add_export_constraints(m, p; _n="")
@warn "Adding binary variable for net metering choice. Some solvers are slow with binaries."

# Good to bound the benefit - we use max_bene as a lower bound because the benefit is treated as a negative cost
max_bene = sum([ld*rate for (ld,rate) in zip(p.s.electric_load.loads_kw, p.s.electric_tariff.export_rates[:NEM])])*10
max_bene = sum([ld*rate for (ld,rate) in zip(p.s.electric_load.loads_kw, p.s.electric_tariff.export_rates[:NEM])])*p.pwf_e*p.hours_per_time_step*10
NEM_benefit = @variable(m, lower_bound = max_bene)


Expand Down Expand Up @@ -135,7 +135,7 @@ function add_export_constraints(m, p; _n="")
else
binWHL = @variable(m, binary = true)
@warn "Adding binary variable for wholesale export choice. Some solvers are slow with binaries."
max_bene = sum([ld*rate for (ld,rate) in zip(p.s.electric_load.loads_kw, p.s.electric_tariff.export_rates[:WHL])])*10
max_bene = sum([ld*rate for (ld,rate) in zip(p.s.electric_load.loads_kw, p.s.electric_tariff.export_rates[:WHL])])*p.pwf_e*p.hours_per_time_step*100
WHL_benefit = @variable(m, lower_bound = max_bene)

@constraint(m, binNEM + binWHL == 1) # can either NEM or WHL export, not both
Expand Down Expand Up @@ -200,7 +200,7 @@ function add_monthly_peak_constraint(m, p; _n="")
b = m[Symbol(dv)]
# Upper bound on peak electrical power demand by month, tier; if tier is selected (0 o.w.)
@constraint(m, [mth in p.months, tier in 1:ntiers],
m[Symbol("dvPeakDemandMonth"*_n)][mth, tier] <= p.s.electric_tariff.monthly_demand_tier_limits[tier] *
m[Symbol("dvPeakDemandMonth"*_n)][mth, tier] <= p.s.electric_tariff.monthly_demand_tier_limits[mth, tier] *
b[mth, tier]
)

Expand All @@ -209,7 +209,7 @@ function add_monthly_peak_constraint(m, p; _n="")

# One monthly peak electrical power demand tier must be full before next one is active
@constraint(m, [mth in p.months, tier in 2:ntiers],
b[mth, tier] * p.s.electric_tariff.monthly_demand_tier_limits[tier-1] <=
b[mth, tier] * p.s.electric_tariff.monthly_demand_tier_limits[mth, tier-1] <=
m[Symbol("dvPeakDemandMonth"*_n)][mth, tier-1]
)
# TODO implement NewMaxDemandMonthsInTier, which adds mth index to monthly_demand_tier_limits
Expand All @@ -233,7 +233,7 @@ function add_tou_peak_constraint(m, p; _n="")

# Upper bound on peak electrical power demand by tier, by ratchet, if tier is selected (0 o.w.)
@constraint(m, [r in p.ratchets, tier in 1:ntiers],
m[Symbol("dvPeakDemandTOU"*_n)][r, tier] <= p.s.electric_tariff.tou_demand_tier_limits[tier] * b[r, tier]
m[Symbol("dvPeakDemandTOU"*_n)][r, tier] <= p.s.electric_tariff.tou_demand_tier_limits[r, tier] * b[r, tier]
)

# Ratchet peak electrical power ratchet tier ordering
Expand All @@ -243,7 +243,7 @@ function add_tou_peak_constraint(m, p; _n="")

# One ratchet peak electrical power demand tier must be full before next one is active
@constraint(m, [r in p.ratchets, tier in 2:ntiers],
b[r, tier] * p.s.electric_tariff.tou_demand_tier_limits[tier-1]
b[r, tier] * p.s.electric_tariff.tou_demand_tier_limits[r, tier-1]
<= m[Symbol("dvPeakDemandTOU"*_n)][r, tier-1]
)
end
Expand Down Expand Up @@ -299,15 +299,15 @@ function add_energy_tier_constraints(m, p; _n="")
##Constraint (10a): Usage limits by pricing tier, by month
@constraint(m, [mth in p.months, tier in 1:p.s.electric_tariff.n_energy_tiers],
p.hours_per_time_step * sum( m[Symbol("dvGridPurchase"*_n)][ts, tier] for ts in p.s.electric_tariff.time_steps_monthly[mth] )
<= b[mth, tier] * p.s.electric_tariff.energy_tier_limits[tier]
<= b[mth, tier] * p.s.electric_tariff.energy_tier_limits[mth, tier]
)
##Constraint (10b): Ordering of pricing tiers
@constraint(m, [mth in p.months, tier in 2:p.s.electric_tariff.n_energy_tiers],
b[mth, tier] - b[mth, tier-1] <= 0
)
## Constraint (10c): One tier must be full before any usage in next tier
@constraint(m, [mth in p.months, tier in 2:p.s.electric_tariff.n_energy_tiers],
b[mth, tier] * p.s.electric_tariff.energy_tier_limits[tier-1] -
b[mth, tier] * p.s.electric_tariff.energy_tier_limits[mth, tier-1] -
sum( m[Symbol("dvGridPurchase"*_n)][ts, tier-1] for ts in p.s.electric_tariff.time_steps_monthly[mth])
<= 0
)
Expand Down
16 changes: 8 additions & 8 deletions src/core/electric_tariff.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
"""
struct ElectricTariff
energy_rates::AbstractArray{Float64, 2} # gets a second dim with tiers
energy_tier_limits::AbstractArray{Float64,1}
energy_tier_limits::AbstractArray{Float64,2} # month X tier
n_energy_tiers::Int

monthly_demand_rates::AbstractArray{Float64, 2} # gets a second dim with tiers
time_steps_monthly::AbstractArray{AbstractArray{Int64,1},1} # length = 0 or 12
monthly_demand_tier_limits::AbstractArray{Float64,1}
monthly_demand_tier_limits::AbstractArray{Float64,2} # month X tier
n_monthly_demand_tiers::Int

tou_demand_rates::AbstractArray{Float64, 2} # gets a second dim with tiers
tou_demand_ratchet_time_steps::AbstractArray{AbstractArray{Int64,1},1} # length = n_tou_demand_ratchets
tou_demand_tier_limits::AbstractArray{Float64,1}
tou_demand_tier_limits::AbstractArray{Float64,2} # ratchet X tier
n_tou_demand_tiers::Int

demand_lookback_months::AbstractArray{Int,1}
Expand All @@ -42,7 +42,7 @@ end
`ElectricTariff` is a required REopt input for on-grid scenarios only (it cannot be supplied when `Settings.off_grid_flag` is true) with the following keys and default values:
```julia
urdb_label::String="",
urdb_response::Dict=Dict(),
urdb_response::Dict=Dict(), # Response JSON for URDB rates. Note: if creating your own urdb_response, ensure periods are zero-indexed.
urdb_utility_name::String="",
urdb_rate_name::String="",
wholesale_rate::T1=nothing, # Price of electricity sold back to the grid in absence of net metering. Can be a scalar value, which applies for all-time, or an array with time-sensitive values. If an array is input then it must have a length of 8760, 17520, or 35040. The inputed array values are up/down-sampled using mean values to match the Settings.time_steps_per_hour.
Expand Down Expand Up @@ -120,11 +120,11 @@ function ElectricTariff(;
# TODO remove_tiers for multinode models
nem_rate = Float64[]

energy_tier_limits = Float64[]
energy_tier_limits = Array{Float64,2}(undef, 0, 0)
n_energy_tiers = 1
monthly_demand_tier_limits = Float64[]
monthly_demand_tier_limits = Array{Float64,2}(undef, 0, 0)
n_monthly_demand_tiers = 1
tou_demand_tier_limits = Float64[]
tou_demand_tier_limits = Array{Float64,2}(undef, 0, 0)
n_tou_demand_tiers = 1
time_steps_monthly = get_monthly_time_steps(year, time_steps_per_hour=time_steps_per_hour)

Expand Down Expand Up @@ -241,7 +241,7 @@ function ElectricTariff(;
if remove_tiers
energy_rates, monthly_demand_rates, tou_demand_rates = remove_tiers_from_urdb_rate(u)
energy_tier_limits, monthly_demand_tier_limits, tou_demand_tier_limits =
Float64[], Float64[], Float64[]
Array{Float64,2}(undef, 0, 0), Array{Float64,2}(undef, 0, 0), Array{Float64,2}(undef, 0, 0)
n_energy_tiers, n_monthly_demand_tiers, n_tou_demand_tiers = 1, 1, 1
end

Expand Down
Loading

0 comments on commit ea92b7c

Please sign in to comment.