mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-07 00:38:51 -06:00
stochastic extension w/ scenarios
This commit is contained in:
@@ -5,13 +5,15 @@
|
||||
function _enforce_transmission(
|
||||
model::JuMP.Model,
|
||||
violations::Vector{_Violation},
|
||||
sc::UnitCommitmentScenario
|
||||
)::Nothing
|
||||
for v in violations
|
||||
_enforce_transmission(
|
||||
model = model,
|
||||
sc = sc,
|
||||
violation = v,
|
||||
isf = model[:isf],
|
||||
lodf = model[:lodf],
|
||||
isf = sc.isf,
|
||||
lodf = sc.lodf,
|
||||
)
|
||||
end
|
||||
return
|
||||
@@ -19,6 +21,7 @@ end
|
||||
|
||||
function _enforce_transmission(;
|
||||
model::JuMP.Model,
|
||||
sc::UnitCommitmentScenario,
|
||||
violation::_Violation,
|
||||
isf::Matrix{Float64},
|
||||
lodf::Matrix{Float64},
|
||||
@@ -51,7 +54,7 @@ function _enforce_transmission(;
|
||||
t = violation.time
|
||||
flow = @variable(model, base_name = "flow[$fm,$t]")
|
||||
|
||||
v = overflow[violation.monitored_line.name, violation.time]
|
||||
v = overflow[sc.name, violation.monitored_line.name, violation.time]
|
||||
@constraint(model, flow <= limit + v)
|
||||
@constraint(model, -flow <= limit + v)
|
||||
|
||||
@@ -59,23 +62,23 @@ function _enforce_transmission(;
|
||||
@constraint(
|
||||
model,
|
||||
flow == sum(
|
||||
net_injection[b.name, violation.time] *
|
||||
net_injection[sc.name, b.name, violation.time] *
|
||||
isf[violation.monitored_line.offset, b.offset] for
|
||||
b in instance.buses if b.offset > 0
|
||||
b in sc.buses if b.offset > 0
|
||||
)
|
||||
)
|
||||
else
|
||||
@constraint(
|
||||
model,
|
||||
flow == sum(
|
||||
net_injection[b.name, violation.time] * (
|
||||
net_injection[sc.name, b.name, violation.time] * (
|
||||
isf[violation.monitored_line.offset, b.offset] + (
|
||||
lodf[
|
||||
violation.monitored_line.offset,
|
||||
violation.outage_line.offset,
|
||||
] * isf[violation.outage_line.offset, b.offset]
|
||||
)
|
||||
) for b in instance.buses if b.offset > 0
|
||||
) for b in sc.buses if b.offset > 0
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@@ -5,32 +5,34 @@
|
||||
import Base.Threads: @threads
|
||||
|
||||
function _find_violations(
|
||||
model::JuMP.Model;
|
||||
model::JuMP.Model,
|
||||
sc::UnitCommitmentScenario;
|
||||
max_per_line::Int,
|
||||
max_per_period::Int,
|
||||
)
|
||||
instance = model[:instance]
|
||||
net_injection = model[:net_injection]
|
||||
overflow = model[:overflow]
|
||||
length(instance.buses) > 1 || return []
|
||||
length(sc.buses) > 1 || return []
|
||||
violations = []
|
||||
@info "Verifying transmission limits..."
|
||||
time_screening = @elapsed begin
|
||||
non_slack_buses = [b for b in instance.buses if b.offset > 0]
|
||||
non_slack_buses = [b for b in sc.buses if b.offset > 0]
|
||||
net_injection_values = [
|
||||
value(net_injection[b.name, t]) for b in non_slack_buses,
|
||||
value(net_injection[sc.name, b.name, t]) for b in non_slack_buses,
|
||||
t in 1:instance.time
|
||||
]
|
||||
overflow_values = [
|
||||
value(overflow[lm.name, t]) for lm in instance.lines,
|
||||
value(overflow[sc.name, lm.name, t]) for lm in sc.lines,
|
||||
t in 1:instance.time
|
||||
]
|
||||
violations = UnitCommitment._find_violations(
|
||||
instance = instance,
|
||||
sc = sc,
|
||||
net_injections = net_injection_values,
|
||||
overflow = overflow_values,
|
||||
isf = model[:isf],
|
||||
lodf = model[:lodf],
|
||||
isf = sc.isf,
|
||||
lodf = sc.lodf,
|
||||
max_per_line = max_per_line,
|
||||
max_per_period = max_per_period,
|
||||
)
|
||||
@@ -64,15 +66,16 @@ matrix, where L is the number of transmission lines.
|
||||
"""
|
||||
function _find_violations(;
|
||||
instance::UnitCommitmentInstance,
|
||||
sc::UnitCommitmentScenario,
|
||||
net_injections::Array{Float64,2},
|
||||
overflow::Array{Float64,2},
|
||||
isf::Array{Float64,2},
|
||||
lodf::Array{Float64,2},
|
||||
max_per_line::Int,
|
||||
max_per_period::Int,
|
||||
max_per_period::Int
|
||||
)::Array{_Violation,1}
|
||||
B = length(instance.buses) - 1
|
||||
L = length(instance.lines)
|
||||
B = length(sc.buses) - 1
|
||||
L = length(sc.lines)
|
||||
T = instance.time
|
||||
K = nthreads()
|
||||
|
||||
@@ -94,16 +97,16 @@ function _find_violations(;
|
||||
|
||||
normal_limits::Array{Float64,2} = [
|
||||
l.normal_flow_limit[t] + overflow[l.offset, t] for
|
||||
l in instance.lines, t in 1:T
|
||||
l in sc.lines, t in 1:T
|
||||
]
|
||||
|
||||
emergency_limits::Array{Float64,2} = [
|
||||
l.emergency_flow_limit[t] + overflow[l.offset, t] for
|
||||
l in instance.lines, t in 1:T
|
||||
l in sc.lines, t in 1:T
|
||||
]
|
||||
|
||||
is_vulnerable::Array{Bool} = zeros(Bool, L)
|
||||
for c in instance.contingencies
|
||||
for c in sc.contingencies
|
||||
is_vulnerable[c.lines[1].offset] = true
|
||||
end
|
||||
|
||||
@@ -111,7 +114,7 @@ function _find_violations(;
|
||||
k = threadid()
|
||||
|
||||
# Pre-contingency flows
|
||||
pre_flow[:, k] = isf * net_injections[:, t]
|
||||
pre_flow[:, k] = isf * net_injections[ :, t]
|
||||
|
||||
# Post-contingency flows
|
||||
for lc in 1:L, lm in 1:L
|
||||
@@ -144,7 +147,7 @@ function _find_violations(;
|
||||
filters[t],
|
||||
_Violation(
|
||||
time = t,
|
||||
monitored_line = instance.lines[lm],
|
||||
monitored_line = sc.lines[lm],
|
||||
outage_line = nothing,
|
||||
amount = pre_v[lm, k],
|
||||
),
|
||||
@@ -159,8 +162,8 @@ function _find_violations(;
|
||||
filters[t],
|
||||
_Violation(
|
||||
time = t,
|
||||
monitored_line = instance.lines[lm],
|
||||
outage_line = instance.lines[lc],
|
||||
monitored_line = sc.lines[lm],
|
||||
outage_line = sc.lines[lc],
|
||||
amount = post_v[lm, lc, k],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -10,43 +10,47 @@ function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Nothing
|
||||
JuMP.set_optimizer_attribute(model, "MIPGap", gap)
|
||||
@info @sprintf("MIP gap tolerance set to %f", gap)
|
||||
end
|
||||
initial_time = time()
|
||||
large_gap = false
|
||||
has_transmission = (length(model[:isf]) > 0)
|
||||
if has_transmission && method.two_phase_gap
|
||||
set_gap(1e-2)
|
||||
large_gap = true
|
||||
end
|
||||
while true
|
||||
time_elapsed = time() - initial_time
|
||||
time_remaining = method.time_limit - time_elapsed
|
||||
if time_remaining < 0
|
||||
@info "Time limit exceeded"
|
||||
break
|
||||
for scenario in model[:instance].scenarios
|
||||
large_gap = false
|
||||
has_transmission = (length(scenario.isf) > 0)
|
||||
if has_transmission && method.two_phase_gap
|
||||
set_gap(1e-2)
|
||||
large_gap = true
|
||||
end
|
||||
@info @sprintf(
|
||||
"Setting MILP time limit to %.2f seconds",
|
||||
time_remaining
|
||||
)
|
||||
JuMP.set_time_limit_sec(model, time_remaining)
|
||||
@info "Solving MILP..."
|
||||
JuMP.optimize!(model)
|
||||
has_transmission || break
|
||||
violations = _find_violations(
|
||||
model,
|
||||
max_per_line = method.max_violations_per_line,
|
||||
max_per_period = method.max_violations_per_period,
|
||||
)
|
||||
if isempty(violations)
|
||||
@info "No violations found"
|
||||
if large_gap
|
||||
large_gap = false
|
||||
set_gap(method.gap_limit)
|
||||
else
|
||||
while true
|
||||
initial_time = time()
|
||||
time_elapsed = time() - initial_time
|
||||
time_remaining = method.time_limit - time_elapsed
|
||||
if time_remaining < 0
|
||||
@info "Time limit exceeded"
|
||||
break
|
||||
end
|
||||
else
|
||||
_enforce_transmission(model, violations)
|
||||
@info @sprintf(
|
||||
"Setting MILP time limit to %.2f seconds",
|
||||
time_remaining
|
||||
)
|
||||
JuMP.set_time_limit_sec(model, time_remaining)
|
||||
@info "Solving MILP..."
|
||||
JuMP.optimize!(model)
|
||||
has_transmission || break
|
||||
violations = _find_violations(
|
||||
model,
|
||||
scenario,
|
||||
max_per_line = method.max_violations_per_line,
|
||||
max_per_period = method.max_violations_per_period,
|
||||
)
|
||||
if isempty(violations)
|
||||
@info "No violations found"
|
||||
if large_gap
|
||||
large_gap = false
|
||||
set_gap(method.gap_limit)
|
||||
else
|
||||
break
|
||||
end
|
||||
else
|
||||
_enforce_transmission(model, violations, scenario)
|
||||
end
|
||||
end
|
||||
end
|
||||
return
|
||||
|
||||
@@ -16,34 +16,40 @@ solution = UnitCommitment.solution(model)
|
||||
"""
|
||||
function solution(model::JuMP.Model)::OrderedDict
|
||||
instance, T = model[:instance], model[:instance].time
|
||||
function timeseries(vars, collection)
|
||||
function timeseries_first_stage(vars, collection)
|
||||
return OrderedDict(
|
||||
b.name => [round(value(vars[b.name, t]), digits = 5) for t in 1:T]
|
||||
for b in collection
|
||||
)
|
||||
end
|
||||
function production_cost(g)
|
||||
function timeseries_second_stage(vars, collection, sc)
|
||||
return OrderedDict(
|
||||
b.name => [round(value(vars[sc.name, b.name, t]), digits = 5) for t in 1:T]
|
||||
for b in collection
|
||||
)
|
||||
end
|
||||
function production_cost(g, sc)
|
||||
return [
|
||||
value(model[:is_on][g.name, t]) * g.min_power_cost[t] + sum(
|
||||
Float64[
|
||||
value(model[:segprod][g.name, t, k]) *
|
||||
value(model[:segprod][sc.name, g.name, t, k]) *
|
||||
g.cost_segments[k].cost[t] for
|
||||
k in 1:length(g.cost_segments)
|
||||
],
|
||||
) for t in 1:T
|
||||
]
|
||||
end
|
||||
function production(g)
|
||||
function production(g, sc)
|
||||
return [
|
||||
value(model[:is_on][g.name, t]) * g.min_power[t] + sum(
|
||||
Float64[
|
||||
value(model[:segprod][g.name, t, k]) for
|
||||
value(model[:segprod][sc.name, g.name, t, k]) for
|
||||
k in 1:length(g.cost_segments)
|
||||
],
|
||||
) for t in 1:T
|
||||
]
|
||||
end
|
||||
function startup_cost(g)
|
||||
function startup_cost(g, sc)
|
||||
S = length(g.startup_categories)
|
||||
return [
|
||||
sum(
|
||||
@@ -53,66 +59,70 @@ function solution(model::JuMP.Model)::OrderedDict
|
||||
]
|
||||
end
|
||||
sol = OrderedDict()
|
||||
sol["Production (MW)"] =
|
||||
OrderedDict(g.name => production(g) for g in instance.units)
|
||||
sol["Production cost (\$)"] =
|
||||
OrderedDict(g.name => production_cost(g) for g in instance.units)
|
||||
sol["Startup cost (\$)"] =
|
||||
OrderedDict(g.name => startup_cost(g) for g in instance.units)
|
||||
sol["Is on"] = timeseries(model[:is_on], instance.units)
|
||||
sol["Switch on"] = timeseries(model[:switch_on], instance.units)
|
||||
sol["Switch off"] = timeseries(model[:switch_off], instance.units)
|
||||
sol["Net injection (MW)"] =
|
||||
timeseries(model[:net_injection], instance.buses)
|
||||
sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses)
|
||||
if !isempty(instance.lines)
|
||||
sol["Line overflow (MW)"] = timeseries(model[:overflow], instance.lines)
|
||||
for sc in instance.scenarios
|
||||
sol[sc.name] = OrderedDict()
|
||||
sol[sc.name]["Production (MW)"] =
|
||||
OrderedDict(g.name => production(g, sc) for g in sc.units)
|
||||
sol[sc.name]["Production cost (\$)"] =
|
||||
OrderedDict(g.name => production_cost(g, sc) for g in sc.units)
|
||||
sol[sc.name]["Startup cost (\$)"] =
|
||||
OrderedDict(g.name => startup_cost(g, sc) for g in sc.units)
|
||||
sol[sc.name]["Is on"] = timeseries_first_stage(model[:is_on], sc.units)
|
||||
sol[sc.name]["Switch on"] = timeseries_first_stage(model[:switch_on], sc.units)
|
||||
sol[sc.name]["Switch off"] = timeseries_first_stage(model[:switch_off], sc.units)
|
||||
sol[sc.name]["Net injection (MW)"] =
|
||||
timeseries_second_stage(model[:net_injection], sc.buses, sc)
|
||||
sol[sc.name]["Load curtail (MW)"] = timeseries_second_stage(model[:curtail], sc.buses, sc)
|
||||
if !isempty(sc.lines)
|
||||
sol[sc.name]["Line overflow (MW)"] = timeseries_second_stage(model[:overflow], sc.lines, sc)
|
||||
end
|
||||
if !isempty(sc.price_sensitive_loads)
|
||||
sol[sc.name]["Price-sensitive loads (MW)"] =
|
||||
timeseries_second_stage(model[:loads], sc.price_sensitive_loads, sc)
|
||||
end
|
||||
sol[sc.name]["Spinning reserve (MW)"] = OrderedDict(
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
value(model[:reserve][sc.name, r.name, g.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for g in r.units
|
||||
) for r in sc.reserves if r.type == "spinning"
|
||||
)
|
||||
sol[sc.name]["Spinning reserve shortfall (MW)"] = OrderedDict(
|
||||
r.name => [
|
||||
value(model[:reserve_shortfall][sc.name, r.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for r in sc.reserves if r.type == "spinning"
|
||||
)
|
||||
sol[sc.name]["Up-flexiramp (MW)"] = OrderedDict(
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
value(model[:upflexiramp][sc.name, r.name, g.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for g in r.units
|
||||
) for r in sc.reserves if r.type == "up-frp"
|
||||
)
|
||||
sol[sc.name]["Up-flexiramp shortfall (MW)"] = OrderedDict(
|
||||
r.name => [
|
||||
value(model[:upflexiramp_shortfall][sc.name, r.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for r in sc.reserves if r.type == "up-frp"
|
||||
)
|
||||
sol[sc.name]["Down-flexiramp (MW)"] = OrderedDict(
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
value(model[:dwflexiramp][sc.name, r.name, g.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for g in r.units
|
||||
) for r in sc.reserves if r.type == "down-frp"
|
||||
)
|
||||
sol[sc.name]["Down-flexiramp shortfall (MW)"] = OrderedDict(
|
||||
r.name => [
|
||||
value(model[:dwflexiramp_shortfall][sc.name, r.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for r in sc.reserves if r.type == "down-frp"
|
||||
)
|
||||
end
|
||||
if !isempty(instance.price_sensitive_loads)
|
||||
sol["Price-sensitive loads (MW)"] =
|
||||
timeseries(model[:loads], instance.price_sensitive_loads)
|
||||
end
|
||||
sol["Spinning reserve (MW)"] = OrderedDict(
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
value(model[:reserve][r.name, g.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for g in r.units
|
||||
) for r in instance.reserves if r.type == "spinning"
|
||||
)
|
||||
sol["Spinning reserve shortfall (MW)"] = OrderedDict(
|
||||
r.name => [
|
||||
value(model[:reserve_shortfall][r.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for r in instance.reserves if r.type == "spinning"
|
||||
)
|
||||
sol["Up-flexiramp (MW)"] = OrderedDict(
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
value(model[:upflexiramp][r.name, g.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for g in r.units
|
||||
) for r in instance.reserves if r.type == "flexiramp"
|
||||
)
|
||||
sol["Up-flexiramp shortfall (MW)"] = OrderedDict(
|
||||
r.name => [
|
||||
value(model[:upflexiramp_shortfall][r.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for r in instance.reserves if r.type == "flexiramp"
|
||||
)
|
||||
sol["Down-flexiramp (MW)"] = OrderedDict(
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
value(model[:dwflexiramp][r.name, g.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for g in r.units
|
||||
) for r in instance.reserves if r.type == "flexiramp"
|
||||
)
|
||||
sol["Down-flexiramp shortfall (MW)"] = OrderedDict(
|
||||
r.name => [
|
||||
value(model[:upflexiramp_shortfall][r.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for r in instance.reserves if r.type == "flexiramp"
|
||||
)
|
||||
length(keys(sol)) > 1 ? nothing : sol = Dict(sol_key => sol_val for scen_key in keys(sol) for (sol_key, sol_val) in sol[scen_key])
|
||||
return sol
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user