mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 08:18:51 -06:00
new formatting
This commit is contained in:
BIN
benchmark/.DS_Store
vendored
Normal file
BIN
benchmark/.DS_Store
vendored
Normal file
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(
|
|
||||||
"Flexiramp is supported only by the WanHob2016 ramping formulation",
|
|
||||||
)
|
)
|
||||||
|
error("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
|
||||||
|
|
||||||
@@ -324,7 +306,8 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
|
|||||||
|
|
||||||
# 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 =
|
||||||
@@ -362,8 +345,7 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
|
|||||||
# 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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user