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

@ -42,22 +42,25 @@ function main()
@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()

@ -25,19 +25,17 @@ function generate_initial_conditions!(
@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(
mip,
power_balance, power_balance,
sum(b.load[t] for b in B) == sum(p[g] for g in G)) 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
@ -60,9 +58,7 @@ 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)

@ -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,12 +114,12 @@ 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[]
@ -137,16 +129,17 @@ function _from_json(json; repair=true)
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
@ -185,8 +178,12 @@ function _from_json(json; repair=true)
# 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]
@ -200,7 +197,7 @@ function _from_json(json; repair=true)
# 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!(
@ -216,10 +213,13 @@ function _from_json(json; repair=true)
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
@ -256,10 +256,8 @@ function _from_json(json; repair=true)
# 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
@ -296,10 +294,12 @@ function _from_json(json; repair=true)
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)
@ -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

@ -8,8 +8,8 @@ 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(;
@ -24,7 +24,8 @@ 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(
logger::TimeLogger,
level, level,
message, message,
_module, _module,
@ -32,7 +33,8 @@ function handle_message(logger::TimeLogger,
id, id,
filepath, filepath,
line; line;
kwargs...) 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)
@ -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)
@ -30,7 +29,6 @@ function build_model(;
optimizer = nothing, optimizer = nothing,
variable_names::Bool = false, variable_names::Bool = false,
)::JuMP.Model )::JuMP.Model
if (filename === nothing) && (instance === nothing) if (filename === nothing) && (instance === nothing)
error("Either filename or instance must be specified") error("Either filename or instance must be specified")
end end
@ -67,7 +65,11 @@ function build_model(;
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(
"Applying PTDF and LODF cutoffs (%.5f, %.5f)",
isf_cutoff,
lodf_cutoff
)
isf[abs.(isf).<isf_cutoff] .= 0 isf[abs.(isf).<isf_cutoff] .= 0
lodf[abs.(lodf).<lodf_cutoff] .= 0 lodf[abs.(lodf).<lodf_cutoff] .= 0
end end
@ -142,7 +144,6 @@ function build_model(;
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]
@ -152,7 +153,6 @@ function _add_transmission_line!(model, lm)
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]
@ -166,7 +166,8 @@ function _add_bus!(model::JuMP.Model, b::Bus)
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)
@ -247,17 +251,26 @@ 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
@ -271,39 +284,50 @@ function _add_unit!(model::JuMP.Model, g::Unit)
# 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
@ -314,39 +338,43 @@ function _add_unit!(model::JuMP.Model, g::Unit)
# 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
@ -354,55 +382,69 @@ function _add_unit!(model::JuMP.Model, g::Unit)
@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,35 +454,26 @@ 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( sum(net_injection[b.name, t] for b in model[:instance].buses) == 0
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] model[:expr_reserve][b.name, t] for b in model[:instance].buses
for b in model[:instance].buses
) >= reserves.spinning[t] ) >= 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},
@ -479,24 +512,32 @@ function _enforce_transmission(
@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(
model,
flow == sum(
net_injection[b.name, violation.time] * (
isf[violation.monitored_line.offset, b.offset] + ( isf[violation.monitored_line.offset, b.offset] + (
lodf[violation.monitored_line.offset, violation.outage_line.offset] * lodf[
isf[violation.outage_line.offset, b.offset] violation.monitored_line.offset,
violation.outage_line.offset,
] * isf[violation.outage_line.offset, b.offset]
)
) for b in instance.buses if b.offset > 0
) )
) )
for b in instance.buses
if b.offset > 0))
end end
nothing 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,7 +559,6 @@ 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)
@ -530,61 +569,65 @@ function solution(model::JuMP.Model)
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,8 +636,10 @@ 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)
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(is_on[g.name, t], is_on_value, force = true)
JuMP.fix( JuMP.fix(
prod_above[g.name, t], prod_above[g.name, t],
@ -604,9 +649,9 @@ function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing
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)
@ -659,7 +709,10 @@ function optimize!(
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..."
@ -681,10 +734,9 @@ function optimize!(
end end
end end
nothing return
end end
function _find_violations(model::JuMP.Model) function _find_violations(model::JuMP.Model)
instance = model[:instance] instance = model[:instance]
net_injection = model[:net_injection] net_injection = model[:net_injection]
@ -695,12 +747,12 @@ 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,
@ -710,11 +762,13 @@ function _find_violations(model::JuMP.Model)
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},

@ -4,11 +4,9 @@
# 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
@ -16,7 +14,6 @@ struct Violation
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,
@ -26,14 +23,12 @@ function 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,
@ -41,10 +36,10 @@ function 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
@ -55,10 +50,9 @@ 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}()
@ -82,7 +76,6 @@ function _query(filter::ViolationFilter)::Array{Violation, 1}
return violations return violations
end end
""" """
function _find_violations( function _find_violations(
@ -104,8 +97,7 @@ 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},
@ -114,7 +106,6 @@ function _find_violations(
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
@ -128,8 +119,7 @@ function _find_violations(
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]
@ -138,13 +128,13 @@ function _find_violations(
post_v::Array{Float64} = zeros(L, L, K) # post_v[lm, lc, thread] post_v::Array{Float64} = zeros(L, L, K) # post_v[lm, lc, thread]
normal_limits::Array{Float64,2} = [ normal_limits::Array{Float64,2} = [
l.normal_flow_limit[t] + overflow[l.offset, t] 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)
@ -160,7 +150,8 @@ function _find_violations(
# 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

@ -13,7 +13,10 @@ 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
@ -21,7 +24,6 @@ function _injection_shift_factors(; buses::Array{Bus}, lines::Array{Transmission
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,13 +10,7 @@ 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(

@ -40,7 +40,6 @@ 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
@ -68,18 +67,15 @@ function repair!(instance::UnitCommitmentInstance)::Int
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,8 +88,9 @@ 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,
solution::Union{Dict,OrderedDict},
)::Bool )::Bool
err_count = 0 err_count = 0
err_count += _validate_units(instance, solution) err_count += _validate_units(instance, solution)
@ -107,7 +104,6 @@ function validate(instance::UnitCommitmentInstance,
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
@ -123,7 +119,8 @@ function _validate_units(instance, solution; tol=0.01)
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]
@ -146,71 +143,120 @@ function _validate_units(instance, solution; tol=0.01)
# 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
@ -243,8 +289,11 @@ function _validate_units(instance, solution; tol=0.01)
# 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
@ -275,50 +324,59 @@ function _validate_units(instance, solution; tol=0.01)
# 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
@ -337,7 +395,8 @@ function _validate_reserve_and_demand(instance, solution, tol=0.01)
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)",
@ -351,4 +410,3 @@ function _validate_reserve_and_demand(instance, solution, tol=0.01)
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

@ -44,7 +44,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@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]
@ -76,7 +76,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@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]
@ -97,8 +97,8 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
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
@ -149,8 +149,10 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
# Should be able to build model without errors # Should be able to build model without errors
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
model = build_model(instance=modified, model = build_model(
instance = modified,
optimizer = optimizer, optimizer = optimizer,
variable_names=true) variable_names = true,
)
end end
end end

@ -16,7 +16,7 @@ import UnitCommitment: Violation, _offer, _query
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(
@ -25,7 +25,7 @@ import UnitCommitment: Violation, _offer, _query
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(
@ -34,7 +34,7 @@ import UnitCommitment: Violation, _offer, _query
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(
@ -43,7 +43,7 @@ import UnitCommitment: Violation, _offer, _query
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(
@ -52,7 +52,7 @@ import UnitCommitment: Violation, _offer, _query
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(
@ -61,8 +61,8 @@ import UnitCommitment: Violation, _offer, _query
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)
@ -71,13 +71,13 @@ import UnitCommitment: Violation, _offer, _query
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

@ -9,10 +9,28 @@ 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,
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 @test round.(actual, digits = 2) == expected
end end
@ -71,26 +89,27 @@ using UnitCommitment, Test, LinearAlgebra
) )
@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
@ -115,7 +134,8 @@ using UnitCommitment, Test, LinearAlgebra
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 =
isf_before[lm.offset, :] +
lodf[lm.offset, lc.offset] * isf_before[lc.offset, :] lodf[lm.offset, lc.offset] * isf_before[lc.offset, :]
@test norm(expected - actual) < 1e-6 @test norm(expected - actual) < 1e-6
end end

@ -4,16 +4,21 @@
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]
json["Generators"]["g1"]["Production cost curve (\$)"] =
[10, 25, 30]
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
@ -34,6 +39,5 @@ parse_case14() = JSON.parse(GZip.gzopen("../instances/test/case14.json.gz"),
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