Skip to content

Commit a0f411d

Browse files
committed
updates from bess-export branch
1 parent a2555b8 commit a0f411d

12 files changed

Lines changed: 61 additions & 61 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Classify the change according to the following categories:
2727

2828
## fixed-bess-soc
2929
### Added
30-
- Add **ElectricStorage** input field `fixed_soc_series_fraction` to allow users to fix the SOC timeseries
30+
- Add **ElectricStorage** inputs field **fixed_soc_series_fraction** and **fixed_soc_series_fraction_tolerance** to allow users to fix the SOC timeseries within a chosen absolute tolerance
3131

3232
## v0.58.1
3333
### Fixed

src/constraints/storage_constraints.jl

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function add_general_storage_dispatch_constraints(m, p, b; _n="")
4444
@constraint(m,
4545
m[Symbol("dvStoredEnergy"*_n)][b, 0] == m[:dvStoredEnergy][b, maximum(p.time_steps)]
4646
)
47-
elseif !hasproperty(p.s.storage.attr[b], :fixed_soc_series_fraction) || isnothing(p.s.storage.attr[b].fixed_soc_series_fraction)
47+
else
4848
@constraint(m,
4949
m[Symbol("dvStoredEnergy"*_n)][b, 0] == p.s.storage.attr[b].soc_init_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
5050
)
@@ -105,32 +105,21 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="")
105105
)
106106
)
107107

108-
# Constraint (4i)-1: Dispatch to electrical storage is no greater than power capacity
109-
@constraint(m, [ts in p.time_steps],
110-
m[Symbol("dvStoragePower"*_n)][b] >=
111-
sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec) + m[Symbol("dvGridToStorage"*_n)][b, ts]
112-
)
113-
114-
#Constraint (4k)-alt: Dispatch to and from electrical storage is no greater than power capacity
115-
@constraint(m, [ts in p.time_steps_with_grid],
116-
m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b, ts] +
117-
sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec) + m[Symbol("dvGridToStorage"*_n)][b, ts]
118-
)
119-
120-
#Constraint (4l)-alt: Dispatch from electrical storage is no greater than power capacity (no grid connection)
121-
@constraint(m, [ts in p.time_steps_without_grid],
122-
m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b,ts] +
123-
sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec)
108+
# Constraint (4i): Dispatch to and from electrical storage is no greater than power capacity
109+
@constraint(m, [ts in p.time_steps],
110+
m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b, ts]
111+
+ sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec)
112+
+ m[Symbol("dvGridToStorage"*_n)][b, ts]
124113
)
125114

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

133-
# Constrain average state of charge
122+
# Constraint (4k): Constrain average state of charge
134123
if p.s.storage.attr[b].minimum_avg_soc_fraction > 0
135124
avg_soc = sum(m[Symbol("dvStoredEnergy"*_n)][b, ts] for ts in p.time_steps) /
136125
(8760. / p.hours_per_time_step)
@@ -139,14 +128,14 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="")
139128
)
140129
end
141130

142-
# Constrain to fixed_soc_series_fraction
143-
if hasproperty(p.s.storage.attr[b], :fixed_soc_series_fraction) && !isnothing(p.s.storage.attr[b].fixed_soc_series_fraction)
131+
# Constraint (4l): Constrain to fixed_soc_series_fraction
132+
if hasproperty(p.s.storage.attr[b], :fixed_soc_series_fraction) && !isnothing(p.s.storage.attr[b].fixed_soc_series_fraction)
133+
# Allow for a percentage point (fractional) buffer on user-provided fixed_soc_series_fraction
144134
@constraint(m, [ts in p.time_steps],
145-
# Allow for a 1 pct point buffer on user-provided fixed_soc_series_fraction
146-
m[Symbol("dvStoredEnergy"*_n)][b, ts] <= (0.02 + p.s.storage.attr[b].fixed_soc_series_fraction[ts]) * m[Symbol("dvStorageEnergy"*_n)][b]
135+
m[Symbol("dvStoredEnergy"*_n)][b, ts] <= (p.s.storage.attr[b].fixed_soc_series_fraction_tolerance + p.s.storage.attr[b].fixed_soc_series_fraction[ts]) * m[Symbol("dvStorageEnergy"*_n)][b]
147136
)
148137
@constraint(m, [ts in p.time_steps],
149-
m[Symbol("dvStoredEnergy"*_n)][b, ts] >= (-0.02 + p.s.storage.attr[b].fixed_soc_series_fraction[ts]) * m[Symbol("dvStorageEnergy"*_n)][b]
138+
m[Symbol("dvStoredEnergy"*_n)][b, ts] >= (-p.s.storage.attr[b].fixed_soc_series_fraction_tolerance + p.s.storage.attr[b].fixed_soc_series_fraction[ts]) * m[Symbol("dvStorageEnergy"*_n)][b]
150139
)
151140
end
152141
end

src/core/electric_tariff.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ end
4949
urdb_utility_name::String="",
5050
urdb_rate_name::String="",
5151
urdb_metadata::Dict=Dict(), # Meta data about the URDB rate, from the URDB API response
52-
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.
52+
wholesale_rate::T1=nothing, # Price of electricity [\$ per kWh] 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.
5353
export_rate_beyond_net_metering_limit::T2=nothing, # Price of electricity sold back to the grid beyond total annual grid purchases, regardless 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
5454
monthly_energy_rates::Array=[], # Array (length of 12) of blended energy rates in dollars per kWh
5555
monthly_demand_rates::Array=[], # Array (length of 12) of blended demand charges in dollars per kW

src/core/energy_storage/electric_storage.jl

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ end
162162

163163

164164
"""
165-
`ElectricStorage` is an optional optional REopt input with the following keys and default values:
165+
`ElectricStorage` is an optional REopt input with the following keys and default values:
166166
167167
```julia
168168
min_kw::Real = 0.0
@@ -199,7 +199,8 @@ end
199199
degradation::Dict = Dict()
200200
minimum_avg_soc_fraction::Float64 = 0.0
201201
optimize_soc_init_fraction::Bool = false # If true, soc_init_fraction will not apply. Model will optimize initial SOC and constrain initial SOC = final SOC.
202-
fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing # If provided, SOC (as fraction of total energy capacity) will not be optimized and will instead be fixed to the values provided here +- 0.02 (this buffer is to avoid infeasible solutions)
202+
fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing # If provided, SOC (as fraction of total energy capacity) will not be optimized and will instead be fixed to the values provided here +- the absolute fixed_soc_series_fraction_tolerance (this buffer is to avoid infeasible solutions)
203+
fixed_soc_series_fraction_tolerance::Real = !isnothing(fixed_soc_series_fraction) ? 0.02 : nothing # Absolute tolerance on fixed_soc_series_fraction to avoid infeasible solutions when fixed_soc_series_fraction is provided.
203204
min_duration_hours::Real = 0.0 # Minimum amount of time storage can discharge at its rated power capacity
204205
max_duration_hours::Real = 100000.0 # Maximum amount of time storage can discharge at its rated power capacity (ratio of ElectricStorage size_kwh to size_kw)
205206
@@ -244,6 +245,7 @@ Base.@kwdef struct ElectricStorageDefaults
244245
min_duration_hours::Real = 0.0
245246
max_duration_hours::Real = 100000.0
246247
fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing
248+
fixed_soc_series_fraction_tolerance::Real = !isnothing(fixed_soc_series_fraction) ? 0.02 : nothing
247249
end
248250

249251

@@ -293,7 +295,8 @@ struct ElectricStorage <: AbstractElectricStorage
293295
optimize_soc_init_fraction::Bool
294296
min_duration_hours::Real
295297
max_duration_hours::Real
296-
fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}}
298+
fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}}
299+
fixed_soc_series_fraction_tolerance::Real
297300

298301
function ElectricStorage(d::Dict, f::Financial, s::Site)
299302
set_sector_defaults!(d; struct_name="Storage", sector=s.sector, federal_procurement_type=s.federal_procurement_type)
@@ -312,12 +315,16 @@ struct ElectricStorage <: AbstractElectricStorage
312315
end
313316

314317
# Copy SOC input in case we need to change them
318+
soc_init_fraction = s.soc_init_fraction
315319
soc_min_fraction = s.soc_min_fraction
316320
optimize_soc_init_fraction = s.optimize_soc_init_fraction
321+
minimum_avg_soc_fraction = s.minimum_avg_soc_fraction
317322
if !isnothing(s.fixed_soc_series_fraction)
318323
@warn "Fixing ElectricStorage soc_series_fraction to the provided fixed_soc_series_fraction. Other SOC inputs will be ignored."
324+
soc_init_fraction = s.fixed_soc_series_fraction[1]
319325
soc_min_fraction = 0.0
320326
optimize_soc_init_fraction = false
327+
minimum_avg_soc_fraction = 0.0
321328
error_if_series_vals_not_0_to_1(s.fixed_soc_series_fraction, "ElectricStorage", "fixed_soc_series_fraction")
322329
end
323330

@@ -409,7 +416,7 @@ struct ElectricStorage <: AbstractElectricStorage
409416
s.rectifier_efficiency_fraction,
410417
soc_min_fraction,
411418
s.soc_min_applies_during_outages,
412-
s.soc_init_fraction,
419+
soc_init_fraction,
413420
s.can_grid_charge,
414421
s.installed_cost_per_kw,
415422
s.installed_cost_per_kwh,
@@ -435,11 +442,12 @@ struct ElectricStorage <: AbstractElectricStorage
435442
net_present_cost_cost_constant,
436443
s.model_degradation,
437444
degr,
438-
s.minimum_avg_soc_fraction,
445+
minimum_avg_soc_fraction,
439446
optimize_soc_init_fraction,
440447
s.min_duration_hours,
441448
s.max_duration_hours,
442-
s.fixed_soc_series_fraction
449+
s.fixed_soc_series_fraction,
450+
s.fixed_soc_series_fraction_tolerance
443451
)
444452
end
445453
end

src/core/reopt.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
192192
fix(m[:dvGridPurchase][ts, tier] , 0.0, force=true)
193193
end
194194

195-
for t in p.s.storage.types.elec
196-
fix(m[:dvGridToStorage][t, ts], 0.0, force=true)
195+
for b in p.s.storage.types.elec
196+
fix(m[:dvGridToStorage][b, ts], 0.0, force=true)
197197
end
198198

199199
if !isempty(p.s.electric_tariff.export_bins)
@@ -421,7 +421,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
421421

422422
degr_bool = p.s.storage.attr[b].model_degradation
423423
if degr_bool
424-
@info "Battery energy capacity degradation costs for $b are being modeled using REopt's Degradation model. ElectricStorageOMCost will include costs to be incurred for power electronics and the cost constant."
424+
@info "Battery energy capacity degradation costs for $b are being modeled using REopt's Degradation model. ElectricStorageOMCost will include costs incurred for power electronics [per kW] and the cost constant, but not for the per kWh components."
425425
add_to_expression!(
426426
m[:ElectricStorageOMCost], -1.0 * p.third_party_factor * p.pwf_om * p.s.storage.attr[b].om_cost_fraction_of_installed_cost * p.s.storage.attr[b].installed_cost_per_kwh * m[:dvStorageEnergy][b]
427427
)

src/core/scenario.jl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ A Scenario struct can contain the following keys:
4444
- [HotThermalStorage](@ref) (optional)
4545
- [HighTempThermalStorage](@ref) (optional)
4646
- [ColdThermalStorage](@ref) (optional)
47-
- [ElectricStorage](@ref) (optional)
4847
- [ElectricUtility](@ref) (optional)
4948
- [Generator](@ref) (optional)
5049
- [HeatingLoad](@ref) (optional)

src/core/utils.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ function dictkeys_tosymbols(d::Dict)
169169
"cooling_cop_reference",
170170
"cooling_cf_reference",
171171
"cooling_reference_temps_degF",
172+
"cycle_fade_coefficient",
173+
"cycle_fade_fraction",
172174
#for ERP
173175
"pv_production_factor_series", "wind_production_factor_series",
174176
"battery_starting_soc_series_fraction",

src/mpc/model.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ function build_mpc!(m::JuMP.AbstractModel, p::MPCInputs)
8282

8383
fix(m[:dvGridPurchase][ts], 0.0, force=true)
8484

85-
for t in p.s.storage.types.elec
86-
fix(m[:dvGridToStorage][t, ts], 0.0, force=true)
85+
for b in p.s.storage.types.elec
86+
fix(m[:dvGridToStorage][b, ts], 0.0, force=true)
8787
end
8888

8989
for t in p.techs.elec, u in p.export_bins_by_tech[t]

src/mpc/structs.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ Base.@kwdef struct MPCElectricStorage < AbstractElectricStorage
228228
can_grid_charge::Bool = true
229229
grid_charge_efficiency::Float64 = 0.96 * 0.975^2
230230
fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing
231+
fixed_soc_series_fraction_tolerance::Real = !isnothing(fixed_soc_series_fraction) ? 0.05 : nothing
231232
end
232233
```
233234
"""
@@ -244,6 +245,7 @@ Base.@kwdef struct MPCElectricStorage <: AbstractElectricStorage
244245
max_kwh::Float64 = size_kwh
245246
minimum_avg_soc_fraction::Float64 = 0.0
246247
fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing
248+
fixed_soc_series_fraction_tolerance::Real = !isnothing(fixed_soc_series_fraction) ? 0.05 : nothing
247249
end
248250

249251

src/results/electric_storage.jl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
`ElectricStorage` results keys:
44
- `size_kw` Optimal inverter capacity
55
- `size_kwh` Optimal storage capacity
6-
- `soc_series_fraction` Vector of normalized (0-1) state of charge values over the first year
7-
- `storage_to_load_series_kw` Vector of power used to meet load over the first year
6+
- `soc_series_fraction` Vector of normalized (0-1) state of charge values over an average year
7+
- `storage_to_load_series_kw` Vector of power used to meet load over an average year
88
- `initial_capital_cost` Upfront capital cost for storage and inverter
99
# The following results are reported if storage degradation is modeled:
10-
- `state_of_health`
10+
- `state_of_health_series_fraction`
1111
- `maintenance_cost`
1212
- `replacement_month` # only applies is maintenance_strategy = "replacement"
1313
- `residual_value`
@@ -38,7 +38,7 @@ function add_electric_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d::
3838
p.s.storage.attr[b].installed_cost_constant
3939

4040
if p.s.storage.attr[b].model_degradation
41-
r["state_of_health"] = round.(value.(m[:SOH]).data / value.(m[:dvStorageEnergy])["ElectricStorage"], digits=3)
41+
r["state_of_health_series_fraction"] = round.(value.(m[:SOH]).data / value.(m[:dvStorageEnergy])["ElectricStorage"], digits=3)
4242
r["maintenance_cost"] = value(m[:degr_cost])
4343
if p.s.storage.attr[b].degradation.maintenance_strategy == "replacement"
4444
r["replacement_month"] = round(Int, value(
@@ -48,9 +48,9 @@ function add_electric_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d::
4848
# Determine fraction of useful life left assuming same replacement frequency.
4949
# Multiply by 0.2 to scale residual useful life since entire BESS is replaced when SOH drops below 80%.
5050
# Total BESS capacity residual is (0.8 + residual useful fraction) * BESS capacity
51-
# If not replacements happen then useful capacity is SOH[end]*BESS capacity.
51+
# If no replacements happen then useful capacity is SOH[end]*BESS capacity.
5252
if iszero(r["replacement_month"])
53-
r["total_residual_kwh"] = r["state_of_health"][end]*r["size_kwh"]
53+
r["total_residual_kwh"] = r["state_of_health_series_fraction"][end]*r["size_kwh"]
5454
else
5555
# SOH[end] can be negative, so alternate method to calculate residual healthy SOH.
5656
total_replacements = (p.s.financial.analysis_years*12)/r["replacement_month"]

0 commit comments

Comments
 (0)