Skip to content

Commit

Permalink
Made stack replacement cost a timeprofile
Browse files Browse the repository at this point in the history
  • Loading branch information
JulStraus committed Nov 27, 2024
1 parent 90dd1e3 commit 03a1dd1
Show file tree
Hide file tree
Showing 8 changed files with 55 additions and 30 deletions.
2 changes: 1 addition & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Release notes

## Unversioned
## Version 0.6.3 (2024-11-27)

### Battery modelling

Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "EnergyModelsRenewableProducers"
uuid = "b007c34f-ba52-4995-ba37-fffe79fbde35"
authors = ["Sigmund Eggen Holm <[email protected]>, Julian Straus <[email protected]>"]
version = "0.6.2"
version = "0.6.3"

[deps]
EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50"
Expand Down
2 changes: 2 additions & 0 deletions docs/src/nodes/battery.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ The battery life is unlimited and unimpacted by the use of the battery.
[`CycleLife`](@ref) includes both linear battery degradation and a lifetime through the maximum number of cycles of the battery.
The linear degradation is dependent on the charging of the battery although this is equivalent to the discharging.
The type allows for replacement of the battery stack to reduce battery degradation to 0 at the beginning of an investment period.
The cost for replacement has to be accessible through a strategic period.
Hence, it can be either a `FixedProfile` or a `StrategicProfile`, but cannot include, *e.g.* `OperationalProfile`.

!!! danger "Varying storage capacities with Batteries"
If you use batteries with varying capacities, it is important to implement one battery node for each investment period.
Expand Down
2 changes: 1 addition & 1 deletion examples/simple_battery.jl
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function generate_battery_example_data()
CycleLife(
900, # Cycles before the battery reach the end of its lifetime
0.2, # Capacity reduction at the end of the lifetime
2e5, # Battery stack replacement cost in EUR/MWh
FixedProfile(2e5), # Battery stack replacement cost in EUR/MWh
),
ResourceCarrier[], # Upwards reserve resource
[reserve_down], # Downwards reserve resource, not included
Expand Down
45 changes: 34 additions & 11 deletions src/checks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ function EMB.check_node(n::AbstractBattery, 𝒯, modeltype::EnergyModel, check_
"The values of the input variables have to be non-negative."
)
end
check_battery_life(n, battery_life(n), 𝒯, modeltype)
check_battery_life(n, battery_life(n), 𝒯, modeltype, check_timeprofiles)
end

"""
Expand Down Expand Up @@ -452,7 +452,7 @@ function EMB.check_node(n::ReserveBattery, 𝒯, modeltype::EnergyModel, check_t
"The values of the input variables have to be non-negative."
)
end
check_battery_life(n, battery_life(n), 𝒯, modeltype)
check_battery_life(n, battery_life(n), 𝒯, modeltype, check_timeprofiles)
if !isempty(reserve_up(n))
@assert_or_log(
any([!haskey(n.input, p) for p reserve_up(n)]),
Expand All @@ -475,8 +475,8 @@ function EMB.check_node(n::ReserveBattery, 𝒯, modeltype::EnergyModel, check_t
end
end
"""
check_battery_life(n::AbstractBattery, bat_life::AbstractBatteryLife, 𝒯, modeltype::EnergyModel)
check_battery_life(n::AbstractBattery, bat_life::CycleLife, 𝒯, modeltype::EnergyModel)
check_battery_life(n::AbstractBattery, bat_life::AbstractBatteryLife, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool)
check_battery_life(n::AbstractBattery, bat_life::CycleLife, 𝒯, modeltype::EnergyModel, check_timeprofiles::Bool)
Check that the included [`AbstractBatteryLife`](@ref) types of an [`AbstractBattery`](@ref)
follows to the
Expand All @@ -486,21 +486,27 @@ follows to the
## Checks [`CycleLife`](@ref)
- All fields must be positive.
- The value of the field `degradation` must be smaller than 0.
- The value of the field `degradation` must be smaller than 1.
- The value of the field `stack_cost` is required to be accessible through a
`StrategicPeriod` as outlined in the function
[`EMB.check_fixed_opex`](@extref EnergyModelsBase.check_fixed_opex).
"""
function check_battery_life(
n::AbstractBattery,
bat_life::AbstractBatteryLife,
𝒯,
modeltype::EnergyModel
modeltype::EnergyModel,
check_timeprofiles::Bool,
)
end
function check_battery_life(
n::AbstractBattery,
bat_life::CycleLife,
𝒯,
modeltype::EnergyModel
modeltype::EnergyModel,
check_timeprofiles::Bool,
)
𝒯ᴵⁿᵛ = strategic_periods(𝒯)
@assert_or_log(
cycles(bat_life) > 0,
"The value of the field `cycles` in the `CycleLife` must be positive."
Expand All @@ -513,8 +519,25 @@ function check_battery_life(
degradation(bat_life) 1,
"The value of the field `degradation` in the `CycleLife` must be smaller or equal to 1."
)
@assert_or_log(
stack_cost(bat_life) > 0,
"The value of the field `stack_cost` in the `CycleLife` must be positive."
)

if isa(stack_cost(bat_life), StrategicProfile) && check_timeprofiles
@assert_or_log(
length(stack_cost(bat_life).vals) == length(𝒯ᴵⁿᵛ),
"The timeprofile provided for the field `stack_cost` does not match the " *
"strategic structure."
)
end

# Check for potential indexing problems
message = "are not allowed for the field `stack_cost`."
bool_sp = EMB.check_strategic_profile(stack_cost(n), message)

# Check that the value is positive in all cases
if bool_sp
@assert_or_log(
all(stack_cost(bat_life, t_inv) > 0 for t_inv 𝒯ᴵⁿᵛ),
"The values of the timeprofiles of the field `stack_cost` in the `CycleLife` " *
"must be positive."
)
end
end
6 changes: 3 additions & 3 deletions src/constraint_functions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,12 @@ function EMB.constraints_flow_out(m, n::ReserveBattery, 𝒯::TimeStructure, mod

# Constraint for storage reserve up delivery
@constraint(m, [t 𝒯],
m[:bat_res_up][n, t] == sum(m[:flow_out][n, t, p] for p in reserve_up(n))
m[:bat_res_up][n, t] == sum(m[:flow_out][n, t, p] for p reserve_up(n))
)

# Constraint for storage reserve down delivery
@constraint(m, [t 𝒯],
m[:bat_res_down][n, t] == sum(m[:flow_out][n, t, p] for p in reserve_down(n))
m[:bat_res_down][n, t] == sum(m[:flow_out][n, t, p] for p reserve_down(n))
)
end

Expand Down Expand Up @@ -436,7 +436,7 @@ function EMB.constraints_opex_fixed(m, n::AbstractBattery, 𝒯ᴵⁿᵛ, modelt
# Extraction of the battery stack replacement variable
stack_replace = multiplication_variables(m, n, 𝒯ᴵⁿᵛ, modeltype)
opex_fixed_degradation = @expression(m, [t_inv 𝒯ᴵⁿᵛ],
stack_replace[t_inv] * stack_cost(n)
stack_replace[t_inv] * stack_cost(n, t_inv)
)
else
opex_fixed_degradation = @expression(m, [t_inv 𝒯ᴵⁿᵛ], 0)
Expand Down
20 changes: 10 additions & 10 deletions src/datastructures.jl
Original file line number Diff line number Diff line change
Expand Up @@ -901,13 +901,13 @@ number of cycles.
- **`cycles::Int`** is the number of cycles that the battery can tolerate.
- **`degradation::Float64`** is the relative allowed capacity reduction at the end of life
of the battery.
- **`stack_cost::Float64`** is the relative cost for replacing a battery stack once it
- **`stack_cost::TimeProfile`** is the relative cost for replacing a battery stack once it
reached its maximum number of cycles.
"""
struct CycleLife <: AbstractBatteryLife
cycles::Int
degradation::Float64
stack_cost::Float64
stack_cost::TimeProfile
end

"""
Expand Down Expand Up @@ -1115,8 +1115,6 @@ has_degradation(n::AbstractBattery) = isa(battery_life(n), CycleLife)

"""
cycles(n::AbstractBattery)
cycles(life::AbstractBatteryLife)
cycles(life::CycleLife)
Returns the maximum number of cycles of AbstractBattery `n` through calling its subfunction.
If the [`battery_life`](@ref) is an [`AbstractBatteryLife`](@ref), it will return `nothing`.
Expand All @@ -1127,8 +1125,6 @@ cycles(life::CycleLife) = life.cycles

"""
degradation(n::AbstractBattery)
degradation(life::AbstractBatteryLife)
degradation(life::CycleLife)
Returns the degradation of the battery storage capacity at the end of its lifetime through
calling its subfunction. If the [`battery_life`](@ref) is an [`AbstractBatteryLife`](@ref),
Expand All @@ -1140,13 +1136,17 @@ degradation(life::CycleLife) = life.degradation

"""
stack_cost(n::AbstractBattery)
stack_cost(life::AbstractBatteryLife)
stack_cost(life::CycleLife)
stack_cost(n::AbstractBattery, t_inv::TS.AbstractStrategicPeriod)
Returns the relative stack cost of the battery storage capacity for replacing the existing
battery capacity. If the [`battery_life`](@ref) is an [`AbstractBatteryLife`](@ref),
it will return `nothing`.
battery capacity as `TimeProfile` or in strategic period `t_inv`.
If the [`battery_life`](@ref) is an [`AbstractBatteryLife`](@ref), it will return `nothing`.
"""
stack_cost(n::AbstractBattery) = stack_cost(battery_life(n))
stack_cost(n::AbstractBattery, t_inv::TS.AbstractStrategicPeriod) =
stack_cost(battery_life(n), t_inv)
stack_cost(life::AbstractBatteryLife) = nothing
stack_cost(life::AbstractBatteryLife, t_inv::TS.AbstractStrategicPeriod) = nothing
stack_cost(life::CycleLife) = life.stack_cost
stack_cost(life::CycleLife, t_inv::TS.AbstractStrategicPeriod) = life.stack_cost[t_inv]
6 changes: 3 additions & 3 deletions test/test_battery.jl
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ end
)
end
@testset "SimpleTimes - Cycle limit" begin
bat_life = CycleLife(900, 0.2, 2e4)
bat_life = CycleLife(900, 0.2, FixedProfile(2e4))
m, case, modeltype = small_graph(supply_price, el_demand; bat_life)

# Extract the data
Expand All @@ -206,7 +206,7 @@ end
end

@testset "SimpleTimes - Cycle limit, reinvest" begin
bat_life = CycleLife(900, 0.2, 2e4)
bat_life = CycleLife(900, 0.2, StrategicProfile([2e5, 1e5, 2e4, 2e4]))
m, case, modeltype = small_graph(supply_price, el_demand; bat_life, n_sp=4)

# Extract the data
Expand Down Expand Up @@ -284,7 +284,7 @@ end
end

@testset "RepresentativePeriods - Cycle limit" begin
bat_life = CycleLife(900, 0.2, 5e4)
bat_life = CycleLife(900, 0.2, FixedProfile(5e4))
m, case, modeltype = small_graph(supply_price, el_demand; ops, bat_life)

# Extract the data
Expand Down

0 comments on commit 03a1dd1

Please sign in to comment.