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=@.
|
JULIA := julia --color=yes --project=@.
|
||||||
VERSION := 0.2
|
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 build
|
||||||
mkdir -p benchmark/results/test
|
mkdir -p benchmark/results/test
|
||||||
cd benchmark; $(JULIA) --trace-compile=../build/precompile.jl run.jl test/case14.1.sol.json
|
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:
|
clean:
|
||||||
rm -rf build/*
|
rm -rf build/*
|
||||||
|
|||||||
@@ -3,12 +3,30 @@
|
|||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
module UnitCommitment
|
module UnitCommitment
|
||||||
include("log.jl")
|
|
||||||
include("instance.jl")
|
include("instance/structs.jl")
|
||||||
include("screening.jl")
|
include("transmission/structs.jl")
|
||||||
include("model.jl")
|
include("solution/structs.jl")
|
||||||
include("sensitivity.jl")
|
include("solution/methods/XaQiWaTh19/structs.jl")
|
||||||
include("validate.jl")
|
|
||||||
include("convert.jl")
|
include("import/egret.jl")
|
||||||
include("initcond.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
|
end
|
||||||
|
|||||||
@@ -4,16 +4,15 @@
|
|||||||
|
|
||||||
using DataStructures, JSON, GZip
|
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)
|
egret = _read_json(path)
|
||||||
T = length(egret["system"]["time_keys"])
|
T = length(egret["system"]["time_keys"])
|
||||||
|
|
||||||
@@ -8,104 +8,35 @@ using DataStructures
|
|||||||
using GZip
|
using GZip
|
||||||
import Base: getindex, time
|
import Base: getindex, time
|
||||||
|
|
||||||
mutable struct Bus
|
"""
|
||||||
name::String
|
read_benchmark(name::AbstractString)::UnitCommitmentInstance
|
||||||
offset::Int
|
|
||||||
load::Vector{Float64}
|
|
||||||
units::Vector
|
|
||||||
price_sensitive_loads::Vector
|
|
||||||
end
|
|
||||||
|
|
||||||
mutable struct CostSegment
|
Read one of the benchmark unit commitment instances included in the package.
|
||||||
mw::Vector{Float64}
|
See "Instances" section of the documentation for the entire list of benchmark
|
||||||
cost::Vector{Float64}
|
instances available.
|
||||||
end
|
|
||||||
|
|
||||||
mutable struct StartupCategory
|
Example
|
||||||
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
|
|
||||||
|
|
||||||
|
import UnitCommitment
|
||||||
|
instance = UnitCommitment.read_benchmark("matpower/case3375wp/2017-02-01")
|
||||||
|
"""
|
||||||
function read_benchmark(name::AbstractString)::UnitCommitmentInstance
|
function read_benchmark(name::AbstractString)::UnitCommitmentInstance
|
||||||
basedir = dirname(@__FILE__)
|
basedir = dirname(@__FILE__)
|
||||||
return UnitCommitment.read("$basedir/../instances/$name.json.gz")
|
return UnitCommitment.read("$basedir/../../instances/$name.json.gz")
|
||||||
end
|
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
|
function read(path::AbstractString)::UnitCommitmentInstance
|
||||||
if endswith(path, ".gz")
|
if endswith(path, ".gz")
|
||||||
return _read(gzopen(path))
|
return _read(gzopen(path))
|
||||||
@@ -120,6 +51,15 @@ function _read(file::IO)::UnitCommitmentInstance
|
|||||||
)
|
)
|
||||||
end
|
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)
|
function _from_json(json; repair = true)
|
||||||
units = Unit[]
|
units = Unit[]
|
||||||
buses = Bus[]
|
buses = Bus[]
|
||||||
@@ -336,54 +276,3 @@ function _from_json(json; repair = true)
|
|||||||
end
|
end
|
||||||
return instance
|
return instance
|
||||||
end
|
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
|
using JuMP, MathOptInterface, DataStructures
|
||||||
import JuMP: value, fix, set_name
|
import JuMP: value, fix, set_name
|
||||||
|
|
||||||
# Extend some JuMP functions so that decision variables can be safely replaced by
|
"""
|
||||||
# (constant) floating point numbers.
|
function build_model(;
|
||||||
function value(x::Float64)
|
instance::UnitCommitmentInstance,
|
||||||
return x
|
isf::Union{Matrix{Float64},Nothing} = nothing,
|
||||||
end
|
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)
|
Build the JuMP model corresponding to the given unit commitment instance.
|
||||||
return abs(x - v) < 1e-6 || error("Value mismatch: $x != $v")
|
|
||||||
end
|
|
||||||
|
|
||||||
function set_name(x::Float64, n::String)
|
Arguments
|
||||||
# nop
|
=========
|
||||||
end
|
- `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(;
|
function build_model(;
|
||||||
filename::Union{String,Nothing} = nothing,
|
instance::UnitCommitmentInstance,
|
||||||
instance::Union{UnitCommitmentInstance,Nothing} = nothing,
|
|
||||||
isf::Union{Matrix{Float64},Nothing} = nothing,
|
isf::Union{Matrix{Float64},Nothing} = nothing,
|
||||||
lodf::Union{Matrix{Float64},Nothing} = nothing,
|
lodf::Union{Matrix{Float64},Nothing} = nothing,
|
||||||
isf_cutoff::Float64 = 0.005,
|
isf_cutoff::Float64 = 0.005,
|
||||||
@@ -29,18 +62,6 @@ function build_model(;
|
|||||||
optimizer = nothing,
|
optimizer = nothing,
|
||||||
variable_names::Bool = false,
|
variable_names::Bool = false,
|
||||||
)::JuMP.Model
|
)::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
|
if length(instance.buses) == 1
|
||||||
isf = zeros(0, 0)
|
isf = zeros(0, 0)
|
||||||
lodf = zeros(0, 0)
|
lodf = zeros(0, 0)
|
||||||
@@ -473,71 +494,6 @@ function _build_reserve_eqs!(model::JuMP.Model)
|
|||||||
end
|
end
|
||||||
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)
|
function _set_names!(model::JuMP.Model)
|
||||||
@info "Setting variable and constraint names..."
|
@info "Setting variable and constraint names..."
|
||||||
time_varnames = @elapsed begin
|
time_varnames = @elapsed begin
|
||||||
@@ -558,230 +514,3 @@ function _set_names!(dict::Dict)
|
|||||||
end
|
end
|
||||||
end
|
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
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
# 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.
|
||||||
# Copyright (C) 2019 Argonne National Laboratory
|
|
||||||
# Written by Alinson Santos Xavier <axavier@anl.gov>
|
|
||||||
|
|
||||||
using DataStructures
|
import Base.Threads: @threads
|
||||||
using Base.Threads
|
|
||||||
|
|
||||||
struct Violation
|
function _find_violations(
|
||||||
time::Int
|
model::JuMP.Model;
|
||||||
monitored_line::TransmissionLine
|
max_per_line::Int,
|
||||||
outage_line::Union{TransmissionLine,Nothing}
|
max_per_period::Int,
|
||||||
amount::Float64 # Violation amount (in MW)
|
)
|
||||||
end
|
instance = model[:instance]
|
||||||
|
net_injection = model[:net_injection]
|
||||||
function Violation(;
|
overflow = model[:overflow]
|
||||||
time::Int,
|
length(instance.buses) > 1 || return []
|
||||||
monitored_line::TransmissionLine,
|
violations = []
|
||||||
outage_line::Union{TransmissionLine,Nothing},
|
@info "Verifying transmission limits..."
|
||||||
amount::Float64,
|
time_screening = @elapsed begin
|
||||||
)::Violation
|
non_slack_buses = [b for b in instance.buses if b.offset > 0]
|
||||||
return Violation(time, monitored_line, outage_line, amount)
|
net_injection_values = [
|
||||||
end
|
value(net_injection[b.name, t]) for b in non_slack_buses,
|
||||||
|
t in 1:instance.time
|
||||||
mutable struct ViolationFilter
|
]
|
||||||
max_per_line::Int
|
overflow_values = [
|
||||||
max_total::Int
|
value(overflow[lm.name, t]) for lm in instance.lines,
|
||||||
queues::Dict{Int,PriorityQueue{Violation,Float64}}
|
t in 1:instance.time
|
||||||
end
|
]
|
||||||
|
violations = UnitCommitment._find_violations(
|
||||||
function ViolationFilter(;
|
instance = instance,
|
||||||
max_per_line::Int = 1,
|
net_injections = net_injection_values,
|
||||||
max_total::Int = 5,
|
overflow = overflow_values,
|
||||||
)::ViolationFilter
|
isf = model[:isf],
|
||||||
return ViolationFilter(max_per_line, max_total, Dict())
|
lodf = model[:lodf],
|
||||||
end
|
max_per_line = max_per_line,
|
||||||
|
max_per_period = max_per_period,
|
||||||
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
|
end
|
||||||
|
@info @sprintf(
|
||||||
|
"Verified transmission limits in %.2f seconds",
|
||||||
|
time_screening
|
||||||
|
)
|
||||||
return violations
|
return violations
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
function _find_violations(
|
function _find_violations(
|
||||||
instance::UnitCommitmentInstance,
|
instance::UnitCommitmentInstance,
|
||||||
net_injections::Array{Float64, 2};
|
net_injections::Array{Float64, 2};
|
||||||
isf::Array{Float64,2},
|
isf::Array{Float64,2},
|
||||||
lodf::Array{Float64,2},
|
lodf::Array{Float64,2},
|
||||||
max_per_line::Int = 1,
|
max_per_line::Int,
|
||||||
max_per_period::Int = 5,
|
max_per_period::Int,
|
||||||
)::Array{Violation, 1}
|
)::Array{_Violation, 1}
|
||||||
|
|
||||||
Find transmission constraint violations (both pre-contingency, as well as
|
Find transmission constraint violations (both pre-contingency, as well as
|
||||||
post-contingency).
|
post-contingency).
|
||||||
@@ -103,9 +68,9 @@ function _find_violations(;
|
|||||||
overflow::Array{Float64,2},
|
overflow::Array{Float64,2},
|
||||||
isf::Array{Float64,2},
|
isf::Array{Float64,2},
|
||||||
lodf::Array{Float64,2},
|
lodf::Array{Float64,2},
|
||||||
max_per_line::Int = 1,
|
max_per_line::Int,
|
||||||
max_per_period::Int = 5,
|
max_per_period::Int,
|
||||||
)::Array{Violation,1}
|
)::Array{_Violation,1}
|
||||||
B = length(instance.buses) - 1
|
B = length(instance.buses) - 1
|
||||||
L = length(instance.lines)
|
L = length(instance.lines)
|
||||||
T = instance.time
|
T = instance.time
|
||||||
@@ -116,7 +81,7 @@ function _find_violations(;
|
|||||||
size(lodf) == (L, L) || error("lodf has incorrect size")
|
size(lodf) == (L, L) || error("lodf has incorrect size")
|
||||||
|
|
||||||
filters = Dict(
|
filters = Dict(
|
||||||
t => ViolationFilter(
|
t => _ViolationFilter(
|
||||||
max_total = max_per_period,
|
max_total = max_per_period,
|
||||||
max_per_line = max_per_line,
|
max_per_line = max_per_line,
|
||||||
) for t in 1:T
|
) for t in 1:T
|
||||||
@@ -177,7 +142,7 @@ function _find_violations(;
|
|||||||
if pre_v[lm, k] > 1e-5
|
if pre_v[lm, k] > 1e-5
|
||||||
_offer(
|
_offer(
|
||||||
filters[t],
|
filters[t],
|
||||||
Violation(
|
_Violation(
|
||||||
time = t,
|
time = t,
|
||||||
monitored_line = instance.lines[lm],
|
monitored_line = instance.lines[lm],
|
||||||
outage_line = nothing,
|
outage_line = nothing,
|
||||||
@@ -192,7 +157,7 @@ function _find_violations(;
|
|||||||
if post_v[lm, lc, k] > 1e-5 && is_vulnerable[lc]
|
if post_v[lm, lc, k] > 1e-5 && is_vulnerable[lc]
|
||||||
_offer(
|
_offer(
|
||||||
filters[t],
|
filters[t],
|
||||||
Violation(
|
_Violation(
|
||||||
time = t,
|
time = t,
|
||||||
monitored_line = instance.lines[lm],
|
monitored_line = instance.lines[lm],
|
||||||
outage_line = instance.lines[lc],
|
outage_line = instance.lines[lc],
|
||||||
@@ -203,7 +168,7 @@ function _find_violations(;
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
violations = Violation[]
|
violations = _Violation[]
|
||||||
for t in 1:instance.time
|
for t in 1:instance.time
|
||||||
append!(violations, _query(filters[t]))
|
append!(violations, _query(filters[t]))
|
||||||
end
|
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]
|
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)
|
function validate(instance_filename::String, solution_filename::String)
|
||||||
instance = UnitCommitment.read(instance_filename)
|
instance = UnitCommitment.read(instance_filename)
|
||||||
solution = JSON.parse(open(solution_filename))
|
solution = JSON.parse(open(solution_filename))
|
||||||
@@ -7,7 +7,7 @@ using UnitCommitment
|
|||||||
@testset "convert" begin
|
@testset "convert" begin
|
||||||
@testset "EGRET solution" begin
|
@testset "EGRET solution" begin
|
||||||
solution =
|
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 (\$)"]
|
for attr in ["Is on", "Production (MW)", "Production cost (\$)"]
|
||||||
@test attr in keys(solution)
|
@test attr in keys(solution)
|
||||||
@test "115_STEAM_1" in keys(solution[attr])
|
@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
|
# 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 = build_model(
|
model = UnitCommitment.build_model(
|
||||||
instance = modified,
|
instance = modified,
|
||||||
optimizer = optimizer,
|
optimizer = optimizer,
|
||||||
variable_names = true,
|
variable_names = true,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP
|
|||||||
line.normal_flow_limit[t] = 10.0
|
line.normal_flow_limit[t] = 10.0
|
||||||
end
|
end
|
||||||
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||||
model = build_model(
|
model = UnitCommitment.build_model(
|
||||||
instance = instance,
|
instance = instance,
|
||||||
optimizer = optimizer,
|
optimizer = optimizer,
|
||||||
variable_names = true,
|
variable_names = true,
|
||||||
|
|||||||
@@ -3,16 +3,17 @@
|
|||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
using UnitCommitment, Test, LinearAlgebra
|
using UnitCommitment, Test, LinearAlgebra
|
||||||
import UnitCommitment: Violation, _offer, _query
|
import UnitCommitment: _Violation, _offer, _query
|
||||||
|
|
||||||
@testset "Screening" begin
|
@testset "Screening" begin
|
||||||
@testset "Violation filter" begin
|
@testset "_Violation filter" begin
|
||||||
instance = UnitCommitment.read_benchmark("test/case14")
|
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(
|
_offer(
|
||||||
filter,
|
filter,
|
||||||
Violation(
|
_Violation(
|
||||||
time = 1,
|
time = 1,
|
||||||
monitored_line = instance.lines[1],
|
monitored_line = instance.lines[1],
|
||||||
outage_line = nothing,
|
outage_line = nothing,
|
||||||
@@ -21,7 +22,7 @@ import UnitCommitment: Violation, _offer, _query
|
|||||||
)
|
)
|
||||||
_offer(
|
_offer(
|
||||||
filter,
|
filter,
|
||||||
Violation(
|
_Violation(
|
||||||
time = 1,
|
time = 1,
|
||||||
monitored_line = instance.lines[1],
|
monitored_line = instance.lines[1],
|
||||||
outage_line = instance.lines[1],
|
outage_line = instance.lines[1],
|
||||||
@@ -30,7 +31,7 @@ import UnitCommitment: Violation, _offer, _query
|
|||||||
)
|
)
|
||||||
_offer(
|
_offer(
|
||||||
filter,
|
filter,
|
||||||
Violation(
|
_Violation(
|
||||||
time = 1,
|
time = 1,
|
||||||
monitored_line = instance.lines[1],
|
monitored_line = instance.lines[1],
|
||||||
outage_line = instance.lines[5],
|
outage_line = instance.lines[5],
|
||||||
@@ -39,7 +40,7 @@ import UnitCommitment: Violation, _offer, _query
|
|||||||
)
|
)
|
||||||
_offer(
|
_offer(
|
||||||
filter,
|
filter,
|
||||||
Violation(
|
_Violation(
|
||||||
time = 1,
|
time = 1,
|
||||||
monitored_line = instance.lines[1],
|
monitored_line = instance.lines[1],
|
||||||
outage_line = instance.lines[4],
|
outage_line = instance.lines[4],
|
||||||
@@ -48,7 +49,7 @@ import UnitCommitment: Violation, _offer, _query
|
|||||||
)
|
)
|
||||||
_offer(
|
_offer(
|
||||||
filter,
|
filter,
|
||||||
Violation(
|
_Violation(
|
||||||
time = 1,
|
time = 1,
|
||||||
monitored_line = instance.lines[2],
|
monitored_line = instance.lines[2],
|
||||||
outage_line = instance.lines[1],
|
outage_line = instance.lines[1],
|
||||||
@@ -57,7 +58,7 @@ import UnitCommitment: Violation, _offer, _query
|
|||||||
)
|
)
|
||||||
_offer(
|
_offer(
|
||||||
filter,
|
filter,
|
||||||
Violation(
|
_Violation(
|
||||||
time = 1,
|
time = 1,
|
||||||
monitored_line = instance.lines[2],
|
monitored_line = instance.lines[2],
|
||||||
outage_line = instance.lines[8],
|
outage_line = instance.lines[8],
|
||||||
@@ -67,13 +68,13 @@ import UnitCommitment: Violation, _offer, _query
|
|||||||
|
|
||||||
actual = _query(filter)
|
actual = _query(filter)
|
||||||
expected = [
|
expected = [
|
||||||
Violation(
|
_Violation(
|
||||||
time = 1,
|
time = 1,
|
||||||
monitored_line = instance.lines[2],
|
monitored_line = instance.lines[2],
|
||||||
outage_line = instance.lines[1],
|
outage_line = instance.lines[1],
|
||||||
amount = 200.0,
|
amount = 200.0,
|
||||||
),
|
),
|
||||||
Violation(
|
_Violation(
|
||||||
time = 1,
|
time = 1,
|
||||||
monitored_line = instance.lines[1],
|
monitored_line = instance.lines[1],
|
||||||
outage_line = instance.lines[5],
|
outage_line = instance.lines[5],
|
||||||
@@ -106,6 +107,8 @@ import UnitCommitment: Violation, _offer, _query
|
|||||||
overflow = overflow,
|
overflow = overflow,
|
||||||
isf = isf,
|
isf = isf,
|
||||||
lodf = lodf,
|
lodf = lodf,
|
||||||
|
max_per_line = 1,
|
||||||
|
max_per_period = 5,
|
||||||
)
|
)
|
||||||
@test length(violations) == 20
|
@test length(violations) == 20
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user