mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 08:18:51 -06:00
Reorganize files; document some methods
This commit is contained in:
4
Makefile
4
Makefile
@@ -5,11 +5,11 @@
|
||||
JULIA := julia --color=yes --project=@.
|
||||
VERSION := 0.2
|
||||
|
||||
build/sysimage.so: src/sysimage.jl Project.toml Manifest.toml
|
||||
build/sysimage.so: src/utils/sysimage.jl Project.toml Manifest.toml
|
||||
mkdir -p build
|
||||
mkdir -p benchmark/results/test
|
||||
cd benchmark; $(JULIA) --trace-compile=../build/precompile.jl run.jl test/case14.1.sol.json
|
||||
$(JULIA) src/sysimage.jl
|
||||
$(JULIA) src/utils/sysimage.jl
|
||||
|
||||
clean:
|
||||
rm -rf build/*
|
||||
|
||||
@@ -3,12 +3,30 @@
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
module UnitCommitment
|
||||
include("log.jl")
|
||||
include("instance.jl")
|
||||
include("screening.jl")
|
||||
include("model.jl")
|
||||
include("sensitivity.jl")
|
||||
include("validate.jl")
|
||||
include("convert.jl")
|
||||
include("initcond.jl")
|
||||
|
||||
include("instance/structs.jl")
|
||||
include("transmission/structs.jl")
|
||||
include("solution/structs.jl")
|
||||
include("solution/methods/XaQiWaTh19/structs.jl")
|
||||
|
||||
include("import/egret.jl")
|
||||
include("instance/read.jl")
|
||||
include("model/build.jl")
|
||||
include("model/jumpext.jl")
|
||||
include("solution/fix.jl")
|
||||
include("solution/methods/XaQiWaTh19/enforce.jl")
|
||||
include("solution/methods/XaQiWaTh19/filter.jl")
|
||||
include("solution/methods/XaQiWaTh19/find.jl")
|
||||
include("solution/methods/XaQiWaTh19/optimize.jl")
|
||||
include("solution/optimize.jl")
|
||||
include("solution/solution.jl")
|
||||
include("solution/warmstart.jl")
|
||||
include("solution/write.jl")
|
||||
include("transforms/initcond.jl")
|
||||
include("transforms/slice.jl")
|
||||
include("transmission/sensitivity.jl")
|
||||
include("utils/log.jl")
|
||||
include("validation/repair.jl")
|
||||
include("validation/validate.jl")
|
||||
|
||||
end
|
||||
|
||||
@@ -4,16 +4,15 @@
|
||||
|
||||
using DataStructures, JSON, GZip
|
||||
|
||||
function _read_json(path::String)::OrderedDict
|
||||
if endswith(path, ".gz")
|
||||
file = GZip.gzopen(path)
|
||||
else
|
||||
file = open(path)
|
||||
end
|
||||
return JSON.parse(file, dicttype = () -> DefaultOrderedDict(nothing))
|
||||
end
|
||||
"""
|
||||
|
||||
function _read_egret_solution(path::String)::OrderedDict
|
||||
read_egret_solution(path::String)::OrderedDict
|
||||
|
||||
Read a JSON solution file produced by EGRET and transforms it into a
|
||||
dictionary having the same structure as the one produced by
|
||||
UnitCommitment.solution(model).
|
||||
"""
|
||||
function read_egret_solution(path::String)::OrderedDict
|
||||
egret = _read_json(path)
|
||||
T = length(egret["system"]["time_keys"])
|
||||
|
||||
@@ -8,104 +8,35 @@ using DataStructures
|
||||
using GZip
|
||||
import Base: getindex, time
|
||||
|
||||
mutable struct Bus
|
||||
name::String
|
||||
offset::Int
|
||||
load::Vector{Float64}
|
||||
units::Vector
|
||||
price_sensitive_loads::Vector
|
||||
end
|
||||
"""
|
||||
read_benchmark(name::AbstractString)::UnitCommitmentInstance
|
||||
|
||||
mutable struct CostSegment
|
||||
mw::Vector{Float64}
|
||||
cost::Vector{Float64}
|
||||
end
|
||||
Read one of the benchmark unit commitment instances included in the package.
|
||||
See "Instances" section of the documentation for the entire list of benchmark
|
||||
instances available.
|
||||
|
||||
mutable struct StartupCategory
|
||||
delay::Int
|
||||
cost::Float64
|
||||
end
|
||||
|
||||
mutable struct Unit
|
||||
name::String
|
||||
bus::Bus
|
||||
max_power::Vector{Float64}
|
||||
min_power::Vector{Float64}
|
||||
must_run::Vector{Bool}
|
||||
min_power_cost::Vector{Float64}
|
||||
cost_segments::Vector{CostSegment}
|
||||
min_uptime::Int
|
||||
min_downtime::Int
|
||||
ramp_up_limit::Float64
|
||||
ramp_down_limit::Float64
|
||||
startup_limit::Float64
|
||||
shutdown_limit::Float64
|
||||
initial_status::Union{Int,Nothing}
|
||||
initial_power::Union{Float64,Nothing}
|
||||
provides_spinning_reserves::Vector{Bool}
|
||||
startup_categories::Vector{StartupCategory}
|
||||
end
|
||||
|
||||
mutable struct TransmissionLine
|
||||
name::String
|
||||
offset::Int
|
||||
source::Bus
|
||||
target::Bus
|
||||
reactance::Float64
|
||||
susceptance::Float64
|
||||
normal_flow_limit::Vector{Float64}
|
||||
emergency_flow_limit::Vector{Float64}
|
||||
flow_limit_penalty::Vector{Float64}
|
||||
end
|
||||
|
||||
mutable struct Reserves
|
||||
spinning::Vector{Float64}
|
||||
end
|
||||
|
||||
mutable struct Contingency
|
||||
name::String
|
||||
lines::Vector{TransmissionLine}
|
||||
units::Vector{Unit}
|
||||
end
|
||||
|
||||
mutable struct PriceSensitiveLoad
|
||||
name::String
|
||||
bus::Bus
|
||||
demand::Vector{Float64}
|
||||
revenue::Vector{Float64}
|
||||
end
|
||||
|
||||
mutable struct UnitCommitmentInstance
|
||||
time::Int
|
||||
power_balance_penalty::Vector{Float64}
|
||||
units::Vector{Unit}
|
||||
buses::Vector{Bus}
|
||||
lines::Vector{TransmissionLine}
|
||||
reserves::Reserves
|
||||
contingencies::Vector{Contingency}
|
||||
price_sensitive_loads::Vector{PriceSensitiveLoad}
|
||||
end
|
||||
|
||||
function Base.show(io::IO, instance::UnitCommitmentInstance)
|
||||
print(io, "UnitCommitmentInstance(")
|
||||
print(io, "$(length(instance.units)) units, ")
|
||||
print(io, "$(length(instance.buses)) buses, ")
|
||||
print(io, "$(length(instance.lines)) lines, ")
|
||||
print(io, "$(length(instance.contingencies)) contingencies, ")
|
||||
print(
|
||||
io,
|
||||
"$(length(instance.price_sensitive_loads)) price sensitive loads, ",
|
||||
)
|
||||
print(io, "$(instance.time) time steps")
|
||||
print(io, ")")
|
||||
return
|
||||
end
|
||||
Example
|
||||
-------
|
||||
|
||||
import UnitCommitment
|
||||
instance = UnitCommitment.read_benchmark("matpower/case3375wp/2017-02-01")
|
||||
"""
|
||||
function read_benchmark(name::AbstractString)::UnitCommitmentInstance
|
||||
basedir = dirname(@__FILE__)
|
||||
return UnitCommitment.read("$basedir/../instances/$name.json.gz")
|
||||
return UnitCommitment.read("$basedir/../../instances/$name.json.gz")
|
||||
end
|
||||
|
||||
"""
|
||||
read(path::AbstractString)::UnitCommitmentInstance
|
||||
|
||||
Read a unit commitment instance from a file. The file may be gzipped.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
import UnitCommitment
|
||||
instance = UnitCommitment.read("/path/to/input.json.gz")
|
||||
"""
|
||||
function read(path::AbstractString)::UnitCommitmentInstance
|
||||
if endswith(path, ".gz")
|
||||
return _read(gzopen(path))
|
||||
@@ -120,6 +51,15 @@ function _read(file::IO)::UnitCommitmentInstance
|
||||
)
|
||||
end
|
||||
|
||||
function _read_json(path::String)::OrderedDict
|
||||
if endswith(path, ".gz")
|
||||
file = GZip.gzopen(path)
|
||||
else
|
||||
file = open(path)
|
||||
end
|
||||
return JSON.parse(file, dicttype = () -> DefaultOrderedDict(nothing))
|
||||
end
|
||||
|
||||
function _from_json(json; repair = true)
|
||||
units = Unit[]
|
||||
buses = Bus[]
|
||||
@@ -336,54 +276,3 @@ function _from_json(json; repair = true)
|
||||
end
|
||||
return instance
|
||||
end
|
||||
|
||||
"""
|
||||
slice(instance, range)
|
||||
|
||||
Creates a new instance, with only a subset of the time periods.
|
||||
This function does not modify the provided instance. The initial
|
||||
conditions are also not modified.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
# Build a 2-hour UC instance
|
||||
instance = UnitCommitment.read_benchmark("test/case14")
|
||||
modified = UnitCommitment.slice(instance, 1:2)
|
||||
|
||||
"""
|
||||
function slice(
|
||||
instance::UnitCommitmentInstance,
|
||||
range::UnitRange{Int},
|
||||
)::UnitCommitmentInstance
|
||||
modified = deepcopy(instance)
|
||||
modified.time = length(range)
|
||||
modified.power_balance_penalty = modified.power_balance_penalty[range]
|
||||
modified.reserves.spinning = modified.reserves.spinning[range]
|
||||
for u in modified.units
|
||||
u.max_power = u.max_power[range]
|
||||
u.min_power = u.min_power[range]
|
||||
u.must_run = u.must_run[range]
|
||||
u.min_power_cost = u.min_power_cost[range]
|
||||
u.provides_spinning_reserves = u.provides_spinning_reserves[range]
|
||||
for s in u.cost_segments
|
||||
s.mw = s.mw[range]
|
||||
s.cost = s.cost[range]
|
||||
end
|
||||
end
|
||||
for b in modified.buses
|
||||
b.load = b.load[range]
|
||||
end
|
||||
for l in modified.lines
|
||||
l.normal_flow_limit = l.normal_flow_limit[range]
|
||||
l.emergency_flow_limit = l.emergency_flow_limit[range]
|
||||
l.flow_limit_penalty = l.flow_limit_penalty[range]
|
||||
end
|
||||
for ps in modified.price_sensitive_loads
|
||||
ps.demand = ps.demand[range]
|
||||
ps.revenue = ps.revenue[range]
|
||||
end
|
||||
return modified
|
||||
end
|
||||
|
||||
export UnitCommitmentInstance
|
||||
98
src/instance/structs.jl
Normal file
98
src/instance/structs.jl
Normal file
@@ -0,0 +1,98 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
mutable struct Bus
|
||||
name::String
|
||||
offset::Int
|
||||
load::Vector{Float64}
|
||||
units::Vector
|
||||
price_sensitive_loads::Vector
|
||||
end
|
||||
|
||||
mutable struct CostSegment
|
||||
mw::Vector{Float64}
|
||||
cost::Vector{Float64}
|
||||
end
|
||||
|
||||
mutable struct StartupCategory
|
||||
delay::Int
|
||||
cost::Float64
|
||||
end
|
||||
|
||||
mutable struct Unit
|
||||
name::String
|
||||
bus::Bus
|
||||
max_power::Vector{Float64}
|
||||
min_power::Vector{Float64}
|
||||
must_run::Vector{Bool}
|
||||
min_power_cost::Vector{Float64}
|
||||
cost_segments::Vector{CostSegment}
|
||||
min_uptime::Int
|
||||
min_downtime::Int
|
||||
ramp_up_limit::Float64
|
||||
ramp_down_limit::Float64
|
||||
startup_limit::Float64
|
||||
shutdown_limit::Float64
|
||||
initial_status::Union{Int,Nothing}
|
||||
initial_power::Union{Float64,Nothing}
|
||||
provides_spinning_reserves::Vector{Bool}
|
||||
startup_categories::Vector{StartupCategory}
|
||||
end
|
||||
|
||||
mutable struct TransmissionLine
|
||||
name::String
|
||||
offset::Int
|
||||
source::Bus
|
||||
target::Bus
|
||||
reactance::Float64
|
||||
susceptance::Float64
|
||||
normal_flow_limit::Vector{Float64}
|
||||
emergency_flow_limit::Vector{Float64}
|
||||
flow_limit_penalty::Vector{Float64}
|
||||
end
|
||||
|
||||
mutable struct Reserves
|
||||
spinning::Vector{Float64}
|
||||
end
|
||||
|
||||
mutable struct Contingency
|
||||
name::String
|
||||
lines::Vector{TransmissionLine}
|
||||
units::Vector{Unit}
|
||||
end
|
||||
|
||||
mutable struct PriceSensitiveLoad
|
||||
name::String
|
||||
bus::Bus
|
||||
demand::Vector{Float64}
|
||||
revenue::Vector{Float64}
|
||||
end
|
||||
|
||||
mutable struct UnitCommitmentInstance
|
||||
time::Int
|
||||
power_balance_penalty::Vector{Float64}
|
||||
units::Vector{Unit}
|
||||
buses::Vector{Bus}
|
||||
lines::Vector{TransmissionLine}
|
||||
reserves::Reserves
|
||||
contingencies::Vector{Contingency}
|
||||
price_sensitive_loads::Vector{PriceSensitiveLoad}
|
||||
end
|
||||
|
||||
function Base.show(io::IO, instance::UnitCommitmentInstance)
|
||||
print(io, "UnitCommitmentInstance(")
|
||||
print(io, "$(length(instance.units)) units, ")
|
||||
print(io, "$(length(instance.buses)) buses, ")
|
||||
print(io, "$(length(instance.lines)) lines, ")
|
||||
print(io, "$(length(instance.contingencies)) contingencies, ")
|
||||
print(
|
||||
io,
|
||||
"$(length(instance.price_sensitive_loads)) price sensitive loads, ",
|
||||
)
|
||||
print(io, "$(instance.time) time steps")
|
||||
print(io, ")")
|
||||
return
|
||||
end
|
||||
|
||||
export UnitCommitmentInstance
|
||||
@@ -5,23 +5,56 @@
|
||||
using JuMP, MathOptInterface, DataStructures
|
||||
import JuMP: value, fix, set_name
|
||||
|
||||
# Extend some JuMP functions so that decision variables can be safely replaced by
|
||||
# (constant) floating point numbers.
|
||||
function value(x::Float64)
|
||||
return x
|
||||
end
|
||||
"""
|
||||
function build_model(;
|
||||
instance::UnitCommitmentInstance,
|
||||
isf::Union{Matrix{Float64},Nothing} = nothing,
|
||||
lodf::Union{Matrix{Float64},Nothing} = nothing,
|
||||
isf_cutoff::Float64 = 0.005,
|
||||
lodf_cutoff::Float64 = 0.001,
|
||||
optimizer = nothing,
|
||||
variable_names::Bool = false,
|
||||
)::JuMP.Model
|
||||
|
||||
function fix(x::Float64, v::Float64; force)
|
||||
return abs(x - v) < 1e-6 || error("Value mismatch: $x != $v")
|
||||
end
|
||||
Build the JuMP model corresponding to the given unit commitment instance.
|
||||
|
||||
function set_name(x::Float64, n::String)
|
||||
# nop
|
||||
end
|
||||
Arguments
|
||||
=========
|
||||
- `instance::UnitCommitmentInstance`:
|
||||
the instance.
|
||||
- `isf::Union{Matrix{Float64},Nothing} = nothing`:
|
||||
the injection shift factors matrix. If not provided, it will be computed.
|
||||
- `lodf::Union{Matrix{Float64},Nothing} = nothing`:
|
||||
the line outage distribution factors matrix. If not provided, it will be
|
||||
computed.
|
||||
- `isf_cutoff::Float64 = 0.005`:
|
||||
the cutoff that should be applied to the ISF matrix. Entries with magnitude
|
||||
smaller than this value will be set to zero.
|
||||
- `lodf_cutoff::Float64 = 0.001`:
|
||||
the cutoff that should be applied to the LODF matrix. Entries with magnitude
|
||||
smaller than this value will be set to zero.
|
||||
- `optimizer = nothing`:
|
||||
the optimizer factory that should be attached to this model (e.g. Cbc.Optimizer).
|
||||
If not provided, no optimizer will be attached.
|
||||
- `variable_names::Bool = false`:
|
||||
If true, set variable and constraint names. Important if the model is going
|
||||
to be exported to an MPS file. For large models, this can take significant
|
||||
time, so it's disabled by default.
|
||||
|
||||
Example
|
||||
=======
|
||||
```jldoctest
|
||||
julia> import Cbc, UnitCommitment
|
||||
julia> instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
||||
julia> model = UnitCommitment.build_model(
|
||||
instance=instance,
|
||||
optimizer=Cbc.Optimizer,
|
||||
variable_names=true,
|
||||
)
|
||||
```
|
||||
"""
|
||||
function build_model(;
|
||||
filename::Union{String,Nothing} = nothing,
|
||||
instance::Union{UnitCommitmentInstance,Nothing} = nothing,
|
||||
instance::UnitCommitmentInstance,
|
||||
isf::Union{Matrix{Float64},Nothing} = nothing,
|
||||
lodf::Union{Matrix{Float64},Nothing} = nothing,
|
||||
isf_cutoff::Float64 = 0.005,
|
||||
@@ -29,18 +62,6 @@ function build_model(;
|
||||
optimizer = nothing,
|
||||
variable_names::Bool = false,
|
||||
)::JuMP.Model
|
||||
if (filename === nothing) && (instance === nothing)
|
||||
error("Either filename or instance must be specified")
|
||||
end
|
||||
|
||||
if filename !== nothing
|
||||
@info "Reading: $(filename)"
|
||||
time_read = @elapsed begin
|
||||
instance = UnitCommitment.read(filename)
|
||||
end
|
||||
@info @sprintf("Read problem in %.2f seconds", time_read)
|
||||
end
|
||||
|
||||
if length(instance.buses) == 1
|
||||
isf = zeros(0, 0)
|
||||
lodf = zeros(0, 0)
|
||||
@@ -473,71 +494,6 @@ function _build_reserve_eqs!(model::JuMP.Model)
|
||||
end
|
||||
end
|
||||
|
||||
function _enforce_transmission(;
|
||||
model::JuMP.Model,
|
||||
violation::Violation,
|
||||
isf::Matrix{Float64},
|
||||
lodf::Matrix{Float64},
|
||||
)::Nothing
|
||||
instance = model[:instance]
|
||||
limit::Float64 = 0.0
|
||||
overflow = model[:overflow]
|
||||
net_injection = model[:net_injection]
|
||||
|
||||
if violation.outage_line === nothing
|
||||
limit = violation.monitored_line.normal_flow_limit[violation.time]
|
||||
@info @sprintf(
|
||||
" %8.3f MW overflow in %-5s time %3d (pre-contingency)",
|
||||
violation.amount,
|
||||
violation.monitored_line.name,
|
||||
violation.time,
|
||||
)
|
||||
else
|
||||
limit = violation.monitored_line.emergency_flow_limit[violation.time]
|
||||
@info @sprintf(
|
||||
" %8.3f MW overflow in %-5s time %3d (outage: line %s)",
|
||||
violation.amount,
|
||||
violation.monitored_line.name,
|
||||
violation.time,
|
||||
violation.outage_line.name,
|
||||
)
|
||||
end
|
||||
|
||||
fm = violation.monitored_line.name
|
||||
t = violation.time
|
||||
flow = @variable(model, base_name = "flow[$fm,$t]")
|
||||
|
||||
v = overflow[violation.monitored_line.name, violation.time]
|
||||
@constraint(model, flow <= limit + v)
|
||||
@constraint(model, -flow <= limit + v)
|
||||
|
||||
if violation.outage_line === nothing
|
||||
@constraint(
|
||||
model,
|
||||
flow == sum(
|
||||
net_injection[b.name, violation.time] *
|
||||
isf[violation.monitored_line.offset, b.offset] for
|
||||
b in instance.buses if b.offset > 0
|
||||
)
|
||||
)
|
||||
else
|
||||
@constraint(
|
||||
model,
|
||||
flow == sum(
|
||||
net_injection[b.name, violation.time] * (
|
||||
isf[violation.monitored_line.offset, b.offset] + (
|
||||
lodf[
|
||||
violation.monitored_line.offset,
|
||||
violation.outage_line.offset,
|
||||
] * isf[violation.outage_line.offset, b.offset]
|
||||
)
|
||||
) for b in instance.buses if b.offset > 0
|
||||
)
|
||||
)
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
function _set_names!(model::JuMP.Model)
|
||||
@info "Setting variable and constraint names..."
|
||||
time_varnames = @elapsed begin
|
||||
@@ -558,230 +514,3 @@ function _set_names!(dict::Dict)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function solution(model::JuMP.Model)
|
||||
instance, T = model[:instance], model[:instance].time
|
||||
function timeseries(vars, collection)
|
||||
return OrderedDict(
|
||||
b.name => [round(value(vars[b.name, t]), digits = 5) for t in 1:T]
|
||||
for b in collection
|
||||
)
|
||||
end
|
||||
function production_cost(g)
|
||||
return [
|
||||
value(model[:is_on][g.name, t]) * g.min_power_cost[t] + sum(
|
||||
Float64[
|
||||
value(model[:segprod][g.name, t, k]) *
|
||||
g.cost_segments[k].cost[t] for
|
||||
k in 1:length(g.cost_segments)
|
||||
],
|
||||
) for t in 1:T
|
||||
]
|
||||
end
|
||||
function production(g)
|
||||
return [
|
||||
value(model[:is_on][g.name, t]) * g.min_power[t] + sum(
|
||||
Float64[
|
||||
value(model[:segprod][g.name, t, k]) for
|
||||
k in 1:length(g.cost_segments)
|
||||
],
|
||||
) for t in 1:T
|
||||
]
|
||||
end
|
||||
function startup_cost(g)
|
||||
S = length(g.startup_categories)
|
||||
return [
|
||||
sum(
|
||||
g.startup_categories[s].cost *
|
||||
value(model[:startup][g.name, t, s]) for s in 1:S
|
||||
) for t in 1:T
|
||||
]
|
||||
end
|
||||
sol = OrderedDict()
|
||||
sol["Production (MW)"] =
|
||||
OrderedDict(g.name => production(g) for g in instance.units)
|
||||
sol["Production cost (\$)"] =
|
||||
OrderedDict(g.name => production_cost(g) for g in instance.units)
|
||||
sol["Startup cost (\$)"] =
|
||||
OrderedDict(g.name => startup_cost(g) for g in instance.units)
|
||||
sol["Is on"] = timeseries(model[:is_on], instance.units)
|
||||
sol["Switch on"] = timeseries(model[:switch_on], instance.units)
|
||||
sol["Switch off"] = timeseries(model[:switch_off], instance.units)
|
||||
sol["Reserve (MW)"] = timeseries(model[:reserve], instance.units)
|
||||
sol["Net injection (MW)"] =
|
||||
timeseries(model[:net_injection], instance.buses)
|
||||
sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses)
|
||||
if !isempty(instance.lines)
|
||||
sol["Line overflow (MW)"] = timeseries(model[:overflow], instance.lines)
|
||||
end
|
||||
if !isempty(instance.price_sensitive_loads)
|
||||
sol["Price-sensitive loads (MW)"] =
|
||||
timeseries(model[:loads], instance.price_sensitive_loads)
|
||||
end
|
||||
return sol
|
||||
end
|
||||
|
||||
function write(filename::AbstractString, solution::AbstractDict)::Nothing
|
||||
open(filename, "w") do file
|
||||
return JSON.print(file, solution, 2)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing
|
||||
instance, T = model[:instance], model[:instance].time
|
||||
is_on = model[:is_on]
|
||||
prod_above = model[:prod_above]
|
||||
reserve = model[:reserve]
|
||||
for g in instance.units
|
||||
for t in 1:T
|
||||
is_on_value = round(solution["Is on"][g.name][t])
|
||||
production_value =
|
||||
round(solution["Production (MW)"][g.name][t], digits = 5)
|
||||
reserve_value =
|
||||
round(solution["Reserve (MW)"][g.name][t], digits = 5)
|
||||
JuMP.fix(is_on[g.name, t], is_on_value, force = true)
|
||||
JuMP.fix(
|
||||
prod_above[g.name, t],
|
||||
production_value - is_on_value * g.min_power[t],
|
||||
force = true,
|
||||
)
|
||||
JuMP.fix(reserve[g.name, t], reserve_value, force = true)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
function set_warm_start!(model::JuMP.Model, solution::AbstractDict)::Nothing
|
||||
instance, T = model[:instance], model[:instance].time
|
||||
is_on = model[:is_on]
|
||||
prod_above = model[:prod_above]
|
||||
reserve = model[:reserve]
|
||||
for g in instance.units
|
||||
for t in 1:T
|
||||
JuMP.set_start_value(is_on[g.name, t], solution["Is on"][g.name][t])
|
||||
JuMP.set_start_value(
|
||||
switch_on[g.name, t],
|
||||
solution["Switch on"][g.name][t],
|
||||
)
|
||||
JuMP.set_start_value(
|
||||
switch_off[g.name, t],
|
||||
solution["Switch off"][g.name][t],
|
||||
)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
function optimize!(
|
||||
model::JuMP.Model;
|
||||
time_limit = 3600,
|
||||
gap_limit = 1e-4,
|
||||
two_phase_gap = true,
|
||||
)::Nothing
|
||||
function set_gap(gap)
|
||||
try
|
||||
JuMP.set_optimizer_attribute(model, "MIPGap", gap)
|
||||
@info @sprintf("MIP gap tolerance set to %f", gap)
|
||||
catch
|
||||
@warn "Could not change MIP gap tolerance"
|
||||
end
|
||||
end
|
||||
|
||||
instance = model[:instance]
|
||||
initial_time = time()
|
||||
|
||||
large_gap = false
|
||||
has_transmission = (length(model[:isf]) > 0)
|
||||
|
||||
if has_transmission && two_phase_gap
|
||||
set_gap(1e-2)
|
||||
large_gap = true
|
||||
else
|
||||
set_gap(gap_limit)
|
||||
end
|
||||
|
||||
while true
|
||||
time_elapsed = time() - initial_time
|
||||
time_remaining = time_limit - time_elapsed
|
||||
if time_remaining < 0
|
||||
@info "Time limit exceeded"
|
||||
break
|
||||
end
|
||||
|
||||
@info @sprintf(
|
||||
"Setting MILP time limit to %.2f seconds",
|
||||
time_remaining
|
||||
)
|
||||
JuMP.set_time_limit_sec(model, time_remaining)
|
||||
|
||||
@info "Solving MILP..."
|
||||
JuMP.optimize!(model)
|
||||
|
||||
has_transmission || break
|
||||
|
||||
violations = _find_violations(model)
|
||||
if isempty(violations)
|
||||
@info "No violations found"
|
||||
if large_gap
|
||||
large_gap = false
|
||||
set_gap(gap_limit)
|
||||
else
|
||||
break
|
||||
end
|
||||
else
|
||||
_enforce_transmission(model, violations)
|
||||
end
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
function _find_violations(model::JuMP.Model)
|
||||
instance = model[:instance]
|
||||
net_injection = model[:net_injection]
|
||||
overflow = model[:overflow]
|
||||
length(instance.buses) > 1 || return []
|
||||
violations = []
|
||||
@info "Verifying transmission limits..."
|
||||
time_screening = @elapsed begin
|
||||
non_slack_buses = [b for b in instance.buses if b.offset > 0]
|
||||
net_injection_values = [
|
||||
value(net_injection[b.name, t]) for b in non_slack_buses,
|
||||
t in 1:instance.time
|
||||
]
|
||||
overflow_values = [
|
||||
value(overflow[lm.name, t]) for lm in instance.lines,
|
||||
t in 1:instance.time
|
||||
]
|
||||
violations = UnitCommitment._find_violations(
|
||||
instance = instance,
|
||||
net_injections = net_injection_values,
|
||||
overflow = overflow_values,
|
||||
isf = model[:isf],
|
||||
lodf = model[:lodf],
|
||||
)
|
||||
end
|
||||
@info @sprintf(
|
||||
"Verified transmission limits in %.2f seconds",
|
||||
time_screening
|
||||
)
|
||||
return violations
|
||||
end
|
||||
|
||||
function _enforce_transmission(
|
||||
model::JuMP.Model,
|
||||
violations::Vector{Violation},
|
||||
)::Nothing
|
||||
for v in violations
|
||||
_enforce_transmission(
|
||||
model = model,
|
||||
violation = v,
|
||||
isf = model[:isf],
|
||||
lodf = model[:lodf],
|
||||
)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
export build_model
|
||||
20
src/model/jumpext.jl
Normal file
20
src/model/jumpext.jl
Normal file
@@ -0,0 +1,20 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
# This file extends some JuMP functions so that decision variables can be safely
|
||||
# replaced by (constant) floating point numbers.
|
||||
|
||||
import JuMP: value, fix, set_name
|
||||
|
||||
function value(x::Float64)
|
||||
return x
|
||||
end
|
||||
|
||||
function fix(x::Float64, v::Float64; force)
|
||||
return abs(x - v) < 1e-6 || error("Value mismatch: $x != $v")
|
||||
end
|
||||
|
||||
function set_name(x::Float64, n::String)
|
||||
# nop
|
||||
end
|
||||
33
src/solution/fix.jl
Normal file
33
src/solution/fix.jl
Normal file
@@ -0,0 +1,33 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
fix!(model::JuMP.Model, solution::AbstractDict)::Nothing
|
||||
|
||||
Fix the value of all binary variables to the ones specified by the given
|
||||
solution. Useful for computing LMPs.
|
||||
"""
|
||||
function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing
|
||||
instance, T = model[:instance], model[:instance].time
|
||||
is_on = model[:is_on]
|
||||
prod_above = model[:prod_above]
|
||||
reserve = model[:reserve]
|
||||
for g in instance.units
|
||||
for t in 1:T
|
||||
is_on_value = round(solution["Is on"][g.name][t])
|
||||
production_value =
|
||||
round(solution["Production (MW)"][g.name][t], digits = 5)
|
||||
reserve_value =
|
||||
round(solution["Reserve (MW)"][g.name][t], digits = 5)
|
||||
JuMP.fix(is_on[g.name, t], is_on_value, force = true)
|
||||
JuMP.fix(
|
||||
prod_above[g.name, t],
|
||||
production_value - is_on_value * g.min_power[t],
|
||||
force = true,
|
||||
)
|
||||
JuMP.fix(reserve[g.name, t], reserve_value, force = true)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
83
src/solution/methods/XaQiWaTh19/enforce.jl
Normal file
83
src/solution/methods/XaQiWaTh19/enforce.jl
Normal file
@@ -0,0 +1,83 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
function _enforce_transmission(
|
||||
model::JuMP.Model,
|
||||
violations::Vector{_Violation},
|
||||
)::Nothing
|
||||
for v in violations
|
||||
_enforce_transmission(
|
||||
model = model,
|
||||
violation = v,
|
||||
isf = model[:isf],
|
||||
lodf = model[:lodf],
|
||||
)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
function _enforce_transmission(;
|
||||
model::JuMP.Model,
|
||||
violation::_Violation,
|
||||
isf::Matrix{Float64},
|
||||
lodf::Matrix{Float64},
|
||||
)::Nothing
|
||||
instance = model[:instance]
|
||||
limit::Float64 = 0.0
|
||||
overflow = model[:overflow]
|
||||
net_injection = model[:net_injection]
|
||||
|
||||
if violation.outage_line === nothing
|
||||
limit = violation.monitored_line.normal_flow_limit[violation.time]
|
||||
@info @sprintf(
|
||||
" %8.3f MW overflow in %-5s time %3d (pre-contingency)",
|
||||
violation.amount,
|
||||
violation.monitored_line.name,
|
||||
violation.time,
|
||||
)
|
||||
else
|
||||
limit = violation.monitored_line.emergency_flow_limit[violation.time]
|
||||
@info @sprintf(
|
||||
" %8.3f MW overflow in %-5s time %3d (outage: line %s)",
|
||||
violation.amount,
|
||||
violation.monitored_line.name,
|
||||
violation.time,
|
||||
violation.outage_line.name,
|
||||
)
|
||||
end
|
||||
|
||||
fm = violation.monitored_line.name
|
||||
t = violation.time
|
||||
flow = @variable(model, base_name = "flow[$fm,$t]")
|
||||
|
||||
v = overflow[violation.monitored_line.name, violation.time]
|
||||
@constraint(model, flow <= limit + v)
|
||||
@constraint(model, -flow <= limit + v)
|
||||
|
||||
if violation.outage_line === nothing
|
||||
@constraint(
|
||||
model,
|
||||
flow == sum(
|
||||
net_injection[b.name, violation.time] *
|
||||
isf[violation.monitored_line.offset, b.offset] for
|
||||
b in instance.buses if b.offset > 0
|
||||
)
|
||||
)
|
||||
else
|
||||
@constraint(
|
||||
model,
|
||||
flow == sum(
|
||||
net_injection[b.name, violation.time] * (
|
||||
isf[violation.monitored_line.offset, b.offset] + (
|
||||
lodf[
|
||||
violation.monitored_line.offset,
|
||||
violation.outage_line.offset,
|
||||
] * isf[violation.outage_line.offset, b.offset]
|
||||
)
|
||||
) for b in instance.buses if b.offset > 0
|
||||
)
|
||||
)
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
44
src/solution/methods/XaQiWaTh19/filter.jl
Normal file
44
src/solution/methods/XaQiWaTh19/filter.jl
Normal file
@@ -0,0 +1,44 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
function _offer(filter::_ViolationFilter, v::_Violation)::Nothing
|
||||
if v.monitored_line.offset ∉ keys(filter.queues)
|
||||
filter.queues[v.monitored_line.offset] =
|
||||
PriorityQueue{_Violation,Float64}()
|
||||
end
|
||||
q::PriorityQueue{_Violation,Float64} =
|
||||
filter.queues[v.monitored_line.offset]
|
||||
if length(q) < filter.max_per_line
|
||||
enqueue!(q, v => v.amount)
|
||||
else
|
||||
if v.amount > peek(q)[1].amount
|
||||
dequeue!(q)
|
||||
enqueue!(q, v => v.amount)
|
||||
end
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
function _query(filter::_ViolationFilter)::Array{_Violation,1}
|
||||
violations = Array{_Violation,1}()
|
||||
time_queue = PriorityQueue{_Violation,Float64}()
|
||||
for l in keys(filter.queues)
|
||||
line_queue = filter.queues[l]
|
||||
while length(line_queue) > 0
|
||||
v = dequeue!(line_queue)
|
||||
if length(time_queue) < filter.max_total
|
||||
enqueue!(time_queue, v => v.amount)
|
||||
else
|
||||
if v.amount > peek(time_queue)[1].amount
|
||||
dequeue!(time_queue)
|
||||
enqueue!(time_queue, v => v.amount)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
while length(time_queue) > 0
|
||||
violations = [violations; dequeue!(time_queue)]
|
||||
end
|
||||
return violations
|
||||
end
|
||||
@@ -1,91 +1,56 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
# Copyright (C) 2019 Argonne National Laboratory
|
||||
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
||||
|
||||
using DataStructures
|
||||
using Base.Threads
|
||||
import Base.Threads: @threads
|
||||
|
||||
struct Violation
|
||||
time::Int
|
||||
monitored_line::TransmissionLine
|
||||
outage_line::Union{TransmissionLine,Nothing}
|
||||
amount::Float64 # Violation amount (in MW)
|
||||
end
|
||||
|
||||
function Violation(;
|
||||
time::Int,
|
||||
monitored_line::TransmissionLine,
|
||||
outage_line::Union{TransmissionLine,Nothing},
|
||||
amount::Float64,
|
||||
)::Violation
|
||||
return Violation(time, monitored_line, outage_line, amount)
|
||||
end
|
||||
|
||||
mutable struct ViolationFilter
|
||||
max_per_line::Int
|
||||
max_total::Int
|
||||
queues::Dict{Int,PriorityQueue{Violation,Float64}}
|
||||
end
|
||||
|
||||
function ViolationFilter(;
|
||||
max_per_line::Int = 1,
|
||||
max_total::Int = 5,
|
||||
)::ViolationFilter
|
||||
return ViolationFilter(max_per_line, max_total, Dict())
|
||||
end
|
||||
|
||||
function _offer(filter::ViolationFilter, v::Violation)::Nothing
|
||||
if v.monitored_line.offset ∉ keys(filter.queues)
|
||||
filter.queues[v.monitored_line.offset] =
|
||||
PriorityQueue{Violation,Float64}()
|
||||
end
|
||||
q::PriorityQueue{Violation,Float64} = filter.queues[v.monitored_line.offset]
|
||||
if length(q) < filter.max_per_line
|
||||
enqueue!(q, v => v.amount)
|
||||
else
|
||||
if v.amount > peek(q)[1].amount
|
||||
dequeue!(q)
|
||||
enqueue!(q, v => v.amount)
|
||||
end
|
||||
end
|
||||
return nothing
|
||||
end
|
||||
|
||||
function _query(filter::ViolationFilter)::Array{Violation,1}
|
||||
violations = Array{Violation,1}()
|
||||
time_queue = PriorityQueue{Violation,Float64}()
|
||||
for l in keys(filter.queues)
|
||||
line_queue = filter.queues[l]
|
||||
while length(line_queue) > 0
|
||||
v = dequeue!(line_queue)
|
||||
if length(time_queue) < filter.max_total
|
||||
enqueue!(time_queue, v => v.amount)
|
||||
else
|
||||
if v.amount > peek(time_queue)[1].amount
|
||||
dequeue!(time_queue)
|
||||
enqueue!(time_queue, v => v.amount)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
while length(time_queue) > 0
|
||||
violations = [violations; dequeue!(time_queue)]
|
||||
function _find_violations(
|
||||
model::JuMP.Model;
|
||||
max_per_line::Int,
|
||||
max_per_period::Int,
|
||||
)
|
||||
instance = model[:instance]
|
||||
net_injection = model[:net_injection]
|
||||
overflow = model[:overflow]
|
||||
length(instance.buses) > 1 || return []
|
||||
violations = []
|
||||
@info "Verifying transmission limits..."
|
||||
time_screening = @elapsed begin
|
||||
non_slack_buses = [b for b in instance.buses if b.offset > 0]
|
||||
net_injection_values = [
|
||||
value(net_injection[b.name, t]) for b in non_slack_buses,
|
||||
t in 1:instance.time
|
||||
]
|
||||
overflow_values = [
|
||||
value(overflow[lm.name, t]) for lm in instance.lines,
|
||||
t in 1:instance.time
|
||||
]
|
||||
violations = UnitCommitment._find_violations(
|
||||
instance = instance,
|
||||
net_injections = net_injection_values,
|
||||
overflow = overflow_values,
|
||||
isf = model[:isf],
|
||||
lodf = model[:lodf],
|
||||
max_per_line = max_per_line,
|
||||
max_per_period = max_per_period,
|
||||
)
|
||||
end
|
||||
@info @sprintf(
|
||||
"Verified transmission limits in %.2f seconds",
|
||||
time_screening
|
||||
)
|
||||
return violations
|
||||
end
|
||||
|
||||
"""
|
||||
|
||||
function _find_violations(
|
||||
instance::UnitCommitmentInstance,
|
||||
net_injections::Array{Float64, 2};
|
||||
isf::Array{Float64,2},
|
||||
lodf::Array{Float64,2},
|
||||
max_per_line::Int = 1,
|
||||
max_per_period::Int = 5,
|
||||
)::Array{Violation, 1}
|
||||
max_per_line::Int,
|
||||
max_per_period::Int,
|
||||
)::Array{_Violation, 1}
|
||||
|
||||
Find transmission constraint violations (both pre-contingency, as well as
|
||||
post-contingency).
|
||||
@@ -103,9 +68,9 @@ function _find_violations(;
|
||||
overflow::Array{Float64,2},
|
||||
isf::Array{Float64,2},
|
||||
lodf::Array{Float64,2},
|
||||
max_per_line::Int = 1,
|
||||
max_per_period::Int = 5,
|
||||
)::Array{Violation,1}
|
||||
max_per_line::Int,
|
||||
max_per_period::Int,
|
||||
)::Array{_Violation,1}
|
||||
B = length(instance.buses) - 1
|
||||
L = length(instance.lines)
|
||||
T = instance.time
|
||||
@@ -116,7 +81,7 @@ function _find_violations(;
|
||||
size(lodf) == (L, L) || error("lodf has incorrect size")
|
||||
|
||||
filters = Dict(
|
||||
t => ViolationFilter(
|
||||
t => _ViolationFilter(
|
||||
max_total = max_per_period,
|
||||
max_per_line = max_per_line,
|
||||
) for t in 1:T
|
||||
@@ -177,7 +142,7 @@ function _find_violations(;
|
||||
if pre_v[lm, k] > 1e-5
|
||||
_offer(
|
||||
filters[t],
|
||||
Violation(
|
||||
_Violation(
|
||||
time = t,
|
||||
monitored_line = instance.lines[lm],
|
||||
outage_line = nothing,
|
||||
@@ -192,7 +157,7 @@ function _find_violations(;
|
||||
if post_v[lm, lc, k] > 1e-5 && is_vulnerable[lc]
|
||||
_offer(
|
||||
filters[t],
|
||||
Violation(
|
||||
_Violation(
|
||||
time = t,
|
||||
monitored_line = instance.lines[lm],
|
||||
outage_line = instance.lines[lc],
|
||||
@@ -203,7 +168,7 @@ function _find_violations(;
|
||||
end
|
||||
end
|
||||
|
||||
violations = Violation[]
|
||||
violations = _Violation[]
|
||||
for t in 1:instance.time
|
||||
append!(violations, _query(filters[t]))
|
||||
end
|
||||
67
src/solution/methods/XaQiWaTh19/optimize.jl
Normal file
67
src/solution/methods/XaQiWaTh19/optimize.jl
Normal file
@@ -0,0 +1,67 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
optimize!(model::JuMP.Model, method::_XaQiWaTh19)::Nothing
|
||||
|
||||
Solve the given unit commitment model, enforcing transmission and N-1
|
||||
security constraints lazily, according to the algorithm described in:
|
||||
|
||||
Xavier, A. S., Qiu, F., Wang, F., & Thimmapuram, P. R. (2019). Transmission
|
||||
constraint filtering in large-scale security-constrained unit commitment.
|
||||
IEEE Transactions on Power Systems, 34(3), 2457-2460.
|
||||
"""
|
||||
function optimize!(model::JuMP.Model, method::_XaQiWaTh19)::Nothing
|
||||
function set_gap(gap)
|
||||
try
|
||||
JuMP.set_optimizer_attribute(model, "MIPGap", gap)
|
||||
@info @sprintf("MIP gap tolerance set to %f", gap)
|
||||
catch
|
||||
@warn "Could not change MIP gap tolerance"
|
||||
end
|
||||
end
|
||||
instance = model[:instance]
|
||||
initial_time = time()
|
||||
large_gap = false
|
||||
has_transmission = (length(model[:isf]) > 0)
|
||||
if has_transmission && method.two_phase_gap
|
||||
set_gap(1e-2)
|
||||
large_gap = true
|
||||
else
|
||||
set_gap(method.gap_limit)
|
||||
end
|
||||
while true
|
||||
time_elapsed = time() - initial_time
|
||||
time_remaining = method.time_limit - time_elapsed
|
||||
if time_remaining < 0
|
||||
@info "Time limit exceeded"
|
||||
break
|
||||
end
|
||||
@info @sprintf(
|
||||
"Setting MILP time limit to %.2f seconds",
|
||||
time_remaining
|
||||
)
|
||||
JuMP.set_time_limit_sec(model, time_remaining)
|
||||
@info "Solving MILP..."
|
||||
JuMP.optimize!(model)
|
||||
has_transmission || break
|
||||
violations = _find_violations(
|
||||
model,
|
||||
max_per_line = method.max_violations_per_line,
|
||||
max_per_period = method.max_violations_per_period,
|
||||
)
|
||||
if isempty(violations)
|
||||
@info "No violations found"
|
||||
if large_gap
|
||||
large_gap = false
|
||||
set_gap(method.gap_limit)
|
||||
else
|
||||
break
|
||||
end
|
||||
else
|
||||
_enforce_transmission(model, violations)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
78
src/solution/methods/XaQiWaTh19/structs.jl
Normal file
78
src/solution/methods/XaQiWaTh19/structs.jl
Normal file
@@ -0,0 +1,78 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
import DataStructures: PriorityQueue
|
||||
|
||||
"""
|
||||
struct _XaQiWaTh19 <: SolutionMethod
|
||||
time_limit::Float64
|
||||
gap_limit::Float64
|
||||
two_phase_gap::Bool
|
||||
end
|
||||
|
||||
Lazy constraint solution method described in:
|
||||
|
||||
Xavier, A. S., Qiu, F., Wang, F., & Thimmapuram, P. R. (2019). Transmission
|
||||
constraint filtering in large-scale security-constrained unit commitment.
|
||||
IEEE Transactions on Power Systems, 34(3), 2457-2460.
|
||||
|
||||
Fields
|
||||
=========
|
||||
- `time_limit`:
|
||||
the time limit over the entire optimization procedure.
|
||||
- `gap_limit`:
|
||||
the desired relative optimality gap.
|
||||
- `two_phase_gap`:
|
||||
if true, solve the problem with large gap tolerance first, then reduce
|
||||
the gap tolerance when no further violated constraints are found.
|
||||
"""
|
||||
struct _XaQiWaTh19
|
||||
time_limit::Float64
|
||||
gap_limit::Float64
|
||||
two_phase_gap::Bool
|
||||
max_violations_per_line::Int
|
||||
max_violations_per_period::Int
|
||||
|
||||
function _XaQiWaTh19(;
|
||||
time_limit::Float64,
|
||||
gap_limit::Float64,
|
||||
two_phase_gap::Bool,
|
||||
max_violations_per_line::Int,
|
||||
max_violations_per_period::Int,
|
||||
)
|
||||
return new(
|
||||
time_limit,
|
||||
gap_limit,
|
||||
two_phase_gap,
|
||||
max_violations_per_line,
|
||||
max_violations_per_period,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
struct _Violation
|
||||
time::Int
|
||||
monitored_line::TransmissionLine
|
||||
outage_line::Union{TransmissionLine,Nothing}
|
||||
amount::Float64
|
||||
|
||||
function _Violation(;
|
||||
time::Int,
|
||||
monitored_line::TransmissionLine,
|
||||
outage_line::Union{TransmissionLine,Nothing},
|
||||
amount::Float64,
|
||||
)
|
||||
return new(time, monitored_line, outage_line, amount)
|
||||
end
|
||||
end
|
||||
|
||||
mutable struct _ViolationFilter
|
||||
max_per_line::Int
|
||||
max_total::Int
|
||||
queues::Dict{Int,PriorityQueue{_Violation,Float64}}
|
||||
|
||||
function _ViolationFilter(; max_per_line::Int = 1, max_total::Int = 5)
|
||||
return new(max_per_line, max_total, Dict())
|
||||
end
|
||||
end
|
||||
23
src/solution/optimize.jl
Normal file
23
src/solution/optimize.jl
Normal file
@@ -0,0 +1,23 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
function optimize!(model::JuMP.Model)::Nothing
|
||||
|
||||
Solve the given unit commitment model. Unlike JuMP.optimize!, this uses more
|
||||
advanced methods to accelerate the solution process and to enforce transmission
|
||||
and N-1 security constraints.
|
||||
"""
|
||||
function optimize!(model::JuMP.Model)::Nothing
|
||||
return UnitCommitment.optimize!(
|
||||
model,
|
||||
_XaQiWaTh19(
|
||||
time_limit = 3600.0,
|
||||
gap_limit = 1e-4,
|
||||
two_phase_gap = true,
|
||||
max_violations_per_line = 1,
|
||||
max_violations_per_period = 5,
|
||||
),
|
||||
)
|
||||
end
|
||||
65
src/solution/solution.jl
Normal file
65
src/solution/solution.jl
Normal file
@@ -0,0 +1,65 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
function solution(model::JuMP.Model)::OrderedDict
|
||||
instance, T = model[:instance], model[:instance].time
|
||||
function timeseries(vars, collection)
|
||||
return OrderedDict(
|
||||
b.name => [round(value(vars[b.name, t]), digits = 5) for t in 1:T]
|
||||
for b in collection
|
||||
)
|
||||
end
|
||||
function production_cost(g)
|
||||
return [
|
||||
value(model[:is_on][g.name, t]) * g.min_power_cost[t] + sum(
|
||||
Float64[
|
||||
value(model[:segprod][g.name, t, k]) *
|
||||
g.cost_segments[k].cost[t] for
|
||||
k in 1:length(g.cost_segments)
|
||||
],
|
||||
) for t in 1:T
|
||||
]
|
||||
end
|
||||
function production(g)
|
||||
return [
|
||||
value(model[:is_on][g.name, t]) * g.min_power[t] + sum(
|
||||
Float64[
|
||||
value(model[:segprod][g.name, t, k]) for
|
||||
k in 1:length(g.cost_segments)
|
||||
],
|
||||
) for t in 1:T
|
||||
]
|
||||
end
|
||||
function startup_cost(g)
|
||||
S = length(g.startup_categories)
|
||||
return [
|
||||
sum(
|
||||
g.startup_categories[s].cost *
|
||||
value(model[:startup][g.name, t, s]) for s in 1:S
|
||||
) for t in 1:T
|
||||
]
|
||||
end
|
||||
sol = OrderedDict()
|
||||
sol["Production (MW)"] =
|
||||
OrderedDict(g.name => production(g) for g in instance.units)
|
||||
sol["Production cost (\$)"] =
|
||||
OrderedDict(g.name => production_cost(g) for g in instance.units)
|
||||
sol["Startup cost (\$)"] =
|
||||
OrderedDict(g.name => startup_cost(g) for g in instance.units)
|
||||
sol["Is on"] = timeseries(model[:is_on], instance.units)
|
||||
sol["Switch on"] = timeseries(model[:switch_on], instance.units)
|
||||
sol["Switch off"] = timeseries(model[:switch_off], instance.units)
|
||||
sol["Reserve (MW)"] = timeseries(model[:reserve], instance.units)
|
||||
sol["Net injection (MW)"] =
|
||||
timeseries(model[:net_injection], instance.buses)
|
||||
sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses)
|
||||
if !isempty(instance.lines)
|
||||
sol["Line overflow (MW)"] = timeseries(model[:overflow], instance.lines)
|
||||
end
|
||||
if !isempty(instance.price_sensitive_loads)
|
||||
sol["Price-sensitive loads (MW)"] =
|
||||
timeseries(model[:loads], instance.price_sensitive_loads)
|
||||
end
|
||||
return sol
|
||||
end
|
||||
5
src/solution/structs.jl
Normal file
5
src/solution/structs.jl
Normal file
@@ -0,0 +1,5 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
abstract type SolutionMethod end
|
||||
24
src/solution/warmstart.jl
Normal file
24
src/solution/warmstart.jl
Normal file
@@ -0,0 +1,24 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
function set_warm_start!(model::JuMP.Model, solution::AbstractDict)::Nothing
|
||||
instance, T = model[:instance], model[:instance].time
|
||||
is_on = model[:is_on]
|
||||
prod_above = model[:prod_above]
|
||||
reserve = model[:reserve]
|
||||
for g in instance.units
|
||||
for t in 1:T
|
||||
JuMP.set_start_value(is_on[g.name, t], solution["Is on"][g.name][t])
|
||||
JuMP.set_start_value(
|
||||
switch_on[g.name, t],
|
||||
solution["Switch on"][g.name][t],
|
||||
)
|
||||
JuMP.set_start_value(
|
||||
switch_off[g.name, t],
|
||||
solution["Switch off"][g.name][t],
|
||||
)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
10
src/solution/write.jl
Normal file
10
src/solution/write.jl
Normal file
@@ -0,0 +1,10 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
function write(filename::AbstractString, solution::AbstractDict)::Nothing
|
||||
open(filename, "w") do file
|
||||
return JSON.print(file, solution, 2)
|
||||
end
|
||||
return
|
||||
end
|
||||
52
src/transforms/slice.jl
Normal file
52
src/transforms/slice.jl
Normal file
@@ -0,0 +1,52 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
slice(instance, range)
|
||||
|
||||
Creates a new instance, with only a subset of the time periods.
|
||||
This function does not modify the provided instance. The initial
|
||||
conditions are also not modified.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
# Build a 2-hour UC instance
|
||||
instance = UnitCommitment.read_benchmark("test/case14")
|
||||
modified = UnitCommitment.slice(instance, 1:2)
|
||||
|
||||
"""
|
||||
function slice(
|
||||
instance::UnitCommitmentInstance,
|
||||
range::UnitRange{Int},
|
||||
)::UnitCommitmentInstance
|
||||
modified = deepcopy(instance)
|
||||
modified.time = length(range)
|
||||
modified.power_balance_penalty = modified.power_balance_penalty[range]
|
||||
modified.reserves.spinning = modified.reserves.spinning[range]
|
||||
for u in modified.units
|
||||
u.max_power = u.max_power[range]
|
||||
u.min_power = u.min_power[range]
|
||||
u.must_run = u.must_run[range]
|
||||
u.min_power_cost = u.min_power_cost[range]
|
||||
u.provides_spinning_reserves = u.provides_spinning_reserves[range]
|
||||
for s in u.cost_segments
|
||||
s.mw = s.mw[range]
|
||||
s.cost = s.cost[range]
|
||||
end
|
||||
end
|
||||
for b in modified.buses
|
||||
b.load = b.load[range]
|
||||
end
|
||||
for l in modified.lines
|
||||
l.normal_flow_limit = l.normal_flow_limit[range]
|
||||
l.emergency_flow_limit = l.emergency_flow_limit[range]
|
||||
l.flow_limit_penalty = l.flow_limit_penalty[range]
|
||||
end
|
||||
for ps in modified.price_sensitive_loads
|
||||
ps.demand = ps.demand[range]
|
||||
ps.revenue = ps.revenue[range]
|
||||
end
|
||||
return modified
|
||||
end
|
||||
3
src/transmission/structs.jl
Normal file
3
src/transmission/structs.jl
Normal file
@@ -0,0 +1,3 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
69
src/validation/repair.jl
Normal file
69
src/validation/repair.jl
Normal file
@@ -0,0 +1,69 @@
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
repair!(instance)
|
||||
|
||||
Verifies that the given unit commitment instance is valid and automatically
|
||||
fixes some validation errors if possible, issuing a warning for each error
|
||||
found. If a validation error cannot be automatically fixed, issues an
|
||||
exception.
|
||||
|
||||
Returns the number of validation errors found.
|
||||
"""
|
||||
function repair!(instance::UnitCommitmentInstance)::Int
|
||||
n_errors = 0
|
||||
|
||||
for g in instance.units
|
||||
|
||||
# Startup costs and delays must be increasing
|
||||
for s in 2:length(g.startup_categories)
|
||||
if g.startup_categories[s].delay <= g.startup_categories[s-1].delay
|
||||
prev_value = g.startup_categories[s].delay
|
||||
new_value = g.startup_categories[s-1].delay + 1
|
||||
@warn "Generator $(g.name) has non-increasing startup delays (category $s). " *
|
||||
"Changing delay: $prev_value → $new_value"
|
||||
g.startup_categories[s].delay = new_value
|
||||
n_errors += 1
|
||||
end
|
||||
|
||||
if g.startup_categories[s].cost < g.startup_categories[s-1].cost
|
||||
prev_value = g.startup_categories[s].cost
|
||||
new_value = g.startup_categories[s-1].cost
|
||||
@warn "Generator $(g.name) has decreasing startup cost (category $s). " *
|
||||
"Changing cost: $prev_value → $new_value"
|
||||
g.startup_categories[s].cost = new_value
|
||||
n_errors += 1
|
||||
end
|
||||
end
|
||||
|
||||
for t in 1:instance.time
|
||||
# Production cost curve should be convex
|
||||
for k in 2:length(g.cost_segments)
|
||||
cost = g.cost_segments[k].cost[t]
|
||||
min_cost = g.cost_segments[k-1].cost[t]
|
||||
if cost < min_cost - 1e-5
|
||||
@warn "Generator $(g.name) has non-convex production cost curve " *
|
||||
"(segment $k, time $t). Changing cost: $cost → $min_cost"
|
||||
g.cost_segments[k].cost[t] = min_cost
|
||||
n_errors += 1
|
||||
end
|
||||
end
|
||||
|
||||
# Startup limit must be greater than min_power
|
||||
if g.startup_limit < g.min_power[t]
|
||||
new_limit = g.min_power[t]
|
||||
prev_limit = g.startup_limit
|
||||
@warn "Generator $(g.name) has startup limit lower than minimum power. " *
|
||||
"Changing startup limit: $prev_limit → $new_limit"
|
||||
g.startup_limit = new_limit
|
||||
n_errors += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return n_errors
|
||||
end
|
||||
|
||||
export repair!
|
||||
@@ -6,70 +6,6 @@ using Printf
|
||||
|
||||
bin(x) = [xi > 0.5 for xi in x]
|
||||
|
||||
"""
|
||||
repair!(instance)
|
||||
|
||||
Verifies that the given unit commitment instance is valid and automatically
|
||||
fixes some validation errors if possible, issuing a warning for each error
|
||||
found. If a validation error cannot be automatically fixed, issues an
|
||||
exception.
|
||||
|
||||
Returns the number of validation errors found.
|
||||
"""
|
||||
function repair!(instance::UnitCommitmentInstance)::Int
|
||||
n_errors = 0
|
||||
|
||||
for g in instance.units
|
||||
|
||||
# Startup costs and delays must be increasing
|
||||
for s in 2:length(g.startup_categories)
|
||||
if g.startup_categories[s].delay <= g.startup_categories[s-1].delay
|
||||
prev_value = g.startup_categories[s].delay
|
||||
new_value = g.startup_categories[s-1].delay + 1
|
||||
@warn "Generator $(g.name) has non-increasing startup delays (category $s). " *
|
||||
"Changing delay: $prev_value → $new_value"
|
||||
g.startup_categories[s].delay = new_value
|
||||
n_errors += 1
|
||||
end
|
||||
|
||||
if g.startup_categories[s].cost < g.startup_categories[s-1].cost
|
||||
prev_value = g.startup_categories[s].cost
|
||||
new_value = g.startup_categories[s-1].cost
|
||||
@warn "Generator $(g.name) has decreasing startup cost (category $s). " *
|
||||
"Changing cost: $prev_value → $new_value"
|
||||
g.startup_categories[s].cost = new_value
|
||||
n_errors += 1
|
||||
end
|
||||
end
|
||||
|
||||
for t in 1:instance.time
|
||||
# Production cost curve should be convex
|
||||
for k in 2:length(g.cost_segments)
|
||||
cost = g.cost_segments[k].cost[t]
|
||||
min_cost = g.cost_segments[k-1].cost[t]
|
||||
if cost < min_cost - 1e-5
|
||||
@warn "Generator $(g.name) has non-convex production cost curve " *
|
||||
"(segment $k, time $t). Changing cost: $cost → $min_cost"
|
||||
g.cost_segments[k].cost[t] = min_cost
|
||||
n_errors += 1
|
||||
end
|
||||
end
|
||||
|
||||
# Startup limit must be greater than min_power
|
||||
if g.startup_limit < g.min_power[t]
|
||||
new_limit = g.min_power[t]
|
||||
prev_limit = g.startup_limit
|
||||
@warn "Generator $(g.name) has startup limit lower than minimum power. " *
|
||||
"Changing startup limit: $prev_limit → $new_limit"
|
||||
g.startup_limit = new_limit
|
||||
n_errors += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return n_errors
|
||||
end
|
||||
|
||||
function validate(instance_filename::String, solution_filename::String)
|
||||
instance = UnitCommitment.read(instance_filename)
|
||||
solution = JSON.parse(open(solution_filename))
|
||||
@@ -7,7 +7,7 @@ using UnitCommitment
|
||||
@testset "convert" begin
|
||||
@testset "EGRET solution" begin
|
||||
solution =
|
||||
UnitCommitment._read_egret_solution("fixtures/egret_output.json.gz")
|
||||
UnitCommitment.read_egret_solution("fixtures/egret_output.json.gz")
|
||||
for attr in ["Is on", "Production (MW)", "Production cost (\$)"]
|
||||
@test attr in keys(solution)
|
||||
@test "115_STEAM_1" in keys(solution[attr])
|
||||
|
||||
@@ -149,7 +149,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
||||
|
||||
# Should be able to build model without errors
|
||||
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||
model = build_model(
|
||||
model = UnitCommitment.build_model(
|
||||
instance = modified,
|
||||
optimizer = optimizer,
|
||||
variable_names = true,
|
||||
|
||||
@@ -11,7 +11,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP
|
||||
line.normal_flow_limit[t] = 10.0
|
||||
end
|
||||
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||
model = build_model(
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = optimizer,
|
||||
variable_names = true,
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
using UnitCommitment, Test, LinearAlgebra
|
||||
import UnitCommitment: Violation, _offer, _query
|
||||
import UnitCommitment: _Violation, _offer, _query
|
||||
|
||||
@testset "Screening" begin
|
||||
@testset "Violation filter" begin
|
||||
@testset "_Violation filter" begin
|
||||
instance = UnitCommitment.read_benchmark("test/case14")
|
||||
filter = UnitCommitment.ViolationFilter(max_per_line = 1, max_total = 2)
|
||||
filter =
|
||||
UnitCommitment._ViolationFilter(max_per_line = 1, max_total = 2)
|
||||
|
||||
_offer(
|
||||
filter,
|
||||
Violation(
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[1],
|
||||
outage_line = nothing,
|
||||
@@ -21,7 +22,7 @@ import UnitCommitment: Violation, _offer, _query
|
||||
)
|
||||
_offer(
|
||||
filter,
|
||||
Violation(
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[1],
|
||||
outage_line = instance.lines[1],
|
||||
@@ -30,7 +31,7 @@ import UnitCommitment: Violation, _offer, _query
|
||||
)
|
||||
_offer(
|
||||
filter,
|
||||
Violation(
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[1],
|
||||
outage_line = instance.lines[5],
|
||||
@@ -39,7 +40,7 @@ import UnitCommitment: Violation, _offer, _query
|
||||
)
|
||||
_offer(
|
||||
filter,
|
||||
Violation(
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[1],
|
||||
outage_line = instance.lines[4],
|
||||
@@ -48,7 +49,7 @@ import UnitCommitment: Violation, _offer, _query
|
||||
)
|
||||
_offer(
|
||||
filter,
|
||||
Violation(
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[2],
|
||||
outage_line = instance.lines[1],
|
||||
@@ -57,7 +58,7 @@ import UnitCommitment: Violation, _offer, _query
|
||||
)
|
||||
_offer(
|
||||
filter,
|
||||
Violation(
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[2],
|
||||
outage_line = instance.lines[8],
|
||||
@@ -67,13 +68,13 @@ import UnitCommitment: Violation, _offer, _query
|
||||
|
||||
actual = _query(filter)
|
||||
expected = [
|
||||
Violation(
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[2],
|
||||
outage_line = instance.lines[1],
|
||||
amount = 200.0,
|
||||
),
|
||||
Violation(
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[1],
|
||||
outage_line = instance.lines[5],
|
||||
@@ -106,6 +107,8 @@ import UnitCommitment: Violation, _offer, _query
|
||||
overflow = overflow,
|
||||
isf = isf,
|
||||
lodf = lodf,
|
||||
max_per_line = 1,
|
||||
max_per_period = 5,
|
||||
)
|
||||
@test length(violations) == 20
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user