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

Allow BESS to Export #471

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Classify the change according to the following categories:
### Deprecated
### Removed

## bess-export
### Added
- Added input option **can_export_to_grid** (defaults to _false_) to `ElectricStorage` and decision variable **dvStorageToGrid**
-

## Develop
### Added
Expand Down
10 changes: 8 additions & 2 deletions src/constraints/electric_utility_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,14 @@ function add_export_constraints(m, p; _n="")
if typeof(binNEM) <: Real # no need for wholesale binary
binWHL = 1
WHL_benefit = @expression(m, p.pwf_e * p.hours_per_time_step *
sum( sum(p.s.electric_tariff.export_rates[:WHL][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts]
for t in p.techs_by_exportbin[:WHL]) for ts in p.time_steps)
# sum( sum(p.s.electric_tariff.export_rates[:WHL][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts]
# for t in p.techs_by_exportbin[:WHL]) for ts in p.time_steps)
sum(p.s.electric_tariff.export_rates[:WHL][ts] *
(
sum(m[Symbol("dvStorageToGrid"*_n)][b, ts] for b in p.s.storage.types.elec) + ## TODO: index on metering type?
sum(m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] for t in p.techs_by_exportbin[:WHL])
) for ts in p.time_steps
)
)
else
binWHL = @variable(m, binary = true)
Expand Down
16 changes: 14 additions & 2 deletions src/constraints/storage_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="")
m[Symbol("dvStoredEnergy"*_n)][b, ts] == m[Symbol("dvStoredEnergy"*_n)][b, ts-1] + p.hours_per_time_step * (
sum(p.s.storage.attr[b].charge_efficiency * m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec)
+ p.s.storage.attr[b].grid_charge_efficiency * m[Symbol("dvGridToStorage"*_n)][b, ts]
- m[Symbol("dvDischargeFromStorage"*_n)][b,ts] / p.s.storage.attr[b].discharge_efficiency
- ((m[Symbol("dvDischargeFromStorage"*_n)][b,ts] + m[Symbol("dvStorageToGrid"*_n)][b, ts])/ p.s.storage.attr[b].discharge_efficiency)
)
)
# Constraint (4g)-2: state-of-charge for electrical storage - no grid
Expand Down Expand Up @@ -104,21 +104,33 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="")
@constraint(m, [ts in p.time_steps_with_grid],
m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b, ts] +
sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec) + m[Symbol("dvGridToStorage"*_n)][b, ts]
+ m[Symbol("dvStorageToGrid"*_n)][b, ts]
)

#Dispatch from electrical storage is no greater than power capacity
@constraint(m, [ts in p.time_steps_without_grid],
m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b,ts] + m[Symbol("dvStorageToGrid"*_n)][b, ts])

#Constraint (4l)-alt: Dispatch from electrical storage is no greater than power capacity (no grid connection)
@constraint(m, [ts in p.time_steps_without_grid],
m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b,ts] +
sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec)
)

# Remove grid-to-storage as an option if option to grid charge is turned off
#Constraint (4m)-1: Remove grid-to-storage as an option if option to grid charge is turned off
if !(p.s.storage.attr[b].can_grid_charge)
for ts in p.time_steps_with_grid
fix(m[Symbol("dvGridToStorage"*_n)][b, ts], 0.0, force=true)
end
end

#Constraint (4m)-2: Force storage export to grid to zero if option to grid export is turned off
if !p.s.storage.attr[b].can_export_to_grid
for ts in p.time_steps
fix(m[Symbol("dvStorageToGrid"*_n)][b, ts], 0.0, force=true)
end
end

if p.s.storage.attr[b].minimum_avg_soc_fraction > 0
avg_soc = sum(m[Symbol("dvStoredEnergy"*_n)][b, ts] for ts in p.time_steps) /
(8760. / p.hours_per_time_step)
Expand Down
2 changes: 1 addition & 1 deletion src/core/electric_tariff.jl
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ Check length of e and upsample if length(e) != N
"""
function create_export_rate(e::AbstractArray{<:Real, 1}, N::Int, ts_per_hour::Int=1)
Ne = length(e)
if Ne != Int(N/ts_per_hour) || Ne != N
if Ne != Int(N/ts_per_hour) && Ne != N
throw(@error("Export rates do not have correct number of entries. Must be $(N) or $(Int(N/ts_per_hour))."))
end
if Ne != N # upsample
Expand Down
4 changes: 4 additions & 0 deletions src/core/energy_storage/electric_storage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ end
soc_min_applies_during_outages::Bool = false
soc_init_fraction::Float64 = off_grid_flag ? 1.0 : 0.5
can_grid_charge::Bool = off_grid_flag ? false : true
can_export_to_grid::Bool = false
installed_cost_per_kw::Real = 910.0
installed_cost_per_kwh::Real = 455.0
replace_cost_per_kw::Real = 715.0
Expand Down Expand Up @@ -202,6 +203,7 @@ Base.@kwdef struct ElectricStorageDefaults
soc_min_applies_during_outages::Bool = false
soc_init_fraction::Float64 = off_grid_flag ? 1.0 : 0.5
can_grid_charge::Bool = off_grid_flag ? false : true
can_export_to_grid::Bool = false
installed_cost_per_kw::Real = 910.0
installed_cost_per_kwh::Real = 455.0
replace_cost_per_kw::Real = 715.0
Expand Down Expand Up @@ -243,6 +245,7 @@ struct ElectricStorage <: AbstractElectricStorage
soc_min_applies_during_outages::Bool
soc_init_fraction::Float64
can_grid_charge::Bool
can_export_to_grid::Bool
installed_cost_per_kw::Real
installed_cost_per_kwh::Real
replace_cost_per_kw::Real
Expand Down Expand Up @@ -336,6 +339,7 @@ struct ElectricStorage <: AbstractElectricStorage
s.soc_min_applies_during_outages,
s.soc_init_fraction,
s.can_grid_charge,
s.can_export_to_grid,
s.installed_cost_per_kw,
s.installed_cost_per_kwh,
replace_cost_per_kw,
Expand Down
2 changes: 2 additions & 0 deletions src/core/reopt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
@constraint(m, [ts in p.time_steps], m[:dvGridToStorage][b, ts] == 0)
@constraint(m, [t in p.techs.elec, ts in p.time_steps_with_grid],
m[:dvProductionToStorage][b, t, ts] == 0)
@constraint(m, [ts in p.time_steps], m[Symbol("dvStorageToGrid"*_n)][b, ts] == 0) # if there isn't a battery, then the battery can't export power to the grid
elseif b in p.s.storage.types.hot
@constraint(m, [q in q in setdiff(p.heating_loads, p.heating_loads_served_by_tes[b]), ts in p.time_steps], m[:dvHeatFromStorage][b,q,ts] == 0)
if "DomesticHotWater" in p.heating_loads_served_by_tes[b]
Expand Down Expand Up @@ -616,6 +617,7 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs)
dvProductionToStorage[p.s.storage.types.all, union(p.techs.ghp,p.techs.all), p.time_steps] >= 0 # Power from technology t used to charge storage system b [kW]
dvDischargeFromStorage[p.s.storage.types.all, p.time_steps] >= 0 # Power discharged from storage system b [kW]
dvGridToStorage[p.s.storage.types.elec, p.time_steps] >= 0 # Electrical power delivered to storage by the grid [kW]
dvStorageToGrid[p.s.storage.types.elec, p.time_steps] >= 0 # TODO, add: "p.StorageSalesTiers" as well? export of energy from storage to the grid
dvStoredEnergy[p.s.storage.types.all, 0:p.time_steps[end]] >= 0 # State of charge of storage system b
dvStoragePower[p.s.storage.types.all] >= 0 # Power capacity of storage system b [kW]
dvStorageEnergy[p.s.storage.types.all] >= 0 # Energy capacity of storage system b [kWh]
Expand Down
3 changes: 2 additions & 1 deletion src/core/reopt_multinode.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ function add_variables!(m::JuMP.AbstractModel, ps::AbstractVector{REoptInputs{T}
"dvStorageEnergy",
]
dvs_idx_on_storagetypes_time_steps = String[
"dvDischargeFromStorage"
"dvDischargeFromStorage",
"dvStorageToGrid"
]
for p in ps
_n = string("_", p.s.site.node)
Expand Down
2 changes: 2 additions & 0 deletions src/mpc/model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ function build_mpc!(m::JuMP.AbstractModel, p::MPCInputs)
@constraint(m, [ts in p.time_steps], m[:dvDischargeFromStorage][b, ts] == 0)
if b in p.s.storage.types.elec
@constraint(m, [ts in p.time_steps], m[:dvGridToStorage][b, ts] == 0)
@constraint(m, [ts in p.time_steps], m[:dvStorageToGrid][b, ts] == 0)
end
else
add_general_storage_dispatch_constraints(m, p, b)
Expand Down Expand Up @@ -230,6 +231,7 @@ function add_variables!(m::JuMP.AbstractModel, p::MPCInputs)
dvCurtail[p.techs.all, p.time_steps] >= 0 # [kW]
dvProductionToStorage[p.s.storage.types.all, p.techs.all, p.time_steps] >= 0 # Power from technology t used to charge storage system b [kW]
dvDischargeFromStorage[p.s.storage.types.all, p.time_steps] >= 0 # Power discharged from storage system b [kW]
dvStorageToGrid[p.s.storage.types.elec, p.time_steps] >= 0 # TODO, add: "p.StorageSalesTiers" as well? export of energy from storage to the grid
dvGridToStorage[p.s.storage.types.elec, p.time_steps] >= 0 # Electrical power delivered to storage by the grid [kW]
dvStoredEnergy[p.s.storage.types.all, 0:p.time_steps[end]] >= 0 # State of charge of storage system b
dvStoragePower[p.s.storage.types.all] >= 0 # Power capacity of storage system b [kW]
Expand Down
3 changes: 2 additions & 1 deletion src/mpc/model_multinode.jl
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ function add_variables!(m::JuMP.AbstractModel, ps::AbstractVector{MPCInputs})
"dvRatedProduction",
]
dvs_idx_on_storagetypes_time_steps = String[
"dvDischargeFromStorage"
"dvDischargeFromStorage",
"dvStorageToGrid"
]
for p in ps
_n = string("_", p.s.node)
Expand Down
2 changes: 2 additions & 0 deletions src/mpc/structs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ Base.@kwdef struct MPCElectricStorage < AbstractElectricStorage
soc_min_fraction::Float64 = 0.2
soc_init_fraction::Float64 = 0.5
can_grid_charge::Bool = true
can_export_to_grid::Bool = false
grid_charge_efficiency::Float64 = 0.96 * 0.975^2
end
```
Expand All @@ -238,6 +239,7 @@ Base.@kwdef struct MPCElectricStorage <: AbstractElectricStorage
soc_min_fraction::Float64 = 0.2
soc_init_fraction::Float64 = 0.5
can_grid_charge::Bool = true
can_export_to_grid::Bool = false
grid_charge_efficiency::Float64 = 0.96 * 0.975^2
max_kw::Float64 = size_kw
max_kwh::Float64 = size_kwh
Expand Down
9 changes: 7 additions & 2 deletions src/results/electric_storage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
`ElectricStorage` results keys:
- `size_kw` Optimal inverter capacity
- `size_kwh` Optimal storage capacity
- `soc_series_fraction` Vector of normalized (0-1) state of charge values over the first year
- `storage_to_load_series_kw` Vector of power used to meet load over the first year
- `soc_series_fraction` Vector of normalized (0-1) state of charge values over an average year
- `storage_to_load_series_kw` Vector of power used to meet load over an average year
- `storage_to_grid_series_kw` Vector of power exported to the grid over an average year
- `initial_capital_cost` Upfront capital cost for storage and inverter
# The following results are reported if storage degradation is modeled:
- `state_of_health`
Expand Down Expand Up @@ -44,9 +45,13 @@ function add_electric_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d::
end
r["residual_value"] = value(m[:residual_value])
end

# report the exported electricity from the battery:
r["storage_to_grid_series_kw"] = round.(value.(m[Symbol("dvStorageToGrid"*_n)][b, ts] for ts in p.time_steps), digits = 3)
else
r["soc_series_fraction"] = []
r["storage_to_load_series_kw"] = []
r["storage_to_grid_series_kw"] = []
end

d[b] = r
Expand Down
8 changes: 4 additions & 4 deletions src/results/pv.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
- `year_one_energy_produced_kwh` Energy produced over the first year
- `annual_energy_produced_kwh` Average annual energy produced when accounting for degradation
- `lcoe_per_kwh` Levelized Cost of Energy produced by the PV system
- `electric_to_load_series_kw` Vector of power used to meet load over the first year
- `electric_to_storage_series_kw` Vector of power used to charge the battery over the first year
- `electric_to_grid_series_kw` Vector of power exported to the grid over the first year
- `electric_curtailed_series_kw` Vector of power curtailed over the first year
- `electric_to_load_series_kw` Vector of power used to meet load over an average year
- `electric_to_storage_series_kw` Vector of power used to charge the battery over an average year
- `electric_to_grid_series_kw` Vector of power exported to the grid over an average year
- `electric_curtailed_series_kw` Vector of power curtailed over an average year
- `annual_energy_exported_kwh` Average annual energy exported to the grid
- `production_factor_series` PV production factor in each time step, either provided by user or obtained from PVWatts

Expand Down
8 changes: 4 additions & 4 deletions src/results/thermal_storage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"""
`HotThermalStorage` results keys:
- `size_gal` Optimal TES capacity, by volume [gal]
- `soc_series_fraction` Vector of normalized (0-1) state of charge values over the first year [-]
- `storage_to_load_series_mmbtu_per_hour` Vector of power used to meet load over the first year [MMBTU/hr]
- `soc_series_fraction` Vector of normalized (0-1) state of charge values over an average year [-]
- `storage_to_load_series_mmbtu_per_hour` Vector of power used to meet load over an average year [MMBTU/hr]

!!! note "'Series' and 'Annual' energy outputs are average annual"
REopt performs load balances using average annual production values for technologies that include degradation.
Expand Down Expand Up @@ -84,8 +84,8 @@ end
"""
`ColdThermalStorage` results:
- `size_gal` Optimal TES capacity, by volume [gal]
- `soc_series_fraction` Vector of normalized (0-1) state of charge values over the first year [-]
- `storage_to_load_series_ton` Vector of power used to meet load over the first year [ton]
- `soc_series_fraction` Vector of normalized (0-1) state of charge values over an average year [-]
- `storage_to_load_series_ton` Vector of power used to meet load over an average year [ton]
"""
function add_cold_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict, b::String; _n="")
#=
Expand Down
Loading