From 4e8426beba65c052872e5f9a8ba01fec08ab0c57 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Fri, 28 May 2021 22:48:12 -0500 Subject: [PATCH] Reorganize files; document some methods --- Makefile | 4 +- src/UnitCommitment.jl | 34 +- src/{convert.jl => import/egret.jl} | 17 +- src/{instance.jl => instance/read.jl} | 171 ++------ src/instance/structs.jl | 98 +++++ src/{model.jl => model/build.jl} | 369 +++--------------- src/model/jumpext.jl | 20 + src/solution/fix.jl | 33 ++ src/solution/methods/XaQiWaTh19/enforce.jl | 83 ++++ src/solution/methods/XaQiWaTh19/filter.jl | 44 +++ .../methods/XaQiWaTh19/find.jl} | 127 +++--- src/solution/methods/XaQiWaTh19/optimize.jl | 67 ++++ src/solution/methods/XaQiWaTh19/structs.jl | 78 ++++ src/solution/optimize.jl | 23 ++ src/solution/solution.jl | 65 +++ src/solution/structs.jl | 5 + src/solution/warmstart.jl | 24 ++ src/solution/write.jl | 10 + src/{ => transforms}/initcond.jl | 0 src/transforms/slice.jl | 52 +++ src/{ => transmission}/sensitivity.jl | 0 src/transmission/structs.jl | 3 + src/{ => utils}/log.jl | 0 src/{ => utils}/sysimage.jl | 0 src/validation/repair.jl | 69 ++++ src/{ => validation}/validate.jl | 64 --- test/convert_test.jl | 2 +- test/instance_test.jl | 2 +- test/model_test.jl | 2 +- test/screening_test.jl | 25 +- 30 files changed, 852 insertions(+), 639 deletions(-) rename src/{convert.jl => import/egret.jl} (86%) rename src/{instance.jl => instance/read.jl} (68%) create mode 100644 src/instance/structs.jl rename src/{model.jl => model/build.jl} (60%) create mode 100644 src/model/jumpext.jl create mode 100644 src/solution/fix.jl create mode 100644 src/solution/methods/XaQiWaTh19/enforce.jl create mode 100644 src/solution/methods/XaQiWaTh19/filter.jl rename src/{screening.jl => solution/methods/XaQiWaTh19/find.jl} (63%) create mode 100644 src/solution/methods/XaQiWaTh19/optimize.jl create mode 100644 src/solution/methods/XaQiWaTh19/structs.jl create mode 100644 src/solution/optimize.jl create mode 100644 src/solution/solution.jl create mode 100644 src/solution/structs.jl create mode 100644 src/solution/warmstart.jl create mode 100644 src/solution/write.jl rename src/{ => transforms}/initcond.jl (100%) create mode 100644 src/transforms/slice.jl rename src/{ => transmission}/sensitivity.jl (100%) create mode 100644 src/transmission/structs.jl rename src/{ => utils}/log.jl (100%) rename src/{ => utils}/sysimage.jl (100%) create mode 100644 src/validation/repair.jl rename src/{ => validation}/validate.jl (82%) diff --git a/Makefile b/Makefile index b58fc45..f7890b5 100644 --- a/Makefile +++ b/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/* diff --git a/src/UnitCommitment.jl b/src/UnitCommitment.jl index 4a36550..51858c5 100644 --- a/src/UnitCommitment.jl +++ b/src/UnitCommitment.jl @@ -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 diff --git a/src/convert.jl b/src/import/egret.jl similarity index 86% rename from src/convert.jl rename to src/import/egret.jl index 7599061..d11bdea 100644 --- a/src/convert.jl +++ b/src/import/egret.jl @@ -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 +""" + + read_egret_solution(path::String)::OrderedDict -function _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"]) diff --git a/src/instance.jl b/src/instance/read.jl similarity index 68% rename from src/instance.jl rename to src/instance/read.jl index 76aa049..06ebfad 100644 --- a/src/instance.jl +++ b/src/instance/read.jl @@ -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 - -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 +""" + read_benchmark(name::AbstractString)::UnitCommitmentInstance -mutable struct Reserves - spinning::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 Contingency - name::String - lines::Vector{TransmissionLine} - units::Vector{Unit} -end +Example +------- -mutable struct PriceSensitiveLoad - name::String - bus::Bus - demand::Vector{Float64} - revenue::Vector{Float64} + 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") 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 +""" + read(path::AbstractString)::UnitCommitmentInstance -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 +Read a unit commitment instance from a file. The file may be gzipped. -function read_benchmark(name::AbstractString)::UnitCommitmentInstance - basedir = dirname(@__FILE__) - return UnitCommitment.read("$basedir/../instances/$name.json.gz") -end +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 diff --git a/src/instance/structs.jl b/src/instance/structs.jl new file mode 100644 index 0000000..d75fba9 --- /dev/null +++ b/src/instance/structs.jl @@ -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 diff --git a/src/model.jl b/src/model/build.jl similarity index 60% rename from src/model.jl rename to src/model/build.jl index acd1e1e..68be9cb 100644 --- a/src/model.jl +++ b/src/model/build.jl @@ -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 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 - +""" + 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 + +Build the JuMP model corresponding to the given unit commitment instance. + +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 diff --git a/src/model/jumpext.jl b/src/model/jumpext.jl new file mode 100644 index 0000000..b493e84 --- /dev/null +++ b/src/model/jumpext.jl @@ -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 diff --git a/src/solution/fix.jl b/src/solution/fix.jl new file mode 100644 index 0000000..a8b4877 --- /dev/null +++ b/src/solution/fix.jl @@ -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 diff --git a/src/solution/methods/XaQiWaTh19/enforce.jl b/src/solution/methods/XaQiWaTh19/enforce.jl new file mode 100644 index 0000000..526274f --- /dev/null +++ b/src/solution/methods/XaQiWaTh19/enforce.jl @@ -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 diff --git a/src/solution/methods/XaQiWaTh19/filter.jl b/src/solution/methods/XaQiWaTh19/filter.jl new file mode 100644 index 0000000..576e8fc --- /dev/null +++ b/src/solution/methods/XaQiWaTh19/filter.jl @@ -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 diff --git a/src/screening.jl b/src/solution/methods/XaQiWaTh19/find.jl similarity index 63% rename from src/screening.jl rename to src/solution/methods/XaQiWaTh19/find.jl index 9a4971b..3cf9094 100644 --- a/src/screening.jl +++ b/src/solution/methods/XaQiWaTh19/find.jl @@ -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 -using DataStructures -using Base.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)] +import Base.Threads: @threads + +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 diff --git a/src/solution/methods/XaQiWaTh19/optimize.jl b/src/solution/methods/XaQiWaTh19/optimize.jl new file mode 100644 index 0000000..cf196c0 --- /dev/null +++ b/src/solution/methods/XaQiWaTh19/optimize.jl @@ -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 diff --git a/src/solution/methods/XaQiWaTh19/structs.jl b/src/solution/methods/XaQiWaTh19/structs.jl new file mode 100644 index 0000000..feeb6d4 --- /dev/null +++ b/src/solution/methods/XaQiWaTh19/structs.jl @@ -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 diff --git a/src/solution/optimize.jl b/src/solution/optimize.jl new file mode 100644 index 0000000..6f65fc7 --- /dev/null +++ b/src/solution/optimize.jl @@ -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 diff --git a/src/solution/solution.jl b/src/solution/solution.jl new file mode 100644 index 0000000..6240d9a --- /dev/null +++ b/src/solution/solution.jl @@ -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 diff --git a/src/solution/structs.jl b/src/solution/structs.jl new file mode 100644 index 0000000..501f902 --- /dev/null +++ b/src/solution/structs.jl @@ -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 diff --git a/src/solution/warmstart.jl b/src/solution/warmstart.jl new file mode 100644 index 0000000..5f09352 --- /dev/null +++ b/src/solution/warmstart.jl @@ -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 diff --git a/src/solution/write.jl b/src/solution/write.jl new file mode 100644 index 0000000..c58a2bc --- /dev/null +++ b/src/solution/write.jl @@ -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 diff --git a/src/initcond.jl b/src/transforms/initcond.jl similarity index 100% rename from src/initcond.jl rename to src/transforms/initcond.jl diff --git a/src/transforms/slice.jl b/src/transforms/slice.jl new file mode 100644 index 0000000..582d47e --- /dev/null +++ b/src/transforms/slice.jl @@ -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 diff --git a/src/sensitivity.jl b/src/transmission/sensitivity.jl similarity index 100% rename from src/sensitivity.jl rename to src/transmission/sensitivity.jl diff --git a/src/transmission/structs.jl b/src/transmission/structs.jl new file mode 100644 index 0000000..260195c --- /dev/null +++ b/src/transmission/structs.jl @@ -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. diff --git a/src/log.jl b/src/utils/log.jl similarity index 100% rename from src/log.jl rename to src/utils/log.jl diff --git a/src/sysimage.jl b/src/utils/sysimage.jl similarity index 100% rename from src/sysimage.jl rename to src/utils/sysimage.jl diff --git a/src/validation/repair.jl b/src/validation/repair.jl new file mode 100644 index 0000000..e1dd964 --- /dev/null +++ b/src/validation/repair.jl @@ -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! diff --git a/src/validate.jl b/src/validation/validate.jl similarity index 82% rename from src/validate.jl rename to src/validation/validate.jl index f9adb14..27000a4 100644 --- a/src/validate.jl +++ b/src/validation/validate.jl @@ -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)) diff --git a/test/convert_test.jl b/test/convert_test.jl index 6c8fb0f..82c8239 100644 --- a/test/convert_test.jl +++ b/test/convert_test.jl @@ -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]) diff --git a/test/instance_test.jl b/test/instance_test.jl index 4fe9b9f..52e0189 100644 --- a/test/instance_test.jl +++ b/test/instance_test.jl @@ -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, diff --git a/test/model_test.jl b/test/model_test.jl index dae7515..9242555 100644 --- a/test/model_test.jl +++ b/test/model_test.jl @@ -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, diff --git a/test/screening_test.jl b/test/screening_test.jl index 7d2201f..19e24c4 100644 --- a/test/screening_test.jl +++ b/test/screening_test.jl @@ -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