Format source code with JuliaFormatter; set up GH Actions

bugfix/formulations
Alinson S. Xavier 4 years ago
parent fb9221b8fb
commit 9224cd2efb

@ -0,0 +1,5 @@
always_for_in = true
always_use_return = true
margin = 80
remove_extra_newlines = true
short_to_long_function_def = true

@ -0,0 +1,28 @@
name: lint
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: julia-actions/setup-julia@latest
with:
version: '1'
- uses: actions/checkout@v1
- name: Format check
shell: julia --color=yes {0}
run: |
using Pkg
Pkg.add(PackageSpec(name="JuliaFormatter", version="0.14.4"))
using JuliaFormatter
format("src", verbose=true)
format("test", verbose=true)
format("benchmark", verbose=true)
out = String(read(Cmd(`git diff`)))
if isempty(out)
exit(0)
end
@error "Some files have not been formatted !!!"
write(stdout, out)
exit(1)

@ -22,4 +22,8 @@ test: build/sysimage.so
@echo Running tests... @echo Running tests...
$(JULIA) --sysimage build/sysimage.so -e 'using Pkg; Pkg.test("UnitCommitment")' | tee build/test.log $(JULIA) --sysimage build/sysimage.so -e 'using Pkg; Pkg.test("UnitCommitment")' | tee build/test.log
format:
julia -e 'using JuliaFormatter; format("src"); format("test"); format("benchmark")'
.PHONY: docs test .PHONY: docs test

@ -30,34 +30,37 @@ function main()
time_model = @elapsed begin time_model = @elapsed begin
model = build_model( model = build_model(
instance=instance, instance = instance,
optimizer=optimizer_with_attributes( optimizer = optimizer_with_attributes(
Gurobi.Optimizer, Gurobi.Optimizer,
"Threads" => 4, "Threads" => 4,
"Seed" => rand(1:1000), "Seed" => rand(1:1000),
), ),
variable_names=true, variable_names = true,
) )
end end
@info "Optimizing..." @info "Optimizing..."
BLAS.set_num_threads(1) BLAS.set_num_threads(1)
UnitCommitment.optimize!(model, time_limit=time_limit, gap_limit=1e-3) UnitCommitment.optimize!(
model,
time_limit = time_limit,
gap_limit = 1e-3,
)
end end
@info @sprintf("Total time was %.2f seconds", total_time) @info @sprintf("Total time was %.2f seconds", total_time)
@info "Writing: $solution_filename" @info "Writing: $solution_filename"
solution = UnitCommitment.solution(model) solution = UnitCommitment.solution(model)
open(solution_filename, "w") do file open(solution_filename, "w") do file
JSON.print(file, solution, 2) return JSON.print(file, solution, 2)
end end
@info "Verifying solution..." @info "Verifying solution..."
UnitCommitment.validate(instance, solution) UnitCommitment.validate(instance, solution)
@info "Exporting model..." @info "Exporting model..."
JuMP.write_to_file(model, model_filename) return JuMP.write_to_file(model, model_filename)
end end
main() main()

@ -3,12 +3,12 @@
# 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("log.jl")
include("instance.jl") include("instance.jl")
include("screening.jl") include("screening.jl")
include("model.jl") include("model.jl")
include("sensitivity.jl") include("sensitivity.jl")
include("validate.jl") include("validate.jl")
include("convert.jl") include("convert.jl")
include("initcond.jl") include("initcond.jl")
end end

@ -10,20 +10,20 @@ function _read_json(path::String)::OrderedDict
else else
file = open(path) file = open(path)
end end
return JSON.parse(file, dicttype=()->DefaultOrderedDict(nothing)) return JSON.parse(file, dicttype = () -> DefaultOrderedDict(nothing))
end end
function _read_egret_solution(path::String)::OrderedDict 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"])
solution = OrderedDict() solution = OrderedDict()
is_on = solution["Is on"] = OrderedDict() is_on = solution["Is on"] = OrderedDict()
production = solution["Production (MW)"] = OrderedDict() production = solution["Production (MW)"] = OrderedDict()
reserve = solution["Reserve (MW)"] = OrderedDict() reserve = solution["Reserve (MW)"] = OrderedDict()
production_cost = solution["Production cost (\$)"] = OrderedDict() production_cost = solution["Production cost (\$)"] = OrderedDict()
startup_cost = solution["Startup cost (\$)"] = OrderedDict() startup_cost = solution["Startup cost (\$)"] = OrderedDict()
for (gen_name, gen_dict) in egret["elements"]["generator"] for (gen_name, gen_dict) in egret["elements"]["generator"]
if endswith(gen_name, "_T") || endswith(gen_name, "_R") if endswith(gen_name, "_T") || endswith(gen_name, "_R")
gen_name = gen_name[1:end-2] gen_name = gen_name[1:end-2]
@ -39,18 +39,18 @@ function _read_egret_solution(path::String)::OrderedDict
else else
reserve[gen_name] = zeros(T) reserve[gen_name] = zeros(T)
end end
startup_cost[gen_name] = zeros(T) startup_cost[gen_name] = zeros(T)
production_cost[gen_name] = zeros(T) production_cost[gen_name] = zeros(T)
if "commitment_cost" in keys(gen_dict) if "commitment_cost" in keys(gen_dict)
for t in 1:T for t in 1:T
x = gen_dict["commitment"]["values"][t] x = gen_dict["commitment"]["values"][t]
commitment_cost = gen_dict["commitment_cost"]["values"][t] commitment_cost = gen_dict["commitment_cost"]["values"][t]
prod_above_cost = gen_dict["production_cost"]["values"][t] prod_above_cost = gen_dict["production_cost"]["values"][t]
prod_base_cost = gen_dict["p_cost"]["values"][1][2] * x prod_base_cost = gen_dict["p_cost"]["values"][1][2] * x
startup_cost[gen_name][t] = commitment_cost - prod_base_cost startup_cost[gen_name][t] = commitment_cost - prod_base_cost
production_cost[gen_name][t] = prod_above_cost + prod_base_cost production_cost[gen_name][t] = prod_above_cost + prod_base_cost
end end
end end
end end
return solution return solution
end end

@ -19,33 +19,31 @@ function generate_initial_conditions!(
B = instance.buses B = instance.buses
t = 1 t = 1
mip = JuMP.Model(optimizer) mip = JuMP.Model(optimizer)
# Decision variables # Decision variables
@variable(mip, x[G], Bin) @variable(mip, x[G], Bin)
@variable(mip, p[G] >= 0) @variable(mip, p[G] >= 0)
# Constraint: Minimum power # Constraint: Minimum power
@constraint(mip, @constraint(mip, min_power[g in G], p[g] >= g.min_power[t] * x[g])
min_power[g in G],
p[g] >= g.min_power[t] * x[g])
# Constraint: Maximum power # Constraint: Maximum power
@constraint(mip, @constraint(mip, max_power[g in G], p[g] <= g.max_power[t] * x[g])
max_power[g in G],
p[g] <= g.max_power[t] * x[g])
# Constraint: Production equals demand # Constraint: Production equals demand
@constraint(mip, @constraint(
power_balance, mip,
sum(b.load[t] for b in B) == sum(p[g] for g in G)) power_balance,
sum(b.load[t] for b in B) == sum(p[g] for g in G)
)
# Constraint: Must run # Constraint: Must run
for g in G for g in G
if g.must_run[t] if g.must_run[t]
@constraint(mip, x[g] == 1) @constraint(mip, x[g] == 1)
end end
end end
# Objective function # Objective function
function cost_slope(g) function cost_slope(g)
mw = g.min_power[t] mw = g.min_power[t]
@ -60,12 +58,10 @@ function generate_initial_conditions!(
return c / mw return c / mw
end end
end end
@objective(mip, @objective(mip, Min, sum(p[g] * cost_slope(g) for g in G))
Min,
sum(p[g] * cost_slope(g) for g in G))
JuMP.optimize!(mip) JuMP.optimize!(mip)
for g in G for g in G
if JuMP.value(x[g]) > 0 if JuMP.value(x[g]) > 0
g.initial_power = JuMP.value(p[g]) g.initial_power = JuMP.value(p[g])

@ -8,7 +8,6 @@ using DataStructures
using GZip using GZip
import Base: getindex, time import Base: getindex, time
mutable struct Bus mutable struct Bus
name::String name::String
offset::Int offset::Int
@ -17,19 +16,16 @@ mutable struct Bus
price_sensitive_loads::Vector price_sensitive_loads::Vector
end end
mutable struct CostSegment mutable struct CostSegment
mw::Vector{Float64} mw::Vector{Float64}
cost::Vector{Float64} cost::Vector{Float64}
end end
mutable struct StartupCategory mutable struct StartupCategory
delay::Int delay::Int
cost::Float64 cost::Float64
end end
mutable struct Unit mutable struct Unit
name::String name::String
bus::Bus bus::Bus
@ -50,7 +46,6 @@ mutable struct Unit
startup_categories::Vector{StartupCategory} startup_categories::Vector{StartupCategory}
end end
mutable struct TransmissionLine mutable struct TransmissionLine
name::String name::String
offset::Int offset::Int
@ -63,19 +58,16 @@ mutable struct TransmissionLine
flow_limit_penalty::Vector{Float64} flow_limit_penalty::Vector{Float64}
end end
mutable struct Reserves mutable struct Reserves
spinning::Vector{Float64} spinning::Vector{Float64}
end end
mutable struct Contingency mutable struct Contingency
name::String name::String
lines::Vector{TransmissionLine} lines::Vector{TransmissionLine}
units::Vector{Unit} units::Vector{Unit}
end end
mutable struct PriceSensitiveLoad mutable struct PriceSensitiveLoad
name::String name::String
bus::Bus bus::Bus
@ -83,7 +75,6 @@ mutable struct PriceSensitiveLoad
revenue::Vector{Float64} revenue::Vector{Float64}
end end
mutable struct UnitCommitmentInstance mutable struct UnitCommitmentInstance
time::Int time::Int
power_balance_penalty::Vector{Float64} power_balance_penalty::Vector{Float64}
@ -95,25 +86,26 @@ mutable struct UnitCommitmentInstance
price_sensitive_loads::Vector{PriceSensitiveLoad} price_sensitive_loads::Vector{PriceSensitiveLoad}
end end
function Base.show(io::IO, instance::UnitCommitmentInstance) function Base.show(io::IO, instance::UnitCommitmentInstance)
print(io, "UnitCommitmentInstance(") print(io, "UnitCommitmentInstance(")
print(io, "$(length(instance.units)) units, ") print(io, "$(length(instance.units)) units, ")
print(io, "$(length(instance.buses)) buses, ") print(io, "$(length(instance.buses)) buses, ")
print(io, "$(length(instance.lines)) lines, ") print(io, "$(length(instance.lines)) lines, ")
print(io, "$(length(instance.contingencies)) contingencies, ") print(io, "$(length(instance.contingencies)) contingencies, ")
print(io, "$(length(instance.price_sensitive_loads)) price sensitive loads, ") print(
io,
"$(length(instance.price_sensitive_loads)) price sensitive loads, ",
)
print(io, "$(instance.time) time steps") print(io, "$(instance.time) time steps")
print(io, ")") print(io, ")")
return
end end
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
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))
@ -122,50 +114,51 @@ function read(path::AbstractString)::UnitCommitmentInstance
end end
end end
function _read(file::IO)::UnitCommitmentInstance function _read(file::IO)::UnitCommitmentInstance
return _from_json(JSON.parse(file, dicttype=()->DefaultOrderedDict(nothing))) return _from_json(
JSON.parse(file, dicttype = () -> DefaultOrderedDict(nothing)),
)
end end
function _from_json(json; repair=true) function _from_json(json; repair = true)
units = Unit[] units = Unit[]
buses = Bus[] buses = Bus[]
contingencies = Contingency[] contingencies = Contingency[]
lines = TransmissionLine[] lines = TransmissionLine[]
loads = PriceSensitiveLoad[] loads = PriceSensitiveLoad[]
function scalar(x; default=nothing) function scalar(x; default = nothing)
x !== nothing || return default x !== nothing || return default
x return x
end end
time_horizon = json["Parameters"]["Time (h)"] time_horizon = json["Parameters"]["Time (h)"]
if time_horizon === nothing if time_horizon === nothing
time_horizon = json["Parameters"]["Time horizon (h)"] time_horizon = json["Parameters"]["Time horizon (h)"]
end end
time_horizon !== nothing || error("Missing required parameter: Time horizon (h)") time_horizon !== nothing || error("Missing parameter: Time horizon (h)")
time_step = scalar(json["Parameters"]["Time step (min)"], default=60) time_step = scalar(json["Parameters"]["Time step (min)"], default = 60)
(60 % time_step == 0) || error("Time step $time_step is not a divisor of 60") (60 % time_step == 0) ||
error("Time step $time_step is not a divisor of 60")
time_multiplier = 60 ÷ time_step time_multiplier = 60 ÷ time_step
T = time_horizon * time_multiplier T = time_horizon * time_multiplier
name_to_bus = Dict{String, Bus}() name_to_bus = Dict{String,Bus}()
name_to_line = Dict{String, TransmissionLine}() name_to_line = Dict{String,TransmissionLine}()
name_to_unit = Dict{String, Unit}() name_to_unit = Dict{String,Unit}()
function timeseries(x; default=nothing) function timeseries(x; default = nothing)
x !== nothing || return default x !== nothing || return default
x isa Array || return [x for t in 1:T] x isa Array || return [x for t in 1:T]
return x return x
end end
# Read parameters # Read parameters
power_balance_penalty = timeseries( power_balance_penalty = timeseries(
json["Parameters"]["Power balance penalty (\$/MW)"], json["Parameters"]["Power balance penalty (\$/MW)"],
default=[1000.0 for t in 1:T], default = [1000.0 for t in 1:T],
) )
# Read buses # Read buses
for (bus_name, dict) in json["Buses"] for (bus_name, dict) in json["Buses"]
bus = Bus( bus = Bus(
@ -178,15 +171,19 @@ function _from_json(json; repair=true)
name_to_bus[bus_name] = bus name_to_bus[bus_name] = bus
push!(buses, bus) push!(buses, bus)
end end
# Read units # Read units
for (unit_name, dict) in json["Generators"] for (unit_name, dict) in json["Generators"]
bus = name_to_bus[dict["Bus"]] bus = name_to_bus[dict["Bus"]]
# Read production cost curve # Read production cost curve
K = length(dict["Production cost curve (MW)"]) K = length(dict["Production cost curve (MW)"])
curve_mw = hcat([timeseries(dict["Production cost curve (MW)"][k]) for k in 1:K]...) curve_mw = hcat(
curve_cost = hcat([timeseries(dict["Production cost curve (\$)"][k]) for k in 1:K]...) [timeseries(dict["Production cost curve (MW)"][k]) for k in 1:K]...,
)
curve_cost = hcat(
[timeseries(dict["Production cost curve (\$)"][k]) for k in 1:K]...,
)
min_power = curve_mw[:, 1] min_power = curve_mw[:, 1]
max_power = curve_mw[:, K] max_power = curve_mw[:, K]
min_power_cost = curve_cost[:, 1] min_power_cost = curve_cost[:, 1]
@ -194,13 +191,13 @@ function _from_json(json; repair=true)
for k in 2:K for k in 2:K
amount = curve_mw[:, k] - curve_mw[:, k-1] amount = curve_mw[:, k] - curve_mw[:, k-1]
cost = (curve_cost[:, k] - curve_cost[:, k-1]) ./ amount cost = (curve_cost[:, k] - curve_cost[:, k-1]) ./ amount
replace!(cost, NaN=>0.0) replace!(cost, NaN => 0.0)
push!(segments, CostSegment(amount, cost)) push!(segments, CostSegment(amount, cost))
end end
# Read startup costs # Read startup costs
startup_delays = scalar(dict["Startup delays (h)"], default=[1]) startup_delays = scalar(dict["Startup delays (h)"], default = [1])
startup_costs = scalar(dict["Startup costs (\$)"], default=[0.]) startup_costs = scalar(dict["Startup costs (\$)"], default = [0.0])
startup_categories = StartupCategory[] startup_categories = StartupCategory[]
for k in 1:length(startup_delays) for k in 1:length(startup_delays)
push!( push!(
@ -211,40 +208,43 @@ function _from_json(json; repair=true)
), ),
) )
end end
# Read and validate initial conditions # Read and validate initial conditions
initial_power = scalar(dict["Initial power (MW)"], default=nothing) initial_power = scalar(dict["Initial power (MW)"], default = nothing)
initial_status = scalar(dict["Initial status (h)"], default=nothing) initial_status = scalar(dict["Initial status (h)"], default = nothing)
if initial_power === nothing if initial_power === nothing
initial_status === nothing || error("unit $unit_name has initial status but no initial power") initial_status === nothing ||
error("unit $unit_name has initial status but no initial power")
else else
initial_status !== nothing || error("unit $unit_name has initial power but no initial status") initial_status !== nothing ||
initial_status != 0 || error("unit $unit_name has invalid initial status") error("unit $unit_name has initial power but no initial status")
initial_status != 0 ||
error("unit $unit_name has invalid initial status")
if initial_status < 0 && initial_power > 1e-3 if initial_status < 0 && initial_power > 1e-3
error("unit $unit_name has invalid initial power") error("unit $unit_name has invalid initial power")
end end
initial_status *= time_multiplier initial_status *= time_multiplier
end end
unit = Unit( unit = Unit(
unit_name, unit_name,
bus, bus,
max_power, max_power,
min_power, min_power,
timeseries(dict["Must run?"], default=[false for t in 1:T]), timeseries(dict["Must run?"], default = [false for t in 1:T]),
min_power_cost, min_power_cost,
segments, segments,
scalar(dict["Minimum uptime (h)"], default=1) * time_multiplier, scalar(dict["Minimum uptime (h)"], default = 1) * time_multiplier,
scalar(dict["Minimum downtime (h)"], default=1) * time_multiplier, scalar(dict["Minimum downtime (h)"], default = 1) * time_multiplier,
scalar(dict["Ramp up limit (MW)"], default=1e6), scalar(dict["Ramp up limit (MW)"], default = 1e6),
scalar(dict["Ramp down limit (MW)"], default=1e6), scalar(dict["Ramp down limit (MW)"], default = 1e6),
scalar(dict["Startup limit (MW)"], default=1e6), scalar(dict["Startup limit (MW)"], default = 1e6),
scalar(dict["Shutdown limit (MW)"], default=1e6), scalar(dict["Shutdown limit (MW)"], default = 1e6),
initial_status, initial_status,
initial_power, initial_power,
timeseries( timeseries(
dict["Provides spinning reserves?"], dict["Provides spinning reserves?"],
default=[true for t in 1:T], default = [true for t in 1:T],
), ),
startup_categories, startup_categories,
) )
@ -252,16 +252,14 @@ function _from_json(json; repair=true)
name_to_unit[unit_name] = unit name_to_unit[unit_name] = unit
push!(units, unit) push!(units, unit)
end end
# Read reserves # Read reserves
reserves = Reserves(zeros(T)) reserves = Reserves(zeros(T))
if "Reserves" in keys(json) if "Reserves" in keys(json)
reserves.spinning = timeseries( reserves.spinning =
json["Reserves"]["Spinning (MW)"], timeseries(json["Reserves"]["Spinning (MW)"], default = zeros(T))
default=zeros(T),
)
end end
# Read transmission lines # Read transmission lines
if "Transmission lines" in keys(json) if "Transmission lines" in keys(json)
for (line_name, dict) in json["Transmission lines"] for (line_name, dict) in json["Transmission lines"]
@ -274,38 +272,40 @@ function _from_json(json; repair=true)
scalar(dict["Susceptance (S)"]), scalar(dict["Susceptance (S)"]),
timeseries( timeseries(
dict["Normal flow limit (MW)"], dict["Normal flow limit (MW)"],
default=[1e8 for t in 1:T], default = [1e8 for t in 1:T],
), ),
timeseries( timeseries(
dict["Emergency flow limit (MW)"], dict["Emergency flow limit (MW)"],
default=[1e8 for t in 1:T], default = [1e8 for t in 1:T],
), ),
timeseries( timeseries(
dict["Flow limit penalty (\$/MW)"], dict["Flow limit penalty (\$/MW)"],
default=[5000.0 for t in 1:T], default = [5000.0 for t in 1:T],
), ),
) )
name_to_line[line_name] = line name_to_line[line_name] = line
push!(lines, line) push!(lines, line)
end end
end end
# Read contingencies # Read contingencies
if "Contingencies" in keys(json) if "Contingencies" in keys(json)
for (cont_name, dict) in json["Contingencies"] for (cont_name, dict) in json["Contingencies"]
affected_units = Unit[] affected_units = Unit[]
affected_lines = TransmissionLine[] affected_lines = TransmissionLine[]
if "Affected lines" in keys(dict) if "Affected lines" in keys(dict)
affected_lines = [name_to_line[l] for l in dict["Affected lines"]] affected_lines =
[name_to_line[l] for l in dict["Affected lines"]]
end end
if "Affected units" in keys(dict) if "Affected units" in keys(dict)
affected_units = [name_to_unit[u] for u in dict["Affected units"]] affected_units =
[name_to_unit[u] for u in dict["Affected units"]]
end end
cont = Contingency(cont_name, affected_lines, affected_units) cont = Contingency(cont_name, affected_lines, affected_units)
push!(contingencies, cont) push!(contingencies, cont)
end end
end end
# Read price-sensitive loads # Read price-sensitive loads
if "Price-sensitive loads" in keys(json) if "Price-sensitive loads" in keys(json)
for (load_name, dict) in json["Price-sensitive loads"] for (load_name, dict) in json["Price-sensitive loads"]
@ -320,7 +320,7 @@ function _from_json(json; repair=true)
push!(loads, load) push!(loads, load)
end end
end end
instance = UnitCommitmentInstance( instance = UnitCommitmentInstance(
T, T,
power_balance_penalty, power_balance_penalty,
@ -337,7 +337,6 @@ function _from_json(json; repair=true)
return instance return instance
end end
""" """
slice(instance, range) slice(instance, range)
@ -387,5 +386,4 @@ function slice(
return modified return modified
end end
export UnitCommitmentInstance export UnitCommitmentInstance

@ -7,35 +7,37 @@ using Base.CoreLogging, Logging, Printf
struct TimeLogger <: AbstractLogger struct TimeLogger <: AbstractLogger
initial_time::Float64 initial_time::Float64
file::Union{Nothing, IOStream} file::Union{Nothing,IOStream}
screen_log_level screen_log_level::Any
io_log_level io_log_level::Any
end end
function TimeLogger(; function TimeLogger(;
initial_time::Float64, initial_time::Float64,
file::Union{Nothing, IOStream} = nothing, file::Union{Nothing,IOStream} = nothing,
screen_log_level = CoreLogging.Info, screen_log_level = CoreLogging.Info,
io_log_level = CoreLogging.Info, io_log_level = CoreLogging.Info,
) :: TimeLogger )::TimeLogger
return TimeLogger(initial_time, file, screen_log_level, io_log_level) return TimeLogger(initial_time, file, screen_log_level, io_log_level)
end end
min_enabled_level(logger::TimeLogger) = logger.io_log_level min_enabled_level(logger::TimeLogger) = logger.io_log_level
shouldlog(logger::TimeLogger, level, _module, group, id) = true shouldlog(logger::TimeLogger, level, _module, group, id) = true
function handle_message(logger::TimeLogger, function handle_message(
level, logger::TimeLogger,
message, level,
_module, message,
group, _module,
id, group,
filepath, id,
line; filepath,
kwargs...) line;
kwargs...,
)
elapsed_time = time() - logger.initial_time elapsed_time = time() - logger.initial_time
time_string = @sprintf("[%12.3f] ", elapsed_time) time_string = @sprintf("[%12.3f] ", elapsed_time)
if level >= Logging.Error if level >= Logging.Error
color = :light_red color = :light_red
elseif level >= Logging.Warn elseif level >= Logging.Warn
@ -43,9 +45,9 @@ function handle_message(logger::TimeLogger,
else else
color = :light_green color = :light_green
end end
if level >= logger.screen_log_level if level >= logger.screen_log_level
printstyled(time_string, color=color) printstyled(time_string, color = color)
println(message) println(message)
end end
if logger.file !== nothing && level >= logger.io_log_level if logger.file !== nothing && level >= logger.io_log_level
@ -58,5 +60,5 @@ end
function _setup_logger() function _setup_logger()
initial_time = time() initial_time = time()
global_logger(TimeLogger(initial_time=initial_time)) return global_logger(TimeLogger(initial_time = initial_time))
end end

@ -5,15 +5,14 @@
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 # Extend some JuMP functions so that decision variables can be safely replaced by
# (constant) floating point numbers. # (constant) floating point numbers.
function value(x::Float64) function value(x::Float64)
x return x
end end
function fix(x::Float64, v::Float64; force) function fix(x::Float64, v::Float64; force)
abs(x - v) < 1e-6 || error("Value mismatch: $x != $v") return abs(x - v) < 1e-6 || error("Value mismatch: $x != $v")
end end
function set_name(x::Float64, n::String) function set_name(x::Float64, n::String)
@ -21,20 +20,19 @@ function set_name(x::Float64, n::String)
end end
function build_model(; function build_model(;
filename::Union{String, Nothing}=nothing, filename::Union{String,Nothing} = nothing,
instance::Union{UnitCommitmentInstance, Nothing}=nothing, 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,
lodf_cutoff::Float64=0.001, lodf_cutoff::Float64 = 0.001,
optimizer=nothing, optimizer = nothing,
variable_names::Bool=false, variable_names::Bool = false,
)::JuMP.Model )::JuMP.Model
if (filename === nothing) && (instance === nothing) if (filename === nothing) && (instance === nothing)
error("Either filename or instance must be specified") error("Either filename or instance must be specified")
end end
if filename !== nothing if filename !== nothing
@info "Reading: $(filename)" @info "Reading: $(filename)"
time_read = @elapsed begin time_read = @elapsed begin
@ -42,7 +40,7 @@ function build_model(;
end end
@info @sprintf("Read problem in %.2f seconds", time_read) @info @sprintf("Read problem in %.2f seconds", time_read)
end 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)
@ -51,25 +49,29 @@ function build_model(;
@info "Computing injection shift factors..." @info "Computing injection shift factors..."
time_isf = @elapsed begin time_isf = @elapsed begin
isf = UnitCommitment._injection_shift_factors( isf = UnitCommitment._injection_shift_factors(
lines=instance.lines, lines = instance.lines,
buses=instance.buses, buses = instance.buses,
) )
end end
@info @sprintf("Computed ISF in %.2f seconds", time_isf) @info @sprintf("Computed ISF in %.2f seconds", time_isf)
@info "Computing line outage factors..." @info "Computing line outage factors..."
time_lodf = @elapsed begin time_lodf = @elapsed begin
lodf = UnitCommitment._line_outage_factors( lodf = UnitCommitment._line_outage_factors(
lines=instance.lines, lines = instance.lines,
buses=instance.buses, buses = instance.buses,
isf=isf, isf = isf,
) )
end end
@info @sprintf("Computed LODF in %.2f seconds", time_lodf) @info @sprintf("Computed LODF in %.2f seconds", time_lodf)
@info @sprintf("Applying PTDF and LODF cutoffs (%.5f, %.5f)", isf_cutoff, lodf_cutoff) @info @sprintf(
isf[abs.(isf) .< isf_cutoff] .= 0 "Applying PTDF and LODF cutoffs (%.5f, %.5f)",
lodf[abs.(lodf) .< lodf_cutoff] .= 0 isf_cutoff,
lodf_cutoff
)
isf[abs.(isf).<isf_cutoff] .= 0
lodf[abs.(lodf).<lodf_cutoff] .= 0
end end
end end
@ -138,21 +140,19 @@ function build_model(;
if variable_names if variable_names
_set_names!(model) _set_names!(model)
end end
return model return model
end end
function _add_transmission_line!(model, lm) function _add_transmission_line!(model, lm)
obj, T = model[:obj], model[:instance].time obj, T = model[:obj], model[:instance].time
overflow = model[:overflow] overflow = model[:overflow]
for t in 1:T for t in 1:T
v = overflow[lm.name, t] = @variable(model, lower_bound=0) v = overflow[lm.name, t] = @variable(model, lower_bound = 0)
add_to_expression!(obj, v, lm.flow_limit_penalty[t]) add_to_expression!(obj, v, lm.flow_limit_penalty[t])
end end
end end
function _add_bus!(model::JuMP.Model, b::Bus) function _add_bus!(model::JuMP.Model, b::Bus)
mip = model mip = model
net_injection = model[:expr_net_injection] net_injection = model[:expr_net_injection]
@ -161,12 +161,13 @@ function _add_bus!(model::JuMP.Model, b::Bus)
for t in 1:model[:instance].time for t in 1:model[:instance].time
# Fixed load # Fixed load
net_injection[b.name, t] = AffExpr(-b.load[t]) net_injection[b.name, t] = AffExpr(-b.load[t])
# Reserves # Reserves
reserve[b.name, t] = AffExpr() reserve[b.name, t] = AffExpr()
# Load curtailment # Load curtailment
curtail[b.name, t] = @variable(mip, lower_bound=0, upper_bound=b.load[t]) curtail[b.name, t] =
@variable(mip, lower_bound = 0, upper_bound = b.load[t])
add_to_expression!(net_injection[b.name, t], curtail[b.name, t], 1.0) add_to_expression!(net_injection[b.name, t], curtail[b.name, t], 1.0)
add_to_expression!( add_to_expression!(
model[:obj], model[:obj],
@ -176,24 +177,27 @@ function _add_bus!(model::JuMP.Model, b::Bus)
end end
end end
function _add_price_sensitive_load!(model::JuMP.Model, ps::PriceSensitiveLoad) function _add_price_sensitive_load!(model::JuMP.Model, ps::PriceSensitiveLoad)
mip = model mip = model
loads = model[:loads] loads = model[:loads]
net_injection = model[:expr_net_injection] net_injection = model[:expr_net_injection]
for t in 1:model[:instance].time for t in 1:model[:instance].time
# Decision variable # Decision variable
loads[ps.name, t] = @variable(mip, lower_bound=0, upper_bound=ps.demand[t]) loads[ps.name, t] =
@variable(mip, lower_bound = 0, upper_bound = ps.demand[t])
# Objective function terms # Objective function terms
add_to_expression!(model[:obj], loads[ps.name, t], -ps.revenue[t]) add_to_expression!(model[:obj], loads[ps.name, t], -ps.revenue[t])
# Net injection # Net injection
add_to_expression!(net_injection[ps.bus.name, t], loads[ps.name, t], -1.0) add_to_expression!(
net_injection[ps.bus.name, t],
loads[ps.name, t],
-1.0,
)
end end
end end
function _add_unit!(model::JuMP.Model, g::Unit) function _add_unit!(model::JuMP.Model, g::Unit)
mip, T = model, model[:instance].time mip, T = model, model[:instance].time
gi, K, S = g.name, length(g.cost_segments), length(g.startup_categories) gi, K, S = g.name, length(g.cost_segments), length(g.startup_categories)
@ -207,11 +211,11 @@ function _add_unit!(model::JuMP.Model, g::Unit)
switch_off = model[:switch_off] switch_off = model[:switch_off]
expr_net_injection = model[:expr_net_injection] expr_net_injection = model[:expr_net_injection]
expr_reserve = model[:expr_reserve] expr_reserve = model[:expr_reserve]
if !all(g.must_run) && any(g.must_run) if !all(g.must_run) && any(g.must_run)
error("Partially must-run units are not currently supported") error("Partially must-run units are not currently supported")
end end
if g.initial_power === nothing || g.initial_status === nothing if g.initial_power === nothing || g.initial_status === nothing
error("Initial conditions for $(g.name) must be provided") error("Initial conditions for $(g.name) must be provided")
end end
@ -221,25 +225,25 @@ function _add_unit!(model::JuMP.Model, g::Unit)
# Decision variables # Decision variables
for t in 1:T for t in 1:T
for k in 1:K for k in 1:K
segprod[gi, t, k] = @variable(model, lower_bound=0) segprod[gi, t, k] = @variable(model, lower_bound = 0)
end end
prod_above[gi, t] = @variable(model, lower_bound=0) prod_above[gi, t] = @variable(model, lower_bound = 0)
if g.provides_spinning_reserves[t] if g.provides_spinning_reserves[t]
reserve[gi, t] = @variable(model, lower_bound=0) reserve[gi, t] = @variable(model, lower_bound = 0)
else else
reserve[gi, t] = 0.0 reserve[gi, t] = 0.0
end end
for s in 1:S for s in 1:S
startup[gi, t, s] = @variable(model, binary=true) startup[gi, t, s] = @variable(model, binary = true)
end end
if g.must_run[t] if g.must_run[t]
is_on[gi, t] = 1.0 is_on[gi, t] = 1.0
switch_on[gi, t] = (t == 1 ? 1.0 - is_initially_on : 0.0) switch_on[gi, t] = (t == 1 ? 1.0 - is_initially_on : 0.0)
switch_off[gi, t] = 0.0 switch_off[gi, t] = 0.0
else else
is_on[gi, t] = @variable(model, binary=true) is_on[gi, t] = @variable(model, binary = true)
switch_on[gi, t] = @variable(model, binary=true) switch_on[gi, t] = @variable(model, binary = true)
switch_off[gi, t] = @variable(model, binary=true) switch_off[gi, t] = @variable(model, binary = true)
end end
end end
@ -247,19 +251,28 @@ function _add_unit!(model::JuMP.Model, g::Unit)
# Time-dependent start-up costs # Time-dependent start-up costs
for s in 1:S for s in 1:S
# If unit is switching on, we must choose a startup category # If unit is switching on, we must choose a startup category
model[:eq_startup_choose][gi, t, s] = model[:eq_startup_choose][gi, t, s] = @constraint(
@constraint(mip, switch_on[gi, t] == sum(startup[gi, t, s] for s in 1:S)) mip,
switch_on[gi, t] == sum(startup[gi, t, s] for s in 1:S)
)
# If unit has not switched off in the last `delay` time periods, startup category is forbidden. # If unit has not switched off in the last `delay` time periods, startup category is forbidden.
# The last startup category is always allowed. # The last startup category is always allowed.
if s < S if s < S
range = (t - g.startup_categories[s + 1].delay + 1):(t - g.startup_categories[s].delay) range_start = t - g.startup_categories[s+1].delay + 1
initial_sum = (g.initial_status < 0 && (g.initial_status + 1 in range) ? 1.0 : 0.0) range_end = t - g.startup_categories[s].delay
model[:eq_startup_restrict][gi, t, s] = range = (range_start:range_end)
@constraint(mip, startup[gi, t, s] initial_sum = (
<= initial_sum + sum(switch_off[gi, i] for i in range if i >= 1)) g.initial_status < 0 && (g.initial_status + 1 in range) ? 1.0 : 0.0
)
model[:eq_startup_restrict][gi, t, s] = @constraint(
mip,
startup[gi, t, s] <=
initial_sum +
sum(switch_off[gi, i] for i in range if i >= 1)
)
end end
# Objective function terms for start-up costs # Objective function terms for start-up costs
add_to_expression!( add_to_expression!(
model[:obj], model[:obj],
@ -267,142 +280,171 @@ function _add_unit!(model::JuMP.Model, g::Unit)
g.startup_categories[s].cost, g.startup_categories[s].cost,
) )
end end
# Objective function terms for production costs # Objective function terms for production costs
add_to_expression!(model[:obj], is_on[gi, t], g.min_power_cost[t]) add_to_expression!(model[:obj], is_on[gi, t], g.min_power_cost[t])
for k in 1:K for k in 1:K
add_to_expression!(model[:obj], segprod[gi, t, k], g.cost_segments[k].cost[t]) add_to_expression!(
model[:obj],
segprod[gi, t, k],
g.cost_segments[k].cost[t],
)
end end
# Production limits (piecewise-linear segments) # Production limits (piecewise-linear segments)
for k in 1:K for k in 1:K
model[:eq_segprod_limit][gi, t, k] = model[:eq_segprod_limit][gi, t, k] = @constraint(
@constraint(mip, segprod[gi, t, k] <= g.cost_segments[k].mw[t] * is_on[gi, t]) mip,
segprod[gi, t, k] <= g.cost_segments[k].mw[t] * is_on[gi, t]
)
end end
# Definition of production # Definition of production
model[:eq_prod_above_def][gi, t] = model[:eq_prod_above_def][gi, t] = @constraint(
@constraint(mip, prod_above[gi, t] == sum(segprod[gi, t, k] for k in 1:K)) mip,
prod_above[gi, t] == sum(segprod[gi, t, k] for k in 1:K)
)
# Production limit # Production limit
model[:eq_prod_limit][gi, t] = model[:eq_prod_limit][gi, t] = @constraint(
@constraint(mip, mip,
prod_above[gi, t] + reserve[gi, t] prod_above[gi, t] + reserve[gi, t] <=
<= (g.max_power[t] - g.min_power[t]) * is_on[gi, t]) (g.max_power[t] - g.min_power[t]) * is_on[gi, t]
)
# Binary variable equations for economic units # Binary variable equations for economic units
if !g.must_run[t] if !g.must_run[t]
# Link binary variables # Link binary variables
if t == 1 if t == 1
model[:eq_binary_link][gi, t] = model[:eq_binary_link][gi, t] = @constraint(
@constraint(mip, mip,
is_on[gi, t] - is_initially_on == is_on[gi, t] - is_initially_on ==
switch_on[gi, t] - switch_off[gi, t]) switch_on[gi, t] - switch_off[gi, t]
)
else else
model[:eq_binary_link][gi, t] = model[:eq_binary_link][gi, t] = @constraint(
@constraint(mip, mip,
is_on[gi, t] - is_on[gi, t-1] == is_on[gi, t] - is_on[gi, t-1] ==
switch_on[gi, t] - switch_off[gi, t]) switch_on[gi, t] - switch_off[gi, t]
)
end end
# Cannot switch on and off at the same time # Cannot switch on and off at the same time
model[:eq_switch_on_off][gi, t] = model[:eq_switch_on_off][gi, t] =
@constraint(mip, switch_on[gi, t] + switch_off[gi, t] <= 1) @constraint(mip, switch_on[gi, t] + switch_off[gi, t] <= 1)
end end
# Ramp up limit # Ramp up limit
if t == 1 if t == 1
if is_initially_on == 1 if is_initially_on == 1
model[:eq_ramp_up][gi, t] = model[:eq_ramp_up][gi, t] = @constraint(
@constraint(mip, mip,
prod_above[gi, t] + reserve[gi, t] <= prod_above[gi, t] + reserve[gi, t] <=
(g.initial_power - g.min_power[t]) + g.ramp_up_limit) (g.initial_power - g.min_power[t]) + g.ramp_up_limit
)
end end
else else
model[:eq_ramp_up][gi, t] = model[:eq_ramp_up][gi, t] = @constraint(
@constraint(mip, mip,
prod_above[gi, t] + reserve[gi, t] <= prod_above[gi, t] + reserve[gi, t] <=
prod_above[gi, t-1] + g.ramp_up_limit) prod_above[gi, t-1] + g.ramp_up_limit
)
end end
# Ramp down limit # Ramp down limit
if t == 1 if t == 1
if is_initially_on == 1 if is_initially_on == 1
model[:eq_ramp_down][gi, t] = model[:eq_ramp_down][gi, t] = @constraint(
@constraint(mip, mip,
prod_above[gi, t] >= prod_above[gi, t] >=
(g.initial_power - g.min_power[t]) - g.ramp_down_limit) (g.initial_power - g.min_power[t]) - g.ramp_down_limit
)
end end
else else
model[:eq_ramp_down][gi, t] = model[:eq_ramp_down][gi, t] = @constraint(
@constraint(mip, mip,
prod_above[gi, t] >= prod_above[gi, t] >= prod_above[gi, t-1] - g.ramp_down_limit
prod_above[gi, t-1] - g.ramp_down_limit) )
end end
# Startup limit # Startup limit
model[:eq_startup_limit][gi, t] = model[:eq_startup_limit][gi, t] = @constraint(
@constraint(mip, mip,
prod_above[gi, t] + reserve[gi, t] <= prod_above[gi, t] + reserve[gi, t] <=
(g.max_power[t] - g.min_power[t]) * is_on[gi, t] (g.max_power[t] - g.min_power[t]) * is_on[gi, t] -
- max(0, g.max_power[t] - g.startup_limit) * switch_on[gi, t]) max(0, g.max_power[t] - g.startup_limit) * switch_on[gi, t]
)
# Shutdown limit # Shutdown limit
if g.initial_power > g.shutdown_limit if g.initial_power > g.shutdown_limit
model[:eq_shutdown_limit][gi, 0] = model[:eq_shutdown_limit][gi, 0] =
@constraint(mip, switch_off[gi, 1] <= 0) @constraint(mip, switch_off[gi, 1] <= 0)
end end
if t < T if t < T
model[:eq_shutdown_limit][gi, t] = model[:eq_shutdown_limit][gi, t] = @constraint(
@constraint(mip, mip,
prod_above[gi, t] <= prod_above[gi, t] <=
(g.max_power[t] - g.min_power[t]) * is_on[gi, t] (g.max_power[t] - g.min_power[t]) * is_on[gi, t] -
- max(0, g.max_power[t] - g.shutdown_limit) * switch_off[gi, t+1]) max(0, g.max_power[t] - g.shutdown_limit) * switch_off[gi, t+1]
)
end end
# Minimum up-time # Minimum up-time
model[:eq_min_uptime][gi, t] = model[:eq_min_uptime][gi, t] = @constraint(
@constraint(mip, mip,
sum(switch_on[gi, i] sum(switch_on[gi, i] for i in (t-g.min_uptime+1):t if i >= 1) <=
for i in (t - g.min_uptime + 1):t if i >= 1 is_on[gi, t]
) <= is_on[gi, t]) )
# # Minimum down-time # # Minimum down-time
model[:eq_min_downtime][gi, t] = model[:eq_min_downtime][gi, t] = @constraint(
@constraint(mip, mip,
sum(switch_off[gi, i] sum(switch_off[gi, i] for i in (t-g.min_downtime+1):t if i >= 1) <= 1 - is_on[gi, t]
for i in (t - g.min_downtime + 1):t if i >= 1 )
) <= 1 - is_on[gi, t])
# Minimum up/down-time for initial periods # Minimum up/down-time for initial periods
if t == 1 if t == 1
if g.initial_status > 0 if g.initial_status > 0
model[:eq_min_uptime][gi, 0] = model[:eq_min_uptime][gi, 0] = @constraint(
@constraint(mip, sum(switch_off[gi, i] mip,
for i in 1:(g.min_uptime - g.initial_status) if i <= T) == 0) sum(
switch_off[gi, i] for
i in 1:(g.min_uptime-g.initial_status) if i <= T
) == 0
)
else else
model[:eq_min_downtime][gi, 0] = model[:eq_min_downtime][gi, 0] = @constraint(
@constraint(mip, sum(switch_on[gi, i] mip,
for i in 1:(g.min_downtime + g.initial_status) if i <= T) == 0) sum(
switch_on[gi, i] for
i in 1:(g.min_downtime+g.initial_status) if i <= T
) == 0
)
end end
end end
# Add to net injection expression # Add to net injection expression
add_to_expression!(expr_net_injection[g.bus.name, t], prod_above[g.name, t], 1.0) add_to_expression!(
add_to_expression!(expr_net_injection[g.bus.name, t], is_on[g.name, t], g.min_power[t]) expr_net_injection[g.bus.name, t],
prod_above[g.name, t],
1.0,
)
add_to_expression!(
expr_net_injection[g.bus.name, t],
is_on[g.name, t],
g.min_power[t],
)
# Add to reserves expression # Add to reserves expression
add_to_expression!(expr_reserve[g.bus.name, t], reserve[gi, t], 1.0) add_to_expression!(expr_reserve[g.bus.name, t], reserve[gi, t], 1.0)
end end
end end
function _build_obj_function!(model::JuMP.Model) function _build_obj_function!(model::JuMP.Model)
@objective(model, Min, model[:obj]) @objective(model, Min, model[:obj])
end end
function _build_net_injection_eqs!(model::JuMP.Model) function _build_net_injection_eqs!(model::JuMP.Model)
T = model[:instance].time T = model[:instance].time
net_injection = model[:net_injection] net_injection = model[:net_injection]
@ -412,45 +454,36 @@ function _build_net_injection_eqs!(model::JuMP.Model)
@constraint(model, n == model[:expr_net_injection][b.name, t]) @constraint(model, n == model[:expr_net_injection][b.name, t])
end end
for t in 1:T for t in 1:T
model[:eq_power_balance][t] = model[:eq_power_balance][t] = @constraint(
@constraint( model,
model, sum(net_injection[b.name, t] for b in model[:instance].buses) == 0
sum( )
net_injection[b.name, t]
for b in model[:instance].buses
) == 0
)
end end
end end
function _build_reserve_eqs!(model::JuMP.Model) function _build_reserve_eqs!(model::JuMP.Model)
reserves = model[:instance].reserves reserves = model[:instance].reserves
for t in 1:model[:instance].time for t in 1:model[:instance].time
model[:eq_min_reserve][t] = model[:eq_min_reserve][t] = @constraint(
@constraint( model,
model, sum(
sum( model[:expr_reserve][b.name, t] for b in model[:instance].buses
model[:expr_reserve][b.name, t] ) >= reserves.spinning[t]
for b in model[:instance].buses )
) >= reserves.spinning[t]
)
end end
end end
function _enforce_transmission(;
function _enforce_transmission(
;
model::JuMP.Model, model::JuMP.Model,
violation::Violation, violation::Violation,
isf::Matrix{Float64}, isf::Matrix{Float64},
lodf::Matrix{Float64}, lodf::Matrix{Float64},
)::Nothing )::Nothing
instance = model[:instance] instance = model[:instance]
limit::Float64 = 0.0 limit::Float64 = 0.0
overflow = model[:overflow] overflow = model[:overflow]
net_injection = model[:net_injection] net_injection = model[:net_injection]
if violation.outage_line === nothing if violation.outage_line === nothing
limit = violation.monitored_line.normal_flow_limit[violation.time] limit = violation.monitored_line.normal_flow_limit[violation.time]
@info @sprintf( @info @sprintf(
@ -469,34 +502,42 @@ function _enforce_transmission(
violation.outage_line.name, violation.outage_line.name,
) )
end end
fm = violation.monitored_line.name fm = violation.monitored_line.name
t = violation.time t = violation.time
flow = @variable(model, base_name="flow[$fm,$t]") flow = @variable(model, base_name = "flow[$fm,$t]")
v = overflow[violation.monitored_line.name, violation.time] v = overflow[violation.monitored_line.name, violation.time]
@constraint(model, flow <= limit + v) @constraint(model, flow <= limit + v)
@constraint(model, -flow <= limit + v) @constraint(model, -flow <= limit + v)
if violation.outage_line === nothing if violation.outage_line === nothing
@constraint(model, flow == sum(net_injection[b.name, violation.time] * @constraint(
isf[violation.monitored_line.offset, b.offset] model,
for b in instance.buses flow == sum(
if b.offset > 0)) net_injection[b.name, violation.time] *
isf[violation.monitored_line.offset, b.offset] for
b in instance.buses if b.offset > 0
)
)
else else
@constraint(model, flow == sum(net_injection[b.name, violation.time] * ( @constraint(
isf[violation.monitored_line.offset, b.offset] + ( model,
lodf[violation.monitored_line.offset, violation.outage_line.offset] * flow == sum(
isf[violation.outage_line.offset, b.offset] net_injection[b.name, violation.time] * (
) isf[violation.monitored_line.offset, b.offset] + (
) lodf[
for b in instance.buses violation.monitored_line.offset,
if b.offset > 0)) violation.outage_line.offset,
end ] * isf[violation.outage_line.offset, b.offset]
nothing )
) for b in instance.buses if b.offset > 0
)
)
end
return nothing
end 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
@ -505,7 +546,6 @@ function _set_names!(model::JuMP.Model)
@info @sprintf("Set names in %.2f seconds", time_varnames) @info @sprintf("Set names in %.2f seconds", time_varnames)
end end
function _set_names!(dict::Dict) function _set_names!(dict::Dict)
for name in keys(dict) for name in keys(dict)
dict[name] isa AbstractDict || continue dict[name] isa AbstractDict || continue
@ -519,72 +559,75 @@ function _set_names!(dict::Dict)
end end
end end
function solution(model::JuMP.Model) function solution(model::JuMP.Model)
instance, T = model[:instance], model[:instance].time instance, T = model[:instance], model[:instance].time
function timeseries(vars, collection) function timeseries(vars, collection)
return OrderedDict( return OrderedDict(
b.name => [round(value(vars[b.name, t]), digits=5) for t in 1:T] b.name => [round(value(vars[b.name, t]), digits = 5) for t in 1:T]
for b in collection for b in collection
) )
end end
function production_cost(g) function production_cost(g)
return [ return [
value(model[:is_on][g.name, t]) * g.min_power_cost[t] + value(model[:is_on][g.name, t]) * g.min_power_cost[t] + sum(
sum(
Float64[ Float64[
value(model[:segprod][g.name, t, k]) * g.cost_segments[k].cost[t] value(model[:segprod][g.name, t, k]) *
for k in 1:length(g.cost_segments) g.cost_segments[k].cost[t] for
] k in 1:length(g.cost_segments)
) ],
for t in 1:T ) for t in 1:T
] ]
end end
function production(g) function production(g)
return [ return [
value(model[:is_on][g.name, t]) * g.min_power[t] + value(model[:is_on][g.name, t]) * g.min_power[t] + sum(
sum(
Float64[ Float64[
value(model[:segprod][g.name, t, k]) value(model[:segprod][g.name, t, k]) for
for k in 1:length(g.cost_segments) k in 1:length(g.cost_segments)
] ],
) ) for t in 1:T
for t in 1:T
] ]
end end
function startup_cost(g) function startup_cost(g)
S = length(g.startup_categories) S = length(g.startup_categories)
return [sum(g.startup_categories[s].cost * value(model[:startup][g.name, t, s]) return [
for s in 1:S) sum(
for t in 1:T] g.startup_categories[s].cost *
value(model[:startup][g.name, t, s]) for s in 1:S
) for t in 1:T
]
end end
sol = OrderedDict() sol = OrderedDict()
sol["Production (MW)"] = OrderedDict(g.name => production(g) for g in instance.units) sol["Production (MW)"] =
sol["Production cost (\$)"] = OrderedDict(g.name => production_cost(g) for g in instance.units) OrderedDict(g.name => production(g) for g in instance.units)
sol["Startup cost (\$)"] = OrderedDict(g.name => startup_cost(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["Is on"] = timeseries(model[:is_on], instance.units)
sol["Switch on"] = timeseries(model[:switch_on], instance.units) sol["Switch on"] = timeseries(model[:switch_on], instance.units)
sol["Switch off"] = timeseries(model[:switch_off], instance.units) sol["Switch off"] = timeseries(model[:switch_off], instance.units)
sol["Reserve (MW)"] = timeseries(model[:reserve], instance.units) sol["Reserve (MW)"] = timeseries(model[:reserve], instance.units)
sol["Net injection (MW)"] = timeseries(model[:net_injection], instance.buses) sol["Net injection (MW)"] =
timeseries(model[:net_injection], instance.buses)
sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses) sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses)
if !isempty(instance.lines) if !isempty(instance.lines)
sol["Line overflow (MW)"] = timeseries(model[:overflow], instance.lines) sol["Line overflow (MW)"] = timeseries(model[:overflow], instance.lines)
end end
if !isempty(instance.price_sensitive_loads) if !isempty(instance.price_sensitive_loads)
sol["Price-sensitive loads (MW)"] = timeseries(model[:loads], instance.price_sensitive_loads) sol["Price-sensitive loads (MW)"] =
timeseries(model[:loads], instance.price_sensitive_loads)
end end
return sol return sol
end end
function write(filename::AbstractString, solution::AbstractDict)::Nothing function write(filename::AbstractString, solution::AbstractDict)::Nothing
open(filename, "w") do file open(filename, "w") do file
JSON.print(file, solution, 2) return JSON.print(file, solution, 2)
end end
return
end end
function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing
instance, T = model[:instance], model[:instance].time instance, T = model[:instance], model[:instance].time
is_on = model[:is_on] is_on = model[:is_on]
@ -593,20 +636,22 @@ function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing
for g in instance.units for g in instance.units
for t in 1:T for t in 1:T
is_on_value = round(solution["Is on"][g.name][t]) is_on_value = round(solution["Is on"][g.name][t])
production_value = round(solution["Production (MW)"][g.name][t], digits=5) production_value =
reserve_value = round(solution["Reserve (MW)"][g.name][t], digits=5) round(solution["Production (MW)"][g.name][t], digits = 5)
JuMP.fix(is_on[g.name, t], is_on_value, force=true) 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( JuMP.fix(
prod_above[g.name, t], prod_above[g.name, t],
production_value - is_on_value * g.min_power[t], production_value - is_on_value * g.min_power[t],
force=true, force = true,
) )
JuMP.fix(reserve[g.name, t], reserve_value, force=true) JuMP.fix(reserve[g.name, t], reserve_value, force = true)
end end
end end
return
end end
function set_warm_start!(model::JuMP.Model, solution::AbstractDict)::Nothing function set_warm_start!(model::JuMP.Model, solution::AbstractDict)::Nothing
instance, T = model[:instance], model[:instance].time instance, T = model[:instance], model[:instance].time
is_on = model[:is_on] is_on = model[:is_on]
@ -615,20 +660,25 @@ function set_warm_start!(model::JuMP.Model, solution::AbstractDict)::Nothing
for g in instance.units for g in instance.units
for t in 1:T for t in 1:T
JuMP.set_start_value(is_on[g.name, t], solution["Is on"][g.name][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(
JuMP.set_start_value(switch_off[g.name, t], solution["Switch off"][g.name][t]) 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
end end
return
end end
function optimize!( function optimize!(
model::JuMP.Model; model::JuMP.Model;
time_limit=3600, time_limit = 3600,
gap_limit=1e-4, gap_limit = 1e-4,
two_phase_gap=true, two_phase_gap = true,
)::Nothing )::Nothing
function set_gap(gap) function set_gap(gap)
try try
JuMP.set_optimizer_attribute(model, "MIPGap", gap) JuMP.set_optimizer_attribute(model, "MIPGap", gap)
@ -637,20 +687,20 @@ function optimize!(
@warn "Could not change MIP gap tolerance" @warn "Could not change MIP gap tolerance"
end end
end end
instance = model[:instance] instance = model[:instance]
initial_time = time() initial_time = time()
large_gap = false large_gap = false
has_transmission = (length(model[:isf]) > 0) has_transmission = (length(model[:isf]) > 0)
if has_transmission && two_phase_gap if has_transmission && two_phase_gap
set_gap(1e-2) set_gap(1e-2)
large_gap = true large_gap = true
else else
set_gap(gap_limit) set_gap(gap_limit)
end end
while true while true
time_elapsed = time() - initial_time time_elapsed = time() - initial_time
time_remaining = time_limit - time_elapsed time_remaining = time_limit - time_elapsed
@ -658,18 +708,21 @@ function optimize!(
@info "Time limit exceeded" @info "Time limit exceeded"
break break
end end
@info @sprintf("Setting MILP time limit to %.2f seconds", time_remaining) @info @sprintf(
"Setting MILP time limit to %.2f seconds",
time_remaining
)
JuMP.set_time_limit_sec(model, time_remaining) JuMP.set_time_limit_sec(model, time_remaining)
@info "Solving MILP..." @info "Solving MILP..."
JuMP.optimize!(model) JuMP.optimize!(model)
has_transmission || break has_transmission || break
violations = _find_violations(model) violations = _find_violations(model)
if isempty(violations) if isempty(violations)
@info "No violations found" @info "No violations found"
if large_gap if large_gap
large_gap = false large_gap = false
set_gap(gap_limit) set_gap(gap_limit)
@ -680,10 +733,9 @@ function optimize!(
_enforce_transmission(model, violations) _enforce_transmission(model, violations)
end end
end end
nothing
end
return
end
function _find_violations(model::JuMP.Model) function _find_violations(model::JuMP.Model)
instance = model[:instance] instance = model[:instance]
@ -695,36 +747,38 @@ function _find_violations(model::JuMP.Model)
time_screening = @elapsed begin time_screening = @elapsed begin
non_slack_buses = [b for b in instance.buses if b.offset > 0] non_slack_buses = [b for b in instance.buses if b.offset > 0]
net_injection_values = [ net_injection_values = [
value(net_injection[b.name, t]) value(net_injection[b.name, t]) for b in non_slack_buses,
for b in non_slack_buses, t in 1:instance.time t in 1:instance.time
] ]
overflow_values = [ overflow_values = [
value(overflow[lm.name, t]) value(overflow[lm.name, t]) for lm in instance.lines,
for lm in instance.lines, t in 1:instance.time t in 1:instance.time
] ]
violations = UnitCommitment._find_violations( violations = UnitCommitment._find_violations(
instance=instance, instance = instance,
net_injections=net_injection_values, net_injections = net_injection_values,
overflow=overflow_values, overflow = overflow_values,
isf=model[:isf], isf = model[:isf],
lodf=model[:lodf], lodf = model[:lodf],
) )
end end
@info @sprintf("Verified transmission limits in %.2f seconds", time_screening) @info @sprintf(
"Verified transmission limits in %.2f seconds",
time_screening
)
return violations return violations
end end
function _enforce_transmission( function _enforce_transmission(
model::JuMP.Model, model::JuMP.Model,
violations::Vector{Violation}, violations::Vector{Violation},
)::Nothing )::Nothing
for v in violations for v in violations
_enforce_transmission( _enforce_transmission(
model=model, model = model,
violation=v, violation = v,
isf=model[:isf], isf = model[:isf],
lodf=model[:lodf], lodf = model[:lodf],
) )
end end
return return

@ -4,49 +4,44 @@
# Copyright (C) 2019 Argonne National Laboratory # Copyright (C) 2019 Argonne National Laboratory
# Written by Alinson Santos Xavier <axavier@anl.gov> # Written by Alinson Santos Xavier <axavier@anl.gov>
using DataStructures using DataStructures
using Base.Threads using Base.Threads
struct Violation struct Violation
time::Int time::Int
monitored_line::TransmissionLine monitored_line::TransmissionLine
outage_line::Union{TransmissionLine, Nothing} outage_line::Union{TransmissionLine,Nothing}
amount::Float64 # Violation amount (in MW) amount::Float64 # Violation amount (in MW)
end end
function Violation(; function Violation(;
time::Int, time::Int,
monitored_line::TransmissionLine, monitored_line::TransmissionLine,
outage_line::Union{TransmissionLine, Nothing}, outage_line::Union{TransmissionLine,Nothing},
amount::Float64, amount::Float64,
)::Violation )::Violation
return Violation(time, monitored_line, outage_line, amount) return Violation(time, monitored_line, outage_line, amount)
end end
mutable struct ViolationFilter mutable struct ViolationFilter
max_per_line::Int max_per_line::Int
max_total::Int max_total::Int
queues::Dict{Int, PriorityQueue{Violation, Float64}} queues::Dict{Int,PriorityQueue{Violation,Float64}}
end end
function ViolationFilter(; function ViolationFilter(;
max_per_line::Int=1, max_per_line::Int = 1,
max_total::Int=5, max_total::Int = 5,
)::ViolationFilter )::ViolationFilter
return ViolationFilter(max_per_line, max_total, Dict()) return ViolationFilter(max_per_line, max_total, Dict())
end end
function _offer(filter::ViolationFilter, v::Violation)::Nothing function _offer(filter::ViolationFilter, v::Violation)::Nothing
if v.monitored_line.offset keys(filter.queues) if v.monitored_line.offset keys(filter.queues)
filter.queues[v.monitored_line.offset] = PriorityQueue{Violation, Float64}() filter.queues[v.monitored_line.offset] =
PriorityQueue{Violation,Float64}()
end end
q::PriorityQueue{Violation, Float64} = filter.queues[v.monitored_line.offset] q::PriorityQueue{Violation,Float64} = filter.queues[v.monitored_line.offset]
if length(q) < filter.max_per_line if length(q) < filter.max_per_line
enqueue!(q, v => v.amount) enqueue!(q, v => v.amount)
else else
@ -55,13 +50,12 @@ function _offer(filter::ViolationFilter, v::Violation)::Nothing
enqueue!(q, v => v.amount) enqueue!(q, v => v.amount)
end end
end end
nothing return nothing
end end
function _query(filter::ViolationFilter)::Array{Violation,1}
function _query(filter::ViolationFilter)::Array{Violation, 1}
violations = Array{Violation,1}() violations = Array{Violation,1}()
time_queue = PriorityQueue{Violation, Float64}() time_queue = PriorityQueue{Violation,Float64}()
for l in keys(filter.queues) for l in keys(filter.queues)
line_queue = filter.queues[l] line_queue = filter.queues[l]
while length(line_queue) > 0 while length(line_queue) > 0
@ -82,7 +76,6 @@ function _query(filter::ViolationFilter)::Array{Violation, 1}
return violations return violations
end end
""" """
function _find_violations( function _find_violations(
@ -104,49 +97,46 @@ UnitCommitment.line_outage_factors. The argument `overflow` specifies how much
flow above the transmission limits (in MW) is allowed. It should be an L x T flow above the transmission limits (in MW) is allowed. It should be an L x T
matrix, where L is the number of transmission lines. matrix, where L is the number of transmission lines.
""" """
function _find_violations( function _find_violations(;
;
instance::UnitCommitmentInstance, instance::UnitCommitmentInstance,
net_injections::Array{Float64, 2}, net_injections::Array{Float64,2},
overflow::Array{Float64, 2}, overflow::Array{Float64,2},
isf::Array{Float64,2}, isf::Array{Float64,2},
lodf::Array{Float64,2}, lodf::Array{Float64,2},
max_per_line::Int = 1, max_per_line::Int = 1,
max_per_period::Int = 5, max_per_period::Int = 5,
)::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
K = nthreads() K = nthreads()
size(net_injections) == (B, T) || error("net_injections has incorrect size") size(net_injections) == (B, T) || error("net_injections has incorrect size")
size(isf) == (L, B) || error("isf has incorrect size") size(isf) == (L, B) || error("isf has incorrect size")
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
) )
pre_flow::Array{Float64} = zeros(L, K) # pre_flow[lm, thread] pre_flow::Array{Float64} = zeros(L, K) # pre_flow[lm, thread]
post_flow::Array{Float64} = zeros(L, L, K) # post_flow[lm, lc, thread] post_flow::Array{Float64} = zeros(L, L, K) # post_flow[lm, lc, thread]
pre_v::Array{Float64} = zeros(L, K) # pre_v[lm, thread] pre_v::Array{Float64} = zeros(L, K) # pre_v[lm, thread]
post_v::Array{Float64} = zeros(L, L, K) # post_v[lm, lc, thread] post_v::Array{Float64} = zeros(L, L, K) # post_v[lm, lc, thread]
normal_limits::Array{Float64,2} = [ normal_limits::Array{Float64,2} = [
l.normal_flow_limit[t] + overflow[l.offset, t] l.normal_flow_limit[t] + overflow[l.offset, t] for
for l in instance.lines, t in 1:T l in instance.lines, t in 1:T
] ]
emergency_limits::Array{Float64,2} = [ emergency_limits::Array{Float64,2} = [
l.emergency_flow_limit[t] + overflow[l.offset, t] l.emergency_flow_limit[t] + overflow[l.offset, t] for
for l in instance.lines, t in 1:T l in instance.lines, t in 1:T
] ]
is_vulnerable::Array{Bool} = zeros(Bool, L) is_vulnerable::Array{Bool} = zeros(Bool, L)
for c in instance.contingencies for c in instance.contingencies
is_vulnerable[c.lines[1].offset] = true is_vulnerable[c.lines[1].offset] = true
@ -154,68 +144,69 @@ function _find_violations(
@threads for t in 1:T @threads for t in 1:T
k = threadid() k = threadid()
# Pre-contingency flows # Pre-contingency flows
pre_flow[:, k] = isf * net_injections[:, t] pre_flow[:, k] = isf * net_injections[:, t]
# Post-contingency flows # Post-contingency flows
for lc in 1:L, lm in 1:L for lc in 1:L, lm in 1:L
post_flow[lm, lc, k] = pre_flow[lm, k] + pre_flow[lc, k] * lodf[lm, lc] post_flow[lm, lc, k] =
pre_flow[lm, k] + pre_flow[lc, k] * lodf[lm, lc]
end end
# Pre-contingency violations # Pre-contingency violations
for lm in 1:L for lm in 1:L
pre_v[lm, k] = max( pre_v[lm, k] = max(
0.0, 0.0,
pre_flow[lm, k] - normal_limits[lm, t], pre_flow[lm, k] - normal_limits[lm, t],
- pre_flow[lm, k] - normal_limits[lm, t], -pre_flow[lm, k] - normal_limits[lm, t],
) )
end end
# Post-contingency violations # Post-contingency violations
for lc in 1:L, lm in 1:L for lc in 1:L, lm in 1:L
post_v[lm, lc, k] = max( post_v[lm, lc, k] = max(
0.0, 0.0,
post_flow[lm, lc, k] - emergency_limits[lm, t], post_flow[lm, lc, k] - emergency_limits[lm, t],
- post_flow[lm, lc, k] - emergency_limits[lm, t], -post_flow[lm, lc, k] - emergency_limits[lm, t],
) )
end end
# Offer pre-contingency violations # Offer pre-contingency violations
for lm in 1:L for lm in 1:L
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,
amount=pre_v[lm, k], amount = pre_v[lm, k],
), ),
) )
end end
end end
# Offer post-contingency violations # Offer post-contingency violations
for lm in 1:L, lc in 1:L for lm in 1:L, lc in 1:L
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],
amount=post_v[lm, lc, k], amount = post_v[lm, lc, k],
), ),
) )
end end
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
return violations return violations
end end

@ -13,15 +13,17 @@ M[l.offset, b.offset] indicates the amount of power (in MW) that flows through
transmission line l when 1 MW of power is injected at the slack bus (the bus transmission line l when 1 MW of power is injected at the slack bus (the bus
that has offset zero) and withdrawn from b. that has offset zero) and withdrawn from b.
""" """
function _injection_shift_factors(; buses::Array{Bus}, lines::Array{TransmissionLine}) function _injection_shift_factors(;
buses::Array{Bus},
lines::Array{TransmissionLine},
)
susceptance = _susceptance_matrix(lines) susceptance = _susceptance_matrix(lines)
incidence = _reduced_incidence_matrix(lines=lines, buses=buses) incidence = _reduced_incidence_matrix(lines = lines, buses = buses)
laplacian = transpose(incidence) * susceptance * incidence laplacian = transpose(incidence) * susceptance * incidence
isf = susceptance * incidence * inv(Array(laplacian)) isf = susceptance * incidence * inv(Array(laplacian))
return isf return isf
end end
""" """
_reduced_incidence_matrix(; buses::Array{Bus}, lines::Array{TransmissionLine}) _reduced_incidence_matrix(; buses::Array{Bus}, lines::Array{TransmissionLine})
@ -31,7 +33,10 @@ is the number of buses and L is the number of lines. For each row, there is a 1
element and a -1 element, indicating the source and target buses, respectively, element and a -1 element, indicating the source and target buses, respectively,
for that line. for that line.
""" """
function _reduced_incidence_matrix(; buses::Array{Bus}, lines::Array{TransmissionLine}) function _reduced_incidence_matrix(;
buses::Array{Bus},
lines::Array{TransmissionLine},
)
matrix = spzeros(Float64, length(lines), length(buses) - 1) matrix = spzeros(Float64, length(lines), length(buses) - 1)
for line in lines for line in lines
if line.source.offset > 0 if line.source.offset > 0
@ -41,7 +46,7 @@ function _reduced_incidence_matrix(; buses::Array{Bus}, lines::Array{Transmissio
matrix[line.offset, line.target.offset] = -1 matrix[line.offset, line.target.offset] = -1
end end
end end
matrix return matrix
end end
""" """
@ -54,7 +59,6 @@ function _susceptance_matrix(lines::Array{TransmissionLine})
return Diagonal([l.susceptance for l in lines]) return Diagonal([l.susceptance for l in lines])
end end
""" """
_line_outage_factors(; buses, lines, isf) _line_outage_factors(; buses, lines, isf)
@ -63,19 +67,13 @@ Returns a LxL matrix containing the Line Outage Distribution Factors (LODFs)
for the given network. This matrix how does the pre-contingency flow change for the given network. This matrix how does the pre-contingency flow change
when each individual transmission line is removed. when each individual transmission line is removed.
""" """
function _line_outage_factors( function _line_outage_factors(;
; buses::Array{Bus,1},
buses::Array{Bus, 1}, lines::Array{TransmissionLine,1},
lines::Array{TransmissionLine, 1},
isf::Array{Float64,2}, isf::Array{Float64,2},
) :: Array{Float64,2} )::Array{Float64,2}
n_lines, n_buses = size(isf) n_lines, n_buses = size(isf)
incidence = Array( incidence = Array(_reduced_incidence_matrix(lines = lines, buses = buses))
_reduced_incidence_matrix(
lines=lines,
buses=buses,
),
)
lodf::Array{Float64,2} = isf * transpose(incidence) lodf::Array{Float64,2} = isf * transpose(incidence)
m, n = size(lodf) m, n = size(lodf)
for i in 1:n for i in 1:n

@ -10,17 +10,11 @@ using JuMP
using MathOptInterface using MathOptInterface
using SparseArrays using SparseArrays
pkg = [ pkg = [:DataStructures, :JSON, :JuMP, :MathOptInterface, :SparseArrays]
:DataStructures,
:JSON,
:JuMP,
:MathOptInterface,
:SparseArrays,
]
@info "Building system image..." @info "Building system image..."
create_sysimage( create_sysimage(
pkg, pkg,
precompile_statements_file="build/precompile.jl", precompile_statements_file = "build/precompile.jl",
sysimage_path="build/sysimage.so", sysimage_path = "build/sysimage.so",
) )

@ -18,9 +18,9 @@ Returns the number of validation errors found.
""" """
function repair!(instance::UnitCommitmentInstance)::Int function repair!(instance::UnitCommitmentInstance)::Int
n_errors = 0 n_errors = 0
for g in instance.units for g in instance.units
# Startup costs and delays must be increasing # Startup costs and delays must be increasing
for s in 2:length(g.startup_categories) for s in 2:length(g.startup_categories)
if g.startup_categories[s].delay <= g.startup_categories[s-1].delay if g.startup_categories[s].delay <= g.startup_categories[s-1].delay
@ -31,7 +31,7 @@ function repair!(instance::UnitCommitmentInstance)::Int
g.startup_categories[s].delay = new_value g.startup_categories[s].delay = new_value
n_errors += 1 n_errors += 1
end end
if g.startup_categories[s].cost < g.startup_categories[s-1].cost if g.startup_categories[s].cost < g.startup_categories[s-1].cost
prev_value = g.startup_categories[s].cost prev_value = g.startup_categories[s].cost
new_value = g.startup_categories[s-1].cost new_value = g.startup_categories[s-1].cost
@ -40,9 +40,8 @@ function repair!(instance::UnitCommitmentInstance)::Int
g.startup_categories[s].cost = new_value g.startup_categories[s].cost = new_value
n_errors += 1 n_errors += 1
end end
end end
for t in 1:instance.time for t in 1:instance.time
# Production cost curve should be convex # Production cost curve should be convex
for k in 2:length(g.cost_segments) for k in 2:length(g.cost_segments)
@ -67,19 +66,16 @@ function repair!(instance::UnitCommitmentInstance)::Int
end end
end end
end end
return n_errors return n_errors
end 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))
return validate(instance, solution) return validate(instance, solution)
end end
""" """
validate(instance, solution)::Bool validate(instance, solution)::Bool
@ -92,38 +88,39 @@ This function is implemented independently from the optimization model in
producing valid solutions. It can also be used to verify the solutions produced producing valid solutions. It can also be used to verify the solutions produced
by other optimization packages. by other optimization packages.
""" """
function validate(instance::UnitCommitmentInstance, function validate(
solution::Union{Dict,OrderedDict}; instance::UnitCommitmentInstance,
)::Bool solution::Union{Dict,OrderedDict},
)::Bool
err_count = 0 err_count = 0
err_count += _validate_units(instance, solution) err_count += _validate_units(instance, solution)
err_count += _validate_reserve_and_demand(instance, solution) err_count += _validate_reserve_and_demand(instance, solution)
if err_count > 0 if err_count > 0
@error "Found $err_count validation errors" @error "Found $err_count validation errors"
return false return false
end end
return true return true
end end
function _validate_units(instance, solution; tol = 0.01)
function _validate_units(instance, solution; tol=0.01)
err_count = 0 err_count = 0
for unit in instance.units for unit in instance.units
production = solution["Production (MW)"][unit.name] production = solution["Production (MW)"][unit.name]
reserve = solution["Reserve (MW)"][unit.name] reserve = solution["Reserve (MW)"][unit.name]
actual_production_cost = solution["Production cost (\$)"][unit.name] actual_production_cost = solution["Production cost (\$)"][unit.name]
actual_startup_cost = solution["Startup cost (\$)"][unit.name] actual_startup_cost = solution["Startup cost (\$)"][unit.name]
is_on = bin(solution["Is on"][unit.name]) is_on = bin(solution["Is on"][unit.name])
for t in 1:instance.time for t in 1:instance.time
# Auxiliary variables # Auxiliary variables
if t == 1 if t == 1
is_starting_up = (unit.initial_status < 0) && is_on[t] is_starting_up = (unit.initial_status < 0) && is_on[t]
is_shutting_down = (unit.initial_status > 0) && !is_on[t] is_shutting_down = (unit.initial_status > 0) && !is_on[t]
ramp_up = max(0, production[t] + reserve[t] - unit.initial_power) ramp_up =
max(0, production[t] + reserve[t] - unit.initial_power)
ramp_down = max(0, unit.initial_power - production[t]) ramp_down = max(0, unit.initial_power - production[t])
else else
is_starting_up = !is_on[t-1] && is_on[t] is_starting_up = !is_on[t-1] && is_on[t]
@ -131,7 +128,7 @@ function _validate_units(instance, solution; tol=0.01)
ramp_up = max(0, production[t] + reserve[t] - production[t-1]) ramp_up = max(0, production[t] + reserve[t] - production[t-1])
ramp_down = max(0, production[t-1] - production[t]) ramp_down = max(0, production[t-1] - production[t])
end end
# Compute production costs # Compute production costs
production_cost, startup_cost = 0, 0 production_cost, startup_cost = 0, 0
if is_on[t] if is_on[t]
@ -143,84 +140,133 @@ function _validate_units(instance, solution; tol=0.01)
residual = max(0, residual - s.mw[t]) residual = max(0, residual - s.mw[t])
end end
end end
# Production should be non-negative # Production should be non-negative
if production[t] < -tol if production[t] < -tol
@error @sprintf("Unit %s produces negative amount of power at time %d (%.2f)", @error @sprintf(
unit.name, t, production[t]) "Unit %s produces negative amount of power at time %d (%.2f)",
unit.name,
t,
production[t]
)
err_count += 1 err_count += 1
end end
# Verify must-run # Verify must-run
if !is_on[t] && unit.must_run[t] if !is_on[t] && unit.must_run[t]
@error @sprintf("Must-run unit %s is offline at time %d", @error @sprintf(
unit.name, t) "Must-run unit %s is offline at time %d",
unit.name,
t
)
err_count += 1 err_count += 1
end end
# Verify reserve eligibility # Verify reserve eligibility
if !unit.provides_spinning_reserves[t] && reserve[t] > tol if !unit.provides_spinning_reserves[t] && reserve[t] > tol
@error @sprintf("Unit %s is not eligible to provide spinning reserves at time %d", @error @sprintf(
unit.name, t) "Unit %s is not eligible to provide spinning reserves at time %d",
unit.name,
t
)
err_count += 1 err_count += 1
end end
# If unit is on, must produce at least its minimum power # If unit is on, must produce at least its minimum power
if is_on[t] && (production[t] < unit.min_power[t] - tol) if is_on[t] && (production[t] < unit.min_power[t] - tol)
@error @sprintf("Unit %s produces below its minimum limit at time %d (%.2f < %.2f)", @error @sprintf(
unit.name, t, production[t], unit.min_power[t]) "Unit %s produces below its minimum limit at time %d (%.2f < %.2f)",
unit.name,
t,
production[t],
unit.min_power[t]
)
err_count += 1 err_count += 1
end end
# If unit is on, must produce at most its maximum power # If unit is on, must produce at most its maximum power
if is_on[t] && (production[t] + reserve[t] > unit.max_power[t] + tol) if is_on[t] &&
@error @sprintf("Unit %s produces above its maximum limit at time %d (%.2f + %.2f> %.2f)", (production[t] + reserve[t] > unit.max_power[t] + tol)
unit.name, t, production[t], reserve[t], unit.max_power[t]) @error @sprintf(
"Unit %s produces above its maximum limit at time %d (%.2f + %.2f> %.2f)",
unit.name,
t,
production[t],
reserve[t],
unit.max_power[t]
)
err_count += 1 err_count += 1
end end
# If unit is off, must produce zero # If unit is off, must produce zero
if !is_on[t] && production[t] + reserve[t] > tol if !is_on[t] && production[t] + reserve[t] > tol
@error @sprintf("Unit %s produces power at time %d while off", @error @sprintf(
unit.name, t) "Unit %s produces power at time %d while off",
unit.name,
t
)
err_count += 1 err_count += 1
end end
# Startup limit # Startup limit
if is_starting_up && (ramp_up > unit.startup_limit + tol) if is_starting_up && (ramp_up > unit.startup_limit + tol)
@error @sprintf("Unit %s exceeds startup limit at time %d (%.2f > %.2f)", @error @sprintf(
unit.name, t, ramp_up, unit.startup_limit) "Unit %s exceeds startup limit at time %d (%.2f > %.2f)",
unit.name,
t,
ramp_up,
unit.startup_limit
)
err_count += 1 err_count += 1
end end
# Shutdown limit # Shutdown limit
if is_shutting_down && (ramp_down > unit.shutdown_limit + tol) if is_shutting_down && (ramp_down > unit.shutdown_limit + tol)
@error @sprintf("Unit %s exceeds shutdown limit at time %d (%.2f > %.2f)", @error @sprintf(
unit.name, t, ramp_down, unit.shutdown_limit) "Unit %s exceeds shutdown limit at time %d (%.2f > %.2f)",
unit.name,
t,
ramp_down,
unit.shutdown_limit
)
err_count += 1 err_count += 1
end end
# Ramp-up limit # Ramp-up limit
if !is_starting_up && !is_shutting_down && (ramp_up > unit.ramp_up_limit + tol) if !is_starting_up &&
@error @sprintf("Unit %s exceeds ramp up limit at time %d (%.2f > %.2f)", !is_shutting_down &&
unit.name, t, ramp_up, unit.ramp_up_limit) (ramp_up > unit.ramp_up_limit + tol)
@error @sprintf(
"Unit %s exceeds ramp up limit at time %d (%.2f > %.2f)",
unit.name,
t,
ramp_up,
unit.ramp_up_limit
)
err_count += 1 err_count += 1
end end
# Ramp-down limit # Ramp-down limit
if !is_starting_up && !is_shutting_down && (ramp_down > unit.ramp_down_limit + tol) if !is_starting_up &&
@error @sprintf("Unit %s exceeds ramp down limit at time %d (%.2f > %.2f)", !is_shutting_down &&
unit.name, t, ramp_down, unit.ramp_down_limit) (ramp_down > unit.ramp_down_limit + tol)
@error @sprintf(
"Unit %s exceeds ramp down limit at time %d (%.2f > %.2f)",
unit.name,
t,
ramp_down,
unit.ramp_down_limit
)
err_count += 1 err_count += 1
end end
# Verify startup costs & minimum downtime # Verify startup costs & minimum downtime
if is_starting_up if is_starting_up
# Calculate how much time the unit has been offline # Calculate how much time the unit has been offline
time_down = 0 time_down = 0
for k in 1:(t-1) for k in 1:(t-1)
if !is_on[t - k] if !is_on[t-k]
time_down += 1 time_down += 1
else else
break break
@ -233,29 +279,32 @@ function _validate_units(instance, solution; tol=0.01)
end end
time_down += initial_down time_down += initial_down
end end
# Calculate startup costs # Calculate startup costs
for c in unit.startup_categories for c in unit.startup_categories
if time_down >= c.delay if time_down >= c.delay
startup_cost = c.cost startup_cost = c.cost
end end
end end
# Check minimum downtime # Check minimum downtime
if time_down < unit.min_downtime if time_down < unit.min_downtime
@error @sprintf("Unit %s violates minimum downtime at time %d", @error @sprintf(
unit.name, t) "Unit %s violates minimum downtime at time %d",
unit.name,
t
)
err_count += 1 err_count += 1
end end
end end
# Verify minimum uptime # Verify minimum uptime
if is_shutting_down if is_shutting_down
# Calculate how much time the unit has been online # Calculate how much time the unit has been online
time_up = 0 time_up = 0
for k in 1:(t-1) for k in 1:(t-1)
if is_on[t - k] if is_on[t-k]
time_up += 1 time_up += 1
else else
break break
@ -268,61 +317,70 @@ function _validate_units(instance, solution; tol=0.01)
end end
time_up += initial_up time_up += initial_up
end end
if (t == time_up + 1) && (unit.initial_status > 0) if (t == time_up + 1) && (unit.initial_status > 0)
time_up += unit.initial_status time_up += unit.initial_status
end end
# Check minimum uptime # Check minimum uptime
if time_up < unit.min_uptime if time_up < unit.min_uptime
@error @sprintf("Unit %s violates minimum uptime at time %d", @error @sprintf(
unit.name, t) "Unit %s violates minimum uptime at time %d",
unit.name,
t
)
err_count += 1 err_count += 1
end end
end end
# Verify production costs # Verify production costs
if abs(actual_production_cost[t] - production_cost) > 1.00 if abs(actual_production_cost[t] - production_cost) > 1.00
@error @sprintf("Unit %s has unexpected production cost at time %d (%.2f should be %.2f)", @error @sprintf(
unit.name, t, actual_production_cost[t], production_cost) "Unit %s has unexpected production cost at time %d (%.2f should be %.2f)",
unit.name,
t,
actual_production_cost[t],
production_cost
)
err_count += 1 err_count += 1
end end
# Verify startup costs # Verify startup costs
if abs(actual_startup_cost[t] - startup_cost) > 1.00 if abs(actual_startup_cost[t] - startup_cost) > 1.00
@error @sprintf("Unit %s has unexpected startup cost at time %d (%.2f should be %.2f)", @error @sprintf(
unit.name, t, actual_startup_cost[t], startup_cost) "Unit %s has unexpected startup cost at time %d (%.2f should be %.2f)",
unit.name,
t,
actual_startup_cost[t],
startup_cost
)
err_count += 1 err_count += 1
end end
end end
end end
return err_count return err_count
end end
function _validate_reserve_and_demand(instance, solution, tol = 0.01)
function _validate_reserve_and_demand(instance, solution, tol=0.01)
err_count = 0 err_count = 0
for t in 1:instance.time for t in 1:instance.time
load_curtail = 0 load_curtail = 0
fixed_load = sum(b.load[t] for b in instance.buses) fixed_load = sum(b.load[t] for b in instance.buses)
ps_load = sum( ps_load = sum(
solution["Price-sensitive loads (MW)"][ps.name][t] solution["Price-sensitive loads (MW)"][ps.name][t] for
for ps in instance.price_sensitive_loads ps in instance.price_sensitive_loads
)
production = sum(
solution["Production (MW)"][g.name][t]
for g in instance.units
) )
production =
sum(solution["Production (MW)"][g.name][t] for g in instance.units)
if "Load curtail (MW)" in keys(solution) if "Load curtail (MW)" in keys(solution)
load_curtail = sum( load_curtail = sum(
solution["Load curtail (MW)"][b.name][t] solution["Load curtail (MW)"][b.name][t] for
for b in instance.buses b in instance.buses
) )
end end
balance = fixed_load - load_curtail - production + ps_load balance = fixed_load - load_curtail - production + ps_load
# Verify that production equals demand # Verify that production equals demand
if abs(balance) > tol if abs(balance) > tol
@error @sprintf( @error @sprintf(
@ -335,9 +393,10 @@ function _validate_reserve_and_demand(instance, solution, tol=0.01)
) )
err_count += 1 err_count += 1
end end
# Verify spinning reserves # Verify spinning reserves
reserve = sum(solution["Reserve (MW)"][g.name][t] for g in instance.units) reserve =
sum(solution["Reserve (MW)"][g.name][t] for g in instance.units)
if reserve < instance.reserves.spinning[t] - tol if reserve < instance.reserves.spinning[t] - tol
@error @sprintf( @error @sprintf(
"Insufficient spinning reserves at time %d (%.2f should be %.2f)", "Insufficient spinning reserves at time %d (%.2f should be %.2f)",
@ -348,7 +407,6 @@ function _validate_reserve_and_demand(instance, solution, tol=0.01)
err_count += 1 err_count += 1
end end
end end
return err_count return err_count
end end

@ -6,14 +6,17 @@ using UnitCommitment
@testset "convert" begin @testset "convert" begin
@testset "EGRET solution" begin @testset "EGRET solution" begin
solution = UnitCommitment._read_egret_solution("fixtures/egret_output.json.gz") solution =
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])
@test length(solution[attr]["115_STEAM_1"]) == 48 @test length(solution[attr]["115_STEAM_1"]) == 48
end end
@test solution["Production cost (\$)"]["315_CT_6"][15:20] == [0., 0., 884.44, 1470.71, 1470.71, 884.44] @test solution["Production cost (\$)"]["315_CT_6"][15:20] ==
@test solution["Startup cost (\$)"]["315_CT_6"][15:20] == [0., 0., 5665.23, 0., 0., 0.] [0.0, 0.0, 884.44, 1470.71, 1470.71, 884.44]
@test solution["Startup cost (\$)"]["315_CT_6"][15:20] ==
[0.0, 0.0, 5665.23, 0.0, 0.0, 0.0]
@test length(keys(solution["Is on"])) == 154 @test length(keys(solution["Is on"])) == 154
end end
end end

@ -8,21 +8,21 @@ using UnitCommitment, Cbc, JuMP
# Load instance # Load instance
instance = UnitCommitment.read("$(pwd())/fixtures/case118-initcond.json.gz") instance = UnitCommitment.read("$(pwd())/fixtures/case118-initcond.json.gz")
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
# All units should have unknown initial conditions # All units should have unknown initial conditions
for g in instance.units for g in instance.units
@test g.initial_power === nothing @test g.initial_power === nothing
@test g.initial_status === nothing @test g.initial_status === nothing
end end
# Generate initial conditions # Generate initial conditions
UnitCommitment.generate_initial_conditions!(instance, optimizer) UnitCommitment.generate_initial_conditions!(instance, optimizer)
# All units should now have known initial conditions # All units should now have known initial conditions
for g in instance.units for g in instance.units
@test g.initial_power !== nothing @test g.initial_power !== nothing
@test g.initial_status !== nothing @test g.initial_status !== nothing
end end
# TODO: Check that initial conditions are feasible # TODO: Check that initial conditions are feasible
end end

@ -15,46 +15,46 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test length(instance.price_sensitive_loads) == 1 @test length(instance.price_sensitive_loads) == 1
@test instance.time == 4 @test instance.time == 4
@test instance.lines[5].name == "l5" @test instance.lines[5].name == "l5"
@test instance.lines[5].source.name == "b2" @test instance.lines[5].source.name == "b2"
@test instance.lines[5].target.name == "b5" @test instance.lines[5].target.name == "b5"
@test instance.lines[5].reactance 0.17388 @test instance.lines[5].reactance 0.17388
@test instance.lines[5].susceptance 10.037550333 @test instance.lines[5].susceptance 10.037550333
@test instance.lines[5].normal_flow_limit == [1e8 for t in 1:4] @test instance.lines[5].normal_flow_limit == [1e8 for t in 1:4]
@test instance.lines[5].emergency_flow_limit == [1e8 for t in 1:4] @test instance.lines[5].emergency_flow_limit == [1e8 for t in 1:4]
@test instance.lines[5].flow_limit_penalty == [5e3 for t in 1:4] @test instance.lines[5].flow_limit_penalty == [5e3 for t in 1:4]
@test instance.lines[1].name == "l1" @test instance.lines[1].name == "l1"
@test instance.lines[1].source.name == "b1" @test instance.lines[1].source.name == "b1"
@test instance.lines[1].target.name == "b2" @test instance.lines[1].target.name == "b2"
@test instance.lines[1].reactance 0.059170 @test instance.lines[1].reactance 0.059170
@test instance.lines[1].susceptance 29.496860773945 @test instance.lines[1].susceptance 29.496860773945
@test instance.lines[1].normal_flow_limit == [300.0 for t in 1:4] @test instance.lines[1].normal_flow_limit == [300.0 for t in 1:4]
@test instance.lines[1].emergency_flow_limit == [400.0 for t in 1:4] @test instance.lines[1].emergency_flow_limit == [400.0 for t in 1:4]
@test instance.lines[1].flow_limit_penalty == [1e3 for t in 1:4] @test instance.lines[1].flow_limit_penalty == [1e3 for t in 1:4]
@test instance.buses[9].name == "b9" @test instance.buses[9].name == "b9"
@test instance.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353] @test instance.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353]
unit = instance.units[1] unit = instance.units[1]
@test unit.name == "g1" @test unit.name == "g1"
@test unit.bus.name == "b1" @test unit.bus.name == "b1"
@test unit.ramp_up_limit == 1e6 @test unit.ramp_up_limit == 1e6
@test unit.ramp_down_limit == 1e6 @test unit.ramp_down_limit == 1e6
@test unit.startup_limit == 1e6 @test unit.startup_limit == 1e6
@test unit.shutdown_limit == 1e6 @test unit.shutdown_limit == 1e6
@test unit.must_run == [false for t in 1:4] @test unit.must_run == [false for t in 1:4]
@test unit.min_power_cost == [1400. for t in 1:4] @test unit.min_power_cost == [1400.0 for t in 1:4]
@test unit.min_uptime == 1 @test unit.min_uptime == 1
@test unit.min_downtime == 1 @test unit.min_downtime == 1
@test unit.provides_spinning_reserves == [true for t in 1:4] @test unit.provides_spinning_reserves == [true for t in 1:4]
for t in 1:1 for t in 1:1
@test unit.cost_segments[1].mw[t] == 10.0 @test unit.cost_segments[1].mw[t] == 10.0
@test unit.cost_segments[2].mw[t] == 20.0 @test unit.cost_segments[2].mw[t] == 20.0
@test unit.cost_segments[3].mw[t] == 5.0 @test unit.cost_segments[3].mw[t] == 5.0
@test unit.cost_segments[1].cost[t] 20.0 @test unit.cost_segments[1].cost[t] 20.0
@test unit.cost_segments[2].cost[t] 30.0 @test unit.cost_segments[2].cost[t] 30.0
@test unit.cost_segments[3].cost[t] 40.0 @test unit.cost_segments[3].cost[t] 40.0
end end
@test length(unit.startup_categories) == 3 @test length(unit.startup_categories) == 3
@test unit.startup_categories[1].delay == 1 @test unit.startup_categories[1].delay == 1
@ -63,42 +63,42 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test unit.startup_categories[1].cost == 1000.0 @test unit.startup_categories[1].cost == 1000.0
@test unit.startup_categories[2].cost == 1500.0 @test unit.startup_categories[2].cost == 1500.0
@test unit.startup_categories[3].cost == 2000.0 @test unit.startup_categories[3].cost == 2000.0
unit = instance.units[2] unit = instance.units[2]
@test unit.name == "g2" @test unit.name == "g2"
@test unit.must_run == [false for t in 1:4] @test unit.must_run == [false for t in 1:4]
unit = instance.units[3] unit = instance.units[3]
@test unit.name == "g3" @test unit.name == "g3"
@test unit.bus.name == "b3" @test unit.bus.name == "b3"
@test unit.ramp_up_limit == 70.0 @test unit.ramp_up_limit == 70.0
@test unit.ramp_down_limit == 70.0 @test unit.ramp_down_limit == 70.0
@test unit.startup_limit == 70.0 @test unit.startup_limit == 70.0
@test unit.shutdown_limit == 70.0 @test unit.shutdown_limit == 70.0
@test unit.must_run == [true for t in 1:4] @test unit.must_run == [true for t in 1:4]
@test unit.min_power_cost == [0. for t in 1:4] @test unit.min_power_cost == [0.0 for t in 1:4]
@test unit.min_uptime == 1 @test unit.min_uptime == 1
@test unit.min_downtime == 1 @test unit.min_downtime == 1
@test unit.provides_spinning_reserves == [true for t in 1:4] @test unit.provides_spinning_reserves == [true for t in 1:4]
for t in 1:4 for t in 1:4
@test unit.cost_segments[1].mw[t] 33 @test unit.cost_segments[1].mw[t] 33
@test unit.cost_segments[2].mw[t] 33 @test unit.cost_segments[2].mw[t] 33
@test unit.cost_segments[3].mw[t] 34 @test unit.cost_segments[3].mw[t] 34
@test unit.cost_segments[1].cost[t] 33.75 @test unit.cost_segments[1].cost[t] 33.75
@test unit.cost_segments[2].cost[t] 38.04 @test unit.cost_segments[2].cost[t] 38.04
@test unit.cost_segments[3].cost[t] 44.77853 @test unit.cost_segments[3].cost[t] 44.77853
end end
@test instance.reserves.spinning == zeros(4) @test instance.reserves.spinning == zeros(4)
@test instance.contingencies[1].lines == [instance.lines[1]] @test instance.contingencies[1].lines == [instance.lines[1]]
@test instance.contingencies[1].units == [] @test instance.contingencies[1].units == []
load = instance.price_sensitive_loads[1] load = instance.price_sensitive_loads[1]
@test load.name == "ps1" @test load.name == "ps1"
@test load.bus.name == "b3" @test load.bus.name == "b3"
@test load.revenue == [100. for t in 1:4] @test load.revenue == [100.0 for t in 1:4]
@test load.demand == [50. for t in 1:4] @test load.demand == [50.0 for t in 1:4]
end end
@testset "read sub-hourly" begin @testset "read sub-hourly" begin
@ -114,11 +114,11 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test unit.startup_categories[3].delay == 6 @test unit.startup_categories[3].delay == 6
@test unit.initial_status == -200 @test unit.initial_status == -200
end end
@testset "slice" begin @testset "slice" begin
instance = UnitCommitment.read_benchmark("test/case14") instance = UnitCommitment.read_benchmark("test/case14")
modified = UnitCommitment.slice(instance, 1:2) modified = UnitCommitment.slice(instance, 1:2)
# Should update all time-dependent fields # Should update all time-dependent fields
@test modified.time == 2 @test modified.time == 2
@test length(modified.power_balance_penalty) == 2 @test length(modified.power_balance_penalty) == 2
@ -146,11 +146,13 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test length(ps.demand) == 2 @test length(ps.demand) == 2
@test length(ps.revenue) == 2 @test length(ps.revenue) == 2
end end
# Should be able to build model without errors # Should be able to build model without errors
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
model = build_model(instance=modified, model = build_model(
optimizer=optimizer, instance = modified,
variable_names=true) optimizer = optimizer,
variable_names = true,
)
end end
end end

@ -12,9 +12,9 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP
end end
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
model = build_model( model = build_model(
instance=instance, instance = instance,
optimizer=optimizer, optimizer = optimizer,
variable_names=true, variable_names = true,
) )
@test name(model[:is_on]["g1", 1]) == "is_on[g1,1]" @test name(model[:is_on]["g1", 1]) == "is_on[g1,1]"
@ -27,7 +27,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP
UnitCommitment.write(filename, solution) UnitCommitment.write(filename, solution)
loaded = JSON.parsefile(filename) loaded = JSON.parsefile(filename)
@test length(loaded["Is on"]) == 6 @test length(loaded["Is on"]) == 6
# Verify solution # Verify solution
@test UnitCommitment.validate(instance, solution) @test UnitCommitment.validate(instance, solution)

@ -8,81 +8,81 @@ 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,
amount=100., amount = 100.0,
), ),
) )
_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],
amount=300., amount = 300.0,
), ),
) )
_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],
amount=500., amount = 500.0,
), ),
) )
_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],
amount=400., amount = 400.0,
), ),
) )
_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],
amount=200., amount = 200.0,
), ),
) )
_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],
amount=100., amount = 100.0,
) ),
) )
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., 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],
amount=500., amount = 500.0,
), ),
] ]
@test actual == expected @test actual == expected
end end
@testset "find_violations" begin @testset "find_violations" begin
instance = UnitCommitment.read_benchmark("test/case14") instance = UnitCommitment.read_benchmark("test/case14")
for line in instance.lines, t in 1:instance.time for line in instance.lines, t in 1:instance.time
@ -90,22 +90,22 @@ import UnitCommitment: Violation, _offer, _query
line.emergency_flow_limit[t] = 1.0 line.emergency_flow_limit[t] = 1.0
end end
isf = UnitCommitment._injection_shift_factors( isf = UnitCommitment._injection_shift_factors(
lines=instance.lines, lines = instance.lines,
buses=instance.buses, buses = instance.buses,
) )
lodf = UnitCommitment._line_outage_factors( lodf = UnitCommitment._line_outage_factors(
lines=instance.lines, lines = instance.lines,
buses=instance.buses, buses = instance.buses,
isf=isf, isf = isf,
) )
inj = [1000.0 for b in 1:13, t in 1:instance.time] inj = [1000.0 for b in 1:13, t in 1:instance.time]
overflow = [0.0 for l in instance.lines, t in 1:instance.time] overflow = [0.0 for l in instance.lines, t in 1:instance.time]
violations = UnitCommitment._find_violations( violations = UnitCommitment._find_violations(
instance=instance, instance = instance,
net_injections=inj, net_injections = inj,
overflow=overflow, overflow = overflow,
isf=isf, isf = isf,
lodf=lodf, lodf = lodf,
) )
@test length(violations) == 20 @test length(violations) == 20
end end

@ -9,117 +9,137 @@ using UnitCommitment, Test, LinearAlgebra
instance = UnitCommitment.read_benchmark("test/case14") instance = UnitCommitment.read_benchmark("test/case14")
actual = UnitCommitment._susceptance_matrix(instance.lines) actual = UnitCommitment._susceptance_matrix(instance.lines)
@test size(actual) == (20, 20) @test size(actual) == (20, 20)
expected = Diagonal([29.5, 7.83, 8.82, 9.9, 10.04, expected = Diagonal([
10.2, 41.45, 8.35, 3.14, 6.93, 29.5,
8.77, 6.82, 13.4, 9.91, 15.87, 7.83,
20.65, 6.46, 9.09, 8.73, 5.02]) 8.82,
@test round.(actual, digits=2) == expected 9.9,
10.04,
10.2,
41.45,
8.35,
3.14,
6.93,
8.77,
6.82,
13.4,
9.91,
15.87,
20.65,
6.46,
9.09,
8.73,
5.02,
])
@test round.(actual, digits = 2) == expected
end end
@testset "Reduced incidence matrix" begin @testset "Reduced incidence matrix" begin
instance = UnitCommitment.read_benchmark("test/case14") instance = UnitCommitment.read_benchmark("test/case14")
actual = UnitCommitment._reduced_incidence_matrix( actual = UnitCommitment._reduced_incidence_matrix(
lines=instance.lines, lines = instance.lines,
buses=instance.buses, buses = instance.buses,
) )
@test size(actual) == (20, 13) @test size(actual) == (20, 13)
@test actual[1, 1] == -1.0 @test actual[1, 1] == -1.0
@test actual[3, 1] == 1.0 @test actual[3, 1] == 1.0
@test actual[4, 1] == 1.0 @test actual[4, 1] == 1.0
@test actual[5, 1] == 1.0 @test actual[5, 1] == 1.0
@test actual[3, 2] == -1.0 @test actual[3, 2] == -1.0
@test actual[6, 2] == 1.0 @test actual[6, 2] == 1.0
@test actual[4, 3] == -1.0 @test actual[4, 3] == -1.0
@test actual[6, 3] == -1.0 @test actual[6, 3] == -1.0
@test actual[7, 3] == 1.0 @test actual[7, 3] == 1.0
@test actual[8, 3] == 1.0 @test actual[8, 3] == 1.0
@test actual[9, 3] == 1.0 @test actual[9, 3] == 1.0
@test actual[2, 4] == -1.0 @test actual[2, 4] == -1.0
@test actual[5, 4] == -1.0 @test actual[5, 4] == -1.0
@test actual[7, 4] == -1.0 @test actual[7, 4] == -1.0
@test actual[10, 4] == 1.0 @test actual[10, 4] == 1.0
@test actual[10, 5] == -1.0 @test actual[10, 5] == -1.0
@test actual[11, 5] == 1.0 @test actual[11, 5] == 1.0
@test actual[12, 5] == 1.0 @test actual[12, 5] == 1.0
@test actual[13, 5] == 1.0 @test actual[13, 5] == 1.0
@test actual[8, 6] == -1.0 @test actual[8, 6] == -1.0
@test actual[14, 6] == 1.0 @test actual[14, 6] == 1.0
@test actual[15, 6] == 1.0 @test actual[15, 6] == 1.0
@test actual[14, 7] == -1.0 @test actual[14, 7] == -1.0
@test actual[9, 8] == -1.0 @test actual[9, 8] == -1.0
@test actual[15, 8] == -1.0 @test actual[15, 8] == -1.0
@test actual[16, 8] == 1.0 @test actual[16, 8] == 1.0
@test actual[17, 8] == 1.0 @test actual[17, 8] == 1.0
@test actual[16, 9] == -1.0 @test actual[16, 9] == -1.0
@test actual[18, 9] == 1.0 @test actual[18, 9] == 1.0
@test actual[11, 10] == -1.0 @test actual[11, 10] == -1.0
@test actual[18, 10] == -1.0 @test actual[18, 10] == -1.0
@test actual[12, 11] == -1.0 @test actual[12, 11] == -1.0
@test actual[19, 11] == 1.0 @test actual[19, 11] == 1.0
@test actual[13, 12] == -1.0 @test actual[13, 12] == -1.0
@test actual[19, 12] == -1.0 @test actual[19, 12] == -1.0
@test actual[20, 12] == 1.0 @test actual[20, 12] == 1.0
@test actual[17, 13] == -1.0 @test actual[17, 13] == -1.0
@test actual[20, 13] == -1.0 @test actual[20, 13] == -1.0
end end
@testset "Injection Shift Factors (ISF)" begin @testset "Injection Shift Factors (ISF)" begin
instance = UnitCommitment.read_benchmark("test/case14") instance = UnitCommitment.read_benchmark("test/case14")
actual = UnitCommitment._injection_shift_factors( actual = UnitCommitment._injection_shift_factors(
lines=instance.lines, lines = instance.lines,
buses=instance.buses, buses = instance.buses,
) )
@test size(actual) == (20, 13) @test size(actual) == (20, 13)
@test round.(actual, digits=2) == [ @test round.(actual, digits = 2) == [
-0.84 -0.75 -0.67 -0.61 -0.63 -0.66 -0.66 -0.65 -0.65 -0.64 -0.63 -0.63 -0.64; -0.84 -0.75 -0.67 -0.61 -0.63 -0.66 -0.66 -0.65 -0.65 -0.64 -0.63 -0.63 -0.64
-0.16 -0.25 -0.33 -0.39 -0.37 -0.34 -0.34 -0.35 -0.35 -0.36 -0.37 -0.37 -0.36; -0.16 -0.25 -0.33 -0.39 -0.37 -0.34 -0.34 -0.35 -0.35 -0.36 -0.37 -0.37 -0.36
0.03 -0.53 -0.15 -0.1 -0.12 -0.14 -0.14 -0.14 -0.13 -0.13 -0.12 -0.12 -0.13; 0.03 -0.53 -0.15 -0.1 -0.12 -0.14 -0.14 -0.14 -0.13 -0.13 -0.12 -0.12 -0.13
0.06 -0.14 -0.32 -0.22 -0.25 -0.3 -0.3 -0.29 -0.28 -0.27 -0.25 -0.26 -0.27; 0.06 -0.14 -0.32 -0.22 -0.25 -0.3 -0.3 -0.29 -0.28 -0.27 -0.25 -0.26 -0.27
0.08 -0.07 -0.2 -0.29 -0.26 -0.22 -0.22 -0.22 -0.23 -0.25 -0.26 -0.26 -0.24; 0.08 -0.07 -0.2 -0.29 -0.26 -0.22 -0.22 -0.22 -0.23 -0.25 -0.26 -0.26 -0.24
0.03 0.47 -0.15 -0.1 -0.12 -0.14 -0.14 -0.14 -0.13 -0.13 -0.12 -0.12 -0.13; 0.03 0.47 -0.15 -0.1 -0.12 -0.14 -0.14 -0.14 -0.13 -0.13 -0.12 -0.12 -0.13
0.08 0.31 0.5 -0.3 -0.03 0.36 0.36 0.28 0.23 0.1 -0.0 0.02 0.17; 0.08 0.31 0.5 -0.3 -0.03 0.36 0.36 0.28 0.23 0.1 -0.0 0.02 0.17
0.0 0.01 0.02 -0.01 -0.22 -0.63 -0.63 -0.45 -0.41 -0.32 -0.24 -0.25 -0.36; 0.0 0.01 0.02 -0.01 -0.22 -0.63 -0.63 -0.45 -0.41 -0.32 -0.24 -0.25 -0.36
0.0 0.01 0.01 -0.01 -0.12 -0.17 -0.17 -0.26 -0.24 -0.18 -0.14 -0.14 -0.21; 0.0 0.01 0.01 -0.01 -0.12 -0.17 -0.17 -0.26 -0.24 -0.18 -0.14 -0.14 -0.21
-0.0 -0.02 -0.03 0.02 -0.66 -0.2 -0.2 -0.29 -0.36 -0.5 -0.63 -0.61 -0.43; -0.0 -0.02 -0.03 0.02 -0.66 -0.2 -0.2 -0.29 -0.36 -0.5 -0.63 -0.61 -0.43
-0.0 -0.01 -0.02 0.01 0.21 -0.12 -0.12 -0.17 -0.28 -0.53 0.18 0.15 -0.03; -0.0 -0.01 -0.02 0.01 0.21 -0.12 -0.12 -0.17 -0.28 -0.53 0.18 0.15 -0.03
-0.0 -0.0 -0.0 0.0 0.03 -0.02 -0.02 -0.03 -0.02 0.01 -0.52 -0.17 -0.09; -0.0 -0.0 -0.0 0.0 0.03 -0.02 -0.02 -0.03 -0.02 0.01 -0.52 -0.17 -0.09
-0.0 -0.01 -0.01 0.01 0.11 -0.06 -0.06 -0.09 -0.05 0.02 -0.28 -0.59 -0.31; -0.0 -0.01 -0.01 0.01 0.11 -0.06 -0.06 -0.09 -0.05 0.02 -0.28 -0.59 -0.31
-0.0 -0.0 -0.0 -0.0 -0.0 -0.0 -1.0 -0.0 -0.0 -0.0 -0.0 -0.0 0.0 ; -0.0 -0.0 -0.0 -0.0 -0.0 -0.0 -1.0 -0.0 -0.0 -0.0 -0.0 -0.0 0.0
0.0 0.01 0.02 -0.01 -0.22 0.37 0.37 -0.45 -0.41 -0.32 -0.24 -0.25 -0.36; 0.0 0.01 0.02 -0.01 -0.22 0.37 0.37 -0.45 -0.41 -0.32 -0.24 -0.25 -0.36
0.0 0.01 0.02 -0.01 -0.21 0.12 0.12 0.17 -0.72 -0.47 -0.18 -0.15 0.03; 0.0 0.01 0.02 -0.01 -0.21 0.12 0.12 0.17 -0.72 -0.47 -0.18 -0.15 0.03
0.0 0.01 0.01 -0.01 -0.14 0.08 0.08 0.12 0.07 -0.03 -0.2 -0.24 -0.6 ; 0.0 0.01 0.01 -0.01 -0.14 0.08 0.08 0.12 0.07 -0.03 -0.2 -0.24 -0.6
0.0 0.01 0.02 -0.01 -0.21 0.12 0.12 0.17 0.28 -0.47 -0.18 -0.15 0.03; 0.0 0.01 0.02 -0.01 -0.21 0.12 0.12 0.17 0.28 -0.47 -0.18 -0.15 0.03
-0.0 -0.0 -0.0 0.0 0.03 -0.02 -0.02 -0.03 -0.02 0.01 0.48 -0.17 -0.09; -0.0 -0.0 -0.0 0.0 0.03 -0.02 -0.02 -0.03 -0.02 0.01 0.48 -0.17 -0.09
-0.0 -0.01 -0.01 0.01 0.14 -0.08 -0.08 -0.12 -0.07 0.03 0.2 0.24 -0.4 ] -0.0 -0.01 -0.01 0.01 0.14 -0.08 -0.08 -0.12 -0.07 0.03 0.2 0.24 -0.4
]
end end
@testset "Line Outage Distribution Factors (LODF)" begin @testset "Line Outage Distribution Factors (LODF)" begin
instance = UnitCommitment.read_benchmark("test/case14") instance = UnitCommitment.read_benchmark("test/case14")
isf_before = UnitCommitment._injection_shift_factors( isf_before = UnitCommitment._injection_shift_factors(
lines=instance.lines, lines = instance.lines,
buses=instance.buses, buses = instance.buses,
) )
lodf = UnitCommitment._line_outage_factors( lodf = UnitCommitment._line_outage_factors(
lines=instance.lines, lines = instance.lines,
buses=instance.buses, buses = instance.buses,
isf=isf_before, isf = isf_before,
) )
for contingency in instance.contingencies for contingency in instance.contingencies
for lc in contingency.lines for lc in contingency.lines
prev_susceptance = lc.susceptance prev_susceptance = lc.susceptance
lc.susceptance = 0.0 lc.susceptance = 0.0
isf_after = UnitCommitment._injection_shift_factors( isf_after = UnitCommitment._injection_shift_factors(
lines=instance.lines, lines = instance.lines,
buses=instance.buses, buses = instance.buses,
) )
lc.susceptance = prev_susceptance lc.susceptance = prev_susceptance
for lm in instance.lines for lm in instance.lines
expected = isf_after[lm.offset, :] expected = isf_after[lm.offset, :]
actual = isf_before[lm.offset, :] + actual =
lodf[lm.offset, lc.offset] * isf_before[lc.offset, :] isf_before[lm.offset, :] +
lodf[lm.offset, lc.offset] * isf_before[lc.offset, :]
@test norm(expected - actual) < 1e-6 @test norm(expected - actual) < 1e-6
end end
end end
end end
end end
end end

@ -4,36 +4,40 @@
using UnitCommitment, JSON, GZip, DataStructures using UnitCommitment, JSON, GZip, DataStructures
parse_case14() = JSON.parse(GZip.gzopen("../instances/test/case14.json.gz"), function parse_case14()
dicttype=()->DefaultOrderedDict(nothing)) return JSON.parse(
GZip.gzopen("../instances/test/case14.json.gz"),
dicttype = () -> DefaultOrderedDict(nothing),
)
end
@testset "Validation" begin @testset "Validation" begin
@testset "repair!" begin @testset "repair!" begin
@testset "Cost curve should be convex" begin @testset "Cost curve should be convex" begin
json = parse_case14() json = parse_case14()
json["Generators"]["g1"]["Production cost curve (MW)"] = [100, 150, 200] json["Generators"]["g1"]["Production cost curve (MW)"] =
json["Generators"]["g1"]["Production cost curve (\$)"] = [10, 25, 30] [100, 150, 200]
instance = UnitCommitment._from_json(json, repair=false) json["Generators"]["g1"]["Production cost curve (\$)"] =
[10, 25, 30]
instance = UnitCommitment._from_json(json, repair = false)
@test UnitCommitment.repair!(instance) == 4 @test UnitCommitment.repair!(instance) == 4
end end
@testset "Startup limit must be greater than Pmin" begin @testset "Startup limit must be greater than Pmin" begin
json = parse_case14() json = parse_case14()
json["Generators"]["g1"]["Production cost curve (MW)"] = [100, 150] json["Generators"]["g1"]["Production cost curve (MW)"] = [100, 150]
json["Generators"]["g1"]["Production cost curve (\$)"] = [100, 150] json["Generators"]["g1"]["Production cost curve (\$)"] = [100, 150]
json["Generators"]["g1"]["Startup limit (MW)"] = 80 json["Generators"]["g1"]["Startup limit (MW)"] = 80
instance = UnitCommitment._from_json(json, repair=false) instance = UnitCommitment._from_json(json, repair = false)
@test UnitCommitment.repair!(instance) == 1 @test UnitCommitment.repair!(instance) == 1
end end
@testset "Startup costs and delays must be increasing" begin @testset "Startup costs and delays must be increasing" begin
json = parse_case14() json = parse_case14()
json["Generators"]["g1"]["Startup costs (\$)"] = [300, 200, 100] json["Generators"]["g1"]["Startup costs (\$)"] = [300, 200, 100]
json["Generators"]["g1"]["Startup delays (h)"] = [8, 4, 2] json["Generators"]["g1"]["Startup delays (h)"] = [8, 4, 2]
instance = UnitCommitment._from_json(json, repair=false) instance = UnitCommitment._from_json(json, repair = false)
@test UnitCommitment.repair!(instance) == 4 @test UnitCommitment.repair!(instance) == 4
end end
end end
end end

Loading…
Cancel
Save