new formatting

pull/21/head
oyurdakul 4 years ago
parent 8988b00b07
commit febb4f1aad

BIN
benchmark/.DS_Store vendored

Binary file not shown.

@ -129,19 +129,15 @@ formulations = Dict(
const gap_limit = parse(Float64, args["--gap"]) const gap_limit = parse(Float64, args["--gap"])
const time_limit = parse(Float64, args["--time-limit"]) const time_limit = parse(Float64, args["--time-limit"])
methods = Dict( methods = Dict(
"default" => XavQiuWanThi2019.Method( "default" =>
time_limit = time_limit, XavQiuWanThi2019.Method(time_limit = time_limit, gap_limit = gap_limit),
gap_limit = gap_limit,
),
) )
# MIP solvers # MIP solvers
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
optimizers = Dict( optimizers = Dict(
"gurobi" => optimizer_with_attributes( "gurobi" =>
Gurobi.Optimizer, optimizer_with_attributes(Gurobi.Optimizer, "Threads" => Threads.nthreads()),
"Threads" => Threads.nthreads(),
),
) )
# Parse command line arguments # Parse command line arguments

@ -41,7 +41,7 @@ function read_egret_solution(path::String)::OrderedDict
startup_cost[gen_name] = zeros(T) startup_cost[gen_name] = zeros(T)
production_cost[gen_name] = zeros(T) production_cost[gen_name] = zeros(T)
if "commitment_cost" in keys(gen_dict) if "commitment_cost" in keys(gen_dict)
for t in 1:T for t = 1:T
x = gen_dict["commitment"]["values"][t] x = gen_dict["commitment"]["values"][t]
commitment_cost = gen_dict["commitment_cost"]["values"][t] commitment_cost = gen_dict["commitment_cost"]["values"][t]
prod_above_cost = gen_dict["production_cost"]["values"][t] prod_above_cost = gen_dict["production_cost"]["values"][t]

@ -23,10 +23,7 @@ Example
import UnitCommitment import UnitCommitment
instance = UnitCommitment.read_benchmark("matpower/case3375wp/2017-02-01") instance = UnitCommitment.read_benchmark("matpower/case3375wp/2017-02-01")
""" """
function read_benchmark( function read_benchmark(name::AbstractString; quiet::Bool = false)::UnitCommitmentInstance
name::AbstractString;
quiet::Bool = false,
)::UnitCommitmentInstance
basedir = dirname(@__FILE__) basedir = dirname(@__FILE__)
filename = "$basedir/../../instances/$name.json.gz" filename = "$basedir/../../instances/$name.json.gz"
url = "$INSTANCES_URL/$name.json.gz" url = "$INSTANCES_URL/$name.json.gz"
@ -65,9 +62,7 @@ function read(path::AbstractString)::UnitCommitmentInstance
end end
function _read(file::IO)::UnitCommitmentInstance function _read(file::IO)::UnitCommitmentInstance
return _from_json( return _from_json(JSON.parse(file, dicttype = () -> DefaultOrderedDict(nothing)))
JSON.parse(file, dicttype = () -> DefaultOrderedDict(nothing)),
)
end end
function _read_json(path::String)::OrderedDict function _read_json(path::String)::OrderedDict
@ -97,8 +92,7 @@ function _from_json(json; repair = true)
end end
time_horizon !== nothing || error("Missing 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) || (60 % time_step == 0) || error("Time step $time_step is not a divisor of 60")
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
@ -108,23 +102,23 @@ function _from_json(json; repair = true)
function timeseries(x; default = nothing) function timeseries(x; default = nothing)
x !== nothing || return default x !== nothing || return default
x isa Array || return [x for t in 1:T] x isa Array || return [x for t = 1:T]
return x return x
end end
# Read parameters # Read parameters
power_balance_penalty = timeseries( power_balance_penalty = timeseries(
json["Parameters"]["Power balance penalty (\$/MW)"], json["Parameters"]["Power balance penalty (\$/MW)"],
default = [1000.0 for t in 1:T], default = [1000.0 for t = 1:T],
) )
# Penalty price for shortage in meeting system-wide flexiramp requirements # Penalty price for shortage in meeting system-wide flexiramp requirements
flexiramp_shortfall_penalty = timeseries( flexiramp_shortfall_penalty = timeseries(
json["Parameters"]["Flexiramp penalty (\$/MW)"], json["Parameters"]["Flexiramp penalty (\$/MW)"],
default = [500.0 for t in 1:T], default = [500.0 for t = 1:T],
) )
shortfall_penalty = timeseries( shortfall_penalty = timeseries(
json["Parameters"]["Reserve shortfall penalty (\$/MW)"], json["Parameters"]["Reserve shortfall penalty (\$/MW)"],
default = [-1.0 for t in 1:T], default = [-1.0 for t = 1:T],
) )
# Read buses # Read buses
@ -146,17 +140,14 @@ 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( curve_mw = hcat([timeseries(dict["Production cost curve (MW)"][k]) for k = 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 = 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]
segments = CostSegment[] segments = CostSegment[]
for k in 2:K for k = 2:K
amount = curve_mw[:, k] - curve_mw[:, k-1] amount = curve_mw[:, k] - curve_mw[:, k-1]
cost = (curve_cost[:, k] - curve_cost[:, k-1]) ./ amount cost = (curve_cost[:, k] - curve_cost[:, k-1]) ./ amount
replace!(cost, NaN => 0.0) replace!(cost, NaN => 0.0)
@ -167,13 +158,10 @@ function _from_json(json; repair = true)
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.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 = 1:length(startup_delays)
push!( push!(
startup_categories, startup_categories,
StartupCategory( StartupCategory(startup_delays[k] .* time_multiplier, startup_costs[k]),
startup_delays[k] .* time_multiplier,
startup_costs[k],
),
) )
end end
@ -186,8 +174,7 @@ function _from_json(json; repair = true)
else else
initial_status !== nothing || initial_status !== nothing ||
error("unit $unit_name has initial power but no initial status") error("unit $unit_name has initial power but no initial status")
initial_status != 0 || initial_status != 0 || error("unit $unit_name has invalid initial status")
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
@ -199,7 +186,7 @@ function _from_json(json; repair = true)
bus, bus,
max_power, max_power,
min_power, min_power,
timeseries(dict["Must run?"], default = [false for t in 1:T]), timeseries(dict["Must run?"], default = [false for t = 1:T]),
min_power_cost, min_power_cost,
segments, segments,
scalar(dict["Minimum uptime (h)"], default = 1) * time_multiplier, scalar(dict["Minimum uptime (h)"], default = 1) * time_multiplier,
@ -210,14 +197,8 @@ function _from_json(json; repair = true)
scalar(dict["Shutdown limit (MW)"], default = 1e6), scalar(dict["Shutdown limit (MW)"], default = 1e6),
initial_status, initial_status,
initial_power, initial_power,
timeseries( timeseries(dict["Provides spinning reserves?"], default = [true for t = 1:T]),
dict["Provides spinning reserves?"], timeseries(dict["Provides flexible capacity?"], default = [true for t = 1:T]),
default = [true for t in 1:T],
),
timeseries(
dict["Provides flexible capacity?"],
default = [true for t in 1:T],
),
startup_categories, startup_categories,
) )
push!(bus.units, unit) push!(bus.units, unit)
@ -230,14 +211,10 @@ function _from_json(json; repair = true)
if "Reserves" in keys(json) if "Reserves" in keys(json)
reserves.spinning = reserves.spinning =
timeseries(json["Reserves"]["Spinning (MW)"], default = zeros(T)) timeseries(json["Reserves"]["Spinning (MW)"], default = zeros(T))
reserves.upflexiramp = timeseries( reserves.upflexiramp =
json["Reserves"]["Up-flexiramp (MW)"], timeseries(json["Reserves"]["Up-flexiramp (MW)"], default = zeros(T))
default = zeros(T), reserves.dwflexiramp =
) timeseries(json["Reserves"]["Down-flexiramp (MW)"], default = zeros(T))
reserves.dwflexiramp = timeseries(
json["Reserves"]["Down-flexiramp (MW)"],
default = zeros(T),
)
end end
# Read transmission lines # Read transmission lines
@ -250,17 +227,11 @@ function _from_json(json; repair = true)
name_to_bus[dict["Target bus"]], name_to_bus[dict["Target bus"]],
scalar(dict["Reactance (ohms)"]), scalar(dict["Reactance (ohms)"]),
scalar(dict["Susceptance (S)"]), scalar(dict["Susceptance (S)"]),
timeseries( timeseries(dict["Normal flow limit (MW)"], default = [1e8 for t = 1:T]),
dict["Normal flow limit (MW)"], timeseries(dict["Emergency flow limit (MW)"], default = [1e8 for t = 1:T]),
default = [1e8 for t in 1:T],
),
timeseries(
dict["Emergency flow limit (MW)"],
default = [1e8 for t in 1:T],
),
timeseries( timeseries(
dict["Flow limit penalty (\$/MW)"], dict["Flow limit penalty (\$/MW)"],
default = [5000.0 for t in 1:T], default = [5000.0 for t = 1:T],
), ),
) )
name_to_line[line_name] = line name_to_line[line_name] = line
@ -274,12 +245,10 @@ 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 = affected_lines = [name_to_line[l] for l in dict["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 = affected_units = [name_to_unit[u] for u in dict["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)

@ -96,10 +96,7 @@ function Base.show(io::IO, instance::UnitCommitmentInstance)
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( print(io, "$(length(instance.price_sensitive_loads)) price sensitive loads, ")
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 return

@ -33,19 +33,15 @@ function build_model(;
variable_names::Bool = false, variable_names::Bool = false,
)::JuMP.Model )::JuMP.Model
if formulation.ramping == WanHob2016.Ramping() && if formulation.ramping == WanHob2016.Ramping() &&
instance.reserves.spinning >= ones(instance.time).*1e-6 instance.reserves.spinning >= ones(instance.time) .* 1e-6
error( error("Spinning reserves are not supported by the WanHob2016 ramping formulation")
"Spinning reserves are not supported by the WanHob2016 ramping formulation",
)
end end
if formulation.ramping !== WanHob2016.Ramping() && ( if formulation.ramping !== WanHob2016.Ramping() && (
instance.reserves.upflexiramp >= ones(instance.time).*1e-6 || instance.reserves.upflexiramp >= ones(instance.time) .* 1e-6 ||
instance.reserves.dwflexiramp >= ones(instance.time).*1e-6 instance.reserves.dwflexiramp >= ones(instance.time) .* 1e-6
) )
error( error("Flexiramp is supported only by the WanHob2016 ramping formulation")
"Flexiramp is supported only by the WanHob2016 ramping formulation",
)
end end
@info "Building model..." @info "Building model..."

@ -32,7 +32,7 @@ function _add_ramp_eqs!(
switch_off = model[:switch_off] switch_off = model[:switch_off]
switch_on = model[:switch_on] switch_on = model[:switch_on]
for t in 1:model[:instance].time for t = 1:model[:instance].time
# Ramp up limit # Ramp up limit
if t == 1 if t == 1
if is_initially_on if is_initially_on
@ -49,12 +49,8 @@ function _add_ramp_eqs!(
max_prod_this_period = max_prod_this_period =
g.min_power[t] * is_on[gn, t] + g.min_power[t] * is_on[gn, t] +
prod_above[gn, t] + prod_above[gn, t] +
( (RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ? reserve[gn, t] : 0.0)
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ? min_prod_last_period = g.min_power[t-1] * is_on[gn, t-1] + prod_above[gn, t-1]
reserve[gn, t] : 0.0
)
min_prod_last_period =
g.min_power[t-1] * is_on[gn, t-1] + prod_above[gn, t-1]
# Equation (24) in Kneuven et al. (2020) # Equation (24) in Kneuven et al. (2020)
eq_ramp_up[gn, t] = @constraint( eq_ramp_up[gn, t] = @constraint(
@ -81,11 +77,10 @@ function _add_ramp_eqs!(
g.min_power[t-1] * is_on[gn, t-1] + g.min_power[t-1] * is_on[gn, t-1] +
prod_above[gn, t-1] + prod_above[gn, t-1] +
( (
RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ? RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ? reserve[gn, t-1] :
reserve[gn, t-1] : 0.0 0.0
) )
min_prod_this_period = min_prod_this_period = g.min_power[t] * is_on[gn, t] + prod_above[gn, t]
g.min_power[t] * is_on[gn, t] + prod_above[gn, t]
# Equation (25) in Kneuven et al. (2020) # Equation (25) in Kneuven et al. (2020)
eq_ramp_down[gn, t] = @constraint( eq_ramp_down[gn, t] = @constraint(

@ -18,18 +18,16 @@ function _add_production_piecewise_linear_eqs!(
prod_above = model[:prod_above] prod_above = model[:prod_above]
K = length(g.cost_segments) K = length(g.cost_segments)
for t in 1:model[:instance].time for t = 1:model[:instance].time
gn = g.name gn = g.name
for k in 1:K for k = 1:K
# Equation (45) in Kneuven et al. (2020) # Equation (45) in Kneuven et al. (2020)
# NB: when reading instance, UnitCommitment.jl already calculates # NB: when reading instance, UnitCommitment.jl already calculates
# difference between max power for segments k and k-1 so the # difference between max power for segments k and k-1 so the
# value of cost_segments[k].mw[t] is the max production *for # value of cost_segments[k].mw[t] is the max production *for
# that segment* # that segment*
eq_segprod_limit[gn, t, k] = @constraint( eq_segprod_limit[gn, t, k] =
model, @constraint(model, segprod[gn, t, k] <= g.cost_segments[k].mw[t])
segprod[gn, t, k] <= g.cost_segments[k].mw[t]
)
# Also add this as an explicit upper bound on segprod to make the # Also add this as an explicit upper bound on segprod to make the
# solver's work a bit easier # solver's work a bit easier
@ -37,18 +35,12 @@ function _add_production_piecewise_linear_eqs!(
# Definition of production # Definition of production
# Equation (43) in Kneuven et al. (2020) # Equation (43) in Kneuven et al. (2020)
eq_prod_above_def[gn, t] = @constraint( eq_prod_above_def[gn, t] =
model, @constraint(model, prod_above[gn, t] == sum(segprod[gn, t, k] for k = 1:K))
prod_above[gn, t] == sum(segprod[gn, t, k] for k in 1:K)
)
# Objective function # Objective function
# Equation (44) in Kneuven et al. (2020) # Equation (44) in Kneuven et al. (2020)
add_to_expression!( add_to_expression!(model[:obj], segprod[gn, t, k], g.cost_segments[k].cost[t])
model[:obj],
segprod[gn, t, k],
g.cost_segments[k].cost[t],
)
end end
end end
end end

@ -33,9 +33,8 @@ function _add_ramp_eqs!(
switch_off = model[:switch_off] switch_off = model[:switch_off]
switch_on = model[:switch_on] switch_on = model[:switch_on]
for t in 1:model[:instance].time for t = 1:model[:instance].time
time_invariant = time_invariant = (t > 1) ? (abs(g.min_power[t] - g.min_power[t-1]) < 1e-7) : true
(t > 1) ? (abs(g.min_power[t] - g.min_power[t-1]) < 1e-7) : true
# if t > 1 && !time_invariant # if t > 1 && !time_invariant
# @warn( # @warn(
@ -48,10 +47,8 @@ function _add_ramp_eqs!(
# end # end
max_prod_this_period = max_prod_this_period =
prod_above[gn, t] + ( prod_above[gn, t] +
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ? (RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ? reserve[gn, t] : 0.0)
reserve[gn, t] : 0.0
)
min_prod_last_period = 0.0 min_prod_last_period = 0.0
if t > 1 && time_invariant if t > 1 && time_invariant
min_prod_last_period = prod_above[gn, t-1] min_prod_last_period = prod_above[gn, t-1]
@ -61,8 +58,7 @@ function _add_ramp_eqs!(
eq_str_ramp_up[gn, t] = @constraint( eq_str_ramp_up[gn, t] = @constraint(
model, model,
max_prod_this_period - min_prod_last_period <= max_prod_this_period - min_prod_last_period <=
(SU - g.min_power[t] - RU) * switch_on[gn, t] + (SU - g.min_power[t] - RU) * switch_on[gn, t] + RU * is_on[gn, t]
RU * is_on[gn, t]
) )
elseif (t == 1 && is_initially_on) || (t > 1 && !time_invariant) elseif (t == 1 && is_initially_on) || (t > 1 && !time_invariant)
if t > 1 if t > 1
@ -103,8 +99,7 @@ function _add_ramp_eqs!(
eq_str_ramp_down[gn, t] = @constraint( eq_str_ramp_down[gn, t] = @constraint(
model, model,
max_prod_last_period - min_prod_this_period <= max_prod_last_period - min_prod_this_period <=
(SD - g.min_power[t] - RD) * switch_off[gn, t] + (SD - g.min_power[t] - RD) * switch_off[gn, t] + RD * on_last_period
RD * on_last_period
) )
elseif (t == 1 && is_initially_on) || (t > 1 && !time_invariant) elseif (t == 1 && is_initially_on) || (t > 1 && !time_invariant)
# Add back in min power # Add back in min power

@ -9,8 +9,8 @@ function _add_production_vars!(
)::Nothing )::Nothing
prod_above = _init(model, :prod_above) prod_above = _init(model, :prod_above)
segprod = _init(model, :segprod) segprod = _init(model, :segprod)
for t in 1:model[:instance].time for t = 1:model[:instance].time
for k in 1:length(g.cost_segments) for k = 1:length(g.cost_segments)
segprod[g.name, t, k] = @variable(model, lower_bound = 0) segprod[g.name, t, k] = @variable(model, lower_bound = 0)
end end
prod_above[g.name, t] = @variable(model, lower_bound = 0) prod_above[g.name, t] = @variable(model, lower_bound = 0)
@ -28,7 +28,7 @@ function _add_production_limit_eqs!(
prod_above = model[:prod_above] prod_above = model[:prod_above]
reserve = model[:reserve] reserve = model[:reserve]
gn = g.name gn = g.name
for t in 1:model[:instance].time for t = 1:model[:instance].time
# Objective function terms for production costs # Objective function terms for production costs
# Part of (69) of Kneuven et al. (2020) as C^R_g * u_g(t) term # Part of (69) of Kneuven et al. (2020) as C^R_g * u_g(t) term
add_to_expression!(model[:obj], is_on[gn, t], g.min_power_cost[t]) add_to_expression!(model[:obj], is_on[gn, t], g.min_power_cost[t])

@ -21,15 +21,13 @@ function _add_production_piecewise_linear_eqs!(
is_on = model[:is_on] is_on = model[:is_on]
K = length(g.cost_segments) K = length(g.cost_segments)
for t in 1:model[:instance].time for t = 1:model[:instance].time
# Definition of production # Definition of production
# Equation (43) in Kneuven et al. (2020) # Equation (43) in Kneuven et al. (2020)
eq_prod_above_def[gn, t] = @constraint( eq_prod_above_def[gn, t] =
model, @constraint(model, prod_above[gn, t] == sum(segprod[gn, t, k] for k = 1:K))
prod_above[gn, t] == sum(segprod[gn, t, k] for k in 1:K)
)
for k in 1:K for k = 1:K
# Equation (42) in Kneuven et al. (2020) # Equation (42) in Kneuven et al. (2020)
# Without this, solvers will add a lot of implied bound cuts to # Without this, solvers will add a lot of implied bound cuts to
# have this same effect. # have this same effect.
@ -48,11 +46,7 @@ function _add_production_piecewise_linear_eqs!(
# Objective function # Objective function
# Equation (44) in Kneuven et al. (2020) # Equation (44) in Kneuven et al. (2020)
add_to_expression!( add_to_expression!(model[:obj], segprod[gn, t, k], g.cost_segments[k].cost[t])
model[:obj],
segprod[gn, t, k],
g.cost_segments[k].cost[t],
)
end end
end end
return return

@ -10,7 +10,7 @@ function _add_status_vars!(
is_on = _init(model, :is_on) is_on = _init(model, :is_on)
switch_on = _init(model, :switch_on) switch_on = _init(model, :switch_on)
switch_off = _init(model, :switch_off) switch_off = _init(model, :switch_off)
for t in 1:model[:instance].time for t = 1:model[:instance].time
if g.must_run[t] if g.must_run[t]
is_on[g.name, t] = 1.0 is_on[g.name, t] = 1.0
switch_on[g.name, t] = (t == 1 ? 1.0 - _is_initially_on(g) : 0.0) switch_on[g.name, t] = (t == 1 ? 1.0 - _is_initially_on(g) : 0.0)
@ -34,7 +34,7 @@ function _add_status_eqs!(
is_on = model[:is_on] is_on = model[:is_on]
switch_off = model[:switch_off] switch_off = model[:switch_off]
switch_on = model[:switch_on] switch_on = model[:switch_on]
for t in 1:model[:instance].time for t = 1:model[:instance].time
if !g.must_run[t] if !g.must_run[t]
# Link binary variables # Link binary variables
if t == 1 if t == 1
@ -51,10 +51,8 @@ function _add_status_eqs!(
) )
end end
# Cannot switch on and off at the same time # Cannot switch on and off at the same time
eq_switch_on_off[g.name, t] = @constraint( eq_switch_on_off[g.name, t] =
model, @constraint(model, switch_on[g.name, t] + switch_off[g.name, t] <= 1)
switch_on[g.name, t] + switch_off[g.name, t] <= 1
)
end end
end end
return return

@ -26,12 +26,12 @@ function _add_production_piecewise_linear_eqs!(
switch_on = model[:switch_on] switch_on = model[:switch_on]
switch_off = model[:switch_off] switch_off = model[:switch_off]
for t in 1:T for t = 1:T
for k in 1:K for k = 1:K
# Pbar^{k-1) # Pbar^{k-1)
Pbar0 = Pbar0 =
g.min_power[t] + g.min_power[t] +
(k > 1 ? sum(g.cost_segments[ell].mw[t] for ell in 1:k-1) : 0.0) (k > 1 ? sum(g.cost_segments[ell].mw[t] for ell = 1:k-1) : 0.0)
# Pbar^k # Pbar^k
Pbar1 = g.cost_segments[k].mw[t] + Pbar0 Pbar1 = g.cost_segments[k].mw[t] + Pbar0
@ -61,8 +61,7 @@ function _add_production_piecewise_linear_eqs!(
eq_segprod_limit_a[gn, t, k] = @constraint( eq_segprod_limit_a[gn, t, k] = @constraint(
model, model,
segprod[gn, t, k] <= segprod[gn, t, k] <=
g.cost_segments[k].mw[t] * is_on[gn, t] - g.cost_segments[k].mw[t] * is_on[gn, t] - Cv * switch_on[gn, t] -
Cv * switch_on[gn, t] -
(t < T ? Cw * switch_off[gn, t+1] : 0.0) (t < T ? Cw * switch_off[gn, t+1] : 0.0)
) )
else else
@ -70,8 +69,7 @@ function _add_production_piecewise_linear_eqs!(
eq_segprod_limit_b[gn, t, k] = @constraint( eq_segprod_limit_b[gn, t, k] = @constraint(
model, model,
segprod[gn, t, k] <= segprod[gn, t, k] <=
g.cost_segments[k].mw[t] * is_on[gn, t] - g.cost_segments[k].mw[t] * is_on[gn, t] - Cv * switch_on[gn, t] -
Cv * switch_on[gn, t] -
(t < T ? max(0, Cv - Cw) * switch_off[gn, t+1] : 0.0) (t < T ? max(0, Cv - Cw) * switch_off[gn, t+1] : 0.0)
) )
@ -87,18 +85,12 @@ function _add_production_piecewise_linear_eqs!(
# Definition of production # Definition of production
# Equation (43) in Kneuven et al. (2020) # Equation (43) in Kneuven et al. (2020)
eq_prod_above_def[gn, t] = @constraint( eq_prod_above_def[gn, t] =
model, @constraint(model, prod_above[gn, t] == sum(segprod[gn, t, k] for k = 1:K))
prod_above[gn, t] == sum(segprod[gn, t, k] for k in 1:K)
)
# Objective function # Objective function
# Equation (44) in Kneuven et al. (2020) # Equation (44) in Kneuven et al. (2020)
add_to_expression!( add_to_expression!(model[:obj], segprod[gn, t, k], g.cost_segments[k].cost[t])
model[:obj],
segprod[gn, t, k],
g.cost_segments[k].cost[t],
)
# Also add an explicit upper bound on segprod to make the solver's # Also add an explicit upper bound on segprod to make the solver's
# work a bit easier # work a bit easier

@ -32,9 +32,8 @@ function _add_ramp_eqs!(
switch_off = model[:switch_off] switch_off = model[:switch_off]
switch_on = model[:switch_on] switch_on = model[:switch_on]
for t in 1:model[:instance].time for t = 1:model[:instance].time
time_invariant = time_invariant = (t > 1) ? (abs(g.min_power[t] - g.min_power[t-1]) < 1e-7) : true
(t > 1) ? (abs(g.min_power[t] - g.min_power[t-1]) < 1e-7) : true
# Ramp up limit # Ramp up limit
if t == 1 if t == 1
@ -60,8 +59,8 @@ function _add_ramp_eqs!(
g.min_power[t] * is_on[gn, t] + g.min_power[t] * is_on[gn, t] +
prod_above[gn, t] + prod_above[gn, t] +
( (
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ? RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ? reserve[gn, t] :
reserve[gn, t] : 0.0 0.0
) )
min_prod_last_period = min_prod_last_period =
g.min_power[t-1] * is_on[gn, t-1] + prod_above[gn, t-1] g.min_power[t-1] * is_on[gn, t-1] + prod_above[gn, t-1]
@ -76,8 +75,7 @@ function _add_ramp_eqs!(
# prod_above[gn, t] when starting up, and creates diff with (24). # prod_above[gn, t] when starting up, and creates diff with (24).
eq_ramp_up[gn, t] = @constraint( eq_ramp_up[gn, t] = @constraint(
model, model,
prod_above[gn, t] + prod_above[gn, t] + (RESERVES_WHEN_RAMP_UP ? reserve[gn, t] : 0.0) -
(RESERVES_WHEN_RAMP_UP ? reserve[gn, t] : 0.0) -
prod_above[gn, t-1] <= RU prod_above[gn, t-1] <= RU
) )
end end
@ -107,8 +105,7 @@ function _add_ramp_eqs!(
RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ? RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ?
reserve[gn, t-1] : 0.0 reserve[gn, t-1] : 0.0
) )
min_prod_this_period = min_prod_this_period = g.min_power[t] * is_on[gn, t] + prod_above[gn, t]
g.min_power[t] * is_on[gn, t] + prod_above[gn, t]
eq_ramp_down[gn, t] = @constraint( eq_ramp_down[gn, t] = @constraint(
model, model,
max_prod_last_period - min_prod_this_period <= max_prod_last_period - min_prod_this_period <=

@ -11,30 +11,27 @@ function _add_startup_cost_eqs!(
eq_startup_restrict = _init(model, :eq_startup_restrict) eq_startup_restrict = _init(model, :eq_startup_restrict)
S = length(g.startup_categories) S = length(g.startup_categories)
startup = model[:startup] startup = model[:startup]
for t in 1:model[:instance].time for t = 1:model[:instance].time
# If unit is switching on, we must choose a startup category # If unit is switching on, we must choose a startup category
eq_startup_choose[g.name, t] = @constraint( eq_startup_choose[g.name, t] = @constraint(
model, model,
model[:switch_on][g.name, t] == model[:switch_on][g.name, t] == sum(startup[g.name, t, s] for s = 1:S)
sum(startup[g.name, t, s] for s in 1:S)
) )
for s in 1:S for s = 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_start = t - g.startup_categories[s+1].delay + 1 range_start = t - g.startup_categories[s+1].delay + 1
range_end = t - g.startup_categories[s].delay range_end = t - g.startup_categories[s].delay
range = (range_start:range_end) range = (range_start:range_end)
initial_sum = ( initial_sum =
g.initial_status < 0 && (g.initial_status + 1 in range) ? 1.0 : 0.0 (g.initial_status < 0 && (g.initial_status + 1 in range) ? 1.0 : 0.0)
)
eq_startup_restrict[g.name, t, s] = @constraint( eq_startup_restrict[g.name, t, s] = @constraint(
model, model,
startup[g.name, t, s] <= startup[g.name, t, s] <=
initial_sum + sum( initial_sum +
model[:switch_off][g.name, i] for i in range if i >= 1 sum(model[:switch_off][g.name, i] for i in range if i >= 1)
)
) )
end end

@ -14,10 +14,8 @@ function _add_ramp_eqs!(
gn = g.name gn = g.name
reserve = model[:reserve] reserve = model[:reserve]
eq_str_prod_limit = _init(model, :eq_str_prod_limit) eq_str_prod_limit = _init(model, :eq_str_prod_limit)
eq_prod_limit_ramp_up_extra_period = eq_prod_limit_ramp_up_extra_period = _init(model, :eq_prod_limit_ramp_up_extra_period)
_init(model, :eq_prod_limit_ramp_up_extra_period) eq_prod_limit_shutdown_trajectory = _init(model, :eq_prod_limit_shutdown_trajectory)
eq_prod_limit_shutdown_trajectory =
_init(model, :eq_prod_limit_shutdown_trajectory)
UT = g.min_uptime UT = g.min_uptime
SU = g.startup_limit # startup rate, i.e., max production right after startup SU = g.startup_limit # startup rate, i.e., max production right after startup
SD = g.shutdown_limit # shutdown rate, i.e., max production right before shutdown SD = g.shutdown_limit # shutdown rate, i.e., max production right before shutdown
@ -33,7 +31,7 @@ function _add_ramp_eqs!(
switch_off = model[:switch_off] switch_off = model[:switch_off]
switch_on = model[:switch_on] switch_on = model[:switch_on]
for t in 1:T for t = 1:T
Pbar = g.max_power[t] Pbar = g.max_power[t]
if Pbar < 1e-7 if Pbar < 1e-7
# Skip this time period if max power = 0 # Skip this time period if max power = 0
@ -54,13 +52,10 @@ function _add_ramp_eqs!(
# then switch_off[gn, t+1] = 0 # then switch_off[gn, t+1] = 0
eq_str_prod_limit[gn, t] = @constraint( eq_str_prod_limit[gn, t] = @constraint(
model, model,
prod_above[gn, t] + prod_above[gn, t] + g.min_power[t] * is_on[gn, t] + reserve[gn, t] <=
g.min_power[t] * is_on[gn, t] + Pbar * is_on[gn, t] - (t < T ? (Pbar - SD) * switch_off[gn, t+1] : 0.0) - sum(
reserve[gn, t] <=
Pbar * is_on[gn, t] -
(t < T ? (Pbar - SD) * switch_off[gn, t+1] : 0.0) - sum(
(Pbar - (SU + i * RU)) * switch_on[gn, t-i] for (Pbar - (SU + i * RU)) * switch_on[gn, t-i] for
i in 0:min(UT - 2, TRU, t - 1) i = 0:min(UT - 2, TRU, t - 1)
) )
) )
@ -69,12 +64,10 @@ function _add_ramp_eqs!(
# Covers an additional time period of the ramp-up trajectory, compared to (38) # Covers an additional time period of the ramp-up trajectory, compared to (38)
eq_prod_limit_ramp_up_extra_period[gn, t] = @constraint( eq_prod_limit_ramp_up_extra_period[gn, t] = @constraint(
model, model,
prod_above[gn, t] + prod_above[gn, t] + g.min_power[t] * is_on[gn, t] + reserve[gn, t] <=
g.min_power[t] * is_on[gn, t] +
reserve[gn, t] <=
Pbar * is_on[gn, t] - sum( Pbar * is_on[gn, t] - sum(
(Pbar - (SU + i * RU)) * switch_on[gn, t-i] for (Pbar - (SU + i * RU)) * switch_on[gn, t-i] for
i in 0:min(UT - 1, TRU, t - 1) i = 0:min(UT - 1, TRU, t - 1)
) )
) )
end end
@ -89,13 +82,9 @@ function _add_ramp_eqs!(
prod_above[gn, t] + prod_above[gn, t] +
g.min_power[t] * is_on[gn, t] + g.min_power[t] * is_on[gn, t] +
(RESERVES_WHEN_SHUT_DOWN ? reserve[gn, t] : 0.0) <= (RESERVES_WHEN_SHUT_DOWN ? reserve[gn, t] : 0.0) <=
Pbar * is_on[gn, t] - sum( Pbar * is_on[gn, t] -
(Pbar - (SD + i * RD)) * switch_off[gn, t+1+i] for sum((Pbar - (SD + i * RD)) * switch_off[gn, t+1+i] for i = 0:KSD) -
i in 0:KSD sum((Pbar - (SU + i * RU)) * switch_on[gn, t-i] for i = 0:KSU) - (
) - sum(
(Pbar - (SU + i * RU)) * switch_on[gn, t-i] for
i in 0:KSU
) - (
(KSU >= TRU || KSU > t - 2) ? 0.0 : (KSU >= TRU || KSU > t - 2) ? 0.0 :
max(0, (SU + (KSU + 1) * RU) - (SD + TRD * RD)) * max(0, (SU + (KSU + 1) * RU) - (SD + TRD * RD)) *
switch_on[gn, t-(KSU+1)] switch_on[gn, t-(KSU+1)]

@ -8,7 +8,7 @@ function _add_flexiramp_vars!(model::JuMP.Model, g::Unit)::Nothing
mfg = _init(model, :mfg) mfg = _init(model, :mfg)
dwflexiramp = _init(model, :dwflexiramp) dwflexiramp = _init(model, :dwflexiramp)
dwflexiramp_shortfall = _init(model, :dwflexiramp_shortfall) dwflexiramp_shortfall = _init(model, :dwflexiramp_shortfall)
for t in 1:model[:instance].time for t = 1:model[:instance].time
# maximum feasible generation, \bar{g_{its}} in Wang & Hobbs (2016) # maximum feasible generation, \bar{g_{its}} in Wang & Hobbs (2016)
mfg[g.name, t] = @variable(model, lower_bound = 0) mfg[g.name, t] = @variable(model, lower_bound = 0)
if g.provides_flexiramp_reserves[t] if g.provides_flexiramp_reserves[t]
@ -51,37 +51,28 @@ function _add_ramp_eqs!(
dwflexiramp = model[:dwflexiramp] dwflexiramp = model[:dwflexiramp]
mfg = model[:mfg] mfg = model[:mfg]
for t in 1:model[:instance].time for t = 1:model[:instance].time
@constraint( @constraint(model, prod_above[gn, t] + (is_on[gn, t] * minp[t]) <= mfg[gn, t]) # Eq. (19) in Wang & Hobbs (2016)
model,
prod_above[gn, t] + (is_on[gn, t] * minp[t]) <= mfg[gn, t]
) # Eq. (19) in Wang & Hobbs (2016)
@constraint(model, mfg[gn, t] <= is_on[gn, t] * maxp[t]) # Eq. (22) in Wang & Hobbs (2016) @constraint(model, mfg[gn, t] <= is_on[gn, t] * maxp[t]) # Eq. (22) in Wang & Hobbs (2016)
if t != model[:instance].time if t != model[:instance].time
@constraint( @constraint(
model, model,
minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <= minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <=
prod_above[gn, t] - dwflexiramp[gn, t] + prod_above[gn, t] - dwflexiramp[gn, t] + (is_on[gn, t] * minp[t])
(is_on[gn, t] * minp[t])
) # first inequality of Eq. (20) in Wang & Hobbs (2016) ) # first inequality of Eq. (20) in Wang & Hobbs (2016)
@constraint( @constraint(
model, model,
prod_above[gn, t] - dwflexiramp[gn, t] + prod_above[gn, t] - dwflexiramp[gn, t] + (is_on[gn, t] * minp[t]) <=
(is_on[gn, t] * minp[t]) <=
mfg[gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1])) mfg[gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
) # second inequality of Eq. (20) in Wang & Hobbs (2016) ) # second inequality of Eq. (20) in Wang & Hobbs (2016)
@constraint( @constraint(
model, model,
minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <= minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <=
prod_above[gn, t] + prod_above[gn, t] + upflexiramp[gn, t] + (is_on[gn, t] * minp[t])
upflexiramp[gn, t] +
(is_on[gn, t] * minp[t])
) # first inequality of Eq. (21) in Wang & Hobbs (2016) ) # first inequality of Eq. (21) in Wang & Hobbs (2016)
@constraint( @constraint(
model, model,
prod_above[gn, t] + prod_above[gn, t] + upflexiramp[gn, t] + (is_on[gn, t] * minp[t]) <=
upflexiramp[gn, t] +
(is_on[gn, t] * minp[t]) <=
mfg[gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1])) mfg[gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
) # second inequality of Eq. (21) in Wang & Hobbs (2016) ) # second inequality of Eq. (21) in Wang & Hobbs (2016)
if t != 1 if t != 1
@ -113,8 +104,7 @@ function _add_ramp_eqs!(
) # Eq. (23) in Wang & Hobbs (2016) for the first time period ) # Eq. (23) in Wang & Hobbs (2016) for the first time period
@constraint( @constraint(
model, model,
initial_power - initial_power - (prod_above[gn, t] + (is_on[gn, t] * minp[t])) <=
(prod_above[gn, t] + (is_on[gn, t] * minp[t])) <=
RD * is_on[gn, t] + RD * is_on[gn, t] +
SD * (is_initially_on - is_on[gn, t]) + SD * (is_initially_on - is_on[gn, t]) +
maxp[t] * (1 - is_initially_on) maxp[t] * (1 - is_initially_on)
@ -123,8 +113,7 @@ function _add_ramp_eqs!(
@constraint( @constraint(
model, model,
mfg[gn, t] <= mfg[gn, t] <=
(SD * (is_on[gn, t] - is_on[gn, t+1])) + (SD * (is_on[gn, t] - is_on[gn, t+1])) + (maxp[t] * is_on[gn, t+1])
(maxp[t] * is_on[gn, t+1])
) # Eq. (24) in Wang & Hobbs (2016) ) # Eq. (24) in Wang & Hobbs (2016)
@constraint( @constraint(
model, model,
@ -152,15 +141,13 @@ function _add_ramp_eqs!(
) # second inequality of Eq. (27) in Wang & Hobbs (2016) ) # second inequality of Eq. (27) in Wang & Hobbs (2016)
@constraint( @constraint(
model, model,
-maxp[t] * is_on[gn, t] + minp[t] * is_on[gn, t+1] <= -maxp[t] * is_on[gn, t] + minp[t] * is_on[gn, t+1] <= upflexiramp[gn, t]
upflexiramp[gn, t]
) # first inequality of Eq. (28) in Wang & Hobbs (2016) ) # first inequality of Eq. (28) in Wang & Hobbs (2016)
@constraint(model, upflexiramp[gn, t] <= maxp[t] * is_on[gn, t+1]) # second inequality of Eq. (28) in Wang & Hobbs (2016) @constraint(model, upflexiramp[gn, t] <= maxp[t] * is_on[gn, t+1]) # second inequality of Eq. (28) in Wang & Hobbs (2016)
@constraint(model, -maxp[t] * is_on[gn, t+1] <= dwflexiramp[gn, t]) # first inequality of Eq. (29) in Wang & Hobbs (2016) @constraint(model, -maxp[t] * is_on[gn, t+1] <= dwflexiramp[gn, t]) # first inequality of Eq. (29) in Wang & Hobbs (2016)
@constraint( @constraint(
model, model,
dwflexiramp[gn, t] <= dwflexiramp[gn, t] <= (maxp[t] * is_on[gn, t]) - (minp[t] * is_on[gn, t+1])
(maxp[t] * is_on[gn, t]) - (minp[t] * is_on[gn, t+1])
) # second inequality of Eq. (29) in Wang & Hobbs (2016) ) # second inequality of Eq. (29) in Wang & Hobbs (2016)
else else
@constraint( @constraint(

@ -5,13 +5,12 @@
function _add_bus!(model::JuMP.Model, b::Bus)::Nothing function _add_bus!(model::JuMP.Model, b::Bus)::Nothing
net_injection = _init(model, :expr_net_injection) net_injection = _init(model, :expr_net_injection)
curtail = _init(model, :curtail) curtail = _init(model, :curtail)
for t in 1:model[:instance].time for t = 1:model[:instance].time
# Fixed load # Fixed load
net_injection[b.name, t] = AffExpr(-b.load[t]) net_injection[b.name, t] = AffExpr(-b.load[t])
# Load curtailment # Load curtailment
curtail[b.name, t] = curtail[b.name, t] = @variable(model, lower_bound = 0, upper_bound = b.load[t])
@variable(model, 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!(

@ -8,13 +8,9 @@ function _add_transmission_line!(
f::ShiftFactorsFormulation, f::ShiftFactorsFormulation,
)::Nothing )::Nothing
overflow = _init(model, :overflow) overflow = _init(model, :overflow)
for t in 1:model[:instance].time for t = 1:model[:instance].time
overflow[lm.name, t] = @variable(model, lower_bound = 0) overflow[lm.name, t] = @variable(model, lower_bound = 0)
add_to_expression!( add_to_expression!(model[:obj], overflow[lm.name, t], lm.flow_limit_penalty[t])
model[:obj],
overflow[lm.name, t],
lm.flow_limit_penalty[t],
)
end end
return return
end end

@ -2,26 +2,18 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
function _add_price_sensitive_load!( function _add_price_sensitive_load!(model::JuMP.Model, ps::PriceSensitiveLoad)::Nothing
model::JuMP.Model,
ps::PriceSensitiveLoad,
)::Nothing
loads = _init(model, :loads) loads = _init(model, :loads)
net_injection = _init(model, :expr_net_injection) net_injection = _init(model, :expr_net_injection)
for t in 1:model[:instance].time for t = 1:model[:instance].time
# Decision variable # Decision variable
loads[ps.name, t] = loads[ps.name, t] = @variable(model, lower_bound = 0, upper_bound = ps.demand[t])
@variable(model, 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!( add_to_expression!(net_injection[ps.bus.name, t], loads[ps.name, t], -1.0)
net_injection[ps.bus.name, t],
loads[ps.name, t],
-1.0,
)
end end
return return
end end

@ -13,10 +13,7 @@ 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(; function _injection_shift_factors(; buses::Array{Bus}, lines::Array{TransmissionLine})
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
@ -33,10 +30,7 @@ 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(; function _reduced_incidence_matrix(; buses::Array{Bus}, lines::Array{TransmissionLine})
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
@ -75,7 +69,7 @@ function _line_outage_factors(;
incidence = Array(_reduced_incidence_matrix(lines = lines, buses = buses)) incidence = Array(_reduced_incidence_matrix(lines = lines, buses = buses))
lodf::Array{Float64,2} = isf * transpose(incidence) lodf::Array{Float64,2} = isf * transpose(incidence)
_, n = size(lodf) _, n = size(lodf)
for i in 1:n for i = 1:n
lodf[:, i] *= 1.0 / (1.0 - lodf[i, i]) lodf[:, i] *= 1.0 / (1.0 - lodf[i, i])
lodf[i, i] = -1 lodf[i, i] = -1
end end

@ -25,14 +25,7 @@ struct Formulation
status_vars::StatusVarsFormulation = Gar1962.StatusVars(), status_vars::StatusVarsFormulation = Gar1962.StatusVars(),
transmission::TransmissionFormulation = ShiftFactorsFormulation(), transmission::TransmissionFormulation = ShiftFactorsFormulation(),
) )
return new( return new(prod_vars, pwl_costs, ramping, startup_costs, status_vars, transmission)
prod_vars,
pwl_costs,
ramping,
startup_costs,
status_vars,
transmission,
)
end end
end end

@ -14,12 +14,12 @@ function _add_net_injection_eqs!(model::JuMP.Model)::Nothing
net_injection = _init(model, :net_injection) net_injection = _init(model, :net_injection)
eq_net_injection = _init(model, :eq_net_injection) eq_net_injection = _init(model, :eq_net_injection)
eq_power_balance = _init(model, :eq_power_balance) eq_power_balance = _init(model, :eq_power_balance)
for t in 1:T, b in model[:instance].buses for t = 1:T, b in model[:instance].buses
n = net_injection[b.name, t] = @variable(model) n = net_injection[b.name, t] = @variable(model)
eq_net_injection[b.name, t] = eq_net_injection[b.name, t] =
@constraint(model, -n + model[:expr_net_injection][b.name, t] == 0) @constraint(model, -n + model[:expr_net_injection][b.name, t] == 0)
end end
for t in 1:T for t = 1:T
eq_power_balance[t] = @constraint( eq_power_balance[t] = @constraint(
model, model,
sum(net_injection[b.name, t] for b in model[:instance].buses) == 0 sum(net_injection[b.name, t] for b in model[:instance].buses) == 0
@ -31,7 +31,7 @@ end
function _add_reserve_eqs!(model::JuMP.Model)::Nothing function _add_reserve_eqs!(model::JuMP.Model)::Nothing
eq_min_reserve = _init(model, :eq_min_reserve) eq_min_reserve = _init(model, :eq_min_reserve)
instance = model[:instance] instance = model[:instance]
for t in 1:instance.time for t = 1:instance.time
# Equation (68) in Kneuven et al. (2020) # Equation (68) in Kneuven et al. (2020)
# As in Morales-España et al. (2013a) # As in Morales-España et al. (2013a)
# Akin to the alternative formulation with max_power_avail # Akin to the alternative formulation with max_power_avail
@ -46,11 +46,7 @@ function _add_reserve_eqs!(model::JuMP.Model)::Nothing
# Account for shortfall contribution to objective # Account for shortfall contribution to objective
if shortfall_penalty >= 0 if shortfall_penalty >= 0
add_to_expression!( add_to_expression!(model[:obj], shortfall_penalty, model[:reserve_shortfall][t])
model[:obj],
shortfall_penalty,
model[:reserve_shortfall][t],
)
end end
end end
return return
@ -65,24 +61,20 @@ function _add_flexiramp_eqs!(model::JuMP.Model)::Nothing
eq_min_upflexiramp = _init(model, :eq_min_upflexiramp) eq_min_upflexiramp = _init(model, :eq_min_upflexiramp)
eq_min_dwflexiramp = _init(model, :eq_min_dwflexiramp) eq_min_dwflexiramp = _init(model, :eq_min_dwflexiramp)
instance = model[:instance] instance = model[:instance]
for t in 1:instance.time for t = 1:instance.time
flexiramp_shortfall_penalty = instance.flexiramp_shortfall_penalty[t] flexiramp_shortfall_penalty = instance.flexiramp_shortfall_penalty[t]
# Eq. (17) in Wang & Hobbs (2016) # Eq. (17) in Wang & Hobbs (2016)
eq_min_upflexiramp[t] = @constraint( eq_min_upflexiramp[t] = @constraint(
model, model,
sum(model[:upflexiramp][g.name, t] for g in instance.units) + sum(model[:upflexiramp][g.name, t] for g in instance.units) + (
( flexiramp_shortfall_penalty >= 0 ? model[:upflexiramp_shortfall][t] : 0.0
flexiramp_shortfall_penalty >= 0 ?
model[:upflexiramp_shortfall][t] : 0.0
) >= instance.reserves.upflexiramp[t] ) >= instance.reserves.upflexiramp[t]
) )
# Eq. (18) in Wang & Hobbs (2016) # Eq. (18) in Wang & Hobbs (2016)
eq_min_dwflexiramp[t] = @constraint( eq_min_dwflexiramp[t] = @constraint(
model, model,
sum(model[:dwflexiramp][g.name, t] for g in instance.units) + sum(model[:dwflexiramp][g.name, t] for g in instance.units) + (
( flexiramp_shortfall_penalty >= 0 ? model[:dwflexiramp_shortfall][t] : 0.0
flexiramp_shortfall_penalty >= 0 ?
model[:dwflexiramp_shortfall][t] : 0.0
) >= instance.reserves.dwflexiramp[t] ) >= instance.reserves.dwflexiramp[t]
) )
@ -91,10 +83,7 @@ function _add_flexiramp_eqs!(model::JuMP.Model)::Nothing
add_to_expression!( add_to_expression!(
model[:obj], model[:obj],
flexiramp_shortfall_penalty, flexiramp_shortfall_penalty,
( (model[:upflexiramp_shortfall][t] + model[:dwflexiramp_shortfall][t]),
model[:upflexiramp_shortfall][t] +
model[:dwflexiramp_shortfall][t]
),
) )
end end
end end

@ -46,7 +46,7 @@ _is_initially_on(g::Unit)::Float64 = (g.initial_status > 0 ? 1.0 : 0.0)
function _add_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing function _add_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing
reserve = _init(model, :reserve) reserve = _init(model, :reserve)
reserve_shortfall = _init(model, :reserve_shortfall) reserve_shortfall = _init(model, :reserve_shortfall)
for t in 1:model[:instance].time for t = 1:model[:instance].time
if g.provides_spinning_reserves[t] if g.provides_spinning_reserves[t]
reserve[g.name, t] = @variable(model, lower_bound = 0) reserve[g.name, t] = @variable(model, lower_bound = 0)
else else
@ -61,7 +61,7 @@ end
function _add_reserve_eqs!(model::JuMP.Model, g::Unit)::Nothing function _add_reserve_eqs!(model::JuMP.Model, g::Unit)::Nothing
reserve = model[:reserve] reserve = model[:reserve]
for t in 1:model[:instance].time for t = 1:model[:instance].time
add_to_expression!(expr_reserve[g.bus.name, t], reserve[g.name, t], 1.0) add_to_expression!(expr_reserve[g.bus.name, t], reserve[g.name, t], 1.0)
end end
return return
@ -69,8 +69,8 @@ end
function _add_startup_shutdown_vars!(model::JuMP.Model, g::Unit)::Nothing function _add_startup_shutdown_vars!(model::JuMP.Model, g::Unit)::Nothing
startup = _init(model, :startup) startup = _init(model, :startup)
for t in 1:model[:instance].time for t = 1:model[:instance].time
for s in 1:length(g.startup_categories) for s = 1:length(g.startup_categories)
startup[g.name, t, s] = @variable(model, binary = true) startup[g.name, t, s] = @variable(model, binary = true)
end end
end end
@ -86,7 +86,7 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
switch_off = model[:switch_off] switch_off = model[:switch_off]
switch_on = model[:switch_on] switch_on = model[:switch_on]
T = model[:instance].time T = model[:instance].time
for t in 1:T for t = 1:T
# Startup limit # Startup limit
eq_startup_limit[g.name, t] = @constraint( eq_startup_limit[g.name, t] = @constraint(
model, model,
@ -96,16 +96,14 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
) )
# Shutdown limit # Shutdown limit
if g.initial_power > g.shutdown_limit if g.initial_power > g.shutdown_limit
eq_shutdown_limit[g.name, 0] = eq_shutdown_limit[g.name, 0] = @constraint(model, switch_off[g.name, 1] <= 0)
@constraint(model, switch_off[g.name, 1] <= 0)
end end
if t < T if t < T
eq_shutdown_limit[g.name, t] = @constraint( eq_shutdown_limit[g.name, t] = @constraint(
model, model,
prod_above[g.name, t] <= prod_above[g.name, t] <=
(g.max_power[t] - g.min_power[t]) * is_on[g.name, t] - (g.max_power[t] - g.min_power[t]) * is_on[g.name, t] -
max(0, g.max_power[t] - g.shutdown_limit) * max(0, g.max_power[t] - g.shutdown_limit) * switch_off[g.name, t+1]
switch_off[g.name, t+1]
) )
end end
end end
@ -121,7 +119,7 @@ function _add_ramp_eqs!(
reserve = model[:reserve] reserve = model[:reserve]
eq_ramp_up = _init(model, :eq_ramp_up) eq_ramp_up = _init(model, :eq_ramp_up)
eq_ramp_down = _init(model, :eq_ramp_down) eq_ramp_down = _init(model, :eq_ramp_down)
for t in 1:model[:instance].time for t = 1:model[:instance].time
# Ramp up limit # Ramp up limit
if t == 1 if t == 1
if _is_initially_on(g) == 1 if _is_initially_on(g) == 1
@ -151,8 +149,7 @@ function _add_ramp_eqs!(
else else
eq_ramp_down[g.name, t] = @constraint( eq_ramp_down[g.name, t] = @constraint(
model, model,
prod_above[g.name, t] >= prod_above[g.name, t] >= prod_above[g.name, t-1] - g.ramp_down_limit
prod_above[g.name, t-1] - g.ramp_down_limit
) )
end end
end end
@ -165,18 +162,18 @@ function _add_min_uptime_downtime_eqs!(model::JuMP.Model, g::Unit)::Nothing
eq_min_uptime = _init(model, :eq_min_uptime) eq_min_uptime = _init(model, :eq_min_uptime)
eq_min_downtime = _init(model, :eq_min_downtime) eq_min_downtime = _init(model, :eq_min_downtime)
T = model[:instance].time T = model[:instance].time
for t in 1:T for t = 1:T
# Minimum up-time # Minimum up-time
eq_min_uptime[g.name, t] = @constraint( eq_min_uptime[g.name, t] = @constraint(
model, model,
sum(switch_on[g.name, i] for i in (t-g.min_uptime+1):t if i >= 1) <= is_on[g.name, t] sum(switch_on[g.name, i] for i in (t-g.min_uptime+1):t if i >= 1) <=
is_on[g.name, t]
) )
# Minimum down-time # Minimum down-time
eq_min_downtime[g.name, t] = @constraint( eq_min_downtime[g.name, t] = @constraint(
model, model,
sum( sum(switch_off[g.name, i] for i in (t-g.min_downtime+1):t if i >= 1) <=
switch_off[g.name, i] for i in (t-g.min_downtime+1):t if i >= 1 1 - is_on[g.name, t]
) <= 1 - is_on[g.name, t]
) )
# Minimum up/down-time for initial periods # Minimum up/down-time for initial periods
if t == 1 if t == 1
@ -203,7 +200,7 @@ end
function _add_net_injection_eqs!(model::JuMP.Model, g::Unit)::Nothing function _add_net_injection_eqs!(model::JuMP.Model, g::Unit)::Nothing
expr_net_injection = model[:expr_net_injection] expr_net_injection = model[:expr_net_injection]
for t in 1:model[:instance].time for t = 1:model[:instance].time
# Add to net injection expression # Add to net injection expression
add_to_expression!( add_to_expression!(
expr_net_injection[g.bus.name, t], expr_net_injection[g.bus.name, t],

@ -14,12 +14,10 @@ function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing
prod_above = model[:prod_above] prod_above = model[:prod_above]
reserve = model[:reserve] reserve = model[:reserve]
for g in instance.units for g in instance.units
for t in 1:T for t = 1:T
is_on_value = round(solution["Is on"][g.name][t]) is_on_value = round(solution["Is on"][g.name][t])
prod_value = prod_value = round(solution["Production (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)
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],

@ -2,10 +2,7 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
function _enforce_transmission( function _enforce_transmission(model::JuMP.Model, violations::Vector{_Violation})::Nothing
model::JuMP.Model,
violations::Vector{_Violation},
)::Nothing
for v in violations for v in violations
_enforce_transmission( _enforce_transmission(
model = model, model = model,

@ -4,11 +4,9 @@
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] = filter.queues[v.monitored_line.offset] = PriorityQueue{_Violation,Float64}()
PriorityQueue{_Violation,Float64}()
end end
q::PriorityQueue{_Violation,Float64} = q::PriorityQueue{_Violation,Float64} = filter.queues[v.monitored_line.offset]
filter.queues[v.monitored_line.offset]
if length(q) < filter.max_per_line if length(q) < filter.max_per_line
enqueue!(q, v => v.amount) enqueue!(q, v => v.amount)
else else

@ -4,11 +4,7 @@
import Base.Threads: @threads import Base.Threads: @threads
function _find_violations( function _find_violations(model::JuMP.Model; max_per_line::Int, max_per_period::Int)
model::JuMP.Model;
max_per_line::Int,
max_per_period::Int,
)
instance = model[:instance] instance = model[:instance]
net_injection = model[:net_injection] net_injection = model[:net_injection]
overflow = model[:overflow] overflow = model[:overflow]
@ -18,13 +14,10 @@ function _find_violations(
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]) for b in non_slack_buses, value(net_injection[b.name, t]) for b in non_slack_buses, t = 1:instance.time
t in 1:instance.time
]
overflow_values = [
value(overflow[lm.name, t]) for lm in instance.lines,
t in 1:instance.time
] ]
overflow_values =
[value(overflow[lm.name, t]) for lm in instance.lines, t = 1:instance.time]
violations = UnitCommitment._find_violations( violations = UnitCommitment._find_violations(
instance = instance, instance = instance,
net_injections = net_injection_values, net_injections = net_injection_values,
@ -35,10 +28,7 @@ function _find_violations(
max_per_period = max_per_period, max_per_period = max_per_period,
) )
end end
@info @sprintf( @info @sprintf("Verified transmission limits in %.2f seconds", time_screening)
"Verified transmission limits in %.2f seconds",
time_screening
)
return violations return violations
end end
@ -81,10 +71,8 @@ function _find_violations(;
size(lodf) == (L, L) || error("lodf has incorrect size") size(lodf) == (L, L) || error("lodf has incorrect size")
filters = Dict( filters = Dict(
t => _ViolationFilter( t => _ViolationFilter(max_total = max_per_period, max_per_line = max_per_line)
max_total = max_per_period, for t = 1:T
max_per_line = max_per_line,
) 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]
@ -92,35 +80,30 @@ function _find_violations(;
pre_v::Array{Float64} = zeros(L, K) # pre_v[lm, thread] pre_v::Array{Float64} = zeros(L, K) # pre_v[lm, thread]
post_v::Array{Float64} = zeros(L, L, K) # post_v[lm, lc, thread] post_v::Array{Float64} = zeros(L, L, K) # post_v[lm, lc, thread]
normal_limits::Array{Float64,2} = [ normal_limits::Array{Float64,2} =
l.normal_flow_limit[t] + overflow[l.offset, t] for [l.normal_flow_limit[t] + overflow[l.offset, t] for l in instance.lines, t = 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] for [l.emergency_flow_limit[t] + overflow[l.offset, t] for l in instance.lines, t = 1:T]
l in instance.lines, t in 1:T
]
is_vulnerable::Array{Bool} = zeros(Bool, L) is_vulnerable::Array{Bool} = zeros(Bool, L)
for c in instance.contingencies for c in instance.contingencies
is_vulnerable[c.lines[1].offset] = true is_vulnerable[c.lines[1].offset] = true
end end
@threads for t in 1:T @threads for t = 1:T
k = threadid() k = threadid()
# Pre-contingency flows # Pre-contingency flows
pre_flow[:, k] = isf * net_injections[:, t] pre_flow[:, k] = isf * net_injections[:, t]
# Post-contingency flows # Post-contingency flows
for lc in 1:L, lm in 1:L for lc = 1:L, lm = 1:L
post_flow[lm, lc, k] = post_flow[lm, lc, k] = pre_flow[lm, k] + pre_flow[lc, k] * lodf[lm, lc]
pre_flow[lm, k] + pre_flow[lc, k] * lodf[lm, lc]
end end
# Pre-contingency violations # Pre-contingency violations
for lm in 1:L for lm = 1:L
pre_v[lm, k] = max( pre_v[lm, k] = max(
0.0, 0.0,
pre_flow[lm, k] - normal_limits[lm, t], pre_flow[lm, k] - normal_limits[lm, t],
@ -129,7 +112,7 @@ function _find_violations(;
end end
# Post-contingency violations # Post-contingency violations
for lc in 1:L, lm in 1:L for lc = 1:L, lm = 1:L
post_v[lm, lc, k] = max( post_v[lm, lc, k] = max(
0.0, 0.0,
post_flow[lm, lc, k] - emergency_limits[lm, t], post_flow[lm, lc, k] - emergency_limits[lm, t],
@ -138,7 +121,7 @@ function _find_violations(;
end end
# Offer pre-contingency violations # Offer pre-contingency violations
for lm in 1:L for lm = 1:L
if pre_v[lm, k] > 1e-5 if pre_v[lm, k] > 1e-5
_offer( _offer(
filters[t], filters[t],
@ -153,7 +136,7 @@ function _find_violations(;
end end
# Offer post-contingency violations # Offer post-contingency violations
for lm in 1:L, lc in 1:L for lm = 1:L, lc = 1:L
if post_v[lm, lc, k] > 1e-5 && is_vulnerable[lc] if post_v[lm, lc, k] > 1e-5 && is_vulnerable[lc]
_offer( _offer(
filters[t], filters[t],
@ -169,7 +152,7 @@ function _find_violations(;
end end
violations = _Violation[] violations = _Violation[]
for t in 1:instance.time for t = 1:instance.time
append!(violations, _query(filters[t])) append!(violations, _query(filters[t]))
end end

@ -27,10 +27,7 @@ function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Nothing
@info "Time limit exceeded" @info "Time limit exceeded"
break break
end end
@info @sprintf( @info @sprintf("Setting MILP time limit to %.2f seconds", time_remaining)
"Setting MILP time limit to %.2f seconds",
time_remaining
)
JuMP.set_time_limit_sec(model, time_remaining) JuMP.set_time_limit_sec(model, time_remaining)
@info "Solving MILP..." @info "Solving MILP..."
JuMP.optimize!(model) JuMP.optimize!(model)

@ -6,43 +6,40 @@ function solution(model::JuMP.Model)::OrderedDict
instance, T = model[:instance], model[:instance].time instance, T = model[:instance], model[:instance].time
function timeseries(vars, collection) function timeseries(vars, collection)
return OrderedDict( return OrderedDict(
b.name => [round(value(vars[b.name, t]), digits = 5) for t in 1:T] b.name => [round(value(vars[b.name, t]), digits = 5) for t = 1:T] for
for b in collection b in collection
) )
end end
function production_cost(g) function production_cost(g)
return [ return [
value(model[:is_on][g.name, t]) * g.min_power_cost[t] + sum( value(model[:is_on][g.name, t]) * g.min_power_cost[t] + sum(
Float64[ Float64[
value(model[:segprod][g.name, t, k]) * value(model[:segprod][g.name, t, k]) * g.cost_segments[k].cost[t] for
g.cost_segments[k].cost[t] for k = 1:length(g.cost_segments)
k in 1:length(g.cost_segments)
], ],
) for t in 1:T ) for t = 1:T
] ]
end end
function production(g) function production(g)
return [ return [
value(model[:is_on][g.name, t]) * g.min_power[t] + sum( value(model[:is_on][g.name, t]) * g.min_power[t] + sum(
Float64[ Float64[
value(model[:segprod][g.name, t, k]) for value(model[:segprod][g.name, t, k]) for k = 1:length(g.cost_segments)
k in 1:length(g.cost_segments)
], ],
) for t in 1:T ) for t = 1:T
] ]
end end
function startup_cost(g) function startup_cost(g)
S = length(g.startup_categories) S = length(g.startup_categories)
return [ return [
sum( sum(
g.startup_categories[s].cost * g.startup_categories[s].cost * value(model[:startup][g.name, t, s]) for
value(model[:startup][g.name, t, s]) for s in 1:S s = 1:S
) for t in 1:T ) for t = 1:T
] ]
end end
sol = OrderedDict() sol = OrderedDict()
sol["Production (MW)"] = sol["Production (MW)"] = OrderedDict(g.name => production(g) for g in instance.units)
OrderedDict(g.name => production(g) for g in instance.units)
sol["Production cost (\$)"] = sol["Production cost (\$)"] =
OrderedDict(g.name => production_cost(g) for g in instance.units) OrderedDict(g.name => production_cost(g) for g in instance.units)
sol["Startup cost (\$)"] = sol["Startup cost (\$)"] =
@ -54,21 +51,19 @@ function solution(model::JuMP.Model)::OrderedDict
instance.reserves.dwflexiramp != zeros(T) instance.reserves.dwflexiramp != zeros(T)
# Report flexiramp solutions only if either of the up-flexiramp and # Report flexiramp solutions only if either of the up-flexiramp and
# down-flexiramp requirements is not a default array of zeros # down-flexiramp requirements is not a default array of zeros
sol["Up-flexiramp (MW)"] = sol["Up-flexiramp (MW)"] = timeseries(model[:upflexiramp], instance.units)
timeseries(model[:upflexiramp], instance.units)
sol["Up-flexiramp shortfall (MW)"] = OrderedDict( sol["Up-flexiramp shortfall (MW)"] = OrderedDict(
t => t =>
(instance.flexiramp_shortfall_penalty[t] >= 0) ? (instance.flexiramp_shortfall_penalty[t] >= 0) ?
round(value(model[:upflexiramp_shortfall][t]), digits = 5) : round(value(model[:upflexiramp_shortfall][t]), digits = 5) : 0.0 for
0.0 for t in 1:instance.time t = 1:instance.time
) )
sol["Down-flexiramp (MW)"] = sol["Down-flexiramp (MW)"] = timeseries(model[:dwflexiramp], instance.units)
timeseries(model[:dwflexiramp], instance.units)
sol["Down-flexiramp shortfall (MW)"] = OrderedDict( sol["Down-flexiramp shortfall (MW)"] = OrderedDict(
t => t =>
(instance.flexiramp_shortfall_penalty[t] >= 0) ? (instance.flexiramp_shortfall_penalty[t] >= 0) ?
round(value(model[:dwflexiramp_shortfall][t]), digits = 5) : round(value(model[:dwflexiramp_shortfall][t]), digits = 5) : 0.0 for
0.0 for t in 1:instance.time t = 1:instance.time
) )
else else
# Report spinning reserve solutions only if both up-flexiramp and # Report spinning reserve solutions only if both up-flexiramp and
@ -77,12 +72,11 @@ function solution(model::JuMP.Model)::OrderedDict
sol["Reserve shortfall (MW)"] = OrderedDict( sol["Reserve shortfall (MW)"] = OrderedDict(
t => t =>
(instance.shortfall_penalty[t] >= 0) ? (instance.shortfall_penalty[t] >= 0) ?
round(value(model[:reserve_shortfall][t]), digits = 5) : round(value(model[:reserve_shortfall][t]), digits = 5) : 0.0 for
0.0 for t in 1:instance.time t = 1:instance.time
) )
end end
sol["Net injection (MW)"] = sol["Net injection (MW)"] = timeseries(model[:net_injection], instance.buses)
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)

@ -6,16 +6,10 @@ 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]
for g in instance.units for g in instance.units
for t in 1:T for t = 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( JuMP.set_start_value(switch_on[g.name, t], solution["Switch on"][g.name][t])
switch_on[g.name, t], JuMP.set_start_value(switch_off[g.name, t], solution["Switch off"][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 return

@ -11,10 +11,7 @@ Generates feasible initial conditions for the given instance, by constructing
and solving a single-period mixed-integer optimization problem, using the given and solving a single-period mixed-integer optimization problem, using the given
optimizer. The instance is modified in-place. optimizer. The instance is modified in-place.
""" """
function generate_initial_conditions!( function generate_initial_conditions!(instance::UnitCommitmentInstance, optimizer)::Nothing
instance::UnitCommitmentInstance,
optimizer,
)::Nothing
G = instance.units G = instance.units
B = instance.buses B = instance.buses
t = 1 t = 1
@ -31,11 +28,7 @@ function generate_initial_conditions!(
@constraint(mip, max_power[g in G], p[g] <= g.max_power[t] * x[g]) @constraint(mip, max_power[g in G], p[g] <= g.max_power[t] * x[g])
# Constraint: Production equals demand # Constraint: Production equals demand
@constraint( @constraint(mip, power_balance, sum(b.load[t] for b in B) == sum(p[g] for g in G))
mip,
power_balance,
sum(b.load[t] for b in B) == sum(p[g] for g in G)
)
# Constraint: Must run # Constraint: Must run
for g in G for g in G

@ -117,10 +117,7 @@ Base.@kwdef struct Randomization
randomize_load_share::Bool = true randomize_load_share::Bool = true
end end
function _randomize_costs( function _randomize_costs(instance::UnitCommitmentInstance, distribution)::Nothing
instance::UnitCommitmentInstance,
distribution,
)::Nothing
for unit in instance.units for unit in instance.units
α = rand(distribution) α = rand(distribution)
unit.min_power_cost *= α unit.min_power_cost *= α
@ -134,17 +131,11 @@ function _randomize_costs(
return return
end end
function _randomize_load_share( function _randomize_load_share(instance::UnitCommitmentInstance, distribution)::Nothing
instance::UnitCommitmentInstance,
distribution,
)::Nothing
α = rand(distribution, length(instance.buses)) α = rand(distribution, length(instance.buses))
for t in 1:instance.time for t = 1:instance.time
total = sum(bus.load[t] for bus in instance.buses) total = sum(bus.load[t] for bus in instance.buses)
den = sum( den = sum(bus.load[t] / total * α[i] for (i, bus) in enumerate(instance.buses))
bus.load[t] / total * α[i] for
(i, bus) in enumerate(instance.buses)
)
for (i, bus) in enumerate(instance.buses) for (i, bus) in enumerate(instance.buses)
bus.load[t] *= α[i] / den bus.load[t] *= α[i] / den
end end
@ -158,11 +149,9 @@ function _randomize_load_profile(
)::Nothing )::Nothing
# Generate new system load # Generate new system load
system_load = [1.0] system_load = [1.0]
for t in 2:instance.time for t = 2:instance.time
idx = (t - 1) % length(params.load_profile_mu) + 1 idx = (t - 1) % length(params.load_profile_mu) + 1
gamma = rand( gamma = rand(Normal(params.load_profile_mu[idx], params.load_profile_sigma[idx]))
Normal(params.load_profile_mu[idx], params.load_profile_sigma[idx]),
)
push!(system_load, system_load[t-1] * gamma) push!(system_load, system_load[t-1] * gamma)
end end
capacity = sum(maximum(u.max_power) for u in instance.units) capacity = sum(maximum(u.max_power) for u in instance.units)
@ -172,7 +161,7 @@ function _randomize_load_profile(
# Scale bus loads to match the new system load # Scale bus loads to match the new system load
prev_system_load = sum(b.load for b in instance.buses) prev_system_load = sum(b.load for b in instance.buses)
for b in instance.buses for b in instance.buses
for t in 1:instance.time for t = 1:instance.time
b.load[t] *= system_load[t] / prev_system_load[t] b.load[t] *= system_load[t] / prev_system_load[t]
end end
end end

@ -93,9 +93,8 @@ function _run_benchmarks(;
trials, trials,
) )
combinations = [ combinations = [
(c, s.first, s.second, m.first, m.second, f.first, f.second, t) for (c, s.first, s.second, m.first, m.second, f.first, f.second, t) for c in cases
c in cases for s in optimizers for f in formulations for for s in optimizers for f in formulations for m in methods for t in trials
m in methods for t in trials
] ]
shuffle!(combinations) shuffle!(combinations)
if nworkers() > 1 if nworkers() > 1

@ -18,7 +18,7 @@ function repair!(instance::UnitCommitmentInstance)::Int
for g in instance.units for g in instance.units
# Startup costs and delays must be increasing # Startup costs and delays must be increasing
for s in 2:length(g.startup_categories) for s = 2:length(g.startup_categories)
if g.startup_categories[s].delay <= g.startup_categories[s-1].delay if g.startup_categories[s].delay <= g.startup_categories[s-1].delay
prev_value = g.startup_categories[s].delay prev_value = g.startup_categories[s].delay
new_value = g.startup_categories[s-1].delay + 1 new_value = g.startup_categories[s-1].delay + 1
@ -38,9 +38,9 @@ function repair!(instance::UnitCommitmentInstance)::Int
end end
end end
for t in 1:instance.time for t = 1:instance.time
# Production cost curve should be convex # Production cost curve should be convex
for k in 2:length(g.cost_segments) for k = 2:length(g.cost_segments)
cost = g.cost_segments[k].cost[t] cost = g.cost_segments[k].cost[t]
min_cost = g.cost_segments[k-1].cost[t] min_cost = g.cost_segments[k-1].cost[t]
if cost < min_cost - 1e-5 if cost < min_cost - 1e-5

@ -24,10 +24,7 @@ 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( function validate(instance::UnitCommitmentInstance, solution::Union{Dict,OrderedDict})::Bool
instance::UnitCommitmentInstance,
solution::Union{Dict,OrderedDict},
)::Bool
err_count = 0 err_count = 0
err_count += _validate_units(instance, solution) err_count += _validate_units(instance, solution)
err_count += _validate_reserve_and_demand(instance, solution) err_count += _validate_reserve_and_demand(instance, solution)
@ -50,13 +47,12 @@ function _validate_units(instance, solution; tol = 0.01)
actual_startup_cost = solution["Startup cost (\$)"][unit.name] actual_startup_cost = solution["Startup cost (\$)"][unit.name]
is_on = bin(solution["Is on"][unit.name]) is_on = bin(solution["Is on"][unit.name])
for t in 1:instance.time for t = 1:instance.time
# Auxiliary variables # Auxiliary variables
if t == 1 if t == 1
is_starting_up = (unit.initial_status < 0) && is_on[t] is_starting_up = (unit.initial_status < 0) && is_on[t]
is_shutting_down = (unit.initial_status > 0) && !is_on[t] is_shutting_down = (unit.initial_status > 0) && !is_on[t]
ramp_up = ramp_up = max(0, production[t] + reserve[t] - unit.initial_power)
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]
@ -90,11 +86,7 @@ function _validate_units(instance, solution; tol = 0.01)
# Verify must-run # Verify must-run
if !is_on[t] && unit.must_run[t] if !is_on[t] && unit.must_run[t]
@error @sprintf( @error @sprintf("Must-run unit %s is offline at time %d", unit.name, t)
"Must-run unit %s is offline at time %d",
unit.name,
t
)
err_count += 1 err_count += 1
end end
@ -121,8 +113,7 @@ function _validate_units(instance, solution; tol = 0.01)
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] && if is_on[t] && (production[t] + reserve[t] > unit.max_power[t] + tol)
(production[t] + reserve[t] > unit.max_power[t] + tol)
@error @sprintf( @error @sprintf(
"Unit %s produces above its maximum limit at time %d (%.2f + %.2f> %.2f)", "Unit %s produces above its maximum limit at time %d (%.2f + %.2f> %.2f)",
unit.name, unit.name,
@ -136,11 +127,7 @@ function _validate_units(instance, solution; tol = 0.01)
# 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( @error @sprintf("Unit %s produces power at time %d while off", unit.name, t)
"Unit %s produces power at time %d while off",
unit.name,
t
)
err_count += 1 err_count += 1
end end
@ -169,9 +156,7 @@ function _validate_units(instance, solution; tol = 0.01)
end end
# Ramp-up limit # Ramp-up limit
if !is_starting_up && if !is_starting_up && !is_shutting_down && (ramp_up > unit.ramp_up_limit + tol)
!is_shutting_down &&
(ramp_up > unit.ramp_up_limit + tol)
@error @sprintf( @error @sprintf(
"Unit %s exceeds ramp up limit at time %d (%.2f > %.2f)", "Unit %s exceeds ramp up limit at time %d (%.2f > %.2f)",
unit.name, unit.name,
@ -201,7 +186,7 @@ function _validate_units(instance, solution; tol = 0.01)
# Calculate how much time the unit has been offline # Calculate how much time the unit has been offline
time_down = 0 time_down = 0
for k in 1:(t-1) for k = 1:(t-1)
if !is_on[t-k] if !is_on[t-k]
time_down += 1 time_down += 1
else else
@ -235,7 +220,7 @@ function _validate_units(instance, solution; tol = 0.01)
# Calculate how much time the unit has been online # Calculate how much time the unit has been online
time_up = 0 time_up = 0
for k in 1:(t-1) for k = 1:(t-1)
if is_on[t-k] if is_on[t-k]
time_up += 1 time_up += 1
else else
@ -288,7 +273,7 @@ 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 = 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 = 0 ps_load = 0
@ -298,13 +283,10 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
ps in instance.price_sensitive_loads ps in instance.price_sensitive_loads
) )
end end
production = production = sum(solution["Production (MW)"][g.name][t] for g in instance.units)
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 =
solution["Load curtail (MW)"][b.name][t] for sum(solution["Load curtail (MW)"][b.name][t] 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
@ -321,12 +303,13 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
err_count += 1 err_count += 1
end end
# Verify flexiramp solutions only if either of the up-flexiramp and # Verify flexiramp solutions only if either of the up-flexiramp and
# down-flexiramp requirements is not a default array of zeros # down-flexiramp requirements is not a default array of zeros
if instance.reserves.upflexiramp != zeros(instance.time) || instance.reserves.dwflexiramp != zeros(instance.time) if instance.reserves.upflexiramp != zeros(instance.time) ||
instance.reserves.dwflexiramp != zeros(instance.time)
upflexiramp = upflexiramp =
sum(solution["Up-flexiramp (MW)"][g.name][t] for g in instance.units) sum(solution["Up-flexiramp (MW)"][g.name][t] for g in instance.units)
upflexiramp_shortfall = upflexiramp_shortfall =
(instance.flexiramp_shortfall_penalty[t] >= 0) ? (instance.flexiramp_shortfall_penalty[t] >= 0) ?
solution["Up-flexiramp shortfall (MW)"][t] : 0 solution["Up-flexiramp shortfall (MW)"][t] : 0
@ -341,7 +324,7 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
) )
err_count += 1 err_count += 1
end end
dwflexiramp = dwflexiramp =
sum(solution["Down-flexiramp (MW)"][g.name][t] for g in instance.units) sum(solution["Down-flexiramp (MW)"][g.name][t] for g in instance.units)
@ -359,11 +342,10 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
) )
err_count += 1 err_count += 1
end end
# Verify spinning reserve solutions only if both up-flexiramp and # Verify spinning reserve solutions only if both up-flexiramp and
# down-flexiramp requirements are arrays of zeros. # down-flexiramp requirements are arrays of zeros.
else else
reserve = reserve = sum(solution["Reserve (MW)"][g.name][t] for g in instance.units)
sum(solution["Reserve (MW)"][g.name][t] for g in instance.units)
reserve_shortfall = reserve_shortfall =
(instance.shortfall_penalty[t] >= 0) ? (instance.shortfall_penalty[t] >= 0) ?
solution["Reserve shortfall (MW)"][t] : 0 solution["Reserve shortfall (MW)"][t] : 0
@ -379,7 +361,7 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
err_count += 1 err_count += 1
end end
end end
end end
return err_count return err_count

@ -7,9 +7,8 @@ using UnitCommitment
basedir = @__DIR__ basedir = @__DIR__
@testset "read_egret_solution" begin @testset "read_egret_solution" begin
solution = UnitCommitment.read_egret_solution( solution =
"$basedir/../fixtures/egret_output.json.gz", UnitCommitment.read_egret_solution("$basedir/../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])

@ -19,9 +19,9 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test instance.lines[5].target.name == "b5" @test instance.lines[5].target.name == "b5"
@test instance.lines[5].reactance 0.17388 @test instance.lines[5].reactance 0.17388
@test instance.lines[5].susceptance 10.037550333 @test instance.lines[5].susceptance 10.037550333
@test instance.lines[5].normal_flow_limit == [1e8 for t in 1:4] @test instance.lines[5].normal_flow_limit == [1e8 for t = 1:4]
@test instance.lines[5].emergency_flow_limit == [1e8 for t in 1:4] @test instance.lines[5].emergency_flow_limit == [1e8 for t = 1:4]
@test instance.lines[5].flow_limit_penalty == [5e3 for t in 1:4] @test instance.lines[5].flow_limit_penalty == [5e3 for t = 1:4]
@test instance.lines_by_name["l5"].name == "l5" @test instance.lines_by_name["l5"].name == "l5"
@test instance.lines[1].name == "l1" @test instance.lines[1].name == "l1"
@ -29,9 +29,9 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test instance.lines[1].target.name == "b2" @test instance.lines[1].target.name == "b2"
@test instance.lines[1].reactance 0.059170 @test instance.lines[1].reactance 0.059170
@test instance.lines[1].susceptance 29.496860773945 @test instance.lines[1].susceptance 29.496860773945
@test instance.lines[1].normal_flow_limit == [300.0 for t in 1:4] @test instance.lines[1].normal_flow_limit == [300.0 for t = 1:4]
@test instance.lines[1].emergency_flow_limit == [400.0 for t in 1:4] @test instance.lines[1].emergency_flow_limit == [400.0 for t = 1:4]
@test instance.lines[1].flow_limit_penalty == [1e3 for t in 1:4] @test instance.lines[1].flow_limit_penalty == [1e3 for t = 1:4]
@test instance.buses[9].name == "b9" @test instance.buses[9].name == "b9"
@test instance.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353] @test instance.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353]
@ -44,12 +44,12 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test unit.ramp_down_limit == 1e6 @test unit.ramp_down_limit == 1e6
@test unit.startup_limit == 1e6 @test unit.startup_limit == 1e6
@test unit.shutdown_limit == 1e6 @test unit.shutdown_limit == 1e6
@test unit.must_run == [false for t in 1:4] @test unit.must_run == [false for t = 1:4]
@test unit.min_power_cost == [1400.0 for t in 1:4] @test unit.min_power_cost == [1400.0 for t = 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 = 1:4]
for t in 1:1 for t = 1:1
@test unit.cost_segments[1].mw[t] == 10.0 @test unit.cost_segments[1].mw[t] == 10.0
@test unit.cost_segments[2].mw[t] == 20.0 @test unit.cost_segments[2].mw[t] == 20.0
@test unit.cost_segments[3].mw[t] == 5.0 @test unit.cost_segments[3].mw[t] == 5.0
@ -68,7 +68,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
unit = instance.units[2] unit = instance.units[2]
@test unit.name == "g2" @test unit.name == "g2"
@test unit.must_run == [false for t in 1:4] @test unit.must_run == [false for t = 1:4]
unit = instance.units[3] unit = instance.units[3]
@test unit.name == "g3" @test unit.name == "g3"
@ -77,12 +77,12 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test unit.ramp_down_limit == 70.0 @test unit.ramp_down_limit == 70.0
@test unit.startup_limit == 70.0 @test unit.startup_limit == 70.0
@test unit.shutdown_limit == 70.0 @test unit.shutdown_limit == 70.0
@test unit.must_run == [true for t in 1:4] @test unit.must_run == [true for t = 1:4]
@test unit.min_power_cost == [0.0 for t in 1:4] @test unit.min_power_cost == [0.0 for t = 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 = 1:4]
for t in 1:4 for t = 1:4
@test unit.cost_segments[1].mw[t] 33 @test unit.cost_segments[1].mw[t] 33
@test unit.cost_segments[2].mw[t] 33 @test unit.cost_segments[2].mw[t] 33
@test unit.cost_segments[3].mw[t] 34 @test unit.cost_segments[3].mw[t] 34
@ -101,8 +101,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.0 for t in 1:4] @test load.revenue == [100.0 for t = 1:4]
@test load.demand == [50.0 for t in 1:4] @test load.demand == [50.0 for t = 1:4]
@test instance.price_sensitive_loads_by_name["ps1"].name == "ps1" @test instance.price_sensitive_loads_by_name["ps1"].name == "ps1"
end end

@ -23,6 +23,7 @@ function _small_test(formulation::Formulation)::Nothing
instances = ["matpower/case118/2017-02-01", "test/case14"] instances = ["matpower/case118/2017-02-01", "test/case14"]
for instance in instances for instance in instances
# Should not crash # Should not crash
@show "$(instance)"
UnitCommitment.build_model( UnitCommitment.build_model(
instance = UnitCommitment.read_benchmark(instance), instance = UnitCommitment.read_benchmark(instance),
formulation = formulation, formulation = formulation,
@ -58,17 +59,26 @@ function _test(formulation::Formulation)::Nothing
end end
@testset "formulations" begin @testset "formulations" begin
@show "testset formulations"
_test(Formulation()) _test(Formulation())
@show "ArrCon2000 ramping"
_test(Formulation(ramping = ArrCon2000.Ramping())) _test(Formulation(ramping = ArrCon2000.Ramping()))
# _test(Formulation(ramping = DamKucRajAta2016.Ramping())) # _test(Formulation(ramping = DamKucRajAta2016.Ramping()))
@show "MorLatRam2013 ramping"
_test( _test(
Formulation( Formulation(
ramping = MorLatRam2013.Ramping(), ramping = MorLatRam2013.Ramping(),
startup_costs = MorLatRam2013.StartupCosts(), startup_costs = MorLatRam2013.StartupCosts(),
), ),
) )
@show "PanGua2016 ramping"
_test(Formulation(ramping = PanGua2016.Ramping())) _test(Formulation(ramping = PanGua2016.Ramping()))
@show "Gar1962 PwlCosts"
_test(Formulation(pwl_costs = Gar1962.PwlCosts())) _test(Formulation(pwl_costs = Gar1962.PwlCosts()))
@show "CarArr2006 PwlCosts"
_test(Formulation(pwl_costs = CarArr2006.PwlCosts())) _test(Formulation(pwl_costs = CarArr2006.PwlCosts()))
@show "KnuOstWat2018 PwlCosts"
_test(Formulation(pwl_costs = KnuOstWat2018.PwlCosts())) _test(Formulation(pwl_costs = KnuOstWat2018.PwlCosts()))
@show "formulations completed"
end end

@ -11,6 +11,7 @@ UnitCommitment._setup_logger()
const ENABLE_LARGE_TESTS = ("UCJL_LARGE_TESTS" in keys(ENV)) const ENABLE_LARGE_TESTS = ("UCJL_LARGE_TESTS" in keys(ENV))
@testset "UnitCommitment" begin @testset "UnitCommitment" begin
@show "running runtests.jl"
include("usage.jl") include("usage.jl")
@testset "import" begin @testset "import" begin
include("import/egret_test.jl") include("import/egret_test.jl")
@ -27,6 +28,7 @@ const ENABLE_LARGE_TESTS = ("UCJL_LARGE_TESTS" in keys(ENV))
include("solution/methods/XavQiuWanThi19/sensitivity_test.jl") include("solution/methods/XavQiuWanThi19/sensitivity_test.jl")
end end
@testset "transform" begin @testset "transform" begin
@show "beginning transform"
include("transform/initcond_test.jl") include("transform/initcond_test.jl")
include("transform/slice_test.jl") include("transform/slice_test.jl")
@testset "randomize" begin @testset "randomize" begin

@ -7,7 +7,7 @@ import UnitCommitment: _Violation, _offer, _query
@testset "find_violations" begin @testset "find_violations" begin
instance = UnitCommitment.read_benchmark("test/case14") instance = UnitCommitment.read_benchmark("test/case14")
for line in instance.lines, t in 1:instance.time for line in instance.lines, t = 1:instance.time
line.normal_flow_limit[t] = 1.0 line.normal_flow_limit[t] = 1.0
line.emergency_flow_limit[t] = 1.0 line.emergency_flow_limit[t] = 1.0
end end
@ -20,8 +20,8 @@ import UnitCommitment: _Violation, _offer, _query
buses = instance.buses, buses = instance.buses,
isf = isf, isf = isf,
) )
inj = [1000.0 for b in 1:13, t in 1:instance.time] inj = [1000.0 for b = 1:13, t = 1:instance.time]
overflow = [0.0 for l in instance.lines, t in 1:instance.time] overflow = [0.0 for l in instance.lines, t = 1:instance.time]
violations = UnitCommitment._find_violations( violations = UnitCommitment._find_violations(
instance = instance, instance = instance,
net_injections = inj, net_injections = inj,

@ -8,8 +8,7 @@ basedir = @__DIR__
@testset "generate_initial_conditions!" begin @testset "generate_initial_conditions!" begin
# Load instance # Load instance
instance = instance = UnitCommitment.read("$basedir/../fixtures/case118-initcond.json.gz")
UnitCommitment.read("$basedir/../fixtures/case118-initcond.json.gz")
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
# All units should have unknown initial conditions # All units should have unknown initial conditions

@ -28,10 +28,7 @@ test_approx(x, y) = @test isapprox(x, y, atol = 1e-3)
test_approx(bus.load[1] / prev_system_load[1], 0.012) test_approx(bus.load[1] / prev_system_load[1], 0.012)
Random.seed!(42) Random.seed!(42)
randomize!( randomize!(instance, XavQiuAhm2021.Randomization(randomize_load_profile = false))
instance,
XavQiuAhm2021.Randomization(randomize_load_profile = false),
)
# Check randomized costs # Check randomized costs
test_approx(unit.min_power_cost[1], 831.977) test_approx(unit.min_power_cost[1], 831.977)

@ -5,6 +5,7 @@
using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@testset "slice" begin @testset "slice" begin
@show "beginning slice"
instance = UnitCommitment.read_benchmark("test/case14") instance = UnitCommitment.read_benchmark("test/case14")
modified = UnitCommitment.slice(instance, 1:2) modified = UnitCommitment.slice(instance, 1:2)
@ -35,7 +36,8 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test length(ps.demand) == 2 @test length(ps.demand) == 2
@test length(ps.revenue) == 2 @test length(ps.revenue) == 2
end end
@show "beginning building model under slice"
@show instance.reserves
# 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 = UnitCommitment.build_model( model = UnitCommitment.build_model(

@ -6,7 +6,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON
@testset "build_model" begin @testset "build_model" begin
instance = UnitCommitment.read_benchmark("test/case14") instance = UnitCommitment.read_benchmark("test/case14")
for line in instance.lines, t in 1:4 for line in instance.lines, t = 1:4
line.normal_flow_limit[t] = 10.0 line.normal_flow_limit[t] = 10.0
end end
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)

Loading…
Cancel
Save