parent
e594a68492
commit
4e8426beba
@ -0,0 +1,98 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
mutable struct Bus
|
||||||
|
name::String
|
||||||
|
offset::Int
|
||||||
|
load::Vector{Float64}
|
||||||
|
units::Vector
|
||||||
|
price_sensitive_loads::Vector
|
||||||
|
end
|
||||||
|
|
||||||
|
mutable struct CostSegment
|
||||||
|
mw::Vector{Float64}
|
||||||
|
cost::Vector{Float64}
|
||||||
|
end
|
||||||
|
|
||||||
|
mutable struct StartupCategory
|
||||||
|
delay::Int
|
||||||
|
cost::Float64
|
||||||
|
end
|
||||||
|
|
||||||
|
mutable struct Unit
|
||||||
|
name::String
|
||||||
|
bus::Bus
|
||||||
|
max_power::Vector{Float64}
|
||||||
|
min_power::Vector{Float64}
|
||||||
|
must_run::Vector{Bool}
|
||||||
|
min_power_cost::Vector{Float64}
|
||||||
|
cost_segments::Vector{CostSegment}
|
||||||
|
min_uptime::Int
|
||||||
|
min_downtime::Int
|
||||||
|
ramp_up_limit::Float64
|
||||||
|
ramp_down_limit::Float64
|
||||||
|
startup_limit::Float64
|
||||||
|
shutdown_limit::Float64
|
||||||
|
initial_status::Union{Int,Nothing}
|
||||||
|
initial_power::Union{Float64,Nothing}
|
||||||
|
provides_spinning_reserves::Vector{Bool}
|
||||||
|
startup_categories::Vector{StartupCategory}
|
||||||
|
end
|
||||||
|
|
||||||
|
mutable struct TransmissionLine
|
||||||
|
name::String
|
||||||
|
offset::Int
|
||||||
|
source::Bus
|
||||||
|
target::Bus
|
||||||
|
reactance::Float64
|
||||||
|
susceptance::Float64
|
||||||
|
normal_flow_limit::Vector{Float64}
|
||||||
|
emergency_flow_limit::Vector{Float64}
|
||||||
|
flow_limit_penalty::Vector{Float64}
|
||||||
|
end
|
||||||
|
|
||||||
|
mutable struct Reserves
|
||||||
|
spinning::Vector{Float64}
|
||||||
|
end
|
||||||
|
|
||||||
|
mutable struct Contingency
|
||||||
|
name::String
|
||||||
|
lines::Vector{TransmissionLine}
|
||||||
|
units::Vector{Unit}
|
||||||
|
end
|
||||||
|
|
||||||
|
mutable struct PriceSensitiveLoad
|
||||||
|
name::String
|
||||||
|
bus::Bus
|
||||||
|
demand::Vector{Float64}
|
||||||
|
revenue::Vector{Float64}
|
||||||
|
end
|
||||||
|
|
||||||
|
mutable struct UnitCommitmentInstance
|
||||||
|
time::Int
|
||||||
|
power_balance_penalty::Vector{Float64}
|
||||||
|
units::Vector{Unit}
|
||||||
|
buses::Vector{Bus}
|
||||||
|
lines::Vector{TransmissionLine}
|
||||||
|
reserves::Reserves
|
||||||
|
contingencies::Vector{Contingency}
|
||||||
|
price_sensitive_loads::Vector{PriceSensitiveLoad}
|
||||||
|
end
|
||||||
|
|
||||||
|
function Base.show(io::IO, instance::UnitCommitmentInstance)
|
||||||
|
print(io, "UnitCommitmentInstance(")
|
||||||
|
print(io, "$(length(instance.units)) units, ")
|
||||||
|
print(io, "$(length(instance.buses)) buses, ")
|
||||||
|
print(io, "$(length(instance.lines)) lines, ")
|
||||||
|
print(io, "$(length(instance.contingencies)) contingencies, ")
|
||||||
|
print(
|
||||||
|
io,
|
||||||
|
"$(length(instance.price_sensitive_loads)) price sensitive loads, ",
|
||||||
|
)
|
||||||
|
print(io, "$(instance.time) time steps")
|
||||||
|
print(io, ")")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
export UnitCommitmentInstance
|
@ -0,0 +1,20 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
# This file extends some JuMP functions so that decision variables can be safely
|
||||||
|
# replaced by (constant) floating point numbers.
|
||||||
|
|
||||||
|
import JuMP: value, fix, set_name
|
||||||
|
|
||||||
|
function value(x::Float64)
|
||||||
|
return x
|
||||||
|
end
|
||||||
|
|
||||||
|
function fix(x::Float64, v::Float64; force)
|
||||||
|
return abs(x - v) < 1e-6 || error("Value mismatch: $x != $v")
|
||||||
|
end
|
||||||
|
|
||||||
|
function set_name(x::Float64, n::String)
|
||||||
|
# nop
|
||||||
|
end
|
@ -0,0 +1,33 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
"""
|
||||||
|
fix!(model::JuMP.Model, solution::AbstractDict)::Nothing
|
||||||
|
|
||||||
|
Fix the value of all binary variables to the ones specified by the given
|
||||||
|
solution. Useful for computing LMPs.
|
||||||
|
"""
|
||||||
|
function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing
|
||||||
|
instance, T = model[:instance], model[:instance].time
|
||||||
|
is_on = model[:is_on]
|
||||||
|
prod_above = model[:prod_above]
|
||||||
|
reserve = model[:reserve]
|
||||||
|
for g in instance.units
|
||||||
|
for t in 1:T
|
||||||
|
is_on_value = round(solution["Is on"][g.name][t])
|
||||||
|
production_value =
|
||||||
|
round(solution["Production (MW)"][g.name][t], digits = 5)
|
||||||
|
reserve_value =
|
||||||
|
round(solution["Reserve (MW)"][g.name][t], digits = 5)
|
||||||
|
JuMP.fix(is_on[g.name, t], is_on_value, force = true)
|
||||||
|
JuMP.fix(
|
||||||
|
prod_above[g.name, t],
|
||||||
|
production_value - is_on_value * g.min_power[t],
|
||||||
|
force = true,
|
||||||
|
)
|
||||||
|
JuMP.fix(reserve[g.name, t], reserve_value, force = true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
@ -0,0 +1,83 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
function _enforce_transmission(
|
||||||
|
model::JuMP.Model,
|
||||||
|
violations::Vector{_Violation},
|
||||||
|
)::Nothing
|
||||||
|
for v in violations
|
||||||
|
_enforce_transmission(
|
||||||
|
model = model,
|
||||||
|
violation = v,
|
||||||
|
isf = model[:isf],
|
||||||
|
lodf = model[:lodf],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
function _enforce_transmission(;
|
||||||
|
model::JuMP.Model,
|
||||||
|
violation::_Violation,
|
||||||
|
isf::Matrix{Float64},
|
||||||
|
lodf::Matrix{Float64},
|
||||||
|
)::Nothing
|
||||||
|
instance = model[:instance]
|
||||||
|
limit::Float64 = 0.0
|
||||||
|
overflow = model[:overflow]
|
||||||
|
net_injection = model[:net_injection]
|
||||||
|
|
||||||
|
if violation.outage_line === nothing
|
||||||
|
limit = violation.monitored_line.normal_flow_limit[violation.time]
|
||||||
|
@info @sprintf(
|
||||||
|
" %8.3f MW overflow in %-5s time %3d (pre-contingency)",
|
||||||
|
violation.amount,
|
||||||
|
violation.monitored_line.name,
|
||||||
|
violation.time,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
limit = violation.monitored_line.emergency_flow_limit[violation.time]
|
||||||
|
@info @sprintf(
|
||||||
|
" %8.3f MW overflow in %-5s time %3d (outage: line %s)",
|
||||||
|
violation.amount,
|
||||||
|
violation.monitored_line.name,
|
||||||
|
violation.time,
|
||||||
|
violation.outage_line.name,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
fm = violation.monitored_line.name
|
||||||
|
t = violation.time
|
||||||
|
flow = @variable(model, base_name = "flow[$fm,$t]")
|
||||||
|
|
||||||
|
v = overflow[violation.monitored_line.name, violation.time]
|
||||||
|
@constraint(model, flow <= limit + v)
|
||||||
|
@constraint(model, -flow <= limit + v)
|
||||||
|
|
||||||
|
if violation.outage_line === nothing
|
||||||
|
@constraint(
|
||||||
|
model,
|
||||||
|
flow == sum(
|
||||||
|
net_injection[b.name, violation.time] *
|
||||||
|
isf[violation.monitored_line.offset, b.offset] for
|
||||||
|
b in instance.buses if b.offset > 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
@constraint(
|
||||||
|
model,
|
||||||
|
flow == sum(
|
||||||
|
net_injection[b.name, violation.time] * (
|
||||||
|
isf[violation.monitored_line.offset, b.offset] + (
|
||||||
|
lodf[
|
||||||
|
violation.monitored_line.offset,
|
||||||
|
violation.outage_line.offset,
|
||||||
|
] * isf[violation.outage_line.offset, b.offset]
|
||||||
|
)
|
||||||
|
) for b in instance.buses if b.offset > 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
return nothing
|
||||||
|
end
|
@ -0,0 +1,44 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
function _offer(filter::_ViolationFilter, v::_Violation)::Nothing
|
||||||
|
if v.monitored_line.offset ∉ keys(filter.queues)
|
||||||
|
filter.queues[v.monitored_line.offset] =
|
||||||
|
PriorityQueue{_Violation,Float64}()
|
||||||
|
end
|
||||||
|
q::PriorityQueue{_Violation,Float64} =
|
||||||
|
filter.queues[v.monitored_line.offset]
|
||||||
|
if length(q) < filter.max_per_line
|
||||||
|
enqueue!(q, v => v.amount)
|
||||||
|
else
|
||||||
|
if v.amount > peek(q)[1].amount
|
||||||
|
dequeue!(q)
|
||||||
|
enqueue!(q, v => v.amount)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nothing
|
||||||
|
end
|
||||||
|
|
||||||
|
function _query(filter::_ViolationFilter)::Array{_Violation,1}
|
||||||
|
violations = Array{_Violation,1}()
|
||||||
|
time_queue = PriorityQueue{_Violation,Float64}()
|
||||||
|
for l in keys(filter.queues)
|
||||||
|
line_queue = filter.queues[l]
|
||||||
|
while length(line_queue) > 0
|
||||||
|
v = dequeue!(line_queue)
|
||||||
|
if length(time_queue) < filter.max_total
|
||||||
|
enqueue!(time_queue, v => v.amount)
|
||||||
|
else
|
||||||
|
if v.amount > peek(time_queue)[1].amount
|
||||||
|
dequeue!(time_queue)
|
||||||
|
enqueue!(time_queue, v => v.amount)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
while length(time_queue) > 0
|
||||||
|
violations = [violations; dequeue!(time_queue)]
|
||||||
|
end
|
||||||
|
return violations
|
||||||
|
end
|
@ -0,0 +1,67 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
"""
|
||||||
|
optimize!(model::JuMP.Model, method::_XaQiWaTh19)::Nothing
|
||||||
|
|
||||||
|
Solve the given unit commitment model, enforcing transmission and N-1
|
||||||
|
security constraints lazily, according to the algorithm described in:
|
||||||
|
|
||||||
|
Xavier, A. S., Qiu, F., Wang, F., & Thimmapuram, P. R. (2019). Transmission
|
||||||
|
constraint filtering in large-scale security-constrained unit commitment.
|
||||||
|
IEEE Transactions on Power Systems, 34(3), 2457-2460.
|
||||||
|
"""
|
||||||
|
function optimize!(model::JuMP.Model, method::_XaQiWaTh19)::Nothing
|
||||||
|
function set_gap(gap)
|
||||||
|
try
|
||||||
|
JuMP.set_optimizer_attribute(model, "MIPGap", gap)
|
||||||
|
@info @sprintf("MIP gap tolerance set to %f", gap)
|
||||||
|
catch
|
||||||
|
@warn "Could not change MIP gap tolerance"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
instance = model[:instance]
|
||||||
|
initial_time = time()
|
||||||
|
large_gap = false
|
||||||
|
has_transmission = (length(model[:isf]) > 0)
|
||||||
|
if has_transmission && method.two_phase_gap
|
||||||
|
set_gap(1e-2)
|
||||||
|
large_gap = true
|
||||||
|
else
|
||||||
|
set_gap(method.gap_limit)
|
||||||
|
end
|
||||||
|
while true
|
||||||
|
time_elapsed = time() - initial_time
|
||||||
|
time_remaining = method.time_limit - time_elapsed
|
||||||
|
if time_remaining < 0
|
||||||
|
@info "Time limit exceeded"
|
||||||
|
break
|
||||||
|
end
|
||||||
|
@info @sprintf(
|
||||||
|
"Setting MILP time limit to %.2f seconds",
|
||||||
|
time_remaining
|
||||||
|
)
|
||||||
|
JuMP.set_time_limit_sec(model, time_remaining)
|
||||||
|
@info "Solving MILP..."
|
||||||
|
JuMP.optimize!(model)
|
||||||
|
has_transmission || break
|
||||||
|
violations = _find_violations(
|
||||||
|
model,
|
||||||
|
max_per_line = method.max_violations_per_line,
|
||||||
|
max_per_period = method.max_violations_per_period,
|
||||||
|
)
|
||||||
|
if isempty(violations)
|
||||||
|
@info "No violations found"
|
||||||
|
if large_gap
|
||||||
|
large_gap = false
|
||||||
|
set_gap(method.gap_limit)
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
else
|
||||||
|
_enforce_transmission(model, violations)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
@ -0,0 +1,78 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
import DataStructures: PriorityQueue
|
||||||
|
|
||||||
|
"""
|
||||||
|
struct _XaQiWaTh19 <: SolutionMethod
|
||||||
|
time_limit::Float64
|
||||||
|
gap_limit::Float64
|
||||||
|
two_phase_gap::Bool
|
||||||
|
end
|
||||||
|
|
||||||
|
Lazy constraint solution method described in:
|
||||||
|
|
||||||
|
Xavier, A. S., Qiu, F., Wang, F., & Thimmapuram, P. R. (2019). Transmission
|
||||||
|
constraint filtering in large-scale security-constrained unit commitment.
|
||||||
|
IEEE Transactions on Power Systems, 34(3), 2457-2460.
|
||||||
|
|
||||||
|
Fields
|
||||||
|
=========
|
||||||
|
- `time_limit`:
|
||||||
|
the time limit over the entire optimization procedure.
|
||||||
|
- `gap_limit`:
|
||||||
|
the desired relative optimality gap.
|
||||||
|
- `two_phase_gap`:
|
||||||
|
if true, solve the problem with large gap tolerance first, then reduce
|
||||||
|
the gap tolerance when no further violated constraints are found.
|
||||||
|
"""
|
||||||
|
struct _XaQiWaTh19
|
||||||
|
time_limit::Float64
|
||||||
|
gap_limit::Float64
|
||||||
|
two_phase_gap::Bool
|
||||||
|
max_violations_per_line::Int
|
||||||
|
max_violations_per_period::Int
|
||||||
|
|
||||||
|
function _XaQiWaTh19(;
|
||||||
|
time_limit::Float64,
|
||||||
|
gap_limit::Float64,
|
||||||
|
two_phase_gap::Bool,
|
||||||
|
max_violations_per_line::Int,
|
||||||
|
max_violations_per_period::Int,
|
||||||
|
)
|
||||||
|
return new(
|
||||||
|
time_limit,
|
||||||
|
gap_limit,
|
||||||
|
two_phase_gap,
|
||||||
|
max_violations_per_line,
|
||||||
|
max_violations_per_period,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
struct _Violation
|
||||||
|
time::Int
|
||||||
|
monitored_line::TransmissionLine
|
||||||
|
outage_line::Union{TransmissionLine,Nothing}
|
||||||
|
amount::Float64
|
||||||
|
|
||||||
|
function _Violation(;
|
||||||
|
time::Int,
|
||||||
|
monitored_line::TransmissionLine,
|
||||||
|
outage_line::Union{TransmissionLine,Nothing},
|
||||||
|
amount::Float64,
|
||||||
|
)
|
||||||
|
return new(time, monitored_line, outage_line, amount)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mutable struct _ViolationFilter
|
||||||
|
max_per_line::Int
|
||||||
|
max_total::Int
|
||||||
|
queues::Dict{Int,PriorityQueue{_Violation,Float64}}
|
||||||
|
|
||||||
|
function _ViolationFilter(; max_per_line::Int = 1, max_total::Int = 5)
|
||||||
|
return new(max_per_line, max_total, Dict())
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,23 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
"""
|
||||||
|
function optimize!(model::JuMP.Model)::Nothing
|
||||||
|
|
||||||
|
Solve the given unit commitment model. Unlike JuMP.optimize!, this uses more
|
||||||
|
advanced methods to accelerate the solution process and to enforce transmission
|
||||||
|
and N-1 security constraints.
|
||||||
|
"""
|
||||||
|
function optimize!(model::JuMP.Model)::Nothing
|
||||||
|
return UnitCommitment.optimize!(
|
||||||
|
model,
|
||||||
|
_XaQiWaTh19(
|
||||||
|
time_limit = 3600.0,
|
||||||
|
gap_limit = 1e-4,
|
||||||
|
two_phase_gap = true,
|
||||||
|
max_violations_per_line = 1,
|
||||||
|
max_violations_per_period = 5,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
end
|
@ -0,0 +1,65 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
function solution(model::JuMP.Model)::OrderedDict
|
||||||
|
instance, T = model[:instance], model[:instance].time
|
||||||
|
function timeseries(vars, collection)
|
||||||
|
return OrderedDict(
|
||||||
|
b.name => [round(value(vars[b.name, t]), digits = 5) for t in 1:T]
|
||||||
|
for b in collection
|
||||||
|
)
|
||||||
|
end
|
||||||
|
function production_cost(g)
|
||||||
|
return [
|
||||||
|
value(model[:is_on][g.name, t]) * g.min_power_cost[t] + sum(
|
||||||
|
Float64[
|
||||||
|
value(model[:segprod][g.name, t, k]) *
|
||||||
|
g.cost_segments[k].cost[t] for
|
||||||
|
k in 1:length(g.cost_segments)
|
||||||
|
],
|
||||||
|
) for t in 1:T
|
||||||
|
]
|
||||||
|
end
|
||||||
|
function production(g)
|
||||||
|
return [
|
||||||
|
value(model[:is_on][g.name, t]) * g.min_power[t] + sum(
|
||||||
|
Float64[
|
||||||
|
value(model[:segprod][g.name, t, k]) for
|
||||||
|
k in 1:length(g.cost_segments)
|
||||||
|
],
|
||||||
|
) for t in 1:T
|
||||||
|
]
|
||||||
|
end
|
||||||
|
function startup_cost(g)
|
||||||
|
S = length(g.startup_categories)
|
||||||
|
return [
|
||||||
|
sum(
|
||||||
|
g.startup_categories[s].cost *
|
||||||
|
value(model[:startup][g.name, t, s]) for s in 1:S
|
||||||
|
) for t in 1:T
|
||||||
|
]
|
||||||
|
end
|
||||||
|
sol = OrderedDict()
|
||||||
|
sol["Production (MW)"] =
|
||||||
|
OrderedDict(g.name => production(g) for g in instance.units)
|
||||||
|
sol["Production cost (\$)"] =
|
||||||
|
OrderedDict(g.name => production_cost(g) for g in instance.units)
|
||||||
|
sol["Startup cost (\$)"] =
|
||||||
|
OrderedDict(g.name => startup_cost(g) for g in instance.units)
|
||||||
|
sol["Is on"] = timeseries(model[:is_on], instance.units)
|
||||||
|
sol["Switch on"] = timeseries(model[:switch_on], instance.units)
|
||||||
|
sol["Switch off"] = timeseries(model[:switch_off], instance.units)
|
||||||
|
sol["Reserve (MW)"] = timeseries(model[:reserve], instance.units)
|
||||||
|
sol["Net injection (MW)"] =
|
||||||
|
timeseries(model[:net_injection], instance.buses)
|
||||||
|
sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses)
|
||||||
|
if !isempty(instance.lines)
|
||||||
|
sol["Line overflow (MW)"] = timeseries(model[:overflow], instance.lines)
|
||||||
|
end
|
||||||
|
if !isempty(instance.price_sensitive_loads)
|
||||||
|
sol["Price-sensitive loads (MW)"] =
|
||||||
|
timeseries(model[:loads], instance.price_sensitive_loads)
|
||||||
|
end
|
||||||
|
return sol
|
||||||
|
end
|
@ -0,0 +1,5 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
abstract type SolutionMethod end
|
@ -0,0 +1,24 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
function set_warm_start!(model::JuMP.Model, solution::AbstractDict)::Nothing
|
||||||
|
instance, T = model[:instance], model[:instance].time
|
||||||
|
is_on = model[:is_on]
|
||||||
|
prod_above = model[:prod_above]
|
||||||
|
reserve = model[:reserve]
|
||||||
|
for g in instance.units
|
||||||
|
for t in 1:T
|
||||||
|
JuMP.set_start_value(is_on[g.name, t], solution["Is on"][g.name][t])
|
||||||
|
JuMP.set_start_value(
|
||||||
|
switch_on[g.name, t],
|
||||||
|
solution["Switch on"][g.name][t],
|
||||||
|
)
|
||||||
|
JuMP.set_start_value(
|
||||||
|
switch_off[g.name, t],
|
||||||
|
solution["Switch off"][g.name][t],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
@ -0,0 +1,10 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
function write(filename::AbstractString, solution::AbstractDict)::Nothing
|
||||||
|
open(filename, "w") do file
|
||||||
|
return JSON.print(file, solution, 2)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
@ -0,0 +1,52 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
"""
|
||||||
|
slice(instance, range)
|
||||||
|
|
||||||
|
Creates a new instance, with only a subset of the time periods.
|
||||||
|
This function does not modify the provided instance. The initial
|
||||||
|
conditions are also not modified.
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
# Build a 2-hour UC instance
|
||||||
|
instance = UnitCommitment.read_benchmark("test/case14")
|
||||||
|
modified = UnitCommitment.slice(instance, 1:2)
|
||||||
|
|
||||||
|
"""
|
||||||
|
function slice(
|
||||||
|
instance::UnitCommitmentInstance,
|
||||||
|
range::UnitRange{Int},
|
||||||
|
)::UnitCommitmentInstance
|
||||||
|
modified = deepcopy(instance)
|
||||||
|
modified.time = length(range)
|
||||||
|
modified.power_balance_penalty = modified.power_balance_penalty[range]
|
||||||
|
modified.reserves.spinning = modified.reserves.spinning[range]
|
||||||
|
for u in modified.units
|
||||||
|
u.max_power = u.max_power[range]
|
||||||
|
u.min_power = u.min_power[range]
|
||||||
|
u.must_run = u.must_run[range]
|
||||||
|
u.min_power_cost = u.min_power_cost[range]
|
||||||
|
u.provides_spinning_reserves = u.provides_spinning_reserves[range]
|
||||||
|
for s in u.cost_segments
|
||||||
|
s.mw = s.mw[range]
|
||||||
|
s.cost = s.cost[range]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for b in modified.buses
|
||||||
|
b.load = b.load[range]
|
||||||
|
end
|
||||||
|
for l in modified.lines
|
||||||
|
l.normal_flow_limit = l.normal_flow_limit[range]
|
||||||
|
l.emergency_flow_limit = l.emergency_flow_limit[range]
|
||||||
|
l.flow_limit_penalty = l.flow_limit_penalty[range]
|
||||||
|
end
|
||||||
|
for ps in modified.price_sensitive_loads
|
||||||
|
ps.demand = ps.demand[range]
|
||||||
|
ps.revenue = ps.revenue[range]
|
||||||
|
end
|
||||||
|
return modified
|
||||||
|
end
|
@ -0,0 +1,3 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
@ -0,0 +1,69 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
"""
|
||||||
|
repair!(instance)
|
||||||
|
|
||||||
|
Verifies that the given unit commitment instance is valid and automatically
|
||||||
|
fixes some validation errors if possible, issuing a warning for each error
|
||||||
|
found. If a validation error cannot be automatically fixed, issues an
|
||||||
|
exception.
|
||||||
|
|
||||||
|
Returns the number of validation errors found.
|
||||||
|
"""
|
||||||
|
function repair!(instance::UnitCommitmentInstance)::Int
|
||||||
|
n_errors = 0
|
||||||
|
|
||||||
|
for g in instance.units
|
||||||
|
|
||||||
|
# Startup costs and delays must be increasing
|
||||||
|
for s in 2:length(g.startup_categories)
|
||||||
|
if g.startup_categories[s].delay <= g.startup_categories[s-1].delay
|
||||||
|
prev_value = g.startup_categories[s].delay
|
||||||
|
new_value = g.startup_categories[s-1].delay + 1
|
||||||
|
@warn "Generator $(g.name) has non-increasing startup delays (category $s). " *
|
||||||
|
"Changing delay: $prev_value → $new_value"
|
||||||
|
g.startup_categories[s].delay = new_value
|
||||||
|
n_errors += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if g.startup_categories[s].cost < g.startup_categories[s-1].cost
|
||||||
|
prev_value = g.startup_categories[s].cost
|
||||||
|
new_value = g.startup_categories[s-1].cost
|
||||||
|
@warn "Generator $(g.name) has decreasing startup cost (category $s). " *
|
||||||
|
"Changing cost: $prev_value → $new_value"
|
||||||
|
g.startup_categories[s].cost = new_value
|
||||||
|
n_errors += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for t in 1:instance.time
|
||||||
|
# Production cost curve should be convex
|
||||||
|
for k in 2:length(g.cost_segments)
|
||||||
|
cost = g.cost_segments[k].cost[t]
|
||||||
|
min_cost = g.cost_segments[k-1].cost[t]
|
||||||
|
if cost < min_cost - 1e-5
|
||||||
|
@warn "Generator $(g.name) has non-convex production cost curve " *
|
||||||
|
"(segment $k, time $t). Changing cost: $cost → $min_cost"
|
||||||
|
g.cost_segments[k].cost[t] = min_cost
|
||||||
|
n_errors += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Startup limit must be greater than min_power
|
||||||
|
if g.startup_limit < g.min_power[t]
|
||||||
|
new_limit = g.min_power[t]
|
||||||
|
prev_limit = g.startup_limit
|
||||||
|
@warn "Generator $(g.name) has startup limit lower than minimum power. " *
|
||||||
|
"Changing startup limit: $prev_limit → $new_limit"
|
||||||
|
g.startup_limit = new_limit
|
||||||
|
n_errors += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return n_errors
|
||||||
|
end
|
||||||
|
|
||||||
|
export repair!
|
Loading…
Reference in new issue