Merge remote-tracking branch 'upstream/dev' into dev

pull/26/head
Jun He 3 years ago
commit 30a4284119

@ -4,6 +4,8 @@
module UnitCommitment module UnitCommitment
using Base: String
include("instance/structs.jl") include("instance/structs.jl")
include("model/formulations/base/structs.jl") include("model/formulations/base/structs.jl")
include("solution/structs.jl") include("solution/structs.jl")

@ -43,26 +43,76 @@ function read_benchmark(
return UnitCommitment.read(filename) return UnitCommitment.read(filename)
end end
function _repair_scenario_names_and_probabilities!(
scenarios::Vector{UnitCommitmentScenario},
path::Vector{String},
)::Nothing
total_weight = sum([sc.probability for sc in scenarios])
for (sc_path, sc) in zip(path, scenarios)
sc.name !== "" ||
(sc.name = first(split(last(split(sc_path, "/")), ".")))
sc.probability = (sc.probability / total_weight)
end
return
end
""" """
read(path::AbstractString)::UnitCommitmentInstance read(path::AbstractString)::UnitCommitmentInstance
Read instance from a file. The file may be gzipped. Read a deterministic test case from the given file. The file may be gzipped.
# Example
```julia
instance = UnitCommitment.read("s1.json.gz")
```
"""
function read(path::String)::UnitCommitmentInstance
scenarios = Vector{UnitCommitmentScenario}()
scenario = _read_scenario(path)
scenario.name = "s1"
scenario.probability = 1.0
scenarios = [scenario]
instance =
UnitCommitmentInstance(time = scenario.time, scenarios = scenarios)
return instance
end
"""
read(path::Vector{String})::UnitCommitmentInstance
Read a stochastic unit commitment instance from the given files. Each file
describes a scenario. The files may be gzipped.
# Example # Example
```julia ```julia
instance = UnitCommitment.read("/path/to/input.json.gz") instance = UnitCommitment.read(["s1.json.gz", "s2.json.gz"])
``` ```
""" """
function read(path::AbstractString)::UnitCommitmentInstance function read(paths::Vector{String})::UnitCommitmentInstance
scenarios = UnitCommitmentScenario[]
for p in paths
push!(scenarios, _read_scenario(p))
end
_repair_scenario_names_and_probabilities!(scenarios, paths)
instance =
UnitCommitmentInstance(time = scenarios[1].time, scenarios = scenarios)
return instance
end
function _read_scenario(path::String)::UnitCommitmentScenario
if endswith(path, ".gz") if endswith(path, ".gz")
return _read(gzopen(path)) scenario = _read(gzopen(path))
elseif endswith(path, ".json")
scenario = _read(open(path))
else else
return _read(open(path)) error("Unsupported input format")
end end
return scenario
end end
function _read(file::IO)::UnitCommitmentInstance function _read(file::IO)::UnitCommitmentScenario
return _from_json( return _from_json(
JSON.parse(file, dicttype = () -> DefaultOrderedDict(nothing)), JSON.parse(file, dicttype = () -> DefaultOrderedDict(nothing)),
) )
@ -77,7 +127,7 @@ function _read_json(path::String)::OrderedDict
return JSON.parse(file, dicttype = () -> DefaultOrderedDict(nothing)) return JSON.parse(file, dicttype = () -> DefaultOrderedDict(nothing))
end end
function _from_json(json; repair = true) function _from_json(json; repair = true)::UnitCommitmentScenario
_migrate(json) _migrate(json)
units = Unit[] units = Unit[]
buses = Bus[] buses = Bus[]
@ -102,6 +152,11 @@ function _from_json(json; repair = true)
time_multiplier = 60 ÷ time_step time_multiplier = 60 ÷ time_step
T = time_horizon * time_multiplier T = time_horizon * time_multiplier
probability = json["Parameters"]["Scenario weight"]
probability !== nothing || (probability = 1)
scenario_name = json["Parameters"]["Scenario name"]
scenario_name !== nothing || (scenario_name = "")
name_to_bus = Dict{String,Bus}() name_to_bus = Dict{String,Bus}()
name_to_line = Dict{String,TransmissionLine}() name_to_line = Dict{String,TransmissionLine}()
name_to_unit = Dict{String,Unit}() name_to_unit = Dict{String,Unit}()
@ -118,15 +173,6 @@ function _from_json(json; repair = true)
json["Parameters"]["Power balance penalty (\$/MW)"], json["Parameters"]["Power balance penalty (\$/MW)"],
default = [1000.0 for t in 1:T], default = [1000.0 for t in 1:T],
) )
# Penalty price for shortage in meeting system-wide flexiramp requirements
flexiramp_shortfall_penalty = timeseries(
json["Parameters"]["Flexiramp penalty (\$/MW)"],
default = [500.0 for t in 1:T],
)
shortfall_penalty = timeseries(
json["Parameters"]["Reserve shortfall penalty (\$/MW)"],
default = [-1.0 for t in 1:T],
)
# Read buses # Read buses
for (bus_name, dict) in json["Buses"] for (bus_name, dict) in json["Buses"]
@ -308,7 +354,9 @@ function _from_json(json; repair = true)
end end
end end
instance = UnitCommitmentInstance( scenario = UnitCommitmentScenario(
name = scenario_name,
probability = probability,
buses_by_name = Dict(b.name => b for b in buses), buses_by_name = Dict(b.name => b for b in buses),
buses = buses, buses = buses,
contingencies_by_name = Dict(c.name => c for c in contingencies), contingencies_by_name = Dict(c.name => c for c in contingencies),
@ -320,14 +368,14 @@ function _from_json(json; repair = true)
price_sensitive_loads = loads, price_sensitive_loads = loads,
reserves = reserves, reserves = reserves,
reserves_by_name = name_to_reserve, reserves_by_name = name_to_reserve,
shortfall_penalty = shortfall_penalty,
flexiramp_shortfall_penalty = flexiramp_shortfall_penalty,
time = T, time = T,
units_by_name = Dict(g.name => g for g in units), units_by_name = Dict(g.name => g for g in units),
units = units, units = units,
isf = spzeros(Float64, length(lines), length(buses) - 1),
lodf = spzeros(Float64, length(lines), length(lines)),
) )
if repair if repair
UnitCommitment.repair!(instance) UnitCommitment.repair!(scenario)
end end
return instance return scenario
end end

@ -73,35 +73,41 @@ mutable struct PriceSensitiveLoad
revenue::Vector{Float64} revenue::Vector{Float64}
end end
Base.@kwdef mutable struct UnitCommitmentInstance Base.@kwdef mutable struct UnitCommitmentScenario
buses_by_name::Dict{AbstractString,Bus} buses_by_name::Dict{AbstractString,Bus}
buses::Vector{Bus} buses::Vector{Bus}
contingencies_by_name::Dict{AbstractString,Contingency} contingencies_by_name::Dict{AbstractString,Contingency}
contingencies::Vector{Contingency} contingencies::Vector{Contingency}
isf::Array{Float64,2}
lines_by_name::Dict{AbstractString,TransmissionLine} lines_by_name::Dict{AbstractString,TransmissionLine}
lines::Vector{TransmissionLine} lines::Vector{TransmissionLine}
lodf::Array{Float64,2}
name::String
power_balance_penalty::Vector{Float64} power_balance_penalty::Vector{Float64}
price_sensitive_loads_by_name::Dict{AbstractString,PriceSensitiveLoad} price_sensitive_loads_by_name::Dict{AbstractString,PriceSensitiveLoad}
price_sensitive_loads::Vector{PriceSensitiveLoad} price_sensitive_loads::Vector{PriceSensitiveLoad}
reserves::Vector{Reserve} probability::Float64
reserves_by_name::Dict{AbstractString,Reserve} reserves_by_name::Dict{AbstractString,Reserve}
shortfall_penalty::Vector{Float64} reserves::Vector{Reserve}
flexiramp_shortfall_penalty::Vector{Float64}
time::Int time::Int
units_by_name::Dict{AbstractString,Unit} units_by_name::Dict{AbstractString,Unit}
units::Vector{Unit} units::Vector{Unit}
end end
Base.@kwdef mutable struct UnitCommitmentInstance
time::Int
scenarios::Vector{UnitCommitmentScenario}
end
function Base.show(io::IO, instance::UnitCommitmentInstance) function Base.show(io::IO, instance::UnitCommitmentInstance)
sc = instance.scenarios[1]
print(io, "UnitCommitmentInstance(") print(io, "UnitCommitmentInstance(")
print(io, "$(length(instance.units)) units, ") print(io, "$(length(instance.scenarios)) scenarios, ")
print(io, "$(length(instance.buses)) buses, ") print(io, "$(length(sc.units)) units, ")
print(io, "$(length(instance.lines)) lines, ") print(io, "$(length(sc.buses)) buses, ")
print(io, "$(length(instance.contingencies)) contingencies, ") print(io, "$(length(sc.lines)) lines, ")
print( print(io, "$(length(sc.contingencies)) contingencies, ")
io, print(io, "$(length(sc.price_sensitive_loads)) price sensitive loads, ")
"$(length(instance.price_sensitive_loads)) price sensitive loads, ",
)
print(io, "$(instance.time) time steps") print(io, "$(instance.time) time steps")
print(io, ")") print(io, ")")
return return

@ -77,20 +77,27 @@ function build_model(;
end end
model[:obj] = AffExpr() model[:obj] = AffExpr()
model[:instance] = instance model[:instance] = instance
_setup_transmission(model, formulation.transmission) for g in instance.scenarios[1].units
for l in instance.lines _add_unit_commitment!(model, g, formulation)
_add_transmission_line!(model, l, formulation.transmission)
end end
for b in instance.buses for sc in instance.scenarios
_add_bus!(model, b) @info "Building scenario $(sc.name) with " *
"probability $(sc.probability)"
_setup_transmission(formulation.transmission, sc)
for l in sc.lines
_add_transmission_line!(model, l, formulation.transmission, sc)
end end
for g in instance.units for b in sc.buses
_add_unit!(model, g, formulation) _add_bus!(model, b, sc)
end end
for ps in instance.price_sensitive_loads for ps in sc.price_sensitive_loads
_add_price_sensitive_load!(model, ps) _add_price_sensitive_load!(model, ps, sc)
end
for g in sc.units
_add_unit_dispatch!(model, g, formulation, sc)
end
_add_system_wide_eqs!(model, sc)
end end
_add_system_wide_eqs!(model)
@objective(model, Min, model[:obj]) @objective(model, Min, model[:obj])
end end
@info @sprintf("Built model in %.2f seconds", time_model) @info @sprintf("Built model in %.2f seconds", time_model)

@ -8,6 +8,7 @@ function _add_ramp_eqs!(
formulation_prod_vars::Gar1962.ProdVars, formulation_prod_vars::Gar1962.ProdVars,
formulation_ramping::ArrCon2000.Ramping, formulation_ramping::ArrCon2000.Ramping,
formulation_status_vars::Gar1962.StatusVars, formulation_status_vars::Gar1962.StatusVars,
sc::UnitCommitmentScenario,
)::Nothing )::Nothing
# TODO: Move upper case constants to model[:instance] # TODO: Move upper case constants to model[:instance]
RESERVES_WHEN_START_UP = true RESERVES_WHEN_START_UP = true
@ -22,7 +23,7 @@ function _add_ramp_eqs!(
eq_ramp_down = _init(model, :eq_ramp_down) eq_ramp_down = _init(model, :eq_ramp_down)
eq_ramp_up = _init(model, :eq_ramp_up) eq_ramp_up = _init(model, :eq_ramp_up)
is_initially_on = (g.initial_status > 0) is_initially_on = (g.initial_status > 0)
reserve = _total_reserves(model, g) reserve = _total_reserves(model, g, sc)
# Gar1962.ProdVars # Gar1962.ProdVars
prod_above = model[:prod_above] prod_above = model[:prod_above]
@ -37,10 +38,10 @@ function _add_ramp_eqs!(
if t == 1 if t == 1
if is_initially_on if is_initially_on
# min power is _not_ multiplied by is_on because if !is_on, then ramp up is irrelevant # min power is _not_ multiplied by is_on because if !is_on, then ramp up is irrelevant
eq_ramp_up[gn, t] = @constraint( eq_ramp_up[sc.name, gn, t] = @constraint(
model, model,
g.min_power[t] + g.min_power[t] +
prod_above[gn, t] + prod_above[sc.name, gn, t] +
(RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) <= (RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) <=
g.initial_power + RU g.initial_power + RU
) )
@ -48,16 +49,16 @@ function _add_ramp_eqs!(
else else
max_prod_this_period = max_prod_this_period =
g.min_power[t] * is_on[gn, t] + g.min_power[t] * is_on[gn, t] +
prod_above[gn, t] + prod_above[sc.name, gn, t] +
( (
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ? RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ?
reserve[t] : 0.0 reserve[t] : 0.0
) )
min_prod_last_period = min_prod_last_period =
g.min_power[t-1] * is_on[gn, t-1] + prod_above[gn, t-1] g.min_power[t-1] * is_on[gn, t-1] + prod_above[sc.name, gn, t-1]
# Equation (24) in Kneuven et al. (2020) # Equation (24) in Kneuven et al. (2020)
eq_ramp_up[gn, t] = @constraint( eq_ramp_up[sc.name, gn, t] = @constraint(
model, model,
max_prod_this_period - min_prod_last_period <= max_prod_this_period - min_prod_last_period <=
RU * is_on[gn, t-1] + SU * switch_on[gn, t] RU * is_on[gn, t-1] + SU * switch_on[gn, t]
@ -71,24 +72,25 @@ function _add_ramp_eqs!(
# min_power + RD < initial_power < SD # min_power + RD < initial_power < SD
# then the generator should be able to shut down at time t = 1, # then the generator should be able to shut down at time t = 1,
# but the constraint below will force the unit to produce power # but the constraint below will force the unit to produce power
eq_ramp_down[gn, t] = @constraint( eq_ramp_down[sc.name, gn, t] = @constraint(
model, model,
g.initial_power - (g.min_power[t] + prod_above[gn, t]) <= RD g.initial_power -
(g.min_power[t] + prod_above[sc.name, gn, t]) <= RD
) )
end end
else else
max_prod_last_period = max_prod_last_period =
g.min_power[t-1] * is_on[gn, t-1] + g.min_power[t-1] * is_on[gn, t-1] +
prod_above[gn, t-1] + prod_above[sc.name, gn, t-1] +
( (
RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ? RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ?
reserve[t-1] : 0.0 reserve[t-1] : 0.0
) )
min_prod_this_period = min_prod_this_period =
g.min_power[t] * is_on[gn, t] + prod_above[gn, t] g.min_power[t] * is_on[gn, t] + prod_above[sc.name, gn, t]
# Equation (25) in Kneuven et al. (2020) # Equation (25) in Kneuven et al. (2020)
eq_ramp_down[gn, t] = @constraint( eq_ramp_down[sc.name, gn, t] = @constraint(
model, model,
max_prod_last_period - min_prod_this_period <= max_prod_last_period - min_prod_this_period <=
RD * is_on[gn, t] + SD * switch_off[gn, t] RD * is_on[gn, t] + SD * switch_off[gn, t]

@ -8,6 +8,7 @@ function _add_production_piecewise_linear_eqs!(
formulation_prod_vars::Gar1962.ProdVars, formulation_prod_vars::Gar1962.ProdVars,
formulation_pwl_costs::CarArr2006.PwlCosts, formulation_pwl_costs::CarArr2006.PwlCosts,
formulation_status_vars::StatusVarsFormulation, formulation_status_vars::StatusVarsFormulation,
sc::UnitCommitmentScenario,
)::Nothing )::Nothing
eq_prod_above_def = _init(model, :eq_prod_above_def) eq_prod_above_def = _init(model, :eq_prod_above_def)
eq_segprod_limit = _init(model, :eq_segprod_limit) eq_segprod_limit = _init(model, :eq_segprod_limit)
@ -26,28 +27,32 @@ function _add_production_piecewise_linear_eqs!(
# difference between max power for segments k and k-1 so the # difference between max power for segments k and k-1 so the
# value of cost_segments[k].mw[t] is the max production *for # value of cost_segments[k].mw[t] is the max production *for
# that segment* # that segment*
eq_segprod_limit[gn, t, k] = @constraint( eq_segprod_limit[sc.name, gn, t, k] = @constraint(
model, model,
segprod[gn, t, k] <= g.cost_segments[k].mw[t] segprod[sc.name, gn, t, k] <= g.cost_segments[k].mw[t]
) )
# Also add this as an explicit upper bound on segprod to make the # Also add this as an explicit upper bound on segprod to make the
# solver's work a bit easier # solver's work a bit easier
set_upper_bound(segprod[gn, t, k], g.cost_segments[k].mw[t]) set_upper_bound(
segprod[sc.name, gn, t, k],
g.cost_segments[k].mw[t],
)
# Definition of production # Definition of production
# Equation (43) in Kneuven et al. (2020) # Equation (43) in Kneuven et al. (2020)
eq_prod_above_def[gn, t] = @constraint( eq_prod_above_def[sc.name, gn, t] = @constraint(
model, model,
prod_above[gn, t] == sum(segprod[gn, t, k] for k in 1:K) prod_above[sc.name, gn, t] ==
sum(segprod[sc.name, gn, t, k] for k in 1:K)
) )
# Objective function # Objective function
# Equation (44) in Kneuven et al. (2020) # Equation (44) in Kneuven et al. (2020)
add_to_expression!( add_to_expression!(
model[:obj], model[:obj],
segprod[gn, t, k], segprod[sc.name, gn, t, k],
g.cost_segments[k].cost[t], sc.probability * g.cost_segments[k].cost[t],
) )
end end
end end

@ -8,6 +8,7 @@ function _add_ramp_eqs!(
formulation_prod_vars::Gar1962.ProdVars, formulation_prod_vars::Gar1962.ProdVars,
formulation_ramping::DamKucRajAta2016.Ramping, formulation_ramping::DamKucRajAta2016.Ramping,
formulation_status_vars::Gar1962.StatusVars, formulation_status_vars::Gar1962.StatusVars,
sc::UnitCommitmentScenario,
)::Nothing )::Nothing
# TODO: Move upper case constants to model[:instance] # TODO: Move upper case constants to model[:instance]
RESERVES_WHEN_START_UP = true RESERVES_WHEN_START_UP = true
@ -23,7 +24,7 @@ function _add_ramp_eqs!(
gn = g.name gn = g.name
eq_str_ramp_down = _init(model, :eq_str_ramp_down) eq_str_ramp_down = _init(model, :eq_str_ramp_down)
eq_str_ramp_up = _init(model, :eq_str_ramp_up) eq_str_ramp_up = _init(model, :eq_str_ramp_up)
reserve = _total_reserves(model, g) reserve = _total_reserves(model, g, sc)
# Gar1962.ProdVars # Gar1962.ProdVars
prod_above = model[:prod_above] prod_above = model[:prod_above]
@ -48,15 +49,15 @@ function _add_ramp_eqs!(
# end # end
max_prod_this_period = max_prod_this_period =
prod_above[gn, t] + prod_above[sc.name, gn, t] +
(RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) (RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0)
min_prod_last_period = 0.0 min_prod_last_period = 0.0
if t > 1 && time_invariant if t > 1 && time_invariant
min_prod_last_period = prod_above[gn, t-1] min_prod_last_period = prod_above[sc.name, gn, t-1]
# Equation (35) in Kneuven et al. (2020) # Equation (35) in Kneuven et al. (2020)
# Sparser version of (24) # Sparser version of (24)
eq_str_ramp_up[gn, t] = @constraint( eq_str_ramp_up[sc.name, gn, t] = @constraint(
model, model,
max_prod_this_period - min_prod_last_period <= max_prod_this_period - min_prod_last_period <=
(SU - g.min_power[t] - RU) * switch_on[gn, t] + (SU - g.min_power[t] - RU) * switch_on[gn, t] +
@ -65,7 +66,8 @@ function _add_ramp_eqs!(
elseif (t == 1 && is_initially_on) || (t > 1 && !time_invariant) elseif (t == 1 && is_initially_on) || (t > 1 && !time_invariant)
if t > 1 if t > 1
min_prod_last_period = min_prod_last_period =
prod_above[gn, t-1] + g.min_power[t-1] * is_on[gn, t-1] prod_above[sc.name, gn, t-1] +
g.min_power[t-1] * is_on[gn, t-1]
else else
min_prod_last_period = max(g.initial_power, 0.0) min_prod_last_period = max(g.initial_power, 0.0)
end end
@ -76,7 +78,7 @@ function _add_ramp_eqs!(
# Modified version of equation (35) in Kneuven et al. (2020) # Modified version of equation (35) in Kneuven et al. (2020)
# Equivalent to (24) # Equivalent to (24)
eq_str_ramp_up[gn, t] = @constraint( eq_str_ramp_up[sc.name, gn, t] = @constraint(
model, model,
max_prod_this_period - min_prod_last_period <= max_prod_this_period - min_prod_last_period <=
(SU - RU) * switch_on[gn, t] + RU * is_on[gn, t] (SU - RU) * switch_on[gn, t] + RU * is_on[gn, t]
@ -88,7 +90,7 @@ function _add_ramp_eqs!(
t > 1 && (RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN) ? t > 1 && (RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN) ?
reserve[t-1] : 0.0 reserve[t-1] : 0.0
) )
min_prod_this_period = prod_above[gn, t] min_prod_this_period = prod_above[sc.name, gn, t]
on_last_period = 0.0 on_last_period = 0.0
if t > 1 if t > 1
on_last_period = is_on[gn, t-1] on_last_period = is_on[gn, t-1]
@ -98,7 +100,7 @@ function _add_ramp_eqs!(
if t > 1 && time_invariant if t > 1 && time_invariant
# Equation (36) in Kneuven et al. (2020) # Equation (36) in Kneuven et al. (2020)
eq_str_ramp_down[gn, t] = @constraint( eq_str_ramp_down[sc.name, gn, t] = @constraint(
model, model,
max_prod_last_period - min_prod_this_period <= max_prod_last_period - min_prod_this_period <=
(SD - g.min_power[t] - RD) * switch_off[gn, t] + (SD - g.min_power[t] - RD) * switch_off[gn, t] +
@ -110,7 +112,7 @@ function _add_ramp_eqs!(
# Modified version of equation (36) in Kneuven et al. (2020) # Modified version of equation (36) in Kneuven et al. (2020)
# Equivalent to (25) # Equivalent to (25)
eq_str_ramp_down[gn, t] = @constraint( eq_str_ramp_down[sc.name, gn, t] = @constraint(
model, model,
max_prod_last_period - min_prod_this_period <= max_prod_last_period - min_prod_this_period <=
(SD - RD) * switch_off[gn, t] + RD * on_last_period (SD - RD) * switch_off[gn, t] + RD * on_last_period

@ -6,14 +6,15 @@ function _add_production_vars!(
model::JuMP.Model, model::JuMP.Model,
g::Unit, g::Unit,
formulation_prod_vars::Gar1962.ProdVars, formulation_prod_vars::Gar1962.ProdVars,
sc::UnitCommitmentScenario,
)::Nothing )::Nothing
prod_above = _init(model, :prod_above) prod_above = _init(model, :prod_above)
segprod = _init(model, :segprod) segprod = _init(model, :segprod)
for t in 1:model[:instance].time for t in 1:model[:instance].time
for k in 1:length(g.cost_segments) for k in 1:length(g.cost_segments)
segprod[g.name, t, k] = @variable(model, lower_bound = 0) segprod[sc.name, g.name, t, k] = @variable(model, lower_bound = 0)
end end
prod_above[g.name, t] = @variable(model, lower_bound = 0) prod_above[sc.name, g.name, t] = @variable(model, lower_bound = 0)
end end
return return
end end
@ -22,16 +23,16 @@ function _add_production_limit_eqs!(
model::JuMP.Model, model::JuMP.Model,
g::Unit, g::Unit,
formulation_prod_vars::Gar1962.ProdVars, formulation_prod_vars::Gar1962.ProdVars,
sc::UnitCommitmentScenario,
)::Nothing )::Nothing
eq_prod_limit = _init(model, :eq_prod_limit) eq_prod_limit = _init(model, :eq_prod_limit)
is_on = model[:is_on] is_on = model[:is_on]
prod_above = model[:prod_above] prod_above = model[:prod_above]
reserve = _total_reserves(model, g) reserve = _total_reserves(model, g, sc)
gn = g.name gn = g.name
for t in 1:model[:instance].time for t in 1:model[:instance].time
# Objective function terms for production costs # Objective function terms for production costs
# Part of (69) of Kneuven et al. (2020) as C^R_g * u_g(t) term # Part of (69) of Kneuven et al. (2020) as C^R_g * u_g(t) term
add_to_expression!(model[:obj], is_on[gn, t], g.min_power_cost[t])
# Production limit # Production limit
# Equation (18) in Kneuven et al. (2020) # Equation (18) in Kneuven et al. (2020)
@ -42,9 +43,10 @@ function _add_production_limit_eqs!(
if power_diff < 1e-7 if power_diff < 1e-7
power_diff = 0.0 power_diff = 0.0
end end
eq_prod_limit[gn, t] = @constraint( eq_prod_limit[sc.name, gn, t] = @constraint(
model, model,
prod_above[gn, t] + reserve[t] <= power_diff * is_on[gn, t] prod_above[sc.name, gn, t] + reserve[t] <=
power_diff * is_on[gn, t]
) )
end end
end end

@ -8,6 +8,7 @@ function _add_production_piecewise_linear_eqs!(
formulation_prod_vars::Gar1962.ProdVars, formulation_prod_vars::Gar1962.ProdVars,
formulation_pwl_costs::Gar1962.PwlCosts, formulation_pwl_costs::Gar1962.PwlCosts,
formulation_status_vars::Gar1962.StatusVars, formulation_status_vars::Gar1962.StatusVars,
sc::UnitCommitmentScenario,
)::Nothing )::Nothing
eq_prod_above_def = _init(model, :eq_prod_above_def) eq_prod_above_def = _init(model, :eq_prod_above_def)
eq_segprod_limit = _init(model, :eq_segprod_limit) eq_segprod_limit = _init(model, :eq_segprod_limit)
@ -24,9 +25,10 @@ function _add_production_piecewise_linear_eqs!(
for t in 1:model[:instance].time for t in 1:model[:instance].time
# Definition of production # Definition of production
# Equation (43) in Kneuven et al. (2020) # Equation (43) in Kneuven et al. (2020)
eq_prod_above_def[gn, t] = @constraint( eq_prod_above_def[sc.name, gn, t] = @constraint(
model, model,
prod_above[gn, t] == sum(segprod[gn, t, k] for k in 1:K) prod_above[sc.name, gn, t] ==
sum(segprod[sc.name, gn, t, k] for k in 1:K)
) )
for k in 1:K for k in 1:K
@ -37,21 +39,25 @@ function _add_production_piecewise_linear_eqs!(
# difference between max power for segments k and k-1 so the # difference between max power for segments k and k-1 so the
# value of cost_segments[k].mw[t] is the max production *for # value of cost_segments[k].mw[t] is the max production *for
# that segment* # that segment*
eq_segprod_limit[gn, t, k] = @constraint( eq_segprod_limit[sc.name, gn, t, k] = @constraint(
model, model,
segprod[gn, t, k] <= g.cost_segments[k].mw[t] * is_on[gn, t] segprod[sc.name, gn, t, k] <=
g.cost_segments[k].mw[t] * is_on[gn, t]
) )
# Also add this as an explicit upper bound on segprod to make the # Also add this as an explicit upper bound on segprod to make the
# solver's work a bit easier # solver's work a bit easier
set_upper_bound(segprod[gn, t, k], g.cost_segments[k].mw[t]) set_upper_bound(
segprod[sc.name, gn, t, k],
g.cost_segments[k].mw[t],
)
# Objective function # Objective function
# Equation (44) in Kneuven et al. (2020) # Equation (44) in Kneuven et al. (2020)
add_to_expression!( add_to_expression!(
model[:obj], model[:obj],
segprod[gn, t, k], segprod[sc.name, gn, t, k],
g.cost_segments[k].cost[t], sc.probability * g.cost_segments[k].cost[t],
) )
end end
end end

@ -20,6 +20,7 @@ function _add_status_vars!(
switch_on[g.name, t] = @variable(model, binary = true) switch_on[g.name, t] = @variable(model, binary = true)
switch_off[g.name, t] = @variable(model, binary = true) switch_off[g.name, t] = @variable(model, binary = true)
end end
add_to_expression!(model[:obj], is_on[g.name, t], g.min_power_cost[t])
end end
return return
end end

@ -8,6 +8,7 @@ function _add_production_piecewise_linear_eqs!(
formulation_prod_vars::Gar1962.ProdVars, formulation_prod_vars::Gar1962.ProdVars,
formulation_pwl_costs::KnuOstWat2018.PwlCosts, formulation_pwl_costs::KnuOstWat2018.PwlCosts,
formulation_status_vars::Gar1962.StatusVars, formulation_status_vars::Gar1962.StatusVars,
sc::UnitCommitmentScenario,
)::Nothing )::Nothing
eq_prod_above_def = _init(model, :eq_prod_above_def) eq_prod_above_def = _init(model, :eq_prod_above_def)
eq_segprod_limit_a = _init(model, :eq_segprod_limit_a) eq_segprod_limit_a = _init(model, :eq_segprod_limit_a)
@ -58,27 +59,27 @@ function _add_production_piecewise_linear_eqs!(
if g.min_uptime > 1 if g.min_uptime > 1
# Equation (46) in Kneuven et al. (2020) # Equation (46) in Kneuven et al. (2020)
eq_segprod_limit_a[gn, t, k] = @constraint( eq_segprod_limit_a[sc.name, gn, t, k] = @constraint(
model, model,
segprod[gn, t, k] <= segprod[sc.name, gn, t, k] <=
g.cost_segments[k].mw[t] * is_on[gn, t] - g.cost_segments[k].mw[t] * is_on[gn, t] -
Cv * switch_on[gn, t] - Cv * switch_on[gn, t] -
(t < T ? Cw * switch_off[gn, t+1] : 0.0) (t < T ? Cw * switch_off[gn, t+1] : 0.0)
) )
else else
# Equation (47a)/(48a) in Kneuven et al. (2020) # Equation (47a)/(48a) in Kneuven et al. (2020)
eq_segprod_limit_b[gn, t, k] = @constraint( eq_segprod_limit_b[sc.name, gn, t, k] = @constraint(
model, model,
segprod[gn, t, k] <= segprod[sc.name, gn, t, k] <=
g.cost_segments[k].mw[t] * is_on[gn, t] - g.cost_segments[k].mw[t] * is_on[gn, t] -
Cv * switch_on[gn, t] - Cv * switch_on[gn, t] -
(t < T ? max(0, Cv - Cw) * switch_off[gn, t+1] : 0.0) (t < T ? max(0, Cv - Cw) * switch_off[gn, t+1] : 0.0)
) )
# Equation (47b)/(48b) in Kneuven et al. (2020) # Equation (47b)/(48b) in Kneuven et al. (2020)
eq_segprod_limit_c[gn, t, k] = @constraint( eq_segprod_limit_c[sc.name, gn, t, k] = @constraint(
model, model,
segprod[gn, t, k] <= segprod[sc.name, gn, t, k] <=
g.cost_segments[k].mw[t] * is_on[gn, t] - g.cost_segments[k].mw[t] * is_on[gn, t] -
max(0, Cw - Cv) * switch_on[gn, t] - max(0, Cw - Cv) * switch_on[gn, t] -
(t < T ? Cw * switch_off[gn, t+1] : 0.0) (t < T ? Cw * switch_off[gn, t+1] : 0.0)
@ -87,22 +88,26 @@ function _add_production_piecewise_linear_eqs!(
# Definition of production # Definition of production
# Equation (43) in Kneuven et al. (2020) # Equation (43) in Kneuven et al. (2020)
eq_prod_above_def[gn, t] = @constraint( eq_prod_above_def[sc.name, gn, t] = @constraint(
model, model,
prod_above[gn, t] == sum(segprod[gn, t, k] for k in 1:K) prod_above[sc.name, gn, t] ==
sum(segprod[sc.name, gn, t, k] for k in 1:K)
) )
# Objective function # Objective function
# Equation (44) in Kneuven et al. (2020) # Equation (44) in Kneuven et al. (2020)
add_to_expression!( add_to_expression!(
model[:obj], model[:obj],
segprod[gn, t, k], segprod[sc.name, gn, t, k],
g.cost_segments[k].cost[t], g.cost_segments[k].cost[t],
) )
# Also add an explicit upper bound on segprod to make the solver's # Also add an explicit upper bound on segprod to make the solver's
# work a bit easier # work a bit easier
set_upper_bound(segprod[gn, t, k], g.cost_segments[k].mw[t]) set_upper_bound(
segprod[sc.name, gn, t, k],
g.cost_segments[k].mw[t],
)
end end
end end
end end

@ -8,6 +8,7 @@ function _add_ramp_eqs!(
formulation_prod_vars::Gar1962.ProdVars, formulation_prod_vars::Gar1962.ProdVars,
formulation_ramping::MorLatRam2013.Ramping, formulation_ramping::MorLatRam2013.Ramping,
formulation_status_vars::Gar1962.StatusVars, formulation_status_vars::Gar1962.StatusVars,
sc::UnitCommitmentScenario,
)::Nothing )::Nothing
# TODO: Move upper case constants to model[:instance] # TODO: Move upper case constants to model[:instance]
RESERVES_WHEN_START_UP = true RESERVES_WHEN_START_UP = true
@ -22,7 +23,7 @@ function _add_ramp_eqs!(
gn = g.name gn = g.name
eq_ramp_down = _init(model, :eq_ramp_down) eq_ramp_down = _init(model, :eq_ramp_down)
eq_ramp_up = _init(model, :eq_str_ramp_up) eq_ramp_up = _init(model, :eq_str_ramp_up)
reserve = _total_reserves(model, g) reserve = _total_reserves(model, g, sc)
# Gar1962.ProdVars # Gar1962.ProdVars
prod_above = model[:prod_above] prod_above = model[:prod_above]
@ -39,10 +40,10 @@ function _add_ramp_eqs!(
# Ramp up limit # Ramp up limit
if t == 1 if t == 1
if is_initially_on if is_initially_on
eq_ramp_up[gn, t] = @constraint( eq_ramp_up[sc.name, gn, t] = @constraint(
model, model,
g.min_power[t] + g.min_power[t] +
prod_above[gn, t] + prod_above[sc.name, gn, t] +
(RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) <= (RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) <=
g.initial_power + RU g.initial_power + RU
) )
@ -58,13 +59,14 @@ function _add_ramp_eqs!(
SU = g.startup_limit SU = g.startup_limit
max_prod_this_period = max_prod_this_period =
g.min_power[t] * is_on[gn, t] + g.min_power[t] * is_on[gn, t] +
prod_above[gn, t] + prod_above[sc.name, gn, t] +
( (
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ? RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ?
reserve[t] : 0.0 reserve[t] : 0.0
) )
min_prod_last_period = min_prod_last_period =
g.min_power[t-1] * is_on[gn, t-1] + prod_above[gn, t-1] g.min_power[t-1] * is_on[gn, t-1] +
prod_above[sc.name, gn, t-1]
eq_ramp_up[gn, t] = @constraint( eq_ramp_up[gn, t] = @constraint(
model, model,
max_prod_this_period - min_prod_last_period <= max_prod_this_period - min_prod_last_period <=
@ -74,11 +76,11 @@ function _add_ramp_eqs!(
# Equation (26) in Kneuven et al. (2020) # Equation (26) in Kneuven et al. (2020)
# TODO: what if RU < SU? places too stringent upper bound # TODO: what if RU < SU? places too stringent upper bound
# prod_above[gn, t] when starting up, and creates diff with (24). # prod_above[gn, t] when starting up, and creates diff with (24).
eq_ramp_up[gn, t] = @constraint( eq_ramp_up[sc.name, gn, t] = @constraint(
model, model,
prod_above[gn, t] + prod_above[sc.name, gn, t] +
(RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) - (RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) -
prod_above[gn, t-1] <= RU prod_above[sc.name, gn, t-1] <= RU
) )
end end
end end
@ -90,9 +92,10 @@ function _add_ramp_eqs!(
# min_power + RD < initial_power < SD # min_power + RD < initial_power < SD
# then the generator should be able to shut down at time t = 1, # then the generator should be able to shut down at time t = 1,
# but the constraint below will force the unit to produce power # but the constraint below will force the unit to produce power
eq_ramp_down[gn, t] = @constraint( eq_ramp_down[sc.name, gn, t] = @constraint(
model, model,
g.initial_power - (g.min_power[t] + prod_above[gn, t]) <= RD g.initial_power -
(g.min_power[t] + prod_above[sc.name, gn, t]) <= RD
) )
end end
else else
@ -102,13 +105,13 @@ function _add_ramp_eqs!(
SD = g.shutdown_limit SD = g.shutdown_limit
max_prod_last_period = max_prod_last_period =
g.min_power[t-1] * is_on[gn, t-1] + g.min_power[t-1] * is_on[gn, t-1] +
prod_above[gn, t-1] + prod_above[sc.name, gn, t-1] +
( (
RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ? RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ?
reserve[t-1] : 0.0 reserve[t-1] : 0.0
) )
min_prod_this_period = min_prod_this_period =
g.min_power[t] * is_on[gn, t] + prod_above[gn, t] g.min_power[t] * is_on[gn, t] + prod_above[sc.name, gn, t]
eq_ramp_down[gn, t] = @constraint( eq_ramp_down[gn, t] = @constraint(
model, model,
max_prod_last_period - min_prod_this_period <= max_prod_last_period - min_prod_this_period <=
@ -118,11 +121,11 @@ function _add_ramp_eqs!(
# Equation (27) in Kneuven et al. (2020) # Equation (27) in Kneuven et al. (2020)
# TODO: Similar to above, what to do if shutting down in time t # TODO: Similar to above, what to do if shutting down in time t
# and RD < SD? There is a difference with (25). # and RD < SD? There is a difference with (25).
eq_ramp_down[gn, t] = @constraint( eq_ramp_down[sc.name, gn, t] = @constraint(
model, model,
prod_above[gn, t-1] + prod_above[sc.name, gn, t-1] +
(RESERVES_WHEN_RAMP_DOWN ? reserve[t-1] : 0.0) - (RESERVES_WHEN_RAMP_DOWN ? reserve[t-1] : 0.0) -
prod_above[gn, t] <= RD prod_above[sc.name, gn, t] <= RD
) )
end end
end end

@ -8,11 +8,12 @@ function _add_ramp_eqs!(
formulation_prod_vars::Gar1962.ProdVars, formulation_prod_vars::Gar1962.ProdVars,
formulation_ramping::PanGua2016.Ramping, formulation_ramping::PanGua2016.Ramping,
formulation_status_vars::Gar1962.StatusVars, formulation_status_vars::Gar1962.StatusVars,
sc::UnitCommitmentScenario,
)::Nothing )::Nothing
# TODO: Move upper case constants to model[:instance] # TODO: Move upper case constants to model[:instance]
RESERVES_WHEN_SHUT_DOWN = true RESERVES_WHEN_SHUT_DOWN = true
gn = g.name gn = g.name
reserve = _total_reserves(model, g) reserve = _total_reserves(model, g, sc)
eq_str_prod_limit = _init(model, :eq_str_prod_limit) eq_str_prod_limit = _init(model, :eq_str_prod_limit)
eq_prod_limit_ramp_up_extra_period = eq_prod_limit_ramp_up_extra_period =
_init(model, :eq_prod_limit_ramp_up_extra_period) _init(model, :eq_prod_limit_ramp_up_extra_period)
@ -52,9 +53,9 @@ function _add_ramp_eqs!(
# Generalization of (20) # Generalization of (20)
# Necessary that if any of the switch_on = 1 in the sum, # Necessary that if any of the switch_on = 1 in the sum,
# then switch_off[gn, t+1] = 0 # then switch_off[gn, t+1] = 0
eq_str_prod_limit[gn, t] = @constraint( eq_str_prod_limit[sc.name, gn, t] = @constraint(
model, model,
prod_above[gn, t] + prod_above[sc.name, gn, t] +
g.min_power[t] * is_on[gn, t] + g.min_power[t] * is_on[gn, t] +
reserve[t] <= reserve[t] <=
Pbar * is_on[gn, t] - Pbar * is_on[gn, t] -
@ -67,9 +68,10 @@ function _add_ramp_eqs!(
if UT - 2 < TRU if UT - 2 < TRU
# Equation (40) in Kneuven et al. (2020) # Equation (40) in Kneuven et al. (2020)
# Covers an additional time period of the ramp-up trajectory, compared to (38) # Covers an additional time period of the ramp-up trajectory, compared to (38)
eq_prod_limit_ramp_up_extra_period[gn, t] = @constraint( eq_prod_limit_ramp_up_extra_period[sc.name, gn, t] =
@constraint(
model, model,
prod_above[gn, t] + prod_above[sc.name, gn, t] +
g.min_power[t] * is_on[gn, t] + g.min_power[t] * is_on[gn, t] +
reserve[t] <= reserve[t] <=
Pbar * is_on[gn, t] - sum( Pbar * is_on[gn, t] - sum(
@ -84,9 +86,9 @@ function _add_ramp_eqs!(
if KSD > 0 if KSD > 0
KSU = min(TRU, UT - 2 - KSD, t - 1) KSU = min(TRU, UT - 2 - KSD, t - 1)
# Equation (41) in Kneuven et al. (2020) # Equation (41) in Kneuven et al. (2020)
eq_prod_limit_shutdown_trajectory[gn, t] = @constraint( eq_prod_limit_shutdown_trajectory[sc.name, gn, t] = @constraint(
model, model,
prod_above[gn, t] + prod_above[sc.name, gn, t] +
g.min_power[t] * is_on[gn, t] + g.min_power[t] * is_on[gn, t] +
(RESERVES_WHEN_SHUT_DOWN ? reserve[t] : 0.0) <= (RESERVES_WHEN_SHUT_DOWN ? reserve[t] : 0.0) <=
Pbar * is_on[gn, t] - sum( Pbar * is_on[gn, t] - sum(

@ -8,6 +8,7 @@ function _add_ramp_eqs!(
::Gar1962.ProdVars, ::Gar1962.ProdVars,
::WanHob2016.Ramping, ::WanHob2016.Ramping,
::Gar1962.StatusVars, ::Gar1962.StatusVars,
sc::UnitCommitmentScenario,
)::Nothing )::Nothing
is_initially_on = (g.initial_status > 0) is_initially_on = (g.initial_status > 0)
SU = g.startup_limit SU = g.startup_limit
@ -38,41 +39,43 @@ function _add_ramp_eqs!(
for t in 1:model[:instance].time for t in 1:model[:instance].time
@constraint( @constraint(
model, model,
prod_above[gn, t] + (is_on[gn, t] * minp[t]) <= mfg[rn, gn, t] prod_above[sc.name, gn, t] + (is_on[gn, t] * minp[t]) <=
mfg[sc.name, gn, t]
) # Eq. (19) in Wang & Hobbs (2016) ) # Eq. (19) in Wang & Hobbs (2016)
@constraint(model, mfg[rn, gn, t] <= is_on[gn, t] * maxp[t]) # Eq. (22) in Wang & Hobbs (2016) @constraint(model, mfg[sc.name, gn, t] <= is_on[gn, t] * maxp[t]) # Eq. (22) in Wang & Hobbs (2016)
if t != model[:instance].time if t != model[:instance].time
@constraint( @constraint(
model, model,
minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <= minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <=
prod_above[gn, t] - dwflexiramp[rn, gn, t] + prod_above[sc.name, gn, t] -
(is_on[gn, t] * minp[t]) dwflexiramp[sc.name, rn, gn, t] + (is_on[gn, t] * minp[t])
) # first inequality of Eq. (20) in Wang & Hobbs (2016) ) # first inequality of Eq. (20) in Wang & Hobbs (2016)
@constraint( @constraint(
model, model,
prod_above[gn, t] - dwflexiramp[rn, gn, t] + prod_above[sc.name, gn, t] -
dwflexiramp[sc.name, rn, gn, t] +
(is_on[gn, t] * minp[t]) <= (is_on[gn, t] * minp[t]) <=
mfg[rn, gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1])) mfg[sc.name, gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
) # second inequality of Eq. (20) in Wang & Hobbs (2016) ) # second inequality of Eq. (20) in Wang & Hobbs (2016)
@constraint( @constraint(
model, model,
minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <= minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <=
prod_above[gn, t] + prod_above[sc.name, gn, t] +
upflexiramp[rn, gn, t] + upflexiramp[sc.name, rn, gn, t] +
(is_on[gn, t] * minp[t]) (is_on[gn, t] * minp[t])
) # first inequality of Eq. (21) in Wang & Hobbs (2016) ) # first inequality of Eq. (21) in Wang & Hobbs (2016)
@constraint( @constraint(
model, model,
prod_above[gn, t] + prod_above[sc.name, gn, t] +
upflexiramp[rn, gn, t] + upflexiramp[sc.name, rn, gn, t] +
(is_on[gn, t] * minp[t]) <= (is_on[gn, t] * minp[t]) <=
mfg[rn, gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1])) mfg[sc.name, gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
) # second inequality of Eq. (21) in Wang & Hobbs (2016) ) # second inequality of Eq. (21) in Wang & Hobbs (2016)
if t != 1 if t != 1
@constraint( @constraint(
model, model,
mfg[rn, gn, t] <= mfg[sc.name, gn, t] <=
prod_above[gn, t-1] + prod_above[sc.name, gn, t-1] +
(is_on[gn, t-1] * minp[t]) + (is_on[gn, t-1] * minp[t]) +
(RU * is_on[gn, t-1]) + (RU * is_on[gn, t-1]) +
(SU * (is_on[gn, t] - is_on[gn, t-1])) + (SU * (is_on[gn, t] - is_on[gn, t-1])) +
@ -80,8 +83,13 @@ function _add_ramp_eqs!(
) # Eq. (23) in Wang & Hobbs (2016) ) # Eq. (23) in Wang & Hobbs (2016)
@constraint( @constraint(
model, model,
(prod_above[gn, t-1] + (is_on[gn, t-1] * minp[t])) - (
(prod_above[gn, t] + (is_on[gn, t] * minp[t])) <= prod_above[sc.name, gn, t-1] +
(is_on[gn, t-1] * minp[t])
) - (
prod_above[sc.name, gn, t] +
(is_on[gn, t] * minp[t])
) <=
RD * is_on[gn, t] + RD * is_on[gn, t] +
SD * (is_on[gn, t-1] - is_on[gn, t]) + SD * (is_on[gn, t-1] - is_on[gn, t]) +
maxp[t] * (1 - is_on[gn, t-1]) maxp[t] * (1 - is_on[gn, t-1])
@ -89,7 +97,7 @@ function _add_ramp_eqs!(
else else
@constraint( @constraint(
model, model,
mfg[rn, gn, t] <= mfg[sc.name, gn, t] <=
initial_power + initial_power +
(RU * is_initially_on) + (RU * is_initially_on) +
(SU * (is_on[gn, t] - is_initially_on)) + (SU * (is_on[gn, t] - is_initially_on)) +
@ -97,8 +105,10 @@ function _add_ramp_eqs!(
) # Eq. (23) in Wang & Hobbs (2016) for the first time period ) # Eq. (23) in Wang & Hobbs (2016) for the first time period
@constraint( @constraint(
model, model,
initial_power - initial_power - (
(prod_above[gn, t] + (is_on[gn, t] * minp[t])) <= prod_above[sc.name, gn, t] +
(is_on[gn, t] * minp[t])
) <=
RD * is_on[gn, t] + RD * is_on[gn, t] +
SD * (is_initially_on - is_on[gn, t]) + SD * (is_initially_on - is_on[gn, t]) +
maxp[t] * (1 - is_initially_on) maxp[t] * (1 - is_initially_on)
@ -106,7 +116,7 @@ function _add_ramp_eqs!(
end end
@constraint( @constraint(
model, model,
mfg[rn, gn, t] <= mfg[sc.name, gn, t] <=
(SD * (is_on[gn, t] - is_on[gn, t+1])) + (SD * (is_on[gn, t] - is_on[gn, t+1])) +
(maxp[t] * is_on[gn, t+1]) (maxp[t] * is_on[gn, t+1])
) # Eq. (24) in Wang & Hobbs (2016) ) # Eq. (24) in Wang & Hobbs (2016)
@ -114,11 +124,12 @@ function _add_ramp_eqs!(
model, model,
-RD * is_on[gn, t+1] - -RD * is_on[gn, t+1] -
SD * (is_on[gn, t] - is_on[gn, t+1]) - SD * (is_on[gn, t] - is_on[gn, t+1]) -
maxp[t] * (1 - is_on[gn, t]) <= upflexiramp[rn, gn, t] maxp[t] * (1 - is_on[gn, t]) <=
upflexiramp[sc.name, rn, gn, t]
) # first inequality of Eq. (26) in Wang & Hobbs (2016) ) # first inequality of Eq. (26) in Wang & Hobbs (2016)
@constraint( @constraint(
model, model,
upflexiramp[rn, gn, t] <= upflexiramp[sc.name, rn, gn, t] <=
RU * is_on[gn, t] + RU * is_on[gn, t] +
SU * (is_on[gn, t+1] - is_on[gn, t]) + SU * (is_on[gn, t+1] - is_on[gn, t]) +
maxp[t] * (1 - is_on[gn, t+1]) maxp[t] * (1 - is_on[gn, t+1])
@ -126,11 +137,12 @@ function _add_ramp_eqs!(
@constraint( @constraint(
model, model,
-RU * is_on[gn, t] - SU * (is_on[gn, t+1] - is_on[gn, t]) - -RU * is_on[gn, t] - SU * (is_on[gn, t+1] - is_on[gn, t]) -
maxp[t] * (1 - is_on[gn, t+1]) <= dwflexiramp[rn, gn, t] maxp[t] * (1 - is_on[gn, t+1]) <=
dwflexiramp[sc.name, rn, gn, t]
) # first inequality of Eq. (27) in Wang & Hobbs (2016) ) # first inequality of Eq. (27) in Wang & Hobbs (2016)
@constraint( @constraint(
model, model,
dwflexiramp[rn, gn, t] <= dwflexiramp[sc.name, rn, gn, t] <=
RD * is_on[gn, t+1] + RD * is_on[gn, t+1] +
SD * (is_on[gn, t] - is_on[gn, t+1]) + SD * (is_on[gn, t] - is_on[gn, t+1]) +
maxp[t] * (1 - is_on[gn, t]) maxp[t] * (1 - is_on[gn, t])
@ -138,26 +150,27 @@ function _add_ramp_eqs!(
@constraint( @constraint(
model, model,
-maxp[t] * is_on[gn, t] + minp[t] * is_on[gn, t+1] <= -maxp[t] * is_on[gn, t] + minp[t] * is_on[gn, t+1] <=
upflexiramp[rn, gn, t] upflexiramp[sc.name, rn, gn, t]
) # first inequality of Eq. (28) in Wang & Hobbs (2016) ) # first inequality of Eq. (28) in Wang & Hobbs (2016)
@constraint( @constraint(
model, model,
upflexiramp[rn, gn, t] <= maxp[t] * is_on[gn, t+1] upflexiramp[sc.name, rn, gn, t] <= maxp[t] * is_on[gn, t+1]
) # second inequality of Eq. (28) in Wang & Hobbs (2016) ) # second inequality of Eq. (28) in Wang & Hobbs (2016)
@constraint( @constraint(
model, model,
-maxp[t] * is_on[gn, t+1] <= dwflexiramp[rn, gn, t] -maxp[t] * is_on[gn, t+1] <=
dwflexiramp[sc.name, rn, gn, t]
) # first inequality of Eq. (29) in Wang & Hobbs (2016) ) # first inequality of Eq. (29) in Wang & Hobbs (2016)
@constraint( @constraint(
model, model,
dwflexiramp[rn, gn, t] <= dwflexiramp[sc.name, rn, gn, t] <=
(maxp[t] * is_on[gn, t]) - (minp[t] * is_on[gn, t+1]) (maxp[t] * is_on[gn, t]) - (minp[t] * is_on[gn, t+1])
) # second inequality of Eq. (29) in Wang & Hobbs (2016) ) # second inequality of Eq. (29) in Wang & Hobbs (2016)
else else
@constraint( @constraint(
model, model,
mfg[rn, gn, t] <= mfg[sc.name, gn, t] <=
prod_above[gn, t-1] + prod_above[sc.name, gn, t-1] +
(is_on[gn, t-1] * minp[t]) + (is_on[gn, t-1] * minp[t]) +
(RU * is_on[gn, t-1]) + (RU * is_on[gn, t-1]) +
(SU * (is_on[gn, t] - is_on[gn, t-1])) + (SU * (is_on[gn, t] - is_on[gn, t-1])) +
@ -165,8 +178,11 @@ function _add_ramp_eqs!(
) # Eq. (23) in Wang & Hobbs (2016) for the last time period ) # Eq. (23) in Wang & Hobbs (2016) for the last time period
@constraint( @constraint(
model, model,
(prod_above[gn, t-1] + (is_on[gn, t-1] * minp[t])) - (
(prod_above[gn, t] + (is_on[gn, t] * minp[t])) <= prod_above[sc.name, gn, t-1] +
(is_on[gn, t-1] * minp[t])
) -
(prod_above[sc.name, gn, t] + (is_on[gn, t] * minp[t])) <=
RD * is_on[gn, t] + RD * is_on[gn, t] +
SD * (is_on[gn, t-1] - is_on[gn, t]) + SD * (is_on[gn, t-1] - is_on[gn, t]) +
maxp[t] * (1 - is_on[gn, t-1]) maxp[t] * (1 - is_on[gn, t-1])

@ -2,22 +2,30 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
function _add_bus!(model::JuMP.Model, b::Bus)::Nothing function _add_bus!(
model::JuMP.Model,
b::Bus,
sc::UnitCommitmentScenario,
)::Nothing
net_injection = _init(model, :expr_net_injection) net_injection = _init(model, :expr_net_injection)
curtail = _init(model, :curtail) curtail = _init(model, :curtail)
for t in 1:model[:instance].time for t in 1:model[:instance].time
# Fixed load # Fixed load
net_injection[b.name, t] = AffExpr(-b.load[t]) net_injection[sc.name, b.name, t] = AffExpr(-b.load[t])
# Load curtailment # Load curtailment
curtail[b.name, t] = curtail[sc.name, b.name, t] =
@variable(model, lower_bound = 0, upper_bound = b.load[t]) @variable(model, lower_bound = 0, upper_bound = b.load[t])
add_to_expression!(net_injection[b.name, t], curtail[b.name, t], 1.0) add_to_expression!(
net_injection[sc.name, b.name, t],
curtail[sc.name, b.name, t],
1.0,
)
add_to_expression!( add_to_expression!(
model[:obj], model[:obj],
curtail[b.name, t], curtail[sc.name, b.name, t],
model[:instance].power_balance_penalty[t], sc.power_balance_penalty[t] * sc.probability,
) )
end end
return return

@ -6,43 +6,43 @@ function _add_transmission_line!(
model::JuMP.Model, model::JuMP.Model,
lm::TransmissionLine, lm::TransmissionLine,
f::ShiftFactorsFormulation, f::ShiftFactorsFormulation,
sc::UnitCommitmentScenario,
)::Nothing )::Nothing
overflow = _init(model, :overflow) overflow = _init(model, :overflow)
for t in 1:model[:instance].time for t in 1:model[:instance].time
overflow[lm.name, t] = @variable(model, lower_bound = 0) overflow[sc.name, lm.name, t] = @variable(model, lower_bound = 0)
add_to_expression!( add_to_expression!(
model[:obj], model[:obj],
overflow[lm.name, t], overflow[sc.name, lm.name, t],
lm.flow_limit_penalty[t], lm.flow_limit_penalty[t] * sc.probability,
) )
end end
return return
end end
function _setup_transmission( function _setup_transmission(
model::JuMP.Model,
formulation::ShiftFactorsFormulation, formulation::ShiftFactorsFormulation,
sc::UnitCommitmentScenario,
)::Nothing )::Nothing
instance = model[:instance]
isf = formulation.precomputed_isf isf = formulation.precomputed_isf
lodf = formulation.precomputed_lodf lodf = formulation.precomputed_lodf
if length(instance.buses) == 1 if length(sc.buses) == 1
isf = zeros(0, 0) isf = zeros(0, 0)
lodf = zeros(0, 0) lodf = zeros(0, 0)
elseif isf === nothing elseif isf === nothing
@info "Computing injection shift factors..." @info "Computing injection shift factors..."
time_isf = @elapsed begin time_isf = @elapsed begin
isf = UnitCommitment._injection_shift_factors( isf = UnitCommitment._injection_shift_factors(
lines = instance.lines, buses = sc.buses,
buses = instance.buses, lines = sc.lines,
) )
end end
@info @sprintf("Computed ISF in %.2f seconds", time_isf) @info @sprintf("Computed ISF in %.2f seconds", time_isf)
@info "Computing line outage factors..." @info "Computing line outage factors..."
time_lodf = @elapsed begin time_lodf = @elapsed begin
lodf = UnitCommitment._line_outage_factors( lodf = UnitCommitment._line_outage_factors(
lines = instance.lines, buses = sc.buses,
buses = instance.buses, lines = sc.lines,
isf = isf, isf = isf,
) )
end end
@ -55,7 +55,7 @@ function _setup_transmission(
isf[abs.(isf).<formulation.isf_cutoff] .= 0 isf[abs.(isf).<formulation.isf_cutoff] .= 0
lodf[abs.(lodf).<formulation.lodf_cutoff] .= 0 lodf[abs.(lodf).<formulation.lodf_cutoff] .= 0
end end
model[:isf] = isf sc.isf = isf
model[:lodf] = lodf sc.lodf = lodf
return return
end end

@ -5,21 +5,26 @@
function _add_price_sensitive_load!( function _add_price_sensitive_load!(
model::JuMP.Model, model::JuMP.Model,
ps::PriceSensitiveLoad, ps::PriceSensitiveLoad,
sc::UnitCommitmentScenario,
)::Nothing )::Nothing
loads = _init(model, :loads) loads = _init(model, :loads)
net_injection = _init(model, :expr_net_injection) net_injection = _init(model, :expr_net_injection)
for t in 1:model[:instance].time for t in 1:model[:instance].time
# Decision variable # Decision variable
loads[ps.name, t] = loads[sc.name, ps.name, t] =
@variable(model, lower_bound = 0, upper_bound = ps.demand[t]) @variable(model, lower_bound = 0, upper_bound = ps.demand[t])
# Objective function terms # Objective function terms
add_to_expression!(model[:obj], loads[ps.name, t], -ps.revenue[t]) add_to_expression!(
model[:obj],
loads[sc.name, ps.name, t],
-ps.revenue[t] * sc.probability,
)
# Net injection # Net injection
add_to_expression!( add_to_expression!(
net_injection[ps.bus.name, t], net_injection[sc.name, ps.bus.name, t],
loads[ps.name, t], loads[sc.name, ps.name, t],
-1.0, -1.0,
) )
end end

@ -18,7 +18,7 @@ function _injection_shift_factors(;
lines::Array{TransmissionLine}, lines::Array{TransmissionLine},
) )
susceptance = _susceptance_matrix(lines) susceptance = _susceptance_matrix(lines)
incidence = _reduced_incidence_matrix(lines = lines, buses = buses) incidence = _reduced_incidence_matrix(buses = buses, lines = lines)
laplacian = transpose(incidence) * susceptance * incidence laplacian = transpose(incidence) * susceptance * incidence
isf = susceptance * incidence * inv(Array(laplacian)) isf = susceptance * incidence * inv(Array(laplacian))
return isf return isf

@ -2,54 +2,68 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
function _add_system_wide_eqs!(model::JuMP.Model)::Nothing function _add_system_wide_eqs!(
_add_net_injection_eqs!(model) model::JuMP.Model,
_add_spinning_reserve_eqs!(model) sc::UnitCommitmentScenario,
_add_flexiramp_reserve_eqs!(model) )::Nothing
_add_net_injection_eqs!(model, sc)
_add_spinning_reserve_eqs!(model, sc)
_add_flexiramp_reserve_eqs!(model, sc)
return return
end end
function _add_net_injection_eqs!(model::JuMP.Model)::Nothing function _add_net_injection_eqs!(
model::JuMP.Model,
sc::UnitCommitmentScenario,
)::Nothing
T = model[:instance].time T = model[:instance].time
net_injection = _init(model, :net_injection) net_injection = _init(model, :net_injection)
eq_net_injection = _init(model, :eq_net_injection) eq_net_injection = _init(model, :eq_net_injection)
eq_power_balance = _init(model, :eq_power_balance) eq_power_balance = _init(model, :eq_power_balance)
for t in 1:T, b in model[:instance].buses for t in 1:T, b in sc.buses
n = net_injection[b.name, t] = @variable(model) n = net_injection[sc.name, b.name, t] = @variable(model)
eq_net_injection[b.name, t] = eq_net_injection[sc.name, b.name, t] = @constraint(
@constraint(model, -n + model[:expr_net_injection][b.name, t] == 0) model,
-n + model[:expr_net_injection][sc.name, b.name, t] == 0
)
end end
for t in 1:T for t in 1:T
eq_power_balance[t] = @constraint( eq_power_balance[sc.name, t] = @constraint(
model, model,
sum(net_injection[b.name, t] for b in model[:instance].buses) == 0 sum(net_injection[sc.name, b.name, t] for b in sc.buses) == 0
) )
end end
return return
end end
function _add_spinning_reserve_eqs!(model::JuMP.Model)::Nothing function _add_spinning_reserve_eqs!(
instance = model[:instance] model::JuMP.Model,
sc::UnitCommitmentScenario,
)::Nothing
T = model[:instance].time
eq_min_spinning_reserve = _init(model, :eq_min_spinning_reserve) eq_min_spinning_reserve = _init(model, :eq_min_spinning_reserve)
for r in instance.reserves for r in sc.reserves
r.type == "spinning" || continue r.type == "spinning" || continue
for t in 1:instance.time for t in 1:T
# Equation (68) in Kneuven et al. (2020) # Equation (68) in Kneuven et al. (2020)
# As in Morales-España et al. (2013a) # As in Morales-España et al. (2013a)
# Akin to the alternative formulation with max_power_avail # Akin to the alternative formulation with max_power_avail
# from Carrión and Arroyo (2006) and Ostrowski et al. (2012) # from Carrión and Arroyo (2006) and Ostrowski et al. (2012)
eq_min_spinning_reserve[r.name, t] = @constraint( eq_min_spinning_reserve[sc.name, r.name, t] = @constraint(
model, model,
sum(model[:reserve][r.name, g.name, t] for g in r.units) + sum(
model[:reserve_shortfall][r.name, t] >= r.amount[t] model[:reserve][sc.name, r.name, g.name, t] for
g in r.units
) + model[:reserve_shortfall][sc.name, r.name, t] >=
r.amount[t]
) )
# Account for shortfall contribution to objective # Account for shortfall contribution to objective
if r.shortfall_penalty >= 0 if r.shortfall_penalty >= 0
add_to_expression!( add_to_expression!(
model[:obj], model[:obj],
r.shortfall_penalty, r.shortfall_penalty * sc.probability,
model[:reserve_shortfall][r.name, t], model[:reserve_shortfall][sc.name, r.name, t],
) )
end end
end end
@ -57,7 +71,10 @@ function _add_spinning_reserve_eqs!(model::JuMP.Model)::Nothing
return return
end end
function _add_flexiramp_reserve_eqs!(model::JuMP.Model)::Nothing function _add_flexiramp_reserve_eqs!(
model::JuMP.Model,
sc::UnitCommitmentScenario,
)::Nothing
# Note: The flexpramp requirements in Wang & Hobbs (2016) are imposed as hard constraints # Note: The flexpramp requirements in Wang & Hobbs (2016) are imposed as hard constraints
# through Eq. (17) and Eq. (18). The constraints eq_min_upflexiramp and eq_min_dwflexiramp # through Eq. (17) and Eq. (18). The constraints eq_min_upflexiramp and eq_min_dwflexiramp
# provided below are modified versions of Eq. (17) and Eq. (18), respectively, in that # provided below are modified versions of Eq. (17) and Eq. (18), respectively, in that
@ -65,29 +82,37 @@ function _add_flexiramp_reserve_eqs!(model::JuMP.Model)::Nothing
# objective function. # objective function.
eq_min_upflexiramp = _init(model, :eq_min_upflexiramp) eq_min_upflexiramp = _init(model, :eq_min_upflexiramp)
eq_min_dwflexiramp = _init(model, :eq_min_dwflexiramp) eq_min_dwflexiramp = _init(model, :eq_min_dwflexiramp)
instance = model[:instance] T = model[:instance].time
for r in instance.reserves for r in sc.reserves
r.type == "flexiramp" || continue r.type == "flexiramp" || continue
for t in 1:instance.time for t in 1:T
# Eq. (17) in Wang & Hobbs (2016) # Eq. (17) in Wang & Hobbs (2016)
eq_min_upflexiramp[r.name, t] = @constraint( eq_min_upflexiramp[sc.name, r.name, t] = @constraint(
model, model,
sum(model[:upflexiramp][r.name, g.name, t] for g in r.units) + model[:upflexiramp_shortfall][r.name, t] >= r.amount[t] sum(
model[:upflexiramp][sc.name, r.name, g.name, t] for
g in r.units
) + model[:upflexiramp_shortfall][sc.name, r.name, t] >=
r.amount[t]
) )
# Eq. (18) in Wang & Hobbs (2016) # Eq. (18) in Wang & Hobbs (2016)
eq_min_dwflexiramp[r.name, t] = @constraint( eq_min_dwflexiramp[sc.name, r.name, t] = @constraint(
model, model,
sum(model[:dwflexiramp][r.name, g.name, t] for g in r.units) + model[:dwflexiramp_shortfall][r.name, t] >= r.amount[t] sum(
model[:dwflexiramp][sc.name, r.name, g.name, t] for
g in r.units
) + model[:dwflexiramp_shortfall][sc.name, r.name, t] >=
r.amount[t]
) )
# Account for flexiramp shortfall contribution to objective # Account for flexiramp shortfall contribution to objective
if r.shortfall_penalty >= 0 if r.shortfall_penalty >= 0
add_to_expression!( add_to_expression!(
model[:obj], model[:obj],
r.shortfall_penalty, r.shortfall_penalty * sc.probability,
( (
model[:upflexiramp_shortfall][r.name, t] + model[:upflexiramp_shortfall][sc.name, r.name, t] +
model[:dwflexiramp_shortfall][r.name, t] model[:dwflexiramp_shortfall][sc.name, r.name, t]
), ),
) )
end end

@ -2,7 +2,13 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
function _add_unit!(model::JuMP.Model, g::Unit, formulation::Formulation) # Function for adding variables, constraints, and objective function terms
# related to the binary commitment, startup and shutdown decisions of units
function _add_unit_commitment!(
model::JuMP.Model,
g::Unit,
formulation::Formulation,
)
if !all(g.must_run) && any(g.must_run) if !all(g.must_run) && any(g.must_run)
error("Partially must-run units are not currently supported") error("Partially must-run units are not currently supported")
end end
@ -11,22 +17,40 @@ function _add_unit!(model::JuMP.Model, g::Unit, formulation::Formulation)
end end
# Variables # Variables
_add_production_vars!(model, g, formulation.prod_vars)
_add_spinning_reserve_vars!(model, g)
_add_flexiramp_reserve_vars!(model, g)
_add_startup_shutdown_vars!(model, g) _add_startup_shutdown_vars!(model, g)
_add_status_vars!(model, g, formulation.status_vars) _add_status_vars!(model, g, formulation.status_vars)
# Constraints and objective function # Constraints and objective function
_add_min_uptime_downtime_eqs!(model, g) _add_min_uptime_downtime_eqs!(model, g)
_add_net_injection_eqs!(model, g) _add_startup_cost_eqs!(model, g, formulation.startup_costs)
_add_production_limit_eqs!(model, g, formulation.prod_vars) _add_status_eqs!(model, g, formulation.status_vars)
return
end
# Function for adding variables, constraints, and objective function terms
# related to the continuous dispatch decisions of units
function _add_unit_dispatch!(
model::JuMP.Model,
g::Unit,
formulation::Formulation,
sc::UnitCommitmentScenario,
)
# Variables
_add_production_vars!(model, g, formulation.prod_vars, sc)
_add_spinning_reserve_vars!(model, g, sc)
_add_flexiramp_reserve_vars!(model, g, sc)
# Constraints and objective function
_add_net_injection_eqs!(model, g, sc)
_add_production_limit_eqs!(model, g, formulation.prod_vars, sc)
_add_production_piecewise_linear_eqs!( _add_production_piecewise_linear_eqs!(
model, model,
g, g,
formulation.prod_vars, formulation.prod_vars,
formulation.pwl_costs, formulation.pwl_costs,
formulation.status_vars, formulation.status_vars,
sc,
) )
_add_ramp_eqs!( _add_ramp_eqs!(
model, model,
@ -34,26 +58,31 @@ function _add_unit!(model::JuMP.Model, g::Unit, formulation::Formulation)
formulation.prod_vars, formulation.prod_vars,
formulation.ramping, formulation.ramping,
formulation.status_vars, formulation.status_vars,
sc,
) )
_add_startup_cost_eqs!(model, g, formulation.startup_costs) _add_startup_shutdown_limit_eqs!(model, g, sc)
_add_startup_shutdown_limit_eqs!(model, g)
_add_status_eqs!(model, g, formulation.status_vars)
return return
end end
_is_initially_on(g::Unit)::Float64 = (g.initial_status > 0 ? 1.0 : 0.0) _is_initially_on(g::Unit)::Float64 = (g.initial_status > 0 ? 1.0 : 0.0)
function _add_spinning_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing function _add_spinning_reserve_vars!(
model::JuMP.Model,
g::Unit,
sc::UnitCommitmentScenario,
)::Nothing
reserve = _init(model, :reserve) reserve = _init(model, :reserve)
reserve_shortfall = _init(model, :reserve_shortfall) reserve_shortfall = _init(model, :reserve_shortfall)
for r in g.reserves for r in g.reserves
r.type == "spinning" || continue r.type == "spinning" || continue
for t in 1:model[:instance].time for t in 1:model[:instance].time
reserve[r.name, g.name, t] = @variable(model, lower_bound = 0) reserve[sc.name, r.name, g.name, t] =
if (r.name, t) keys(reserve_shortfall) @variable(model, lower_bound = 0)
reserve_shortfall[r.name, t] = @variable(model, lower_bound = 0) if (sc.name, r.name, t) keys(reserve_shortfall)
reserve_shortfall[sc.name, r.name, t] =
@variable(model, lower_bound = 0)
if r.shortfall_penalty < 0 if r.shortfall_penalty < 0
set_upper_bound(reserve_shortfall[r.name, t], 0.0) set_upper_bound(reserve_shortfall[sc.name, r.name, t], 0.0)
end end
end end
end end
@ -61,27 +90,37 @@ function _add_spinning_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing
return return
end end
function _add_flexiramp_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing function _add_flexiramp_reserve_vars!(
model::JuMP.Model,
g::Unit,
sc::UnitCommitmentScenario,
)::Nothing
upflexiramp = _init(model, :upflexiramp) upflexiramp = _init(model, :upflexiramp)
upflexiramp_shortfall = _init(model, :upflexiramp_shortfall) upflexiramp_shortfall = _init(model, :upflexiramp_shortfall)
mfg = _init(model, :mfg) mfg = _init(model, :mfg)
dwflexiramp = _init(model, :dwflexiramp) dwflexiramp = _init(model, :dwflexiramp)
dwflexiramp_shortfall = _init(model, :dwflexiramp_shortfall) dwflexiramp_shortfall = _init(model, :dwflexiramp_shortfall)
for r in g.reserves
r.type == "flexiramp" || continue
for t in 1:model[:instance].time for t in 1:model[:instance].time
# maximum feasible generation, \bar{g_{its}} in Wang & Hobbs (2016) # maximum feasible generation, \bar{g_{its}} in Wang & Hobbs (2016)
mfg[r.name, g.name, t] = @variable(model, lower_bound = 0) mfg[sc.name, g.name, t] = @variable(model, lower_bound = 0)
upflexiramp[r.name, g.name, t] = @variable(model) # up-flexiramp, ur_{it} in Wang & Hobbs (2016) for r in g.reserves
dwflexiramp[r.name, g.name, t] = @variable(model) # down-flexiramp, dr_{it} in Wang & Hobbs (2016) r.type == "flexiramp" || continue
if (r.name, t) keys(upflexiramp_shortfall) upflexiramp[sc.name, r.name, g.name, t] = @variable(model) # up-flexiramp, ur_{it} in Wang & Hobbs (2016)
upflexiramp_shortfall[r.name, t] = dwflexiramp[sc.name, r.name, g.name, t] = @variable(model) # down-flexiramp, dr_{it} in Wang & Hobbs (2016)
if (sc.name, r.name, t) keys(upflexiramp_shortfall)
upflexiramp_shortfall[sc.name, r.name, t] =
@variable(model, lower_bound = 0) @variable(model, lower_bound = 0)
dwflexiramp_shortfall[r.name, t] = dwflexiramp_shortfall[sc.name, r.name, t] =
@variable(model, lower_bound = 0) @variable(model, lower_bound = 0)
if r.shortfall_penalty < 0 if r.shortfall_penalty < 0
set_upper_bound(upflexiramp_shortfall[r.name, t], 0.0) set_upper_bound(
set_upper_bound(dwflexiramp_shortfall[r.name, t], 0.0) upflexiramp_shortfall[sc.name, r.name, t],
0.0,
)
set_upper_bound(
dwflexiramp_shortfall[sc.name, r.name, t],
0.0,
)
end end
end end
end end
@ -99,32 +138,36 @@ function _add_startup_shutdown_vars!(model::JuMP.Model, g::Unit)::Nothing
return return
end end
function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing function _add_startup_shutdown_limit_eqs!(
model::JuMP.Model,
g::Unit,
sc::UnitCommitmentScenario,
)::Nothing
eq_shutdown_limit = _init(model, :eq_shutdown_limit) eq_shutdown_limit = _init(model, :eq_shutdown_limit)
eq_startup_limit = _init(model, :eq_startup_limit) eq_startup_limit = _init(model, :eq_startup_limit)
is_on = model[:is_on] is_on = model[:is_on]
prod_above = model[:prod_above] prod_above = model[:prod_above]
reserve = _total_reserves(model, g) reserve = _total_reserves(model, g, sc)
switch_off = model[:switch_off] switch_off = model[:switch_off]
switch_on = model[:switch_on] switch_on = model[:switch_on]
T = model[:instance].time T = model[:instance].time
for t in 1:T for t in 1:T
# Startup limit # Startup limit
eq_startup_limit[g.name, t] = @constraint( eq_startup_limit[sc.name, g.name, t] = @constraint(
model, model,
prod_above[g.name, t] + reserve[t] <= prod_above[sc.name, g.name, t] + reserve[t] <=
(g.max_power[t] - g.min_power[t]) * is_on[g.name, t] - (g.max_power[t] - g.min_power[t]) * is_on[g.name, t] -
max(0, g.max_power[t] - g.startup_limit) * switch_on[g.name, t] max(0, g.max_power[t] - g.startup_limit) * switch_on[g.name, t]
) )
# Shutdown limit # Shutdown limit
if g.initial_power > g.shutdown_limit if g.initial_power > g.shutdown_limit
eq_shutdown_limit[g.name, 0] = eq_shutdown_limit[sc.name, g.name, 0] =
@constraint(model, switch_off[g.name, 1] <= 0) @constraint(model, switch_off[g.name, 1] <= 0)
end end
if t < T if t < T
eq_shutdown_limit[g.name, t] = @constraint( eq_shutdown_limit[sc.name, g.name, t] = @constraint(
model, model,
prod_above[g.name, t] <= prod_above[sc.name, g.name, t] <=
(g.max_power[t] - g.min_power[t]) * is_on[g.name, t] - (g.max_power[t] - g.min_power[t]) * is_on[g.name, t] -
max(0, g.max_power[t] - g.shutdown_limit) * max(0, g.max_power[t] - g.shutdown_limit) *
switch_off[g.name, t+1] switch_off[g.name, t+1]
@ -138,43 +181,44 @@ function _add_ramp_eqs!(
model::JuMP.Model, model::JuMP.Model,
g::Unit, g::Unit,
formulation::RampingFormulation, formulation::RampingFormulation,
sc::UnitCommitmentScenario,
)::Nothing )::Nothing
prod_above = model[:prod_above] prod_above = model[:prod_above]
reserve = _total_reserves(model, g) reserve = _total_reserves(model, g, sc)
eq_ramp_up = _init(model, :eq_ramp_up) eq_ramp_up = _init(model, :eq_ramp_up)
eq_ramp_down = _init(model, :eq_ramp_down) eq_ramp_down = _init(model, :eq_ramp_down)
for t in 1:model[:instance].time for t in 1:model[:instance].time
# Ramp up limit # Ramp up limit
if t == 1 if t == 1
if _is_initially_on(g) == 1 if _is_initially_on(g) == 1
eq_ramp_up[g.name, t] = @constraint( eq_ramp_up[sc.name, g.name, t] = @constraint(
model, model,
prod_above[g.name, t] + reserve[t] <= prod_above[sc.name, g.name, t] + reserve[t] <=
(g.initial_power - g.min_power[t]) + g.ramp_up_limit (g.initial_power - g.min_power[t]) + g.ramp_up_limit
) )
end end
else else
eq_ramp_up[g.name, t] = @constraint( eq_ramp_up[sc.name, g.name, t] = @constraint(
model, model,
prod_above[g.name, t] + reserve[t] <= prod_above[sc.name, g.name, t] + reserve[t] <=
prod_above[g.name, t-1] + g.ramp_up_limit prod_above[sc.name, g.name, t-1] + g.ramp_up_limit
) )
end end
# Ramp down limit # Ramp down limit
if t == 1 if t == 1
if _is_initially_on(g) == 1 if _is_initially_on(g) == 1
eq_ramp_down[g.name, t] = @constraint( eq_ramp_down[sc.name, g.name, t] = @constraint(
model, model,
prod_above[g.name, t] >= prod_above[sc.name, g.name, t] >=
(g.initial_power - g.min_power[t]) - g.ramp_down_limit (g.initial_power - g.min_power[t]) - g.ramp_down_limit
) )
end end
else else
eq_ramp_down[g.name, t] = @constraint( eq_ramp_down[sc.name, g.name, t] = @constraint(
model, model,
prod_above[g.name, t] >= prod_above[sc.name, g.name, t] >=
prod_above[g.name, t-1] - g.ramp_down_limit prod_above[sc.name, g.name, t-1] - g.ramp_down_limit
) )
end end
end end
@ -223,30 +267,37 @@ function _add_min_uptime_downtime_eqs!(model::JuMP.Model, g::Unit)::Nothing
end end
end end
function _add_net_injection_eqs!(model::JuMP.Model, g::Unit)::Nothing function _add_net_injection_eqs!(
model::JuMP.Model,
g::Unit,
sc::UnitCommitmentScenario,
)::Nothing
expr_net_injection = model[:expr_net_injection] expr_net_injection = model[:expr_net_injection]
for t in 1:model[:instance].time for t in 1:model[:instance].time
# Add to net injection expression # Add to net injection expression
add_to_expression!( add_to_expression!(
expr_net_injection[g.bus.name, t], expr_net_injection[sc.name, g.bus.name, t],
model[:prod_above][g.name, t], model[:prod_above][sc.name, g.name, t],
1.0, 1.0,
) )
add_to_expression!( add_to_expression!(
expr_net_injection[g.bus.name, t], expr_net_injection[sc.name, g.bus.name, t],
model[:is_on][g.name, t], model[:is_on][g.name, t],
g.min_power[t], g.min_power[t],
) )
end end
end end
function _total_reserves(model, g)::Vector function _total_reserves(model, g, sc)::Vector
T = model[:instance].time T = model[:instance].time
reserve = [0.0 for _ in 1:T] reserve = [0.0 for _ in 1:T]
spinning_reserves = [r for r in g.reserves if r.type == "spinning"] spinning_reserves = [r for r in g.reserves if r.type == "spinning"]
if !isempty(spinning_reserves) if !isempty(spinning_reserves)
reserve += [ reserve += [
sum(model[:reserve][r.name, g.name, t] for r in spinning_reserves) for t in 1:model[:instance].time sum(
model[:reserve][sc.name, r.name, g.name, t] for
r in spinning_reserves
) for t in 1:model[:instance].time
] ]
end end
return reserve return reserve

@ -10,37 +10,43 @@ solution. Useful for computing LMPs.
""" """
function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing
instance, T = model[:instance], model[:instance].time instance, T = model[:instance], model[:instance].time
"Production (MW)" keys(solution) ? solution = Dict("s1" => solution) :
nothing
is_on = model[:is_on] is_on = model[:is_on]
prod_above = model[:prod_above] prod_above = model[:prod_above]
reserve = model[:reserve] reserve = model[:reserve]
for g in instance.units for sc in instance.scenarios
for g in sc.units
for t in 1:T for t in 1:T
is_on_value = round(solution["Is on"][g.name][t]) is_on_value = round(solution[sc.name]["Is on"][g.name][t])
prod_value = prod_value = round(
round(solution["Production (MW)"][g.name][t], digits = 5) solution[sc.name]["Production (MW)"][g.name][t],
digits = 5,
)
JuMP.fix(is_on[g.name, t], is_on_value, force = true) JuMP.fix(is_on[g.name, t], is_on_value, force = true)
JuMP.fix( JuMP.fix(
prod_above[g.name, t], prod_above[sc.name, g.name, t],
prod_value - is_on_value * g.min_power[t], prod_value - is_on_value * g.min_power[t],
force = true, force = true,
) )
end end
end end
for r in instance.reserves for r in sc.reserves
r.type == "spinning" || continue r.type == "spinning" || continue
for g in r.units for g in r.units
for t in 1:T for t in 1:T
reserve_value = round( reserve_value = round(
solution["Spinning reserve (MW)"][r.name][g.name][t], solution[sc.name]["Spinning reserve (MW)"][r.name][g.name][t],
digits = 5, digits = 5,
) )
JuMP.fix( JuMP.fix(
reserve[r.name, g.name, t], reserve[sc.name, r.name, g.name, t],
reserve_value, reserve_value,
force = true, force = true,
) )
end end
end end
end end
end
return return
end end

@ -5,13 +5,15 @@
function _enforce_transmission( function _enforce_transmission(
model::JuMP.Model, model::JuMP.Model,
violations::Vector{_Violation}, violations::Vector{_Violation},
sc::UnitCommitmentScenario,
)::Nothing )::Nothing
for v in violations for v in violations
_enforce_transmission( _enforce_transmission(
model = model, model = model,
sc = sc,
violation = v, violation = v,
isf = model[:isf], isf = sc.isf,
lodf = model[:lodf], lodf = sc.lodf,
) )
end end
return return
@ -19,6 +21,7 @@ end
function _enforce_transmission(; function _enforce_transmission(;
model::JuMP.Model, model::JuMP.Model,
sc::UnitCommitmentScenario,
violation::_Violation, violation::_Violation,
isf::Matrix{Float64}, isf::Matrix{Float64},
lodf::Matrix{Float64}, lodf::Matrix{Float64},
@ -31,19 +34,21 @@ function _enforce_transmission(;
if violation.outage_line === nothing if violation.outage_line === nothing
limit = violation.monitored_line.normal_flow_limit[violation.time] limit = violation.monitored_line.normal_flow_limit[violation.time]
@info @sprintf( @info @sprintf(
" %8.3f MW overflow in %-5s time %3d (pre-contingency)", " %8.3f MW overflow in %-5s time %3d (pre-contingency, scenario %s)",
violation.amount, violation.amount,
violation.monitored_line.name, violation.monitored_line.name,
violation.time, violation.time,
sc.name,
) )
else else
limit = violation.monitored_line.emergency_flow_limit[violation.time] limit = violation.monitored_line.emergency_flow_limit[violation.time]
@info @sprintf( @info @sprintf(
" %8.3f MW overflow in %-5s time %3d (outage: line %s)", " %8.3f MW overflow in %-5s time %3d (outage: line %s, scenario %s)",
violation.amount, violation.amount,
violation.monitored_line.name, violation.monitored_line.name,
violation.time, violation.time,
violation.outage_line.name, violation.outage_line.name,
sc.name,
) )
end end
@ -51,7 +56,7 @@ function _enforce_transmission(;
t = violation.time t = violation.time
flow = @variable(model, base_name = "flow[$fm,$t]") flow = @variable(model, base_name = "flow[$fm,$t]")
v = overflow[violation.monitored_line.name, violation.time] v = overflow[sc.name, violation.monitored_line.name, violation.time]
@constraint(model, flow <= limit + v) @constraint(model, flow <= limit + v)
@constraint(model, -flow <= limit + v) @constraint(model, -flow <= limit + v)
@ -59,23 +64,23 @@ function _enforce_transmission(;
@constraint( @constraint(
model, model,
flow == sum( flow == sum(
net_injection[b.name, violation.time] * net_injection[sc.name, b.name, violation.time] *
isf[violation.monitored_line.offset, b.offset] for isf[violation.monitored_line.offset, b.offset] for
b in instance.buses if b.offset > 0 b in sc.buses if b.offset > 0
) )
) )
else else
@constraint( @constraint(
model, model,
flow == sum( flow == sum(
net_injection[b.name, violation.time] * ( net_injection[sc.name, b.name, violation.time] * (
isf[violation.monitored_line.offset, b.offset] + ( isf[violation.monitored_line.offset, b.offset] + (
lodf[ lodf[
violation.monitored_line.offset, violation.monitored_line.offset,
violation.outage_line.offset, violation.outage_line.offset,
] * isf[violation.outage_line.offset, b.offset] ] * isf[violation.outage_line.offset, b.offset]
) )
) for b in instance.buses if b.offset > 0 ) for b in sc.buses if b.offset > 0
) )
) )
end end

@ -5,40 +5,36 @@
import Base.Threads: @threads import Base.Threads: @threads
function _find_violations( function _find_violations(
model::JuMP.Model; model::JuMP.Model,
sc::UnitCommitmentScenario;
max_per_line::Int, max_per_line::Int,
max_per_period::Int, max_per_period::Int,
) )
instance = model[:instance] instance = model[:instance]
net_injection = model[:net_injection] net_injection = model[:net_injection]
overflow = model[:overflow] overflow = model[:overflow]
length(instance.buses) > 1 || return [] length(sc.buses) > 1 || return []
violations = [] violations = []
@info "Verifying transmission limits..."
time_screening = @elapsed begin non_slack_buses = [b for b in sc.buses if b.offset > 0]
non_slack_buses = [b for b in instance.buses if b.offset > 0]
net_injection_values = [ net_injection_values = [
value(net_injection[b.name, t]) for b in non_slack_buses, value(net_injection[sc.name, b.name, t]) for b in non_slack_buses,
t in 1:instance.time t in 1:instance.time
] ]
overflow_values = [ overflow_values = [
value(overflow[lm.name, t]) for lm in instance.lines, value(overflow[sc.name, lm.name, t]) for lm in sc.lines,
t in 1:instance.time t in 1:instance.time
] ]
violations = UnitCommitment._find_violations( violations = UnitCommitment._find_violations(
instance = instance, instance = instance,
sc = sc,
net_injections = net_injection_values, net_injections = net_injection_values,
overflow = overflow_values, overflow = overflow_values,
isf = model[:isf], isf = sc.isf,
lodf = model[:lodf], lodf = sc.lodf,
max_per_line = max_per_line, max_per_line = max_per_line,
max_per_period = max_per_period, max_per_period = max_per_period,
) )
end
@info @sprintf(
"Verified transmission limits in %.2f seconds",
time_screening
)
return violations return violations
end end
@ -64,6 +60,7 @@ matrix, where L is the number of transmission lines.
""" """
function _find_violations(; function _find_violations(;
instance::UnitCommitmentInstance, instance::UnitCommitmentInstance,
sc::UnitCommitmentScenario,
net_injections::Array{Float64,2}, net_injections::Array{Float64,2},
overflow::Array{Float64,2}, overflow::Array{Float64,2},
isf::Array{Float64,2}, isf::Array{Float64,2},
@ -71,8 +68,8 @@ function _find_violations(;
max_per_line::Int, max_per_line::Int,
max_per_period::Int, max_per_period::Int,
)::Array{_Violation,1} )::Array{_Violation,1}
B = length(instance.buses) - 1 B = length(sc.buses) - 1
L = length(instance.lines) L = length(sc.lines)
T = instance.time T = instance.time
K = nthreads() K = nthreads()
@ -93,17 +90,17 @@ function _find_violations(;
post_v::Array{Float64} = zeros(L, L, K) # post_v[lm, lc, thread] post_v::Array{Float64} = zeros(L, L, K) # post_v[lm, lc, thread]
normal_limits::Array{Float64,2} = [ normal_limits::Array{Float64,2} = [
l.normal_flow_limit[t] + overflow[l.offset, t] for l.normal_flow_limit[t] + overflow[l.offset, t] for l in sc.lines,
l in instance.lines, t in 1:T t in 1:T
] ]
emergency_limits::Array{Float64,2} = [ emergency_limits::Array{Float64,2} = [
l.emergency_flow_limit[t] + overflow[l.offset, t] for l.emergency_flow_limit[t] + overflow[l.offset, t] for l in sc.lines,
l in instance.lines, t in 1:T t in 1:T
] ]
is_vulnerable::Array{Bool} = zeros(Bool, L) is_vulnerable::Array{Bool} = zeros(Bool, L)
for c in instance.contingencies for c in sc.contingencies
is_vulnerable[c.lines[1].offset] = true is_vulnerable[c.lines[1].offset] = true
end end
@ -144,7 +141,7 @@ function _find_violations(;
filters[t], filters[t],
_Violation( _Violation(
time = t, time = t,
monitored_line = instance.lines[lm], monitored_line = sc.lines[lm],
outage_line = nothing, outage_line = nothing,
amount = pre_v[lm, k], amount = pre_v[lm, k],
), ),
@ -159,8 +156,8 @@ function _find_violations(;
filters[t], filters[t],
_Violation( _Violation(
time = t, time = t,
monitored_line = instance.lines[lm], monitored_line = sc.lines[lm],
outage_line = instance.lines[lc], outage_line = sc.lines[lc],
amount = post_v[lm, lc, k], amount = post_v[lm, lc, k],
), ),
) )

@ -12,11 +12,16 @@ function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Nothing
end end
initial_time = time() initial_time = time()
large_gap = false large_gap = false
has_transmission = (length(model[:isf]) > 0) has_transmission = false
for sc in model[:instance].scenarios
if length(sc.isf) > 0
has_transmission = true
end
if has_transmission && method.two_phase_gap if has_transmission && method.two_phase_gap
set_gap(1e-2) set_gap(1e-2)
large_gap = true large_gap = true
end end
end
while true while true
time_elapsed = time() - initial_time time_elapsed = time() - initial_time
time_remaining = method.time_limit - time_elapsed time_remaining = method.time_limit - time_elapsed
@ -31,13 +36,41 @@ function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Nothing
JuMP.set_time_limit_sec(model, time_remaining) JuMP.set_time_limit_sec(model, time_remaining)
@info "Solving MILP..." @info "Solving MILP..."
JuMP.optimize!(model) JuMP.optimize!(model)
has_transmission || break has_transmission || break
violations = _find_violations(
@info "Verifying transmission limits..."
time_screening = @elapsed begin
violations = []
for sc in model[:instance].scenarios
push!(
violations,
_find_violations(
model, model,
sc,
max_per_line = method.max_violations_per_line, max_per_line = method.max_violations_per_line,
max_per_period = method.max_violations_per_period, max_per_period = method.max_violations_per_period,
),
)
end
end
@info @sprintf(
"Verified transmission limits in %.2f seconds",
time_screening
) )
if isempty(violations)
violations_found = false
for v in violations
if !isempty(v)
violations_found = true
end
end
if violations_found
for (i, v) in enumerate(violations)
_enforce_transmission(model, v, model[:instance].scenarios[i])
end
else
@info "No violations found" @info "No violations found"
if large_gap if large_gap
large_gap = false large_gap = false
@ -45,8 +78,6 @@ function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Nothing
else else
break break
end end
else
_enforce_transmission(model, violations)
end end
end end
return return

@ -16,34 +16,44 @@ solution = UnitCommitment.solution(model)
""" """
function solution(model::JuMP.Model)::OrderedDict function solution(model::JuMP.Model)::OrderedDict
instance, T = model[:instance], model[:instance].time instance, T = model[:instance], model[:instance].time
function timeseries(vars, collection) function timeseries(vars, collection; sc = nothing)
if sc === nothing
return OrderedDict( return OrderedDict(
b.name => [round(value(vars[b.name, t]), digits = 5) for t in 1:T] b.name =>
for b in collection [round(value(vars[b.name, t]), digits = 5) for t in 1:T] for
b in collection
) )
else
return OrderedDict(
b.name => [
round(value(vars[sc.name, b.name, t]), digits = 5) for
t in 1:T
] for b in collection
)
end
end end
function production_cost(g) function production_cost(g, sc)
return [ return [
value(model[:is_on][g.name, t]) * g.min_power_cost[t] + sum( value(model[:is_on][g.name, t]) * g.min_power_cost[t] + sum(
Float64[ Float64[
value(model[:segprod][g.name, t, k]) * value(model[:segprod][sc.name, g.name, t, k]) *
g.cost_segments[k].cost[t] for g.cost_segments[k].cost[t] for
k in 1:length(g.cost_segments) k in 1:length(g.cost_segments)
], ],
) for t in 1:T ) for t in 1:T
] ]
end end
function production(g) function production(g, sc)
return [ return [
value(model[:is_on][g.name, t]) * g.min_power[t] + sum( value(model[:is_on][g.name, t]) * g.min_power[t] + sum(
Float64[ Float64[
value(model[:segprod][g.name, t, k]) for value(model[:segprod][sc.name, g.name, t, k]) for
k in 1:length(g.cost_segments) k in 1:length(g.cost_segments)
], ],
) for t in 1:T ) for t in 1:T
] ]
end end
function startup_cost(g) function startup_cost(g, sc)
S = length(g.startup_categories) S = length(g.startup_categories)
return [ return [
sum( sum(
@ -53,66 +63,70 @@ function solution(model::JuMP.Model)::OrderedDict
] ]
end end
sol = OrderedDict() sol = OrderedDict()
sol["Production (MW)"] = for sc in instance.scenarios
OrderedDict(g.name => production(g) for g in instance.units) sol[sc.name] = OrderedDict()
sol["Production cost (\$)"] = sol[sc.name]["Production (MW)"] =
OrderedDict(g.name => production_cost(g) for g in instance.units) OrderedDict(g.name => production(g, sc) for g in sc.units)
sol["Startup cost (\$)"] = sol[sc.name]["Production cost (\$)"] =
OrderedDict(g.name => startup_cost(g) for g in instance.units) OrderedDict(g.name => production_cost(g, sc) for g in sc.units)
sol["Is on"] = timeseries(model[:is_on], instance.units) sol[sc.name]["Startup cost (\$)"] =
sol["Switch on"] = timeseries(model[:switch_on], instance.units) OrderedDict(g.name => startup_cost(g, sc) for g in sc.units)
sol["Switch off"] = timeseries(model[:switch_off], instance.units) sol[sc.name]["Is on"] = timeseries(model[:is_on], sc.units)
sol["Net injection (MW)"] = sol[sc.name]["Switch on"] = timeseries(model[:switch_on], sc.units)
timeseries(model[:net_injection], instance.buses) sol[sc.name]["Switch off"] = timeseries(model[:switch_off], sc.units)
sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses) sol[sc.name]["Net injection (MW)"] =
if !isempty(instance.lines) timeseries(model[:net_injection], sc.buses, sc = sc)
sol["Line overflow (MW)"] = timeseries(model[:overflow], instance.lines) sol[sc.name]["Load curtail (MW)"] =
timeseries(model[:curtail], sc.buses, sc = sc)
if !isempty(sc.lines)
sol[sc.name]["Line overflow (MW)"] =
timeseries(model[:overflow], sc.lines, sc = sc)
end end
if !isempty(instance.price_sensitive_loads) if !isempty(sc.price_sensitive_loads)
sol["Price-sensitive loads (MW)"] = sol[sc.name]["Price-sensitive loads (MW)"] =
timeseries(model[:loads], instance.price_sensitive_loads) timeseries(model[:loads], sc.price_sensitive_loads, sc = sc)
end end
sol["Spinning reserve (MW)"] = OrderedDict( sol[sc.name]["Spinning reserve (MW)"] = OrderedDict(
r.name => OrderedDict( r.name => OrderedDict(
g.name => [ g.name => [
value(model[:reserve][r.name, g.name, t]) for value(model[:reserve][sc.name, r.name, g.name, t]) for t in 1:instance.time
t in 1:instance.time
] for g in r.units ] for g in r.units
) for r in instance.reserves if r.type == "spinning" ) for r in sc.reserves if r.type == "spinning"
) )
sol["Spinning reserve shortfall (MW)"] = OrderedDict( sol[sc.name]["Spinning reserve shortfall (MW)"] = OrderedDict(
r.name => [ r.name => [
value(model[:reserve_shortfall][r.name, t]) for value(model[:reserve_shortfall][sc.name, r.name, t]) for
t in 1:instance.time t in 1:instance.time
] for r in instance.reserves if r.type == "spinning" ] for r in sc.reserves if r.type == "spinning"
) )
sol["Up-flexiramp (MW)"] = OrderedDict( sol[sc.name]["Up-flexiramp (MW)"] = OrderedDict(
r.name => OrderedDict( r.name => OrderedDict(
g.name => [ g.name => [
value(model[:upflexiramp][r.name, g.name, t]) for value(model[:upflexiramp][sc.name, r.name, g.name, t]) for t in 1:instance.time
t in 1:instance.time
] for g in r.units ] for g in r.units
) for r in instance.reserves if r.type == "flexiramp" ) for r in sc.reserves if r.type == "flexiramp"
) )
sol["Up-flexiramp shortfall (MW)"] = OrderedDict( sol[sc.name]["Up-flexiramp shortfall (MW)"] = OrderedDict(
r.name => [ r.name => [
value(model[:upflexiramp_shortfall][r.name, t]) for value(model[:upflexiramp_shortfall][sc.name, r.name, t]) for t in 1:instance.time
t in 1:instance.time ] for r in sc.reserves if r.type == "flexiramp"
] for r in instance.reserves if r.type == "flexiramp"
) )
sol["Down-flexiramp (MW)"] = OrderedDict( sol[sc.name]["Down-flexiramp (MW)"] = OrderedDict(
r.name => OrderedDict( r.name => OrderedDict(
g.name => [ g.name => [
value(model[:dwflexiramp][r.name, g.name, t]) for value(model[:dwflexiramp][sc.name, r.name, g.name, t]) for t in 1:instance.time
t in 1:instance.time
] for g in r.units ] for g in r.units
) for r in instance.reserves if r.type == "flexiramp" ) for r in sc.reserves if r.type == "flexiramp"
) )
sol["Down-flexiramp shortfall (MW)"] = OrderedDict( sol[sc.name]["Down-flexiramp shortfall (MW)"] = OrderedDict(
r.name => [ r.name => [
value(model[:upflexiramp_shortfall][r.name, t]) for value(model[:dwflexiramp_shortfall][sc.name, r.name, t]) for t in 1:instance.time
t in 1:instance.time ] for r in sc.reserves if r.type == "flexiramp"
] for r in instance.reserves if r.type == "flexiramp"
) )
end
if length(instance.scenarios) == 1
return first(values(sol))
else
return sol return sol
end end
end

@ -5,18 +5,18 @@
using JuMP using JuMP
""" """
generate_initial_conditions!(instance, optimizer) generate_initial_conditions!(sc, optimizer)
Generates feasible initial conditions for the given instance, by constructing Generates feasible initial conditions for the given scenario, by constructing
and solving a single-period mixed-integer optimization problem, using the given and solving a single-period mixed-integer optimization problem, using the given
optimizer. The instance is modified in-place. optimizer. The scenario is modified in-place.
""" """
function generate_initial_conditions!( function generate_initial_conditions!(
instance::UnitCommitmentInstance, sc::UnitCommitmentScenario,
optimizer, optimizer,
)::Nothing )::Nothing
G = instance.units G = sc.units
B = instance.buses B = sc.buses
t = 1 t = 1
mip = JuMP.Model(optimizer) mip = JuMP.Model(optimizer)

@ -6,6 +6,7 @@ module XavQiuAhm2021
using Distributions using Distributions
import ..UnitCommitmentInstance import ..UnitCommitmentInstance
import ..UnitCommitmentScenario
""" """
struct Randomization struct Randomization
@ -119,10 +120,10 @@ end
function _randomize_costs( function _randomize_costs(
rng, rng,
instance::UnitCommitmentInstance, sc::UnitCommitmentScenario,
distribution, distribution,
)::Nothing )::Nothing
for unit in instance.units for unit in sc.units
α = rand(rng, distribution) α = rand(rng, distribution)
unit.min_power_cost *= α unit.min_power_cost *= α
for k in unit.cost_segments for k in unit.cost_segments
@ -137,17 +138,15 @@ end
function _randomize_load_share( function _randomize_load_share(
rng, rng,
instance::UnitCommitmentInstance, sc::UnitCommitmentScenario,
distribution, distribution,
)::Nothing )::Nothing
α = rand(rng, distribution, length(instance.buses)) α = rand(rng, distribution, length(sc.buses))
for t in 1:instance.time for t in 1:sc.time
total = sum(bus.load[t] for bus in instance.buses) total = sum(bus.load[t] for bus in sc.buses)
den = sum( den =
bus.load[t] / total * α[i] for sum(bus.load[t] / total * α[i] for (i, bus) in enumerate(sc.buses))
(i, bus) in enumerate(instance.buses) for (i, bus) in enumerate(sc.buses)
)
for (i, bus) in enumerate(instance.buses)
bus.load[t] *= α[i] / den bus.load[t] *= α[i] / den
end end
end end
@ -156,12 +155,12 @@ end
function _randomize_load_profile( function _randomize_load_profile(
rng, rng,
instance::UnitCommitmentInstance, sc::UnitCommitmentScenario,
params::Randomization, params::Randomization,
)::Nothing )::Nothing
# Generate new system load # Generate new system load
system_load = [1.0] system_load = [1.0]
for t in 2:instance.time for t in 2:sc.time
idx = (t - 1) % length(params.load_profile_mu) + 1 idx = (t - 1) % length(params.load_profile_mu) + 1
gamma = rand( gamma = rand(
rng, rng,
@ -169,14 +168,14 @@ function _randomize_load_profile(
) )
push!(system_load, system_load[t-1] * gamma) push!(system_load, system_load[t-1] * gamma)
end end
capacity = sum(maximum(u.max_power) for u in instance.units) capacity = sum(maximum(u.max_power) for u in sc.units)
peak_load = rand(rng, params.peak_load) * capacity peak_load = rand(rng, params.peak_load) * capacity
system_load = system_load ./ maximum(system_load) .* peak_load system_load = system_load ./ maximum(system_load) .* peak_load
# Scale bus loads to match the new system load # Scale bus loads to match the new system load
prev_system_load = sum(b.load for b in instance.buses) prev_system_load = sum(b.load for b in sc.buses)
for b in instance.buses for b in sc.buses
for t in 1:instance.time for t in 1:sc.time
b.load[t] *= system_load[t] / prev_system_load[t] b.load[t] *= system_load[t] / prev_system_load[t]
end end
end end
@ -199,15 +198,26 @@ function randomize!(
instance::UnitCommitment.UnitCommitmentInstance, instance::UnitCommitment.UnitCommitmentInstance,
method::XavQiuAhm2021.Randomization; method::XavQiuAhm2021.Randomization;
rng = MersenneTwister(), rng = MersenneTwister(),
)::Nothing
for sc in instance.scenarios
randomize!(sc, method; rng)
end
return
end
function randomize!(
sc::UnitCommitment.UnitCommitmentScenario,
method::XavQiuAhm2021.Randomization;
rng = MersenneTwister(),
)::Nothing )::Nothing
if method.randomize_costs if method.randomize_costs
XavQiuAhm2021._randomize_costs(rng, instance, method.cost) XavQiuAhm2021._randomize_costs(rng, sc, method.cost)
end end
if method.randomize_load_share if method.randomize_load_share
XavQiuAhm2021._randomize_load_share(rng, instance, method.load_share) XavQiuAhm2021._randomize_load_share(rng, sc, method.load_share)
end end
if method.randomize_load_profile if method.randomize_load_profile
XavQiuAhm2021._randomize_load_profile(rng, instance, method) XavQiuAhm2021._randomize_load_profile(rng, sc, method)
end end
return return
end end

@ -24,11 +24,12 @@ function slice(
)::UnitCommitmentInstance )::UnitCommitmentInstance
modified = deepcopy(instance) modified = deepcopy(instance)
modified.time = length(range) modified.time = length(range)
modified.power_balance_penalty = modified.power_balance_penalty[range] for sc in modified.scenarios
for r in modified.reserves sc.power_balance_penalty = sc.power_balance_penalty[range]
for r in sc.reserves
r.amount = r.amount[range] r.amount = r.amount[range]
end end
for u in modified.units for u in sc.units
u.max_power = u.max_power[range] u.max_power = u.max_power[range]
u.min_power = u.min_power[range] u.min_power = u.min_power[range]
u.must_run = u.must_run[range] u.must_run = u.must_run[range]
@ -38,17 +39,18 @@ function slice(
s.cost = s.cost[range] s.cost = s.cost[range]
end end
end end
for b in modified.buses for b in sc.buses
b.load = b.load[range] b.load = b.load[range]
end end
for l in modified.lines for l in sc.lines
l.normal_flow_limit = l.normal_flow_limit[range] l.normal_flow_limit = l.normal_flow_limit[range]
l.emergency_flow_limit = l.emergency_flow_limit[range] l.emergency_flow_limit = l.emergency_flow_limit[range]
l.flow_limit_penalty = l.flow_limit_penalty[range] l.flow_limit_penalty = l.flow_limit_penalty[range]
end end
for ps in modified.price_sensitive_loads for ps in sc.price_sensitive_loads
ps.demand = ps.demand[range] ps.demand = ps.demand[range]
ps.revenue = ps.revenue[range] ps.revenue = ps.revenue[range]
end end
end
return modified return modified
end end

@ -3,19 +3,19 @@
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
""" """
repair!(instance) repair!(sc)
Verifies that the given unit commitment instance is valid and automatically Verifies that the given unit commitment scenario is valid and automatically
fixes some validation errors if possible, issuing a warning for each error fixes some validation errors if possible, issuing a warning for each error
found. If a validation error cannot be automatically fixed, issues an found. If a validation error cannot be automatically fixed, issues an
exception. exception.
Returns the number of validation errors found. Returns the number of validation errors found.
""" """
function repair!(instance::UnitCommitmentInstance)::Int function repair!(sc::UnitCommitmentScenario)::Int
n_errors = 0 n_errors = 0
for g in instance.units for g in sc.units
# Startup costs and delays must be increasing # Startup costs and delays must be increasing
for s in 2:length(g.startup_categories) for s in 2:length(g.startup_categories)
@ -38,7 +38,7 @@ function repair!(instance::UnitCommitmentInstance)::Int
end end
end end
for t in 1:instance.time for t in 1:sc.time
# Production cost curve should be convex # Production cost curve should be convex
for k in 2:length(g.cost_segments) for k in 2:length(g.cost_segments)
cost = g.cost_segments[k].cost[t] cost = g.cost_segments[k].cost[t]

@ -28,6 +28,8 @@ function validate(
instance::UnitCommitmentInstance, instance::UnitCommitmentInstance,
solution::Union{Dict,OrderedDict}, solution::Union{Dict,OrderedDict},
)::Bool )::Bool
"Production (MW)" keys(solution) ? solution = Dict("s1" => solution) :
nothing
err_count = 0 err_count = 0
err_count += _validate_units(instance, solution) err_count += _validate_units(instance, solution)
err_count += _validate_reserve_and_demand(instance, solution) err_count += _validate_reserve_and_demand(instance, solution)
@ -42,20 +44,23 @@ end
function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01) function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01)
err_count = 0 err_count = 0
for sc in instance.scenarios
for unit in instance.units for unit in sc.units
production = solution["Production (MW)"][unit.name] production = solution[sc.name]["Production (MW)"][unit.name]
reserve = [0.0 for _ in 1:instance.time] reserve = [0.0 for _ in 1:instance.time]
spinning_reserves = [r for r in unit.reserves if r.type == "spinning"] spinning_reserves =
[r for r in unit.reserves if r.type == "spinning"]
if !isempty(spinning_reserves) if !isempty(spinning_reserves)
reserve += sum( reserve += sum(
solution["Spinning reserve (MW)"][r.name][unit.name] for solution[sc.name]["Spinning reserve (MW)"][r.name][unit.name]
r in spinning_reserves for r in spinning_reserves
) )
end end
actual_production_cost = solution["Production cost (\$)"][unit.name] actual_production_cost =
actual_startup_cost = solution["Startup cost (\$)"][unit.name] solution[sc.name]["Production cost (\$)"][unit.name]
is_on = bin(solution["Is on"][unit.name]) actual_startup_cost =
solution[sc.name]["Startup cost (\$)"][unit.name]
is_on = bin(solution[sc.name]["Is on"][unit.name])
for t in 1:instance.time for t in 1:instance.time
# Auxiliary variables # Auxiliary variables
@ -68,7 +73,8 @@ function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01)
else else
is_starting_up = !is_on[t-1] && is_on[t] is_starting_up = !is_on[t-1] && is_on[t]
is_shutting_down = is_on[t-1] && !is_on[t] is_shutting_down = is_on[t-1] && !is_on[t]
ramp_up = max(0, production[t] + reserve[t] - production[t-1]) ramp_up =
max(0, production[t] + reserve[t] - production[t-1])
ramp_down = max(0, production[t-1] - production[t]) ramp_down = max(0, production[t-1] - production[t])
end end
@ -106,10 +112,13 @@ function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01)
end end
# Verify reserve eligibility # Verify reserve eligibility
for r in instance.reserves for r in sc.reserves
if r.type == "spinning" if r.type == "spinning"
if unit r.units && if unit r.units && (
(unit in keys(solution["Spinning reserve (MW)"][r.name])) unit in keys(
solution[sc.name]["Spinning reserve (MW)"][r.name],
)
)
@error @sprintf( @error @sprintf(
"Unit %s is not eligible to provide reserve %s", "Unit %s is not eligible to provide reserve %s",
unit.name, unit.name,
@ -296,28 +305,31 @@ function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01)
end end
end end
end end
end
return err_count return err_count
end end
function _validate_reserve_and_demand(instance, solution, tol = 0.01) function _validate_reserve_and_demand(instance, solution, tol = 0.01)
err_count = 0 err_count = 0
for sc in instance.scenarios
for t in 1:instance.time for t in 1:instance.time
load_curtail = 0 load_curtail = 0
fixed_load = sum(b.load[t] for b in instance.buses) fixed_load = sum(b.load[t] for b in sc.buses)
ps_load = 0 ps_load = 0
if length(instance.price_sensitive_loads) > 0 if length(sc.price_sensitive_loads) > 0
ps_load = sum( ps_load = sum(
solution["Price-sensitive loads (MW)"][ps.name][t] for solution[sc.name]["Price-sensitive loads (MW)"][ps.name][t]
ps in instance.price_sensitive_loads for ps in sc.price_sensitive_loads
) )
end end
production = production = sum(
sum(solution["Production (MW)"][g.name][t] for g in instance.units) solution[sc.name]["Production (MW)"][g.name][t] for
g in sc.units
)
if "Load curtail (MW)" in keys(solution) if "Load curtail (MW)" in keys(solution)
load_curtail = sum( load_curtail = sum(
solution["Load curtail (MW)"][b.name][t] for solution[sc.name]["Load curtail (MW)"][b.name][t] for
b in instance.buses b in sc.buses
) )
end end
balance = fixed_load - load_curtail - production + ps_load balance = fixed_load - load_curtail - production + ps_load
@ -336,14 +348,14 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
end end
# Verify reserves # Verify reserves
for r in instance.reserves for r in sc.reserves
if r.type == "spinning" if r.type == "spinning"
provided = sum( provided = sum(
solution["Spinning reserve (MW)"][r.name][g.name][t] for solution[sc.name]["Spinning reserve (MW)"][r.name][g.name][t]
g in r.units for g in r.units
) )
shortfall = shortfall =
solution["Spinning reserve shortfall (MW)"][r.name][t] solution[sc.name]["Spinning reserve shortfall (MW)"][r.name][t]
required = r.amount[t] required = r.amount[t]
if provided + shortfall < required - tol if provided + shortfall < required - tol
@ -358,11 +370,11 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
end end
elseif r.type == "flexiramp" elseif r.type == "flexiramp"
upflexiramp = sum( upflexiramp = sum(
solution["Up-flexiramp (MW)"][r.name][g.name][t] for solution[sc.name]["Up-flexiramp (MW)"][r.name][g.name][t]
g in r.units for g in r.units
) )
upflexiramp_shortfall = upflexiramp_shortfall =
solution["Up-flexiramp shortfall (MW)"][r.name][t] solution[sc.name]["Up-flexiramp shortfall (MW)"][r.name][t]
if upflexiramp + upflexiramp_shortfall < r.amount[t] - tol if upflexiramp + upflexiramp_shortfall < r.amount[t] - tol
@error @sprintf( @error @sprintf(
@ -376,11 +388,11 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
end end
dwflexiramp = sum( dwflexiramp = sum(
solution["Down-flexiramp (MW)"][r.name][g.name][t] for solution[sc.name]["Down-flexiramp (MW)"][r.name][g.name][t]
g in r.units for g in r.units
) )
dwflexiramp_shortfall = dwflexiramp_shortfall =
solution["Down-flexiramp shortfall (MW)"][r.name][t] solution[sc.name]["Down-flexiramp shortfall (MW)"][r.name][t]
if dwflexiramp + dwflexiramp_shortfall < r.amount[t] - tol if dwflexiramp + dwflexiramp_shortfall < r.amount[t] - tol
@error @sprintf( @error @sprintf(
@ -397,6 +409,7 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
end end
end end
end end
end
return err_count return err_count
end end

@ -6,13 +6,17 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@testset "read v0.2" begin @testset "read v0.2" begin
instance = UnitCommitment.read("$FIXTURES/ucjl-0.2.json.gz") instance = UnitCommitment.read("$FIXTURES/ucjl-0.2.json.gz")
@test length(instance.reserves_by_name["r1"].amount) == 4 @test length(instance.scenarios) == 1
@test instance.units_by_name["g2"].reserves[1].name == "r1" sc = instance.scenarios[1]
@test length(sc.reserves_by_name["r1"].amount) == 4
@test sc.units_by_name["g2"].reserves[1].name == "r1"
end end
@testset "read v0.3" begin @testset "read v0.3" begin
instance = UnitCommitment.read("$FIXTURES/ucjl-0.3.json.gz") instance = UnitCommitment.read("$FIXTURES/ucjl-0.3.json.gz")
@test length(instance.units) == 6 @test length(instance.scenarios) == 1
@test length(instance.buses) == 14 sc = instance.scenarios[1]
@test length(instance.lines) == 20 @test length(sc.units) == 6
@test length(sc.buses) == 14
@test length(sc.lines) == 20
end end

@ -7,42 +7,49 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@testset "read_benchmark" begin @testset "read_benchmark" begin
instance = UnitCommitment.read("$FIXTURES/case14.json.gz") instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
@test length(instance.lines) == 20 @test repr(instance) == (
@test length(instance.buses) == 14 "UnitCommitmentInstance(1 scenarios, 6 units, 14 buses, " *
@test length(instance.units) == 6 "20 lines, 19 contingencies, 1 price sensitive loads, 4 time steps)"
@test length(instance.contingencies) == 19 )
@test length(instance.price_sensitive_loads) == 1
@test length(instance.scenarios) == 1
sc = instance.scenarios[1]
@test length(sc.lines) == 20
@test length(sc.buses) == 14
@test length(sc.units) == 6
@test length(sc.contingencies) == 19
@test length(sc.price_sensitive_loads) == 1
@test instance.time == 4 @test instance.time == 4
@test instance.lines[5].name == "l5" @test sc.lines[5].name == "l5"
@test instance.lines[5].source.name == "b2" @test sc.lines[5].source.name == "b2"
@test instance.lines[5].target.name == "b5" @test sc.lines[5].target.name == "b5"
@test instance.lines[5].reactance 0.17388 @test sc.lines[5].reactance 0.17388
@test instance.lines[5].susceptance 10.037550333 @test sc.lines[5].susceptance 10.037550333
@test instance.lines[5].normal_flow_limit == [1e8 for t in 1:4] @test sc.lines[5].normal_flow_limit == [1e8 for t in 1:4]
@test instance.lines[5].emergency_flow_limit == [1e8 for t in 1:4] @test sc.lines[5].emergency_flow_limit == [1e8 for t in 1:4]
@test instance.lines[5].flow_limit_penalty == [5e3 for t in 1:4] @test sc.lines[5].flow_limit_penalty == [5e3 for t in 1:4]
@test instance.lines_by_name["l5"].name == "l5" @test sc.lines_by_name["l5"].name == "l5"
@test instance.lines[1].name == "l1" @test sc.lines[1].name == "l1"
@test instance.lines[1].source.name == "b1" @test sc.lines[1].source.name == "b1"
@test instance.lines[1].target.name == "b2" @test sc.lines[1].target.name == "b2"
@test instance.lines[1].reactance 0.059170 @test sc.lines[1].reactance 0.059170
@test instance.lines[1].susceptance 29.496860773945 @test sc.lines[1].susceptance 29.496860773945
@test instance.lines[1].normal_flow_limit == [300.0 for t in 1:4] @test sc.lines[1].normal_flow_limit == [300.0 for t in 1:4]
@test instance.lines[1].emergency_flow_limit == [400.0 for t in 1:4] @test sc.lines[1].emergency_flow_limit == [400.0 for t in 1:4]
@test instance.lines[1].flow_limit_penalty == [1e3 for t in 1:4] @test sc.lines[1].flow_limit_penalty == [1e3 for t in 1:4]
@test instance.buses[9].name == "b9" @test sc.buses[9].name == "b9"
@test instance.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353] @test sc.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353]
@test instance.buses_by_name["b9"].name == "b9" @test sc.buses_by_name["b9"].name == "b9"
@test instance.reserves[1].name == "r1" @test sc.reserves[1].name == "r1"
@test instance.reserves[1].type == "spinning" @test sc.reserves[1].type == "spinning"
@test instance.reserves[1].amount == [100.0, 100.0, 100.0, 100.0] @test sc.reserves[1].amount == [100.0, 100.0, 100.0, 100.0]
@test instance.reserves_by_name["r1"].name == "r1" @test sc.reserves_by_name["r1"].name == "r1"
unit = instance.units[1] unit = sc.units[1]
@test unit.name == "g1" @test unit.name == "g1"
@test unit.bus.name == "b1" @test unit.bus.name == "b1"
@test unit.ramp_up_limit == 1e6 @test unit.ramp_up_limit == 1e6
@ -69,14 +76,14 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test unit.startup_categories[2].cost == 1500.0 @test unit.startup_categories[2].cost == 1500.0
@test unit.startup_categories[3].cost == 2000.0 @test unit.startup_categories[3].cost == 2000.0
@test length(unit.reserves) == 0 @test length(unit.reserves) == 0
@test instance.units_by_name["g1"].name == "g1" @test sc.units_by_name["g1"].name == "g1"
unit = instance.units[2] unit = sc.units[2]
@test unit.name == "g2" @test unit.name == "g2"
@test unit.must_run == [false for t in 1:4] @test unit.must_run == [false for t in 1:4]
@test length(unit.reserves) == 1 @test length(unit.reserves) == 1
unit = instance.units[3] unit = sc.units[3]
@test unit.name == "g3" @test unit.name == "g3"
@test unit.bus.name == "b3" @test unit.bus.name == "b3"
@test unit.ramp_up_limit == 70.0 @test unit.ramp_up_limit == 70.0
@ -98,23 +105,23 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test length(unit.reserves) == 1 @test length(unit.reserves) == 1
@test unit.reserves[1].name == "r1" @test unit.reserves[1].name == "r1"
@test instance.contingencies[1].lines == [instance.lines[1]] @test sc.contingencies[1].lines == [sc.lines[1]]
@test instance.contingencies[1].units == [] @test sc.contingencies[1].units == []
@test instance.contingencies[1].name == "c1" @test sc.contingencies[1].name == "c1"
@test instance.contingencies_by_name["c1"].name == "c1" @test sc.contingencies_by_name["c1"].name == "c1"
load = instance.price_sensitive_loads[1] load = sc.price_sensitive_loads[1]
@test load.name == "ps1" @test load.name == "ps1"
@test load.bus.name == "b3" @test load.bus.name == "b3"
@test load.revenue == [100.0 for t in 1:4] @test load.revenue == [100.0 for t in 1:4]
@test load.demand == [50.0 for t in 1:4] @test load.demand == [50.0 for t in 1:4]
@test instance.price_sensitive_loads_by_name["ps1"].name == "ps1" @test sc.price_sensitive_loads_by_name["ps1"].name == "ps1"
end end
@testset "read_benchmark sub-hourly" begin @testset "read_benchmark sub-hourly" begin
instance = UnitCommitment.read("$FIXTURES/case14-sub-hourly.json.gz") instance = UnitCommitment.read("$FIXTURES/case14-sub-hourly.json.gz")
@test instance.time == 4 @test instance.time == 4
unit = instance.units[1] unit = instance.scenarios[1].units[1]
@test unit.name == "g1" @test unit.name == "g1"
@test unit.min_uptime == 2 @test unit.min_uptime == 2
@test unit.min_downtime == 2 @test unit.min_downtime == 2

@ -7,13 +7,13 @@ import UnitCommitment: _Violation, _offer, _query
@testset "_ViolationFilter" begin @testset "_ViolationFilter" begin
instance = UnitCommitment.read("$FIXTURES/case14.json.gz") instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
sc = instance.scenarios[1]
filter = UnitCommitment._ViolationFilter(max_per_line = 1, max_total = 2) filter = UnitCommitment._ViolationFilter(max_per_line = 1, max_total = 2)
_offer( _offer(
filter, filter,
_Violation( _Violation(
time = 1, time = 1,
monitored_line = instance.lines[1], monitored_line = sc.lines[1],
outage_line = nothing, outage_line = nothing,
amount = 100.0, amount = 100.0,
), ),
@ -22,8 +22,8 @@ import UnitCommitment: _Violation, _offer, _query
filter, filter,
_Violation( _Violation(
time = 1, time = 1,
monitored_line = instance.lines[1], monitored_line = sc.lines[1],
outage_line = instance.lines[1], outage_line = sc.lines[1],
amount = 300.0, amount = 300.0,
), ),
) )
@ -31,8 +31,8 @@ import UnitCommitment: _Violation, _offer, _query
filter, filter,
_Violation( _Violation(
time = 1, time = 1,
monitored_line = instance.lines[1], monitored_line = sc.lines[1],
outage_line = instance.lines[5], outage_line = sc.lines[5],
amount = 500.0, amount = 500.0,
), ),
) )
@ -40,8 +40,8 @@ import UnitCommitment: _Violation, _offer, _query
filter, filter,
_Violation( _Violation(
time = 1, time = 1,
monitored_line = instance.lines[1], monitored_line = sc.lines[1],
outage_line = instance.lines[4], outage_line = sc.lines[4],
amount = 400.0, amount = 400.0,
), ),
) )
@ -49,8 +49,8 @@ import UnitCommitment: _Violation, _offer, _query
filter, filter,
_Violation( _Violation(
time = 1, time = 1,
monitored_line = instance.lines[2], monitored_line = sc.lines[2],
outage_line = instance.lines[1], outage_line = sc.lines[1],
amount = 200.0, amount = 200.0,
), ),
) )
@ -58,8 +58,8 @@ import UnitCommitment: _Violation, _offer, _query
filter, filter,
_Violation( _Violation(
time = 1, time = 1,
monitored_line = instance.lines[2], monitored_line = sc.lines[2],
outage_line = instance.lines[8], outage_line = sc.lines[8],
amount = 100.0, amount = 100.0,
), ),
) )
@ -68,14 +68,14 @@ import UnitCommitment: _Violation, _offer, _query
expected = [ expected = [
_Violation( _Violation(
time = 1, time = 1,
monitored_line = instance.lines[2], monitored_line = sc.lines[2],
outage_line = instance.lines[1], outage_line = sc.lines[1],
amount = 200.0, amount = 200.0,
), ),
_Violation( _Violation(
time = 1, time = 1,
monitored_line = instance.lines[1], monitored_line = sc.lines[1],
outage_line = instance.lines[5], outage_line = sc.lines[5],
amount = 500.0, amount = 500.0,
), ),
] ]

@ -7,23 +7,25 @@ import UnitCommitment: _Violation, _offer, _query
@testset "find_violations" begin @testset "find_violations" begin
instance = UnitCommitment.read("$FIXTURES/case14.json.gz") instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
for line in instance.lines, t in 1:instance.time sc = instance.scenarios[1]
for line in sc.lines, t in 1:instance.time
line.normal_flow_limit[t] = 1.0 line.normal_flow_limit[t] = 1.0
line.emergency_flow_limit[t] = 1.0 line.emergency_flow_limit[t] = 1.0
end end
isf = UnitCommitment._injection_shift_factors( isf = UnitCommitment._injection_shift_factors(
lines = instance.lines, lines = sc.lines,
buses = instance.buses, buses = sc.buses,
) )
lodf = UnitCommitment._line_outage_factors( lodf = UnitCommitment._line_outage_factors(
lines = instance.lines, lines = sc.lines,
buses = instance.buses, buses = sc.buses,
isf = isf, isf = isf,
) )
inj = [1000.0 for b in 1:13, t in 1:instance.time] inj = [1000.0 for b in 1:13, t in 1:instance.time]
overflow = [0.0 for l in instance.lines, t in 1:instance.time] overflow = [0.0 for l in sc.lines, t in 1:instance.time]
violations = UnitCommitment._find_violations( violations = UnitCommitment._find_violations(
instance = instance, instance = instance,
sc = sc,
net_injections = inj, net_injections = inj,
overflow = overflow, overflow = overflow,
isf = isf, isf = isf,

@ -6,7 +6,8 @@ using UnitCommitment, Test, LinearAlgebra
@testset "_susceptance_matrix" begin @testset "_susceptance_matrix" begin
instance = UnitCommitment.read("$FIXTURES/case14.json.gz") instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
actual = UnitCommitment._susceptance_matrix(instance.lines) sc = instance.scenarios[1]
actual = UnitCommitment._susceptance_matrix(sc.lines)
@test size(actual) == (20, 20) @test size(actual) == (20, 20)
expected = Diagonal([ expected = Diagonal([
29.5, 29.5,
@ -35,9 +36,10 @@ end
@testset "_reduced_incidence_matrix" begin @testset "_reduced_incidence_matrix" begin
instance = UnitCommitment.read("$FIXTURES/case14.json.gz") instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
sc = instance.scenarios[1]
actual = UnitCommitment._reduced_incidence_matrix( actual = UnitCommitment._reduced_incidence_matrix(
lines = instance.lines, lines = sc.lines,
buses = instance.buses, buses = sc.buses,
) )
@test size(actual) == (20, 13) @test size(actual) == (20, 13)
@test actual[1, 1] == -1.0 @test actual[1, 1] == -1.0
@ -82,9 +84,10 @@ end
@testset "_injection_shift_factors" begin @testset "_injection_shift_factors" begin
instance = UnitCommitment.read("$FIXTURES/case14.json.gz") instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
sc = instance.scenarios[1]
actual = UnitCommitment._injection_shift_factors( actual = UnitCommitment._injection_shift_factors(
lines = instance.lines, lines = sc.lines,
buses = instance.buses, buses = sc.buses,
) )
@test size(actual) == (20, 13) @test size(actual) == (20, 13)
@test round.(actual, digits = 2) == [ @test round.(actual, digits = 2) == [
@ -113,25 +116,26 @@ end
@testset "_line_outage_factors" begin @testset "_line_outage_factors" begin
instance = UnitCommitment.read("$FIXTURES/case14.json.gz") instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
sc = instance.scenarios[1]
isf_before = UnitCommitment._injection_shift_factors( isf_before = UnitCommitment._injection_shift_factors(
lines = instance.lines, lines = sc.lines,
buses = instance.buses, buses = sc.buses,
) )
lodf = UnitCommitment._line_outage_factors( lodf = UnitCommitment._line_outage_factors(
lines = instance.lines, lines = sc.lines,
buses = instance.buses, buses = sc.buses,
isf = isf_before, isf = isf_before,
) )
for contingency in instance.contingencies for contingency in sc.contingencies
for lc in contingency.lines for lc in contingency.lines
prev_susceptance = lc.susceptance prev_susceptance = lc.susceptance
lc.susceptance = 0.0 lc.susceptance = 0.0
isf_after = UnitCommitment._injection_shift_factors( isf_after = UnitCommitment._injection_shift_factors(
lines = instance.lines, lines = sc.lines,
buses = instance.buses, buses = sc.buses,
) )
lc.susceptance = prev_susceptance lc.susceptance = prev_susceptance
for lm in instance.lines for lm in sc.lines
expected = isf_after[lm.offset, :] expected = isf_after[lm.offset, :]
actual = actual =
isf_before[lm.offset, :] + isf_before[lm.offset, :] +

@ -8,18 +8,18 @@ using UnitCommitment, Cbc, JuMP
# Load instance # Load instance
instance = UnitCommitment.read("$FIXTURES/case118-initcond.json.gz") instance = UnitCommitment.read("$FIXTURES/case118-initcond.json.gz")
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
sc = instance.scenarios[1]
# All units should have unknown initial conditions # All units should have unknown initial conditions
for g in instance.units for g in sc.units
@test g.initial_power === nothing @test g.initial_power === nothing
@test g.initial_status === nothing @test g.initial_status === nothing
end end
# Generate initial conditions # Generate initial conditions
UnitCommitment.generate_initial_conditions!(instance, optimizer) UnitCommitment.generate_initial_conditions!(sc, optimizer)
# All units should now have known initial conditions # All units should now have known initial conditions
for g in instance.units for g in sc.units
@test g.initial_power !== nothing @test g.initial_power !== nothing
@test g.initial_status !== nothing @test g.initial_status !== nothing
end end

@ -9,30 +9,31 @@ using Distributions
using Random using Random
using UnitCommitment, Cbc, JuMP using UnitCommitment, Cbc, JuMP
get_instance() = UnitCommitment.read_benchmark("matpower/case118/2017-02-01") function get_scenario()
system_load(instance) = sum(b.load for b in instance.buses) return UnitCommitment.read_benchmark(
"matpower/case118/2017-02-01",
).scenarios[1]
end
system_load(sc) = sum(b.load for b in sc.buses)
test_approx(x, y) = @test isapprox(x, y, atol = 1e-3) test_approx(x, y) = @test isapprox(x, y, atol = 1e-3)
@testset "XavQiuAhm2021" begin @testset "XavQiuAhm2021" begin
@testset "cost and load share" begin @testset "cost and load share" begin
instance = get_instance() sc = get_scenario()
# Check original costs # Check original costs
unit = instance.units[10] unit = sc.units[10]
test_approx(unit.min_power_cost[1], 825.023) test_approx(unit.min_power_cost[1], 825.023)
test_approx(unit.cost_segments[1].cost[1], 36.659) test_approx(unit.cost_segments[1].cost[1], 36.659)
test_approx(unit.startup_categories[1].cost[1], 7570.42) test_approx(unit.startup_categories[1].cost[1], 7570.42)
# Check original load share # Check original load share
bus = instance.buses[1] bus = sc.buses[1]
prev_system_load = system_load(instance) prev_system_load = system_load(sc)
test_approx(bus.load[1] / prev_system_load[1], 0.012) test_approx(bus.load[1] / prev_system_load[1], 0.012)
randomize!( randomize!(
instance, sc,
method = XavQiuAhm2021.Randomization( XavQiuAhm2021.Randomization(randomize_load_profile = false),
randomize_load_profile = false,
),
rng = MersenneTwister(42), rng = MersenneTwister(42),
) )
@ -42,7 +43,7 @@ test_approx(x, y) = @test isapprox(x, y, atol = 1e-3)
test_approx(unit.startup_categories[1].cost[1], 7634.226) test_approx(unit.startup_categories[1].cost[1], 7634.226)
# Check randomized load share # Check randomized load share
curr_system_load = system_load(instance) curr_system_load = system_load(sc)
test_approx(bus.load[1] / curr_system_load[1], 0.013) test_approx(bus.load[1] / curr_system_load[1], 0.013)
# System load should not change # System load should not change
@ -50,20 +51,15 @@ test_approx(x, y) = @test isapprox(x, y, atol = 1e-3)
end end
@testset "load profile" begin @testset "load profile" begin
instance = get_instance() sc = get_scenario()
# Check original load profile # Check original load profile
@test round.(system_load(instance), digits = 1)[1:8] @test round.(system_load(sc), digits = 1)[1:8]
[3059.5, 2983.2, 2937.5, 2953.9, 3073.1, 3356.4, 4068.5, 4018.8] [3059.5, 2983.2, 2937.5, 2953.9, 3073.1, 3356.4, 4068.5, 4018.8]
randomize!( randomize!(sc, XavQiuAhm2021.Randomization(); rng = MersenneTwister(42))
instance,
XavQiuAhm2021.Randomization(),
rng = MersenneTwister(42),
)
# Check randomized load profile # Check randomized load profile
@test round.(system_load(instance), digits = 1)[1:8] @test round.(system_load(sc), digits = 1)[1:8]
[4854.7, 4849.2, 4732.7, 4848.2, 4948.4, 5231.1, 5874.8, 5934.8] [4854.7, 4849.2, 4732.7, 4848.2, 4948.4, 5231.1, 5874.8, 5934.8]
end end
end end

@ -7,12 +7,13 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@testset "slice" begin @testset "slice" begin
instance = UnitCommitment.read("$FIXTURES/case14.json.gz") instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
modified = UnitCommitment.slice(instance, 1:2) modified = UnitCommitment.slice(instance, 1:2)
sc = modified.scenarios[1]
# Should update all time-dependent fields # Should update all time-dependent fields
@test modified.time == 2 @test modified.time == 2
@test length(modified.power_balance_penalty) == 2 @test length(sc.power_balance_penalty) == 2
@test length(modified.reserves_by_name["r1"].amount) == 2 @test length(sc.reserves_by_name["r1"].amount) == 2
for u in modified.units for u in sc.units
@test length(u.max_power) == 2 @test length(u.max_power) == 2
@test length(u.min_power) == 2 @test length(u.min_power) == 2
@test length(u.must_run) == 2 @test length(u.must_run) == 2
@ -22,18 +23,19 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test length(s.cost) == 2 @test length(s.cost) == 2
end end
end end
for b in modified.buses for b in sc.buses
@test length(b.load) == 2 @test length(b.load) == 2
end end
for l in modified.lines for l in sc.lines
@test length(l.normal_flow_limit) == 2 @test length(l.normal_flow_limit) == 2
@test length(l.emergency_flow_limit) == 2 @test length(l.emergency_flow_limit) == 2
@test length(l.flow_limit_penalty) == 2 @test length(l.flow_limit_penalty) == 2
end end
for ps in modified.price_sensitive_loads for ps in sc.price_sensitive_loads
@test length(ps.demand) == 2 @test length(ps.demand) == 2
@test length(ps.revenue) == 2 @test length(ps.revenue) == 2
end end
# Should be able to build model without errors # Should be able to build model without errors
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
model = UnitCommitment.build_model( model = UnitCommitment.build_model(

@ -4,11 +4,19 @@
using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON
@testset "usage" begin function _set_flow_limits!(instance)
instance = UnitCommitment.read("$FIXTURES/case14.json.gz") for sc in instance.scenarios
for line in instance.lines, t in 1:4 sc.power_balance_penalty = [100_000 for _ in 1:instance.time]
for line in sc.lines, t in 1:4
line.normal_flow_limit[t] = 10.0 line.normal_flow_limit[t] = 10.0
end end
end
end
@testset "usage" begin
@testset "deterministic" begin
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
_set_flow_limits!(instance)
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
model = UnitCommitment.build_model( model = UnitCommitment.build_model(
instance = instance, instance = instance,
@ -35,3 +43,20 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON
UnitCommitment.optimize!(model) UnitCommitment.optimize!(model)
@test UnitCommitment.validate(instance, solution) @test UnitCommitment.validate(instance, solution)
end end
@testset "stochastic" begin
instance = UnitCommitment.read([
"$FIXTURES/case14.json.gz",
"$FIXTURES/case14.json.gz",
])
_set_flow_limits!(instance)
@test length(instance.scenarios) == 2
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
model = UnitCommitment.build_model(
instance = instance,
optimizer = optimizer,
)
UnitCommitment.optimize!(model)
solution = UnitCommitment.solution(model)
end
end

@ -16,8 +16,8 @@ end
json = parse_case14() json = parse_case14()
json["Generators"]["g1"]["Production cost curve (MW)"] = [100, 150, 200] json["Generators"]["g1"]["Production cost curve (MW)"] = [100, 150, 200]
json["Generators"]["g1"]["Production cost curve (\$)"] = [10, 25, 30] json["Generators"]["g1"]["Production cost curve (\$)"] = [10, 25, 30]
instance = UnitCommitment._from_json(json, repair = false) sc = UnitCommitment._from_json(json, repair = false)
@test UnitCommitment.repair!(instance) == 4 @test UnitCommitment.repair!(sc) == 4
end end
@testset "Startup limit must be greater than Pmin" begin @testset "Startup limit must be greater than Pmin" begin
@ -25,15 +25,15 @@ end
json["Generators"]["g1"]["Production cost curve (MW)"] = [100, 150] json["Generators"]["g1"]["Production cost curve (MW)"] = [100, 150]
json["Generators"]["g1"]["Production cost curve (\$)"] = [100, 150] json["Generators"]["g1"]["Production cost curve (\$)"] = [100, 150]
json["Generators"]["g1"]["Startup limit (MW)"] = 80 json["Generators"]["g1"]["Startup limit (MW)"] = 80
instance = UnitCommitment._from_json(json, repair = false) sc = UnitCommitment._from_json(json, repair = false)
@test UnitCommitment.repair!(instance) == 1 @test UnitCommitment.repair!(sc) == 1
end end
@testset "Startup costs and delays must be increasing" begin @testset "Startup costs and delays must be increasing" begin
json = parse_case14() json = parse_case14()
json["Generators"]["g1"]["Startup costs (\$)"] = [300, 200, 100] json["Generators"]["g1"]["Startup costs (\$)"] = [300, 200, 100]
json["Generators"]["g1"]["Startup delays (h)"] = [8, 4, 2] json["Generators"]["g1"]["Startup delays (h)"] = [8, 4, 2]
instance = UnitCommitment._from_json(json, repair = false) sc = UnitCommitment._from_json(json, repair = false)
@test UnitCommitment.repair!(instance) == 4 @test UnitCommitment.repair!(sc) == 4
end end
end end

Loading…
Cancel
Save