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/unit.jl")
|
||||
include("model/formulations/base/punit.jl")
|
||||
include("model/formulations/base/storage.jl")
|
||||
include("model/formulations/CarArr2006/pwlcosts.jl")
|
||||
include("model/formulations/DamKucRajAta2016/ramp.jl")
|
||||
include("model/formulations/Gar1962/pwlcosts.jl")
|
||||
|
||||
@@ -136,6 +136,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
loads = PriceSensitiveLoad[]
|
||||
reserves = Reserve[]
|
||||
profiled_units = ProfiledUnit[]
|
||||
storage_units = StorageUnit[]
|
||||
|
||||
function scalar(x; default = nothing)
|
||||
x !== nothing || return default
|
||||
@@ -196,6 +197,7 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
ThermalUnit[],
|
||||
PriceSensitiveLoad[],
|
||||
ProfiledUnit[],
|
||||
StorageUnit[],
|
||||
)
|
||||
name_to_bus[bus_name] = bus
|
||||
push!(buses, bus)
|
||||
@@ -405,6 +407,52 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
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(
|
||||
name = scenario_name,
|
||||
probability = probability,
|
||||
@@ -425,6 +473,8 @@ function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
thermal_units = thermal_units,
|
||||
profiled_units_by_name = Dict(pu.name => pu for pu in 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),
|
||||
lodf = spzeros(Float64, length(lines), length(lines)),
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ mutable struct Bus
|
||||
thermal_units::Vector
|
||||
price_sensitive_loads::Vector
|
||||
profiled_units::Vector
|
||||
storage_units::Vector
|
||||
end
|
||||
|
||||
mutable struct CostSegment
|
||||
@@ -83,6 +84,26 @@ mutable struct ProfiledUnit
|
||||
cost::Vector{Float64}
|
||||
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
|
||||
buses_by_name::Dict{AbstractString,Bus}
|
||||
buses::Vector{Bus}
|
||||
@@ -103,6 +124,8 @@ Base.@kwdef mutable struct UnitCommitmentScenario
|
||||
reserves::Vector{Reserve}
|
||||
thermal_units_by_name::Dict{AbstractString,ThermalUnit}
|
||||
thermal_units::Vector{ThermalUnit}
|
||||
storage_units_by_name::Dict{AbstractString,StorageUnit}
|
||||
storage_units::Vector{StorageUnit}
|
||||
time::Int
|
||||
time_step::Int
|
||||
end
|
||||
|
||||
@@ -99,6 +99,9 @@ function build_model(;
|
||||
for pu in sc.profiled_units
|
||||
_add_profiled_unit!(model, pu, sc)
|
||||
end
|
||||
for su in sc.storage_units
|
||||
_add_storage_unit!(model, su, sc)
|
||||
end
|
||||
_add_system_wide_eqs!(model, sc)
|
||||
end
|
||||
@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
|
||||
)
|
||||
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(
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
|
||||
@@ -137,6 +137,11 @@ function _randomize_costs(
|
||||
α = rand(rng, distribution)
|
||||
pu.cost *= α
|
||||
end
|
||||
for su in sc.storage_units
|
||||
α = rand(rng, distribution)
|
||||
su.charge_cost *= α
|
||||
su.discharge_cost *= α
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
@@ -56,6 +56,21 @@ function slice(
|
||||
ps.demand = ps.demand[range]
|
||||
ps.revenue = ps.revenue[range]
|
||||
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
|
||||
return modified
|
||||
end
|
||||
|
||||
@@ -334,6 +334,195 @@ function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01)
|
||||
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
|
||||
return err_count
|
||||
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)
|
||||
ps_load = 0
|
||||
production = 0
|
||||
storage_charge = 0
|
||||
storage_discharge = 0
|
||||
if length(sc.price_sensitive_loads) > 0
|
||||
ps_load = sum(
|
||||
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
|
||||
)
|
||||
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)
|
||||
load_curtail = sum(
|
||||
solution[sc.name]["Load curtail (MW)"][b.name][t] for
|
||||
b in sc.buses
|
||||
)
|
||||
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
|
||||
if abs(balance) > tol
|
||||
@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,
|
||||
fixed_load,
|
||||
ps_load,
|
||||
load_curtail,
|
||||
production,
|
||||
storage_charge,
|
||||
storage_discharge,
|
||||
)
|
||||
err_count += 1
|
||||
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]
|
||||
@test length(sc.profiled_units) == 2
|
||||
|
||||
first_pu = sc.profiled_units[1]
|
||||
@test first_pu.name == "g7"
|
||||
@test first_pu.bus.name == "b4"
|
||||
@test first_pu.cost == [100.0 for t in 1:4]
|
||||
@test first_pu.min_power == [60.0 for t in 1:4]
|
||||
@test first_pu.max_power == [100.0 for t in 1:4]
|
||||
pu1 = sc.profiled_units[1]
|
||||
@test pu1.name == "g7"
|
||||
@test pu1.bus.name == "b4"
|
||||
@test pu1.cost == [100.0 for t in 1:4]
|
||||
@test pu1.min_power == [60.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"
|
||||
|
||||
second_pu = sc.profiled_units[2]
|
||||
@test second_pu.name == "g8"
|
||||
@test second_pu.bus.name == "b5"
|
||||
@test second_pu.cost == [50.0 for t in 1:4]
|
||||
@test second_pu.min_power == [0.0 for t in 1:4]
|
||||
@test second_pu.max_power == [120.0 for t in 1:4]
|
||||
pu2 = sc.profiled_units[2]
|
||||
@test pu2.name == "g8"
|
||||
@test pu2.bus.name == "b5"
|
||||
@test pu2.cost == [50.0 for t in 1:4]
|
||||
@test pu2.min_power == [0.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"
|
||||
end
|
||||
|
||||
@@ -166,4 +166,64 @@ function instance_read_test()
|
||||
@test sc.thermal_units[6].commitment_status ==
|
||||
[false, nothing, true, nothing]
|
||||
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
|
||||
|
||||
@@ -102,5 +102,28 @@ function transform_randomize_XavQiuAhm2021_test()
|
||||
test_approx(pu1.cost[1], 98.039)
|
||||
test_approx(pu2.cost[1], 48.385)
|
||||
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
|
||||
|
||||
@@ -65,4 +65,34 @@ function transform_slice_test()
|
||||
variable_names = true,
|
||||
)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user