Skip to content
Open
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ Classify the change according to the following categories:
### Deprecated
### Removed

## fixed-bess-soc
### Added
- Add **ElectricStorage** input field `fixed_soc_series_fraction` to allow users to fix the SOC timeseries

## test-runners
## Develop
### Added
- Added constraints in `src/constraints/battery_degradation.jl` to allow use of segmented cycle fade coefficients in the model.
Expand Down
14 changes: 13 additions & 1 deletion src/constraints/storage_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function add_general_storage_dispatch_constraints(m, p, b; _n="")
@constraint(m,
m[Symbol("dvStoredEnergy"*_n)][b, 0] == m[:dvStoredEnergy][b, maximum(p.time_steps)]
)
else
elseif !hasproperty(p.s.storage.attr[b], :fixed_soc_series_fraction) || isnothing(p.s.storage.attr[b].fixed_soc_series_fraction)
@constraint(m,
m[Symbol("dvStoredEnergy"*_n)][b, 0] == p.s.storage.attr[b].soc_init_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
)
Expand Down Expand Up @@ -130,13 +130,25 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="")
end
end

# Constrain average state of charge
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)
@constraint(m, avg_soc >= p.s.storage.attr[b].minimum_avg_soc_fraction *
sum(m[Symbol("dvStorageEnergy"*_n)][b])
)
end

# Constrain to fixed_soc_series_fraction
if hasproperty(p.s.storage.attr[b], :fixed_soc_series_fraction) && !isnothing(p.s.storage.attr[b].fixed_soc_series_fraction)
@constraint(m, [ts in p.time_steps],
# Allow for a 1 pct point buffer on user-provided fixed_soc_series_fraction
m[Symbol("dvStoredEnergy"*_n)][b, ts] <= (0.02 + p.s.storage.attr[b].fixed_soc_series_fraction[ts]) * m[Symbol("dvStorageEnergy"*_n)][b]
)
@constraint(m, [ts in p.time_steps],
m[Symbol("dvStoredEnergy"*_n)][b, ts] >= (-0.02 + p.s.storage.attr[b].fixed_soc_series_fraction[ts]) * m[Symbol("dvStorageEnergy"*_n)][b]
)
end
end

function add_elec_storage_cost_constant_constraints(m, p, b; _n="")
Expand Down
24 changes: 20 additions & 4 deletions src/core/energy_storage/electric_storage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,10 @@ end
degradation::Dict = Dict()
minimum_avg_soc_fraction::Float64 = 0.0
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.
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)
min_duration_hours::Real = 0.0 # Minimum amount of time storage can discharge at its rated power capacity
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)

```
"""
Base.@kwdef struct ElectricStorageDefaults
Expand Down Expand Up @@ -241,6 +243,7 @@ Base.@kwdef struct ElectricStorageDefaults
optimize_soc_init_fraction::Bool = false
min_duration_hours::Real = 0.0
max_duration_hours::Real = 100000.0
fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing
end


Expand Down Expand Up @@ -290,8 +293,10 @@ struct ElectricStorage <: AbstractElectricStorage
optimize_soc_init_fraction::Bool
min_duration_hours::Real
max_duration_hours::Real
fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}}

function ElectricStorage(d::Dict, f::Financial)

function ElectricStorage(d::Dict, f::Financial)
s = ElectricStorageDefaults(;d...)

if s.inverter_replacement_year >= f.analysis_years
Expand All @@ -306,6 +311,16 @@ struct ElectricStorage <: AbstractElectricStorage
throw(@error("ElectricStorage min_duration_hours must be less than max_duration_hours."))
end

# Copy SOC input in case we need to change them
soc_min_fraction = s.soc_min_fraction
optimize_soc_init_fraction = s.optimize_soc_init_fraction
if !isnothing(s.fixed_soc_series_fraction)
@warn "Fixing ElectricStorage soc_series_fraction to the provided fixed_soc_series_fraction. Other SOC inputs will be ignored."
soc_min_fraction = 0.0
optimize_soc_init_fraction = false
error_if_series_vals_not_0_to_1(s.fixed_soc_series_fraction, "ElectricStorage", "fixed_soc_series_fraction")
end

macrs_schedule = [0.0]
if s.macrs_option_years == 5 || s.macrs_option_years == 7
macrs_schedule = s.macrs_option_years == 7 ? f.macrs_seven_year : f.macrs_five_year
Expand Down Expand Up @@ -392,7 +407,7 @@ struct ElectricStorage <: AbstractElectricStorage
s.internal_efficiency_fraction,
s.inverter_efficiency_fraction,
s.rectifier_efficiency_fraction,
s.soc_min_fraction,
soc_min_fraction,
s.soc_min_applies_during_outages,
s.soc_init_fraction,
s.can_grid_charge,
Expand Down Expand Up @@ -421,9 +436,10 @@ struct ElectricStorage <: AbstractElectricStorage
s.model_degradation,
degr,
s.minimum_avg_soc_fraction,
s.optimize_soc_init_fraction,
optimize_soc_init_fraction,
s.min_duration_hours,
s.max_duration_hours
s.max_duration_hours,
s.fixed_soc_series_fraction
)
end
end
3 changes: 2 additions & 1 deletion src/core/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ function dictkeys_tosymbols(d::Dict)
#for ERP
"pv_production_factor_series", "wind_production_factor_series",
"battery_starting_soc_series_fraction",
"monthly_mmbtu", "monthly_tonhour"
"monthly_mmbtu", "monthly_tonhour",
"fixed_soc_series_fraction"
] && !isnothing(v)
try
v = convert(Array{Real, 1}, v)
Expand Down
5 changes: 5 additions & 0 deletions src/mpc/structs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ Base.@kwdef struct MPCElectricStorage < AbstractElectricStorage
soc_init_fraction::Float64 = 0.5
can_grid_charge::Bool = true
grid_charge_efficiency::Float64 = 0.96 * 0.975^2
max_kw::Float64 = size_kw
max_kwh::Float64 = size_kwh
minimum_avg_soc_fraction::Float64 = 0.0
fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing
end
```
"""
Expand All @@ -242,6 +246,7 @@ Base.@kwdef struct MPCElectricStorage <: AbstractElectricStorage
max_kw::Float64 = size_kw
max_kwh::Float64 = size_kwh
minimum_avg_soc_fraction::Float64 = 0.0
fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing
end


Expand Down
20 changes: 20 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3798,6 +3798,26 @@ else # run HiGHS tests
empty!(m2)
GC.gc()
end

@testset "Fixed ElectricStorage state of charge" begin
post_name = "fixed_pv_bess"
post = JSON.parsefile("./scenarios/$post_name.json")

# Get optimal SOC
m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false))
results = run_reopt(m1 , post)
lcc1 = results["Financial"]["lcc"]
soc_series = results["ElectricStorage"]["soc_series_fraction"]

# Fix soc_series to optimal from previous run
m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false))
post["ElectricStorage"]["fixed_soc_series_fraction"] = soc_series
results = run_reopt(m1 , post)
lcc2 = results["Financial"]["lcc"]

@test lcc1 ≈ lcc2 rtol=0.001
@test maximum(abs.(soc_series - results["ElectricStorage"]["soc_series_fraction"])) <= 0.0200001
end

@testset "Existing HVAC (Boiler and Chiller) Costs for BAU" begin
"""
Expand Down
31 changes: 31 additions & 0 deletions test/scenarios/fixed_pv_bess.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"Site": {
"longitude": -118.1164613,
"latitude": 34.5794343
},
"ElectricLoad": {
"doe_reference_name": "RetailStore",
"annual_kwh": 876000.0
},
"ElectricTariff": {
"blended_annual_energy_rate": 0.10,
"blended_annual_demand_rate": 0
},
"Financial": {
"elec_cost_escalation_rate_fraction": 0.026,
"offtaker_discount_rate_fraction": 0.081,
"analysis_years": 20,
"offtaker_tax_rate_fraction": 0.4,
"om_cost_escalation_rate_fraction": 0.025
},
"PV" : {
"min_kw": 100,
"max_kw": 100
},
"ElectricStorage" : {
"min_kw": 100,
"max_kw": 100,
"min_kwh": 200,
"max_kwh": 200
}
}
Loading