mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 08:18:51 -06:00
storage units
This commit is contained in:
@@ -36,6 +36,7 @@ include("model/formulations/base/sensitivity.jl")
|
|||||||
include("model/formulations/base/system.jl")
|
include("model/formulations/base/system.jl")
|
||||||
include("model/formulations/base/unit.jl")
|
include("model/formulations/base/unit.jl")
|
||||||
include("model/formulations/base/punit.jl")
|
include("model/formulations/base/punit.jl")
|
||||||
|
include("model/formulations/base/storage.jl")
|
||||||
include("model/formulations/CarArr2006/pwlcosts.jl")
|
include("model/formulations/CarArr2006/pwlcosts.jl")
|
||||||
include("model/formulations/DamKucRajAta2016/ramp.jl")
|
include("model/formulations/DamKucRajAta2016/ramp.jl")
|
||||||
include("model/formulations/Gar1962/pwlcosts.jl")
|
include("model/formulations/Gar1962/pwlcosts.jl")
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
|||||||
loads = PriceSensitiveLoad[]
|
loads = PriceSensitiveLoad[]
|
||||||
reserves = Reserve[]
|
reserves = Reserve[]
|
||||||
profiled_units = ProfiledUnit[]
|
profiled_units = ProfiledUnit[]
|
||||||
|
storage_units = StorageUnit[]
|
||||||
|
|
||||||
function scalar(x; default = nothing)
|
function scalar(x; default = nothing)
|
||||||
x !== nothing || return default
|
x !== nothing || return default
|
||||||
@@ -196,6 +197,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
|||||||
ThermalUnit[],
|
ThermalUnit[],
|
||||||
PriceSensitiveLoad[],
|
PriceSensitiveLoad[],
|
||||||
ProfiledUnit[],
|
ProfiledUnit[],
|
||||||
|
StorageUnit[],
|
||||||
)
|
)
|
||||||
name_to_bus[bus_name] = bus
|
name_to_bus[bus_name] = bus
|
||||||
push!(buses, bus)
|
push!(buses, bus)
|
||||||
@@ -405,6 +407,52 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Read storage units
|
||||||
|
if "Storage units" in keys(json)
|
||||||
|
for (storage_name, dict) in json["Storage units"]
|
||||||
|
bus = name_to_bus[dict["Bus"]]
|
||||||
|
min_level =
|
||||||
|
timeseries(scalar(dict["Minimum level (MWh)"], default = 0.0))
|
||||||
|
max_level = timeseries(dict["Maximum level (MWh)"])
|
||||||
|
storage = StorageUnit(
|
||||||
|
storage_name,
|
||||||
|
bus,
|
||||||
|
min_level,
|
||||||
|
max_level,
|
||||||
|
timeseries(
|
||||||
|
scalar(
|
||||||
|
dict["Allow simultaneous charging and discharging"],
|
||||||
|
default = true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
timeseries(dict["Charge cost (\$/MW)"]),
|
||||||
|
timeseries(dict["Discharge cost (\$/MW)"]),
|
||||||
|
timeseries(scalar(dict["Charge efficiency"], default = 1.0)),
|
||||||
|
timeseries(scalar(dict["Discharge efficiency"], default = 1.0)),
|
||||||
|
timeseries(scalar(dict["Loss factor"], default = 0.0)),
|
||||||
|
timeseries(
|
||||||
|
scalar(dict["Minimum charge rate (MW)"], default = 0.0),
|
||||||
|
),
|
||||||
|
timeseries(dict["Maximum charge rate (MW)"]),
|
||||||
|
timeseries(
|
||||||
|
scalar(dict["Minimum discharge rate (MW)"], default = 0.0),
|
||||||
|
),
|
||||||
|
timeseries(dict["Maximum discharge rate (MW)"]),
|
||||||
|
scalar(dict["Initial level (MWh)"], default = 0.0),
|
||||||
|
scalar(
|
||||||
|
dict["Last period minimum level (MWh)"],
|
||||||
|
default = min_level[T],
|
||||||
|
),
|
||||||
|
scalar(
|
||||||
|
dict["Last period maximum level (MWh)"],
|
||||||
|
default = max_level[T],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
push!(bus.storage_units, storage)
|
||||||
|
push!(storage_units, storage)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
scenario = UnitCommitmentScenario(
|
scenario = UnitCommitmentScenario(
|
||||||
name = scenario_name,
|
name = scenario_name,
|
||||||
probability = probability,
|
probability = probability,
|
||||||
@@ -425,6 +473,8 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
|||||||
thermal_units = thermal_units,
|
thermal_units = thermal_units,
|
||||||
profiled_units_by_name = Dict(pu.name => pu for pu in profiled_units),
|
profiled_units_by_name = Dict(pu.name => pu for pu in profiled_units),
|
||||||
profiled_units = profiled_units,
|
profiled_units = profiled_units,
|
||||||
|
storage_units_by_name = Dict(su.name => su for su in storage_units),
|
||||||
|
storage_units = storage_units,
|
||||||
isf = spzeros(Float64, length(lines), length(buses) - 1),
|
isf = spzeros(Float64, length(lines), length(buses) - 1),
|
||||||
lodf = spzeros(Float64, length(lines), length(lines)),
|
lodf = spzeros(Float64, length(lines), length(lines)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ mutable struct Bus
|
|||||||
thermal_units::Vector
|
thermal_units::Vector
|
||||||
price_sensitive_loads::Vector
|
price_sensitive_loads::Vector
|
||||||
profiled_units::Vector
|
profiled_units::Vector
|
||||||
|
storage_units::Vector
|
||||||
end
|
end
|
||||||
|
|
||||||
mutable struct CostSegment
|
mutable struct CostSegment
|
||||||
@@ -83,6 +84,26 @@ mutable struct ProfiledUnit
|
|||||||
cost::Vector{Float64}
|
cost::Vector{Float64}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
mutable struct StorageUnit
|
||||||
|
name::String
|
||||||
|
bus::Bus
|
||||||
|
min_level::Vector{Float64}
|
||||||
|
max_level::Vector{Float64}
|
||||||
|
simultaneous_charge_and_discharge::Vector{Bool}
|
||||||
|
charge_cost::Vector{Float64}
|
||||||
|
discharge_cost::Vector{Float64}
|
||||||
|
charge_efficiency::Vector{Float64}
|
||||||
|
discharge_efficiency::Vector{Float64}
|
||||||
|
loss_factor::Vector{Float64}
|
||||||
|
min_charge_rate::Vector{Float64}
|
||||||
|
max_charge_rate::Vector{Float64}
|
||||||
|
min_discharge_rate::Vector{Float64}
|
||||||
|
max_discharge_rate::Vector{Float64}
|
||||||
|
initial_level::Float64
|
||||||
|
min_ending_level::Float64
|
||||||
|
max_ending_level::Float64
|
||||||
|
end
|
||||||
|
|
||||||
Base.@kwdef mutable struct UnitCommitmentScenario
|
Base.@kwdef mutable struct UnitCommitmentScenario
|
||||||
buses_by_name::Dict{AbstractString,Bus}
|
buses_by_name::Dict{AbstractString,Bus}
|
||||||
buses::Vector{Bus}
|
buses::Vector{Bus}
|
||||||
@@ -103,6 +124,8 @@ Base.@kwdef mutable struct UnitCommitmentScenario
|
|||||||
reserves::Vector{Reserve}
|
reserves::Vector{Reserve}
|
||||||
thermal_units_by_name::Dict{AbstractString,ThermalUnit}
|
thermal_units_by_name::Dict{AbstractString,ThermalUnit}
|
||||||
thermal_units::Vector{ThermalUnit}
|
thermal_units::Vector{ThermalUnit}
|
||||||
|
storage_units_by_name::Dict{AbstractString,StorageUnit}
|
||||||
|
storage_units::Vector{StorageUnit}
|
||||||
time::Int
|
time::Int
|
||||||
time_step::Int
|
time_step::Int
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ function build_model(;
|
|||||||
for pu in sc.profiled_units
|
for pu in sc.profiled_units
|
||||||
_add_profiled_unit!(model, pu, sc)
|
_add_profiled_unit!(model, pu, sc)
|
||||||
end
|
end
|
||||||
|
for su in sc.storage_units
|
||||||
|
_add_storage_unit!(model, su, sc)
|
||||||
|
end
|
||||||
_add_system_wide_eqs!(model, sc)
|
_add_system_wide_eqs!(model, sc)
|
||||||
end
|
end
|
||||||
@objective(model, Min, model[:obj])
|
@objective(model, Min, model[:obj])
|
||||||
|
|||||||
125
src/model/formulations/base/storage.jl
Normal file
125
src/model/formulations/base/storage.jl
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
function _add_storage_unit!(
|
||||||
|
model::JuMP.Model,
|
||||||
|
su::StorageUnit,
|
||||||
|
sc::UnitCommitmentScenario,
|
||||||
|
)::Nothing
|
||||||
|
# Initialize variables
|
||||||
|
storage_level = _init(model, :storage_level)
|
||||||
|
charge_rate = _init(model, :charge_rate)
|
||||||
|
discharge_rate = _init(model, :discharge_rate)
|
||||||
|
is_charging = _init(model, :is_charging)
|
||||||
|
is_discharging = _init(model, :is_discharging)
|
||||||
|
eq_min_charge_rate = _init(model, :eq_min_charge_rate)
|
||||||
|
eq_max_charge_rate = _init(model, :eq_max_charge_rate)
|
||||||
|
eq_min_discharge_rate = _init(model, :eq_min_discharge_rate)
|
||||||
|
eq_max_discharge_rate = _init(model, :eq_max_discharge_rate)
|
||||||
|
# Initialize constraints
|
||||||
|
net_injection = _init(model, :expr_net_injection)
|
||||||
|
eq_storage_transition = _init(model, :eq_storage_transition)
|
||||||
|
eq_ending_level = _init(model, :eq_ending_level)
|
||||||
|
# time in hours
|
||||||
|
time_step = sc.time_step / 60
|
||||||
|
|
||||||
|
for t in 1:model[:instance].time
|
||||||
|
# Decision variable
|
||||||
|
storage_level[sc.name, su.name, t] = @variable(
|
||||||
|
model,
|
||||||
|
lower_bound = su.min_level[t],
|
||||||
|
upper_bound = su.max_level[t]
|
||||||
|
)
|
||||||
|
charge_rate[sc.name, su.name, t] = @variable(model)
|
||||||
|
discharge_rate[sc.name, su.name, t] = @variable(model)
|
||||||
|
is_charging[sc.name, su.name, t] = @variable(model, binary = true)
|
||||||
|
is_discharging[sc.name, su.name, t] = @variable(model, binary = true)
|
||||||
|
|
||||||
|
# Objective function terms ##### CHECK & FIXME
|
||||||
|
add_to_expression!(
|
||||||
|
model[:obj],
|
||||||
|
charge_rate[sc.name, su.name, t],
|
||||||
|
su.charge_cost[t] * sc.probability,
|
||||||
|
)
|
||||||
|
|
||||||
|
add_to_expression!(
|
||||||
|
model[:obj],
|
||||||
|
discharge_rate[sc.name, su.name, t],
|
||||||
|
su.discharge_cost[t] * sc.probability,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Net injection
|
||||||
|
add_to_expression!(
|
||||||
|
net_injection[sc.name, su.bus.name, t],
|
||||||
|
discharge_rate[sc.name, su.name, t],
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
|
add_to_expression!(
|
||||||
|
net_injection[sc.name, su.bus.name, t],
|
||||||
|
charge_rate[sc.name, su.name, t],
|
||||||
|
-1.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Simultaneous charging and discharging
|
||||||
|
if !su.simultaneous_charge_and_discharge[t]
|
||||||
|
# Initialize the model dictionary
|
||||||
|
eq_simultaneous_charge_and_discharge =
|
||||||
|
_init(model, :eq_simultaneous_charge_and_discharge)
|
||||||
|
# Constraints
|
||||||
|
eq_simultaneous_charge_and_discharge[sc.name, su.name, t] =
|
||||||
|
@constraint(
|
||||||
|
model,
|
||||||
|
is_charging[sc.name, su.name, t] +
|
||||||
|
is_discharging[sc.name, su.name, t] <= 1.0
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Charge and discharge constraints
|
||||||
|
eq_min_charge_rate[sc.name, su.name, t] = @constraint(
|
||||||
|
model,
|
||||||
|
charge_rate[sc.name, su.name, t] >=
|
||||||
|
is_charging[sc.name, su.name, t] * su.min_charge_rate[t]
|
||||||
|
)
|
||||||
|
eq_max_charge_rate[sc.name, su.name, t] = @constraint(
|
||||||
|
model,
|
||||||
|
charge_rate[sc.name, su.name, t] <=
|
||||||
|
is_charging[sc.name, su.name, t] * su.max_charge_rate[t]
|
||||||
|
)
|
||||||
|
eq_min_discharge_rate[sc.name, su.name, t] = @constraint(
|
||||||
|
model,
|
||||||
|
discharge_rate[sc.name, su.name, t] >=
|
||||||
|
is_discharging[sc.name, su.name, t] * su.min_discharge_rate[t]
|
||||||
|
)
|
||||||
|
eq_max_discharge_rate[sc.name, su.name, t] = @constraint(
|
||||||
|
model,
|
||||||
|
discharge_rate[sc.name, su.name, t] <=
|
||||||
|
is_discharging[sc.name, su.name, t] * su.max_discharge_rate[t]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Storage energy transition constraint
|
||||||
|
prev_storage_level =
|
||||||
|
t == 1 ? su.initial_level : storage_level[sc.name, su.name, t-1]
|
||||||
|
eq_storage_transition[sc.name, su.name, t] = @constraint(
|
||||||
|
model,
|
||||||
|
storage_level[sc.name, su.name, t] ==
|
||||||
|
(1 - su.loss_factor[t]) * prev_storage_level +
|
||||||
|
charge_rate[sc.name, su.name, t] *
|
||||||
|
time_step *
|
||||||
|
su.charge_efficiency[t] -
|
||||||
|
discharge_rate[sc.name, su.name, t] * time_step /
|
||||||
|
su.discharge_efficiency[t]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Storage ending level constraint
|
||||||
|
if t == sc.time
|
||||||
|
eq_ending_level[sc.name, su.name] = @constraint(
|
||||||
|
model,
|
||||||
|
su.min_ending_level <=
|
||||||
|
storage_level[sc.name, su.name, t] <=
|
||||||
|
su.max_ending_level
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
@@ -103,6 +103,30 @@ function solution(model::JuMP.Model)::OrderedDict
|
|||||||
] for pu in sc.profiled_units
|
] for pu in sc.profiled_units
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
if !isempty(sc.storage_units)
|
||||||
|
sol[sc.name]["Storage level (MWh)"] =
|
||||||
|
timeseries(model[:storage_level], sc.storage_units, sc = sc)
|
||||||
|
sol[sc.name]["Is charging"] =
|
||||||
|
timeseries(model[:is_charging], sc.storage_units, sc = sc)
|
||||||
|
sol[sc.name]["Storage charging rates (MW)"] =
|
||||||
|
timeseries(model[:charge_rate], sc.storage_units, sc = sc)
|
||||||
|
sol[sc.name]["Storage charging cost (\$)"] = OrderedDict(
|
||||||
|
su.name => [
|
||||||
|
value(model[:charge_rate][sc.name, su.name, t]) *
|
||||||
|
su.charge_cost[t] for t in 1:instance.time
|
||||||
|
] for su in sc.storage_units
|
||||||
|
)
|
||||||
|
sol[sc.name]["Is discharging"] =
|
||||||
|
timeseries(model[:is_discharging], sc.storage_units, sc = sc)
|
||||||
|
sol[sc.name]["Storage discharging rates (MW)"] =
|
||||||
|
timeseries(model[:discharge_rate], sc.storage_units, sc = sc)
|
||||||
|
sol[sc.name]["Storage discharging cost (\$)"] = OrderedDict(
|
||||||
|
su.name => [
|
||||||
|
value(model[:discharge_rate][sc.name, su.name, t]) *
|
||||||
|
su.discharge_cost[t] for t in 1:instance.time
|
||||||
|
] for su in sc.storage_units
|
||||||
|
)
|
||||||
|
end
|
||||||
sol[sc.name]["Spinning reserve (MW)"] = OrderedDict(
|
sol[sc.name]["Spinning reserve (MW)"] = OrderedDict(
|
||||||
r.name => OrderedDict(
|
r.name => OrderedDict(
|
||||||
g.name => [
|
g.name => [
|
||||||
|
|||||||
@@ -137,6 +137,11 @@ function _randomize_costs(
|
|||||||
α = rand(rng, distribution)
|
α = rand(rng, distribution)
|
||||||
pu.cost *= α
|
pu.cost *= α
|
||||||
end
|
end
|
||||||
|
for su in sc.storage_units
|
||||||
|
α = rand(rng, distribution)
|
||||||
|
su.charge_cost *= α
|
||||||
|
su.discharge_cost *= α
|
||||||
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,21 @@ function slice(
|
|||||||
ps.demand = ps.demand[range]
|
ps.demand = ps.demand[range]
|
||||||
ps.revenue = ps.revenue[range]
|
ps.revenue = ps.revenue[range]
|
||||||
end
|
end
|
||||||
|
for su in sc.storage_units
|
||||||
|
su.min_level = su.min_level[range]
|
||||||
|
su.max_level = su.max_level[range]
|
||||||
|
su.simultaneous_charge_and_discharge =
|
||||||
|
su.simultaneous_charge_and_discharge[range]
|
||||||
|
su.charge_cost = su.charge_cost[range]
|
||||||
|
su.discharge_cost = su.discharge_cost[range]
|
||||||
|
su.charge_efficiency = su.charge_efficiency[range]
|
||||||
|
su.discharge_efficiency = su.discharge_efficiency[range]
|
||||||
|
su.loss_factor = su.loss_factor[range]
|
||||||
|
su.min_charge_rate = su.min_charge_rate[range]
|
||||||
|
su.max_charge_rate = su.max_charge_rate[range]
|
||||||
|
su.min_discharge_rate = su.min_discharge_rate[range]
|
||||||
|
su.max_discharge_rate = su.max_discharge_rate[range]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
return modified
|
return modified
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -334,6 +334,195 @@ function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
for su in sc.storage_units
|
||||||
|
storage_level = solution[sc.name]["Storage level (MWh)"][su.name]
|
||||||
|
charge_rate =
|
||||||
|
solution[sc.name]["Storage charging rates (MW)"][su.name]
|
||||||
|
discharge_rate =
|
||||||
|
solution[sc.name]["Storage discharging rates (MW)"][su.name]
|
||||||
|
actual_charge_cost =
|
||||||
|
solution[sc.name]["Storage charging cost (\$)"][su.name]
|
||||||
|
actual_discharge_cost =
|
||||||
|
solution[sc.name]["Storage discharging cost (\$)"][su.name]
|
||||||
|
is_charging = bin(solution[sc.name]["Is charging"][su.name])
|
||||||
|
is_discharging = bin(solution[sc.name]["Is discharging"][su.name])
|
||||||
|
# time in hours
|
||||||
|
time_step = sc.time_step / 60
|
||||||
|
|
||||||
|
for t in 1:instance.time
|
||||||
|
# Unit must store at least its minimum level
|
||||||
|
if storage_level[t] < su.min_level[t] - tol
|
||||||
|
@error @sprintf(
|
||||||
|
"Storage unit %s stores below its minimum level at time %d (%.2f < %.2f)",
|
||||||
|
su.name,
|
||||||
|
t,
|
||||||
|
storage_level[t],
|
||||||
|
su.min_level[t]
|
||||||
|
)
|
||||||
|
err_count += 1
|
||||||
|
end
|
||||||
|
# Unit must store at most its maximum level
|
||||||
|
if storage_level[t] > su.max_level[t] + tol
|
||||||
|
@error @sprintf(
|
||||||
|
"Storage unit %s stores above its maximum level at time %d (%.2f > %.2f)",
|
||||||
|
su.name,
|
||||||
|
t,
|
||||||
|
storage_level[t],
|
||||||
|
su.max_level[t]
|
||||||
|
)
|
||||||
|
err_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if t == instance.time
|
||||||
|
# Unit must store at least its minimum level at last time period
|
||||||
|
if storage_level[t] < su.min_ending_level - tol
|
||||||
|
@error @sprintf(
|
||||||
|
"Storage unit %s stores below its minimum ending level (%.2f < %.2f)",
|
||||||
|
su.name,
|
||||||
|
storage_level[t],
|
||||||
|
su.min_ending_level
|
||||||
|
)
|
||||||
|
err_count += 1
|
||||||
|
end
|
||||||
|
# Unit must store at most its maximum level at last time period
|
||||||
|
if storage_level[t] > su.max_ending_level + tol
|
||||||
|
@error @sprintf(
|
||||||
|
"Storage unit %s stores above its maximum ending level (%.2f > %.2f)",
|
||||||
|
su.name,
|
||||||
|
storage_level[t],
|
||||||
|
su.max_ending_level
|
||||||
|
)
|
||||||
|
err_count += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unit must follow the energy transition constraint
|
||||||
|
prev_level = t == 1 ? su.initial_level : storage_level[t-1]
|
||||||
|
current_level =
|
||||||
|
(1 - su.loss_factor[t]) * prev_level +
|
||||||
|
time_step * (
|
||||||
|
charge_rate[t] * su.charge_efficiency[t] -
|
||||||
|
discharge_rate[t] / su.discharge_efficiency[t]
|
||||||
|
)
|
||||||
|
if abs(storage_level[t] - current_level) > tol
|
||||||
|
@error @sprintf(
|
||||||
|
"Storage unit %s has unexpected level at time %d (%.2f should be %.2f)",
|
||||||
|
unit.name,
|
||||||
|
t,
|
||||||
|
storage_level[t],
|
||||||
|
current_level
|
||||||
|
)
|
||||||
|
err_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unit cannot simultaneous charge and discharge if it is not allowed
|
||||||
|
if !su.simultaneous_charge_and_discharge[t] &&
|
||||||
|
is_charging[t] &&
|
||||||
|
is_discharging[t]
|
||||||
|
@error @sprintf(
|
||||||
|
"Storage unit %s is charging and discharging simultaneous at time %d",
|
||||||
|
su.name,
|
||||||
|
t
|
||||||
|
)
|
||||||
|
err_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unit must charge at least its minimum rate
|
||||||
|
if is_charging[t] &&
|
||||||
|
(charge_rate[t] < su.min_charge_rate[t] - tol)
|
||||||
|
@error @sprintf(
|
||||||
|
"Storage unit %s charges below its minimum limit at time %d (%.2f < %.2f)",
|
||||||
|
unit.name,
|
||||||
|
t,
|
||||||
|
charge_rate[t],
|
||||||
|
su.min_charge_rate[t]
|
||||||
|
)
|
||||||
|
err_count += 1
|
||||||
|
end
|
||||||
|
# Unit must charge at most its maximum rate
|
||||||
|
if is_charging[t] &&
|
||||||
|
(charge_rate[t] > su.max_charge_rate[t] + tol)
|
||||||
|
@error @sprintf(
|
||||||
|
"Storage unit %s charges above its maximum limit at time %d (%.2f > %.2f)",
|
||||||
|
unit.name,
|
||||||
|
t,
|
||||||
|
charge_rate[t],
|
||||||
|
su.max_charge_rate[t]
|
||||||
|
)
|
||||||
|
err_count += 1
|
||||||
|
end
|
||||||
|
# Unit must have zero charge when it is not charging
|
||||||
|
if !is_charging[t] && (charge_rate[t] > tol)
|
||||||
|
@error @sprintf(
|
||||||
|
"Storage unit %s charges power at time %d while not charging (%.2f > 0)",
|
||||||
|
unit.name,
|
||||||
|
t,
|
||||||
|
charge_rate[t]
|
||||||
|
)
|
||||||
|
err_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unit must discharge at least its minimum rate
|
||||||
|
if is_discharging[t] &&
|
||||||
|
(discharge_rate[t] < su.min_discharge_rate[t] - tol)
|
||||||
|
@error @sprintf(
|
||||||
|
"Storage unit %s discharges below its minimum limit at time %d (%.2f < %.2f)",
|
||||||
|
unit.name,
|
||||||
|
t,
|
||||||
|
discharge_rate[t],
|
||||||
|
su.min_discharge_rate[t]
|
||||||
|
)
|
||||||
|
err_count += 1
|
||||||
|
end
|
||||||
|
# Unit must discharge at most its maximum rate
|
||||||
|
if is_discharging[t] &&
|
||||||
|
(discharge_rate[t] > su.max_discharge_rate[t] + tol)
|
||||||
|
@error @sprintf(
|
||||||
|
"Storage unit %s discharges above its maximum limit at time %d (%.2f > %.2f)",
|
||||||
|
unit.name,
|
||||||
|
t,
|
||||||
|
discharge_rate[t],
|
||||||
|
su.max_discharge_rate[t]
|
||||||
|
)
|
||||||
|
err_count += 1
|
||||||
|
end
|
||||||
|
# Unit must have zero discharge when it is not charging
|
||||||
|
if !is_discharging[t] && (discharge_rate[t] > tol)
|
||||||
|
@error @sprintf(
|
||||||
|
"Storage unit %s discharges power at time %d while not discharging (%.2f > 0)",
|
||||||
|
unit.name,
|
||||||
|
t,
|
||||||
|
discharge_rate[t]
|
||||||
|
)
|
||||||
|
err_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Compute storage costs
|
||||||
|
charge_cost = su.charge_cost[t] * charge_rate[t]
|
||||||
|
discharge_cost = su.discharge_cost[t] * discharge_rate[t]
|
||||||
|
# Compare costs
|
||||||
|
if abs(actual_charge_cost[t] - charge_cost) > tol
|
||||||
|
@error @sprintf(
|
||||||
|
"Storage unit %s has unexpected charge cost at time %d (%.2f should be %.2f)",
|
||||||
|
unit.name,
|
||||||
|
t,
|
||||||
|
actual_charge_cost[t],
|
||||||
|
charge_cost
|
||||||
|
)
|
||||||
|
err_count += 1
|
||||||
|
end
|
||||||
|
if abs(actual_discharge_cost[t] - discharge_cost) > tol
|
||||||
|
@error @sprintf(
|
||||||
|
"Storage unit %s has unexpected discharge cost at time %d (%.2f should be %.2f)",
|
||||||
|
unit.name,
|
||||||
|
t,
|
||||||
|
actual_discharge_cost[t],
|
||||||
|
discharge_cost
|
||||||
|
)
|
||||||
|
err_count += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
return err_count
|
return err_count
|
||||||
end
|
end
|
||||||
@@ -346,6 +535,8 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
|
|||||||
fixed_load = sum(b.load[t] for b in sc.buses)
|
fixed_load = sum(b.load[t] for b in sc.buses)
|
||||||
ps_load = 0
|
ps_load = 0
|
||||||
production = 0
|
production = 0
|
||||||
|
storage_charge = 0
|
||||||
|
storage_discharge = 0
|
||||||
if length(sc.price_sensitive_loads) > 0
|
if length(sc.price_sensitive_loads) > 0
|
||||||
ps_load = sum(
|
ps_load = sum(
|
||||||
solution[sc.name]["Price-sensitive loads (MW)"][ps.name][t]
|
solution[sc.name]["Price-sensitive loads (MW)"][ps.name][t]
|
||||||
@@ -364,23 +555,35 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
|
|||||||
for pu in sc.profiled_units
|
for pu in sc.profiled_units
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
if length(sc.storage_units) > 0
|
||||||
|
storage_charge += sum(
|
||||||
|
solution[sc.name]["Storage charging rates (MW)"][su.name][t]
|
||||||
|
for su in sc.storage_units
|
||||||
|
)
|
||||||
|
storage_discharge += sum(
|
||||||
|
solution[sc.name]["Storage discharging rates (MW)"][su.name][t]
|
||||||
|
for su in sc.storage_units
|
||||||
|
)
|
||||||
|
end
|
||||||
if "Load curtail (MW)" in keys(solution)
|
if "Load curtail (MW)" in keys(solution)
|
||||||
load_curtail = sum(
|
load_curtail = sum(
|
||||||
solution[sc.name]["Load curtail (MW)"][b.name][t] for
|
solution[sc.name]["Load curtail (MW)"][b.name][t] for
|
||||||
b in sc.buses
|
b in sc.buses
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
balance = fixed_load - load_curtail - production + ps_load
|
balance = fixed_load - load_curtail - production + ps_load + storage_charge - storage_discharge
|
||||||
|
|
||||||
# Verify that production equals demand
|
# Verify that production equals demand
|
||||||
if abs(balance) > tol
|
if abs(balance) > tol
|
||||||
@error @sprintf(
|
@error @sprintf(
|
||||||
"Non-zero power balance at time %d (%.2f + %.2f - %.2f - %.2f != 0)",
|
"Non-zero power balance at time %d (%.2f + %.2f - %.2f - %.2f + %.2f - %.2f != 0)",
|
||||||
t,
|
t,
|
||||||
fixed_load,
|
fixed_load,
|
||||||
ps_load,
|
ps_load,
|
||||||
load_curtail,
|
load_curtail,
|
||||||
production,
|
production,
|
||||||
|
storage_charge,
|
||||||
|
storage_discharge,
|
||||||
)
|
)
|
||||||
err_count += 1
|
err_count += 1
|
||||||
end
|
end
|
||||||
|
|||||||
BIN
test/fixtures/case14-storage.json.gz
vendored
Normal file
BIN
test/fixtures/case14-storage.json.gz
vendored
Normal file
Binary file not shown.
@@ -139,20 +139,20 @@ function instance_read_test()
|
|||||||
sc = instance.scenarios[1]
|
sc = instance.scenarios[1]
|
||||||
@test length(sc.profiled_units) == 2
|
@test length(sc.profiled_units) == 2
|
||||||
|
|
||||||
first_pu = sc.profiled_units[1]
|
pu1 = sc.profiled_units[1]
|
||||||
@test first_pu.name == "g7"
|
@test pu1.name == "g7"
|
||||||
@test first_pu.bus.name == "b4"
|
@test pu1.bus.name == "b4"
|
||||||
@test first_pu.cost == [100.0 for t in 1:4]
|
@test pu1.cost == [100.0 for t in 1:4]
|
||||||
@test first_pu.min_power == [60.0 for t in 1:4]
|
@test pu1.min_power == [60.0 for t in 1:4]
|
||||||
@test first_pu.max_power == [100.0 for t in 1:4]
|
@test pu1.max_power == [100.0 for t in 1:4]
|
||||||
@test sc.profiled_units_by_name["g7"].name == "g7"
|
@test sc.profiled_units_by_name["g7"].name == "g7"
|
||||||
|
|
||||||
second_pu = sc.profiled_units[2]
|
pu2 = sc.profiled_units[2]
|
||||||
@test second_pu.name == "g8"
|
@test pu2.name == "g8"
|
||||||
@test second_pu.bus.name == "b5"
|
@test pu2.bus.name == "b5"
|
||||||
@test second_pu.cost == [50.0 for t in 1:4]
|
@test pu2.cost == [50.0 for t in 1:4]
|
||||||
@test second_pu.min_power == [0.0 for t in 1:4]
|
@test pu2.min_power == [0.0 for t in 1:4]
|
||||||
@test second_pu.max_power == [120.0 for t in 1:4]
|
@test pu2.max_power == [120.0 for t in 1:4]
|
||||||
@test sc.profiled_units_by_name["g8"].name == "g8"
|
@test sc.profiled_units_by_name["g8"].name == "g8"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -166,4 +166,64 @@ function instance_read_test()
|
|||||||
@test sc.thermal_units[6].commitment_status ==
|
@test sc.thermal_units[6].commitment_status ==
|
||||||
[false, nothing, true, nothing]
|
[false, nothing, true, nothing]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@testset "read_benchmark storage" begin
|
||||||
|
instance = UnitCommitment.read(fixture("case14-storage.json.gz"))
|
||||||
|
sc = instance.scenarios[1]
|
||||||
|
@test length(sc.storage_units) == 4
|
||||||
|
|
||||||
|
su1 = sc.storage_units[1]
|
||||||
|
@test su1.name == "su1"
|
||||||
|
@test su1.bus.name == "b2"
|
||||||
|
@test su1.min_level == [0.0 for t in 1:4]
|
||||||
|
@test su1.max_level == [100.0 for t in 1:4]
|
||||||
|
@test su1.simultaneous_charge_and_discharge == [true for t in 1:4]
|
||||||
|
@test su1.charge_cost == [2.0 for t in 1:4]
|
||||||
|
@test su1.discharge_cost == [2.5 for t in 1:4]
|
||||||
|
@test su1.charge_efficiency == [1.0 for t in 1:4]
|
||||||
|
@test su1.discharge_efficiency == [1.0 for t in 1:4]
|
||||||
|
@test su1.loss_factor == [0.0 for t in 1:4]
|
||||||
|
@test su1.min_charge_rate == [0.0 for t in 1:4]
|
||||||
|
@test su1.max_charge_rate == [10.0 for t in 1:4]
|
||||||
|
@test su1.min_discharge_rate == [0.0 for t in 1:4]
|
||||||
|
@test su1.max_discharge_rate == [8.0 for t in 1:4]
|
||||||
|
@test su1.initial_level == 0.0
|
||||||
|
@test su1.min_ending_level == 0.0
|
||||||
|
@test su1.max_ending_level == 100.0
|
||||||
|
@test sc.storage_units_by_name["su1"].name == "su1"
|
||||||
|
|
||||||
|
su2 = sc.storage_units[2]
|
||||||
|
@test su2.name == "su2"
|
||||||
|
@test su2.bus.name == "b2"
|
||||||
|
@test su2.min_level == [10.0 for t in 1:4]
|
||||||
|
@test su2.simultaneous_charge_and_discharge == [false for t in 1:4]
|
||||||
|
@test su2.charge_cost == [3.0 for t in 1:4]
|
||||||
|
@test su2.discharge_cost == [3.5 for t in 1:4]
|
||||||
|
@test su2.charge_efficiency == [0.8 for t in 1:4]
|
||||||
|
@test su2.discharge_efficiency == [0.85 for t in 1:4]
|
||||||
|
@test su2.loss_factor == [0.01 for t in 1:4]
|
||||||
|
@test su2.min_charge_rate == [5.0 for t in 1:4]
|
||||||
|
@test su2.min_discharge_rate == [2.0 for t in 1:4]
|
||||||
|
@test su2.initial_level == 70.0
|
||||||
|
@test su2.min_ending_level == 80.0
|
||||||
|
@test su2.max_ending_level == 85.0
|
||||||
|
@test sc.storage_units_by_name["su2"].name == "su2"
|
||||||
|
|
||||||
|
su3 = sc.storage_units[3]
|
||||||
|
@test su3.bus.name == "b9"
|
||||||
|
@test su3.min_level == [10.0, 11.0, 12.0, 13.0]
|
||||||
|
@test su3.max_level == [100.0, 110.0, 120.0, 130.0]
|
||||||
|
@test su3.charge_cost == [2.0, 2.1, 2.2, 2.3]
|
||||||
|
@test su3.discharge_cost == [1.0, 1.1, 1.2, 1.3]
|
||||||
|
@test su3.charge_efficiency == [0.8, 0.81, 0.82, 0.82]
|
||||||
|
@test su3.discharge_efficiency == [0.85, 0.86, 0.87, 0.88]
|
||||||
|
@test su3.min_charge_rate == [5.0, 5.1, 5.2, 5.3]
|
||||||
|
@test su3.max_charge_rate == [10.0, 10.1, 10.2, 10.3]
|
||||||
|
@test su3.min_discharge_rate == [4.0, 4.1, 4.2, 4.3]
|
||||||
|
@test su3.max_discharge_rate == [8.0, 8.1, 8.2, 8.3]
|
||||||
|
|
||||||
|
su4 = sc.storage_units[4]
|
||||||
|
@test su4.simultaneous_charge_and_discharge ==
|
||||||
|
[false, false, true, true]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -102,5 +102,28 @@ function transform_randomize_XavQiuAhm2021_test()
|
|||||||
test_approx(pu1.cost[1], 98.039)
|
test_approx(pu1.cost[1], 98.039)
|
||||||
test_approx(pu2.cost[1], 48.385)
|
test_approx(pu2.cost[1], 48.385)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@testset "storage unit cost" begin
|
||||||
|
sc = UnitCommitment.read(
|
||||||
|
fixture("case14-storage.json.gz"),
|
||||||
|
).scenarios[1]
|
||||||
|
# Check original costs
|
||||||
|
su1 = sc.storage_units[1]
|
||||||
|
su3 = sc.storage_units[3]
|
||||||
|
test_approx(su1.charge_cost[4], 2.0)
|
||||||
|
test_approx(su1.discharge_cost[1], 2.5)
|
||||||
|
test_approx(su3.charge_cost[2], 2.1)
|
||||||
|
test_approx(su3.discharge_cost[3], 1.2)
|
||||||
|
randomize!(
|
||||||
|
sc,
|
||||||
|
XavQiuAhm2021.Randomization(randomize_load_profile = false),
|
||||||
|
rng = MersenneTwister(42),
|
||||||
|
)
|
||||||
|
# Check randomized costs
|
||||||
|
test_approx(su1.charge_cost[4], 1.961)
|
||||||
|
test_approx(su1.discharge_cost[1], 2.451)
|
||||||
|
test_approx(su3.charge_cost[2], 2.196)
|
||||||
|
test_approx(su3.discharge_cost[3], 1.255)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -65,4 +65,34 @@ function transform_slice_test()
|
|||||||
variable_names = true,
|
variable_names = true,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@testset "slice storage units" begin
|
||||||
|
instance = UnitCommitment.read(fixture("case14-storage.json.gz"))
|
||||||
|
modified = UnitCommitment.slice(instance, 2:4)
|
||||||
|
sc = modified.scenarios[1]
|
||||||
|
|
||||||
|
# Should update all time-dependent fields
|
||||||
|
for su in sc.storage_units
|
||||||
|
@test length(su.min_level) == 3
|
||||||
|
@test length(su.max_level) == 3
|
||||||
|
@test length(su.simultaneous_charge_and_discharge) == 3
|
||||||
|
@test length(su.charge_cost) == 3
|
||||||
|
@test length(su.discharge_cost) == 3
|
||||||
|
@test length(su.charge_efficiency) == 3
|
||||||
|
@test length(su.discharge_efficiency) == 3
|
||||||
|
@test length(su.loss_factor) == 3
|
||||||
|
@test length(su.min_charge_rate) == 3
|
||||||
|
@test length(su.max_charge_rate) == 3
|
||||||
|
@test length(su.min_discharge_rate) == 3
|
||||||
|
@test length(su.max_discharge_rate) == 3
|
||||||
|
end
|
||||||
|
|
||||||
|
# Should be able to build model without errors
|
||||||
|
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||||
|
model = UnitCommitment.build_model(
|
||||||
|
instance = modified,
|
||||||
|
optimizer = optimizer,
|
||||||
|
variable_names = true,
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user