From a6c74d09653eb216d101d3bd80dddcb8a35ec2b3 Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 7 Jan 2025 08:52:14 -0700 Subject: [PATCH] initial commit --- CHANGELOG.md | 4 ++++ src/constraints/electric_utility_constraints.jl | 10 ++++++++-- src/constraints/storage_constraints.jl | 16 ++++++++++++++-- src/core/electric_tariff.jl | 2 +- src/core/energy_storage/electric_storage.jl | 4 ++++ src/core/reopt.jl | 2 ++ src/core/reopt_multinode.jl | 3 ++- src/mpc/model.jl | 2 ++ src/mpc/model_multinode.jl | 3 ++- src/mpc/structs.jl | 2 ++ src/results/electric_storage.jl | 9 +++++++-- src/results/pv.jl | 8 ++++---- src/results/thermal_storage.jl | 8 ++++---- 13 files changed, 56 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14896e890..6c74534b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index c1842f578..fae322cae 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -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) diff --git a/src/constraints/storage_constraints.jl b/src/constraints/storage_constraints.jl index c46d4e775..2c8448d32 100644 --- a/src/constraints/storage_constraints.jl +++ b/src/constraints/storage_constraints.jl @@ -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 @@ -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) diff --git a/src/core/electric_tariff.jl b/src/core/electric_tariff.jl index b18049a26..0a224f54d 100644 --- a/src/core/electric_tariff.jl +++ b/src/core/electric_tariff.jl @@ -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 diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index 771a84364..f01887022 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -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 @@ -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 @@ -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 @@ -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, diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 662d249cd..22ce4ec4c 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -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] @@ -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] diff --git a/src/core/reopt_multinode.jl b/src/core/reopt_multinode.jl index a41a80a35..c45887d7a 100644 --- a/src/core/reopt_multinode.jl +++ b/src/core/reopt_multinode.jl @@ -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) diff --git a/src/mpc/model.jl b/src/mpc/model.jl index d40a80da3..7a995a176 100644 --- a/src/mpc/model.jl +++ b/src/mpc/model.jl @@ -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) @@ -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] diff --git a/src/mpc/model_multinode.jl b/src/mpc/model_multinode.jl index 68a3ef34a..245dd3582 100644 --- a/src/mpc/model_multinode.jl +++ b/src/mpc/model_multinode.jl @@ -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) diff --git a/src/mpc/structs.jl b/src/mpc/structs.jl index 340bb07c0..73a6ba022 100644 --- a/src/mpc/structs.jl +++ b/src/mpc/structs.jl @@ -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 ``` @@ -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 diff --git a/src/results/electric_storage.jl b/src/results/electric_storage.jl index ef07eb48c..bd77baf11 100644 --- a/src/results/electric_storage.jl +++ b/src/results/electric_storage.jl @@ -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` @@ -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 diff --git a/src/results/pv.jl b/src/results/pv.jl index 4c5774faa..b843ea957 100644 --- a/src/results/pv.jl +++ b/src/results/pv.jl @@ -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 diff --git a/src/results/thermal_storage.jl b/src/results/thermal_storage.jl index 48e9f607c..f6f0d7bfa 100644 --- a/src/results/thermal_storage.jl +++ b/src/results/thermal_storage.jl @@ -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. @@ -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="") #=