Merge pull request #40 from hejun0524/storage_units

Storage units
pull/33/merge
Alinson S. Xavier 2 years ago committed by GitHub
commit 9853b15f1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,6 +9,7 @@ An instance of the stochastic security-constrained unit commitment (SCUC) proble
* [Parameters](#Parameters) * [Parameters](#Parameters)
* [Buses](#Buses) * [Buses](#Buses)
* [Generators](#Generators) * [Generators](#Generators)
* [Storage units](#Storage-units)
* [Price-sensitive loads](#Price-sensitive-loads) * [Price-sensitive loads](#Price-sensitive-loads)
* [Transmission lines](#Transmission-lines) * [Transmission lines](#Transmission-lines)
* [Reserves](#Reserves) * [Reserves](#Reserves)
@ -173,6 +174,81 @@ Note that this curve also specifies the production limits. Specifically, the fir
} }
``` ```
### Storage units
This section describes energy storage units in the system which charge and discharge power. The storage units consume power while charging, and generate power while discharging.
| Key | Description | Default | Time series? | Uncertain?
| :---------------- | :------------------------------------------------ | :------: | :------------: | :----:
| `Bus` | Bus where the storage unit is located. Multiple storage units may be placed at the same bus. | Required | No | Yes
| `Minimum level (MWh)` | Minimum of energy level this storage unit may contain. | `0.0` | Yes | Yes
| `Maximum level (MWh)` | Maximum of energy level this storage unit may contain. | Required | Yes | Yes
| `Allow simultaneous charging and discharging` | If `false`, the storage unit is not allowed to charge and discharge at the same time (Boolean). | `true` | Yes | Yes
| `Charge cost ($/MW)` | Cost incurred for charging each MW of power into this storage unit. | Required | Yes | Yes
| `Discharge cost ($/MW)` | Cost incurred for discharging each MW of power from this storage unit. | Required | Yes | Yes
| `Charge efficiency` | Efficiency rate to charge power into this storage unit. This value must be greater than or equal to `0.0`, and less than or equal to `1.0`. | `1.0` | Yes | Yes
| `Discharge efficiency` | Efficiency rate to discharge power from this storage unit. This value must be greater than or equal to `0.0`, and less than or equal to `1.0`. | `1.0` | Yes | Yes
| `Loss factor` | The energy dissipation rate of this storage unit. This value must be greater than or equal to `0.0`, and less than or equal to `1.0`. | `0.0` | Yes | Yes
| `Minimum charge rate (MW)` | Minimum amount of power rate this storage unit may charge. | `0.0` | Yes | Yes
| `Maximum charge rate (MW)` | Maximum amount of power rate this storage unit may charge. | Required | Yes | Yes
| `Minimum discharge rate (MW)` | Minimum amount of power rate this storage unit may discharge. | `0.0` | Yes | Yes
| `Maximum discharge rate (MW)` | Maximum amount of power rate this storage unit may discharge. | Required | Yes | Yes
| `Initial level (MWh)` | Amount of energy this storage unit at time step `-1`, immediately before the planning horizon starts. | `0.0` | No | Yes
| `Last period minimum level (MWh)` | Minimum of energy level this storage unit may contain in the last time step. By default, this value is the same as the last value of `Minimum level (MWh)`. | `Minimum level (MWh)` | No | Yes
| `Last period maximum level (MWh)` | Maximum of energy level this storage unit may contain in the last time step. By default, this value is the same as the last value of `Maximum level (MWh)`. | `Maximum level (MWh)` | No | Yes
#### Example
```json
{
"Storage units": {
"su1": {
"Bus": "b2",
"Maximum level (MWh)": 100.0,
"Charge cost ($/MW)": 2.0,
"Discharge cost ($/MW)": 2.5,
"Maximum charge rate (MW)": 10.0,
"Maximum discharge rate (MW)": 8.0
},
"su2": {
"Bus": "b2",
"Minimum level (MWh)": 10.0,
"Maximum level (MWh)": 100.0,
"Allow simultaneous charging and discharging": false,
"Charge cost ($/MW)": 3.0,
"Discharge cost ($/MW)": 3.5,
"Charge efficiency": 0.8,
"Discharge efficiency": 0.85,
"Loss factor": 0.01,
"Minimum charge rate (MW)": 5.0,
"Maximum charge rate (MW)": 10.0,
"Minimum discharge rate (MW)": 2.0,
"Maximum discharge rate (MW)": 10.0,
"Initial level (MWh)": 70.0,
"Last period minimum level (MWh)": 80.0,
"Last period maximum level (MWh)": 85.0
},
"su3": {
"Bus": "b9",
"Minimum level (MWh)": [10.0, 11.0, 12.0, 13.0],
"Maximum level (MWh)": [100.0, 110.0, 120.0, 130.0],
"Allow simultaneous charging and discharging": [false, false, true, true],
"Charge cost ($/MW)": [2.0, 2.1, 2.2, 2.3],
"Discharge cost ($/MW)": [1.0, 1.1, 1.2, 1.3],
"Charge efficiency": [0.8, 0.81, 0.82, 0.82],
"Discharge efficiency": [0.85, 0.86, 0.87, 0.88],
"Loss factor": [0.01, 0.01, 0.02, 0.02],
"Minimum charge rate (MW)": [5.0, 5.1, 5.2, 5.3],
"Maximum charge rate (MW)": [10.0, 10.1, 10.2, 10.3],
"Minimum discharge rate (MW)": [4.0, 4.1, 4.2, 4.3],
"Maximum discharge rate (MW)": [8.0, 8.1, 8.2, 8.3],
"Initial level (MWh)": 20.0,
"Last period minimum level (MWh)": 21.0,
"Last period maximum level (MWh)": 22.0
}
}
}
```
### Price-sensitive loads ### Price-sensitive loads
This section describes components in the system which may increase or reduce their energy consumption according to the energy prices. Fixed loads (as described in the `buses` section) are always served, regardless of the price, unless there is significant congestion in the system or insufficient production capacity. Price-sensitive loads, on the other hand, are only served if it is economical to do so. This section describes components in the system which may increase or reduce their energy consumption according to the energy prices. Fixed loads (as described in the `buses` section) are always served, regardless of the price, unless there is significant congestion in the system or insufficient production capacity. Price-sensitive loads, on the other hand, are only served if it is economical to do so.

@ -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])

@ -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,38 @@ 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

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

Loading…
Cancel
Save