mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 08:18:51 -06:00
Implement new reserves
This commit is contained in:
@@ -28,7 +28,7 @@ Each section is described in detail below. For a complete example, see [case14](
|
|||||||
|
|
||||||
### Parameters
|
### Parameters
|
||||||
|
|
||||||
This section describes system-wide parameters, such as power balance and reserve shortfall penalties, and optimization parameters, such as the length of the planning horizon and the time.
|
This section describes system-wide parameters, such as power balance penalty, and optimization parameters, such as the length of the planning horizon and the time.
|
||||||
|
|
||||||
| Key | Description | Default | Time series?
|
| Key | Description | Default | Time series?
|
||||||
| :----------------------------- | :------------------------------------------------ | :------: | :------------:
|
| :----------------------------- | :------------------------------------------------ | :------: | :------------:
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Name | Symbol | Description | Unit
|
|||||||
`switch_off[g,t]` | $w_{g}(t)$ | True if generator `g` switches off at time `t`. | Binary
|
`switch_off[g,t]` | $w_{g}(t)$ | True if generator `g` switches off at time `t`. | Binary
|
||||||
`prod_above[g,t]` |$p'_{g}(t)$ | Amount of power produced by generator `g` above its minimum power output at time `t`. For example, if the minimum power of generator `g` is 100 MW and `g` is producing 115 MW of power at time `t`, then `prod_above[g,t]` equals `15.0`. | MW
|
`prod_above[g,t]` |$p'_{g}(t)$ | Amount of power produced by generator `g` above its minimum power output at time `t`. For example, if the minimum power of generator `g` is 100 MW and `g` is producing 115 MW of power at time `t`, then `prod_above[g,t]` equals `15.0`. | MW
|
||||||
`segprod[g,t,k]` | $p^k_g(t)$ | Amount of power from piecewise linear segment `k` produced by generator `g` at time `t`. For example, if cost curve for generator `g` is defined by the points `(100, 1400)`, `(110, 1600)`, `(130, 2200)` and `(135, 2400)`, and if the generator is producing 115 MW of power at time `t`, then `segprod[g,t,:]` equals `[10.0, 5.0, 0.0]`.| MW
|
`segprod[g,t,k]` | $p^k_g(t)$ | Amount of power from piecewise linear segment `k` produced by generator `g` at time `t`. For example, if cost curve for generator `g` is defined by the points `(100, 1400)`, `(110, 1600)`, `(130, 2200)` and `(135, 2400)`, and if the generator is producing 115 MW of power at time `t`, then `segprod[g,t,:]` equals `[10.0, 5.0, 0.0]`.| MW
|
||||||
`reserve[g,t]` | $r_g(t)$ | Amount of reserves provided by generator `g` at time `t`. | MW
|
`reserve[r,g,t]` | $r_g(t)$ | Amount of reserve `r` provided by unit `g` at time `t`. | MW
|
||||||
`startup[g,t,s]` | $\delta^s_g(t)$ | True if generator `g` switches on at time `t` incurring start-up costs from start-up category `s`. | Binary
|
`startup[g,t,s]` | $\delta^s_g(t)$ | True if generator `g` switches on at time `t` incurring start-up costs from start-up category `s`. | Binary
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -125,6 +125,11 @@ function _from_json(json; repair = true)
|
|||||||
name = reserve_name,
|
name = reserve_name,
|
||||||
type = lowercase(dict["Type"]),
|
type = lowercase(dict["Type"]),
|
||||||
amount = timeseries(dict["Amount (MW)"]),
|
amount = timeseries(dict["Amount (MW)"]),
|
||||||
|
units = [],
|
||||||
|
shortfall_penalty = scalar(
|
||||||
|
dict["Shortfall penalty (\$/MW)"],
|
||||||
|
default = -1,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
name_to_reserve[reserve_name] = reserve
|
name_to_reserve[reserve_name] = reserve
|
||||||
push!(reserves2, reserve)
|
push!(reserves2, reserve)
|
||||||
@@ -171,7 +176,8 @@ function _from_json(json; repair = true)
|
|||||||
# Read reserves
|
# Read reserves
|
||||||
unit_reserves = Reserve[]
|
unit_reserves = Reserve[]
|
||||||
if "Reserve eligibility" in keys(dict)
|
if "Reserve eligibility" in keys(dict)
|
||||||
unit_reserves = [name_to_reserve[n] for n in dict["Reserve eligibility"]]
|
unit_reserves =
|
||||||
|
[name_to_reserve[n] for n in dict["Reserve eligibility"]]
|
||||||
end
|
end
|
||||||
|
|
||||||
# Read and validate initial conditions
|
# Read and validate initial conditions
|
||||||
@@ -215,6 +221,9 @@ function _from_json(json; repair = true)
|
|||||||
unit_reserves,
|
unit_reserves,
|
||||||
)
|
)
|
||||||
push!(bus.units, unit)
|
push!(bus.units, unit)
|
||||||
|
for r in unit_reserves
|
||||||
|
push!(r.units, unit)
|
||||||
|
end
|
||||||
name_to_unit[unit_name] = unit
|
name_to_unit[unit_name] = unit
|
||||||
push!(units, unit)
|
push!(units, unit)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ Base.@kwdef mutable struct Reserve
|
|||||||
name::String
|
name::String
|
||||||
type::String
|
type::String
|
||||||
amount::Vector{Float64}
|
amount::Vector{Float64}
|
||||||
|
units::Vector
|
||||||
|
shortfall_penalty::Float64
|
||||||
end
|
end
|
||||||
|
|
||||||
mutable struct Unit
|
mutable struct Unit
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ function _add_ramp_eqs!(
|
|||||||
RD = g.ramp_down_limit
|
RD = g.ramp_down_limit
|
||||||
SU = g.startup_limit
|
SU = g.startup_limit
|
||||||
SD = g.shutdown_limit
|
SD = g.shutdown_limit
|
||||||
reserve = model[:reserve]
|
|
||||||
eq_ramp_down = _init(model, :eq_ramp_down)
|
eq_ramp_down = _init(model, :eq_ramp_down)
|
||||||
eq_ramp_up = _init(model, :eq_ramp_up)
|
eq_ramp_up = _init(model, :eq_ramp_up)
|
||||||
is_initially_on = (g.initial_status > 0)
|
is_initially_on = (g.initial_status > 0)
|
||||||
|
reserve = _total_reserves(model, g)
|
||||||
|
|
||||||
# Gar1962.ProdVars
|
# Gar1962.ProdVars
|
||||||
prod_above = model[:prod_above]
|
prod_above = model[:prod_above]
|
||||||
@@ -41,7 +41,7 @@ function _add_ramp_eqs!(
|
|||||||
model,
|
model,
|
||||||
g.min_power[t] +
|
g.min_power[t] +
|
||||||
prod_above[gn, t] +
|
prod_above[gn, t] +
|
||||||
(RESERVES_WHEN_RAMP_UP ? reserve[gn, t] : 0.0) <=
|
(RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) <=
|
||||||
g.initial_power + RU
|
g.initial_power + RU
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -51,7 +51,7 @@ function _add_ramp_eqs!(
|
|||||||
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[t] : 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]
|
||||||
@@ -82,7 +82,7 @@ function _add_ramp_eqs!(
|
|||||||
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] : 0.0
|
reserve[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]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function _add_ramp_eqs!(
|
|||||||
gn = g.name
|
gn = g.name
|
||||||
eq_str_ramp_down = _init(model, :eq_str_ramp_down)
|
eq_str_ramp_down = _init(model, :eq_str_ramp_down)
|
||||||
eq_str_ramp_up = _init(model, :eq_str_ramp_up)
|
eq_str_ramp_up = _init(model, :eq_str_ramp_up)
|
||||||
reserve = model[:reserve]
|
reserve = _total_reserves(model, g)
|
||||||
|
|
||||||
# Gar1962.ProdVars
|
# Gar1962.ProdVars
|
||||||
prod_above = model[:prod_above]
|
prod_above = model[:prod_above]
|
||||||
@@ -48,10 +48,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[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]
|
||||||
@@ -88,7 +86,7 @@ function _add_ramp_eqs!(
|
|||||||
max_prod_last_period =
|
max_prod_last_period =
|
||||||
min_prod_last_period + (
|
min_prod_last_period + (
|
||||||
t > 1 && (RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN) ?
|
t > 1 && (RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN) ?
|
||||||
reserve[gn, t-1] : 0.0
|
reserve[t-1] : 0.0
|
||||||
)
|
)
|
||||||
min_prod_this_period = prod_above[gn, t]
|
min_prod_this_period = prod_above[gn, t]
|
||||||
on_last_period = 0.0
|
on_last_period = 0.0
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function _add_production_limit_eqs!(
|
|||||||
eq_prod_limit = _init(model, :eq_prod_limit)
|
eq_prod_limit = _init(model, :eq_prod_limit)
|
||||||
is_on = model[:is_on]
|
is_on = model[:is_on]
|
||||||
prod_above = model[:prod_above]
|
prod_above = model[:prod_above]
|
||||||
reserve = model[:reserve]
|
reserve = _total_reserves(model, g)
|
||||||
gn = g.name
|
gn = g.name
|
||||||
for t in 1:model[:instance].time
|
for t in 1:model[:instance].time
|
||||||
# Objective function terms for production costs
|
# Objective function terms for production costs
|
||||||
@@ -44,7 +44,7 @@ function _add_production_limit_eqs!(
|
|||||||
end
|
end
|
||||||
eq_prod_limit[gn, t] = @constraint(
|
eq_prod_limit[gn, t] = @constraint(
|
||||||
model,
|
model,
|
||||||
prod_above[gn, t] + reserve[gn, t] <= power_diff * is_on[gn, t]
|
prod_above[gn, t] + reserve[t] <= power_diff * is_on[gn, t]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function _add_ramp_eqs!(
|
|||||||
gn = g.name
|
gn = g.name
|
||||||
eq_ramp_down = _init(model, :eq_ramp_down)
|
eq_ramp_down = _init(model, :eq_ramp_down)
|
||||||
eq_ramp_up = _init(model, :eq_str_ramp_up)
|
eq_ramp_up = _init(model, :eq_str_ramp_up)
|
||||||
reserve = model[:reserve]
|
reserve = _total_reserves(model, g)
|
||||||
|
|
||||||
# Gar1962.ProdVars
|
# Gar1962.ProdVars
|
||||||
prod_above = model[:prod_above]
|
prod_above = model[:prod_above]
|
||||||
@@ -43,7 +43,7 @@ function _add_ramp_eqs!(
|
|||||||
model,
|
model,
|
||||||
g.min_power[t] +
|
g.min_power[t] +
|
||||||
prod_above[gn, t] +
|
prod_above[gn, t] +
|
||||||
(RESERVES_WHEN_RAMP_UP ? reserve[gn, t] : 0.0) <=
|
(RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) <=
|
||||||
g.initial_power + RU
|
g.initial_power + RU
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -61,7 +61,7 @@ function _add_ramp_eqs!(
|
|||||||
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[t] : 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]
|
||||||
@@ -77,7 +77,7 @@ function _add_ramp_eqs!(
|
|||||||
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[t] : 0.0) -
|
||||||
prod_above[gn, t-1] <= RU
|
prod_above[gn, t-1] <= RU
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -105,7 +105,7 @@ function _add_ramp_eqs!(
|
|||||||
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] : 0.0
|
reserve[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]
|
||||||
@@ -121,7 +121,7 @@ function _add_ramp_eqs!(
|
|||||||
eq_ramp_down[gn, t] = @constraint(
|
eq_ramp_down[gn, t] = @constraint(
|
||||||
model,
|
model,
|
||||||
prod_above[gn, t-1] +
|
prod_above[gn, t-1] +
|
||||||
(RESERVES_WHEN_RAMP_DOWN ? reserve[gn, t-1] : 0.0) -
|
(RESERVES_WHEN_RAMP_DOWN ? reserve[t-1] : 0.0) -
|
||||||
prod_above[gn, t] <= RD
|
prod_above[gn, t] <= RD
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function _add_ramp_eqs!(
|
|||||||
# TODO: Move upper case constants to model[:instance]
|
# TODO: Move upper case constants to model[:instance]
|
||||||
RESERVES_WHEN_SHUT_DOWN = true
|
RESERVES_WHEN_SHUT_DOWN = true
|
||||||
gn = g.name
|
gn = g.name
|
||||||
reserve = model[:reserve]
|
reserve = _total_reserves(model, g)
|
||||||
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)
|
||||||
@@ -56,7 +56,7 @@ function _add_ramp_eqs!(
|
|||||||
model,
|
model,
|
||||||
prod_above[gn, t] +
|
prod_above[gn, t] +
|
||||||
g.min_power[t] * is_on[gn, t] +
|
g.min_power[t] * is_on[gn, t] +
|
||||||
reserve[gn, t] <=
|
reserve[t] <=
|
||||||
Pbar * is_on[gn, t] -
|
Pbar * is_on[gn, t] -
|
||||||
(t < T ? (Pbar - SD) * switch_off[gn, t+1] : 0.0) - sum(
|
(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
|
||||||
@@ -71,7 +71,7 @@ function _add_ramp_eqs!(
|
|||||||
model,
|
model,
|
||||||
prod_above[gn, t] +
|
prod_above[gn, t] +
|
||||||
g.min_power[t] * is_on[gn, t] +
|
g.min_power[t] * is_on[gn, t] +
|
||||||
reserve[gn, t] <=
|
reserve[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 in 0:min(UT - 1, TRU, t - 1)
|
||||||
@@ -88,7 +88,7 @@ function _add_ramp_eqs!(
|
|||||||
model,
|
model,
|
||||||
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[t] : 0.0) <=
|
||||||
Pbar * is_on[gn, t] - sum(
|
Pbar * is_on[gn, t] - sum(
|
||||||
(Pbar - (SD + i * RD)) * switch_off[gn, t+1+i] for
|
(Pbar - (SD + i * RD)) * switch_off[gn, t+1+i] for
|
||||||
i in 0:KSD
|
i in 0:KSD
|
||||||
|
|||||||
@@ -52,5 +52,29 @@ function _add_reserve_eqs!(model::JuMP.Model)::Nothing
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
eq_min_reserve2 = _init(model, :eq_min_reserve2)
|
||||||
|
for r in instance.reserves2
|
||||||
|
for t in 1:instance.time
|
||||||
|
# Equation (68) in Kneuven et al. (2020)
|
||||||
|
# As in Morales-España et al. (2013a)
|
||||||
|
# Akin to the alternative formulation with max_power_avail
|
||||||
|
# from Carrión and Arroyo (2006) and Ostrowski et al. (2012)
|
||||||
|
eq_min_reserve2[r.name, t] = @constraint(
|
||||||
|
model,
|
||||||
|
sum(model[:reserve2][r.name, g.name, t] for g in r.units) +
|
||||||
|
model[:reserve_shortfall2][r.name, t] >= r.amount[t]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Account for shortfall contribution to objective
|
||||||
|
if r.shortfall_penalty >= 0
|
||||||
|
add_to_expression!(
|
||||||
|
model[:obj],
|
||||||
|
r.shortfall_penalty,
|
||||||
|
model[:reserve_shortfall2][r.name, t],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -55,13 +55,19 @@ function _add_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing
|
|||||||
(model[:instance].shortfall_penalty[t] >= 0) ?
|
(model[:instance].shortfall_penalty[t] >= 0) ?
|
||||||
@variable(model, lower_bound = 0) : 0.0
|
@variable(model, lower_bound = 0) : 0.0
|
||||||
end
|
end
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
function _add_reserve_eqs!(model::JuMP.Model, g::Unit)::Nothing
|
reserve2 = _init(model, :reserve2)
|
||||||
reserve = model[:reserve]
|
reserve_shortfall2 = _init(model, :reserve_shortfall2)
|
||||||
for t in 1:model[:instance].time
|
for r in g.reserves
|
||||||
add_to_expression!(expr_reserve[g.bus.name, t], reserve[g.name, t], 1.0)
|
for t in 1:model[:instance].time
|
||||||
|
reserve2[r.name, g.name, t] = @variable(model, lower_bound = 0)
|
||||||
|
if (r.name, t) ∉ keys(reserve_shortfall2)
|
||||||
|
reserve_shortfall2[r.name, t] = @variable(model, lower_bound = 0)
|
||||||
|
if r.shortfall_penalty < 0
|
||||||
|
set_upper_bound(reserve_shortfall2[r.name, t], 0.0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -81,7 +87,7 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
|
|||||||
eq_startup_limit = _init(model, :eq_startup_limit)
|
eq_startup_limit = _init(model, :eq_startup_limit)
|
||||||
is_on = model[:is_on]
|
is_on = model[:is_on]
|
||||||
prod_above = model[:prod_above]
|
prod_above = model[:prod_above]
|
||||||
reserve = model[:reserve]
|
reserve = _total_reserves(model, g)
|
||||||
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
|
||||||
@@ -89,7 +95,7 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
|
|||||||
# Startup limit
|
# Startup limit
|
||||||
eq_startup_limit[g.name, t] = @constraint(
|
eq_startup_limit[g.name, t] = @constraint(
|
||||||
model,
|
model,
|
||||||
prod_above[g.name, t] + reserve[g.name, t] <=
|
prod_above[g.name, t] + reserve[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.startup_limit) * switch_on[g.name, t]
|
max(0, g.max_power[t] - g.startup_limit) * switch_on[g.name, t]
|
||||||
)
|
)
|
||||||
@@ -117,7 +123,7 @@ function _add_ramp_eqs!(
|
|||||||
formulation::RampingFormulation,
|
formulation::RampingFormulation,
|
||||||
)::Nothing
|
)::Nothing
|
||||||
prod_above = model[:prod_above]
|
prod_above = model[:prod_above]
|
||||||
reserve = model[:reserve]
|
reserve = _total_reserves(model, g)
|
||||||
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 in 1:model[:instance].time
|
||||||
@@ -126,14 +132,14 @@ function _add_ramp_eqs!(
|
|||||||
if _is_initially_on(g) == 1
|
if _is_initially_on(g) == 1
|
||||||
eq_ramp_up[g.name, t] = @constraint(
|
eq_ramp_up[g.name, t] = @constraint(
|
||||||
model,
|
model,
|
||||||
prod_above[g.name, t] + reserve[g.name, t] <=
|
prod_above[g.name, t] + reserve[t] <=
|
||||||
(g.initial_power - g.min_power[t]) + g.ramp_up_limit
|
(g.initial_power - g.min_power[t]) + g.ramp_up_limit
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
eq_ramp_up[g.name, t] = @constraint(
|
eq_ramp_up[g.name, t] = @constraint(
|
||||||
model,
|
model,
|
||||||
prod_above[g.name, t] + reserve[g.name, t] <=
|
prod_above[g.name, t] + reserve[t] <=
|
||||||
prod_above[g.name, t-1] + g.ramp_up_limit
|
prod_above[g.name, t-1] + g.ramp_up_limit
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -216,3 +222,15 @@ function _add_net_injection_eqs!(model::JuMP.Model, g::Unit)::Nothing
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function _total_reserves(model, g)::Vector
|
||||||
|
T = model[:instance].time
|
||||||
|
reserve = [model[:reserve][g.name, t] for t in 1:T]
|
||||||
|
if !isempty(g.reserves)
|
||||||
|
reserve += [
|
||||||
|
sum(model[:reserve2][r.name, g.name, t] for r in g.reserves) for
|
||||||
|
t in 1:model[:instance].time
|
||||||
|
]
|
||||||
|
end
|
||||||
|
return reserve
|
||||||
|
end
|
||||||
|
|||||||
@@ -67,5 +67,19 @@ function solution(model::JuMP.Model)::OrderedDict
|
|||||||
sol["Price-sensitive loads (MW)"] =
|
sol["Price-sensitive loads (MW)"] =
|
||||||
timeseries(model[:loads], instance.price_sensitive_loads)
|
timeseries(model[:loads], instance.price_sensitive_loads)
|
||||||
end
|
end
|
||||||
|
sol["Reserve 2 (MW)"] = OrderedDict(
|
||||||
|
r.name => OrderedDict(
|
||||||
|
g.name => [
|
||||||
|
value(model[:reserve2][r.name, g.name, t]) for
|
||||||
|
t in 1:instance.time
|
||||||
|
] for g in r.units
|
||||||
|
) for r in instance.reserves2
|
||||||
|
)
|
||||||
|
sol["Reserve shortfall 2 (MW)"] = OrderedDict(
|
||||||
|
r.name => [
|
||||||
|
value(model[:reserve_shortfall2][r.name, t]) for
|
||||||
|
t in 1:instance.time
|
||||||
|
] for r in instance.reserves2
|
||||||
|
)
|
||||||
return sol
|
return sol
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ function _validate_units(instance, solution; tol = 0.01)
|
|||||||
for unit in instance.units
|
for unit in instance.units
|
||||||
production = solution["Production (MW)"][unit.name]
|
production = solution["Production (MW)"][unit.name]
|
||||||
reserve = solution["Reserve (MW)"][unit.name]
|
reserve = solution["Reserve (MW)"][unit.name]
|
||||||
|
if !isempty(unit.reserves)
|
||||||
|
reserve += sum(
|
||||||
|
solution["Reserve 2 (MW)"][r.name][unit.name] for
|
||||||
|
r in unit.reserves
|
||||||
|
)
|
||||||
|
end
|
||||||
actual_production_cost = solution["Production cost (\$)"][unit.name]
|
actual_production_cost = solution["Production cost (\$)"][unit.name]
|
||||||
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])
|
||||||
@@ -137,9 +143,11 @@ 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 %s produces power at time %d while off (%.2f + %.2f > 0)",
|
||||||
unit.name,
|
unit.name,
|
||||||
t
|
t,
|
||||||
|
production[t],
|
||||||
|
reserve[t],
|
||||||
)
|
)
|
||||||
err_count += 1
|
err_count += 1
|
||||||
end
|
end
|
||||||
@@ -338,6 +346,27 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
|
|||||||
)
|
)
|
||||||
err_count += 1
|
err_count += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Verify reserves
|
||||||
|
for r in instance.reserves2
|
||||||
|
provided = sum(
|
||||||
|
solution["Reserve 2 (MW)"][r.name][g.name][t] for g in r.units
|
||||||
|
)
|
||||||
|
shortfall = solution["Reserve shortfall 2 (MW)"][r.name][t]
|
||||||
|
required = r.amount[t]
|
||||||
|
|
||||||
|
if provided + shortfall < required - tol
|
||||||
|
@error @sprintf(
|
||||||
|
"Insufficient reserve %s at time %d (%.2f + %.2f < %.2f)",
|
||||||
|
r.name,
|
||||||
|
t,
|
||||||
|
provided,
|
||||||
|
shortfall,
|
||||||
|
required,
|
||||||
|
)
|
||||||
|
err_count += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return err_count
|
return err_count
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
using UnitCommitment
|
using UnitCommitment
|
||||||
using JuMP
|
using JuMP
|
||||||
|
using Cbc
|
||||||
|
using JSON
|
||||||
import UnitCommitment:
|
import UnitCommitment:
|
||||||
ArrCon2000,
|
ArrCon2000,
|
||||||
CarArr2006,
|
CarArr2006,
|
||||||
@@ -19,56 +21,78 @@ if ENABLE_LARGE_TESTS
|
|||||||
using Gurobi
|
using Gurobi
|
||||||
end
|
end
|
||||||
|
|
||||||
function _small_test(formulation::Formulation)::Nothing
|
function _small_test(formulation::Formulation; dump::Bool = false)::Nothing
|
||||||
instances = ["matpower/case118/2017-02-01", "test/case14"]
|
instance = UnitCommitment.read_benchmark("test/case14")
|
||||||
for instance in instances
|
model = UnitCommitment.build_model(
|
||||||
# Should not crash
|
instance = instance,
|
||||||
UnitCommitment.build_model(
|
formulation = formulation,
|
||||||
instance = UnitCommitment.read_benchmark(instance),
|
optimizer = Cbc.Optimizer,
|
||||||
formulation = formulation,
|
variable_names = true,
|
||||||
)
|
)
|
||||||
|
UnitCommitment.optimize!(model)
|
||||||
|
solution = UnitCommitment.solution(model)
|
||||||
|
if dump
|
||||||
|
open("/tmp/ucjl.json", "w") do f
|
||||||
|
return write(f, JSON.json(solution, 2))
|
||||||
|
end
|
||||||
|
write_to_file(model, "/tmp/ucjl.lp")
|
||||||
end
|
end
|
||||||
|
@test UnitCommitment.validate(instance, solution)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
function _large_test(formulation::Formulation)::Nothing
|
function _large_test(formulation::Formulation)::Nothing
|
||||||
instances = ["pglib-uc/ca/Scenario400_reserves_1"]
|
instance =
|
||||||
for instance in instances
|
UnitCommitment.read_benchmark("pglib-uc/ca/Scenario400_reserves_1")
|
||||||
instance = UnitCommitment.read_benchmark(instance)
|
model = UnitCommitment.build_model(
|
||||||
model = UnitCommitment.build_model(
|
instance = instance,
|
||||||
instance = instance,
|
formulation = formulation,
|
||||||
formulation = formulation,
|
optimizer = Gurobi.Optimizer,
|
||||||
optimizer = Gurobi.Optimizer,
|
)
|
||||||
)
|
UnitCommitment.optimize!(
|
||||||
UnitCommitment.optimize!(
|
model,
|
||||||
model,
|
XavQiuWanThi2019.Method(two_phase_gap = false, gap_limit = 0.1),
|
||||||
XavQiuWanThi2019.Method(two_phase_gap = false, gap_limit = 0.1),
|
)
|
||||||
)
|
solution = UnitCommitment.solution(model)
|
||||||
solution = UnitCommitment.solution(model)
|
@test UnitCommitment.validate(instance, solution)
|
||||||
@test UnitCommitment.validate(instance, solution)
|
|
||||||
end
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
function _test(formulation::Formulation)::Nothing
|
function _test(formulation::Formulation; dump::Bool = false)::Nothing
|
||||||
_small_test(formulation)
|
_small_test(formulation; dump)
|
||||||
if ENABLE_LARGE_TESTS
|
if ENABLE_LARGE_TESTS
|
||||||
_large_test(formulation)
|
_large_test(formulation)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@testset "formulations" begin
|
@testset "formulations" begin
|
||||||
_test(Formulation())
|
@testset "default" begin
|
||||||
_test(Formulation(ramping = ArrCon2000.Ramping()))
|
_test(Formulation())
|
||||||
# _test(Formulation(ramping = DamKucRajAta2016.Ramping()))
|
end
|
||||||
_test(
|
@testset "ArrCon2000" begin
|
||||||
Formulation(
|
_test(Formulation(ramping = ArrCon2000.Ramping()))
|
||||||
ramping = MorLatRam2013.Ramping(),
|
end
|
||||||
startup_costs = MorLatRam2013.StartupCosts(),
|
@testset "DamKucRajAta2016" begin
|
||||||
),
|
_test(Formulation(ramping = DamKucRajAta2016.Ramping()))
|
||||||
)
|
end
|
||||||
_test(Formulation(ramping = PanGua2016.Ramping()))
|
@testset "MorLatRam2013" begin
|
||||||
_test(Formulation(pwl_costs = Gar1962.PwlCosts()))
|
_test(
|
||||||
_test(Formulation(pwl_costs = CarArr2006.PwlCosts()))
|
Formulation(
|
||||||
_test(Formulation(pwl_costs = KnuOstWat2018.PwlCosts()))
|
ramping = MorLatRam2013.Ramping(),
|
||||||
|
startup_costs = MorLatRam2013.StartupCosts(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
@testset "PanGua2016" begin
|
||||||
|
_test(Formulation(ramping = PanGua2016.Ramping()))
|
||||||
|
end
|
||||||
|
@testset "Gar1962" begin
|
||||||
|
_test(Formulation(pwl_costs = Gar1962.PwlCosts()))
|
||||||
|
end
|
||||||
|
@testset "CarArr2006" begin
|
||||||
|
_test(Formulation(pwl_costs = CarArr2006.PwlCosts()))
|
||||||
|
end
|
||||||
|
@testset "KnuOstWat2018" begin
|
||||||
|
_test(Formulation(pwl_costs = KnuOstWat2018.PwlCosts()))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ const ENABLE_LARGE_TESTS = ("UCJL_LARGE_TESTS" in keys(ENV))
|
|||||||
@testset "model" begin
|
@testset "model" begin
|
||||||
include("model/formulations_test.jl")
|
include("model/formulations_test.jl")
|
||||||
end
|
end
|
||||||
@testset "XavQiuWanThi19" begin
|
@testset "solution" begin
|
||||||
include("solution/methods/XavQiuWanThi19/filter_test.jl")
|
@testset "XavQiuWanThi19" begin
|
||||||
include("solution/methods/XavQiuWanThi19/find_test.jl")
|
include("solution/methods/XavQiuWanThi19/filter_test.jl")
|
||||||
include("solution/methods/XavQiuWanThi19/sensitivity_test.jl")
|
include("solution/methods/XavQiuWanThi19/find_test.jl")
|
||||||
|
include("solution/methods/XavQiuWanThi19/sensitivity_test.jl")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@testset "transform" begin
|
@testset "transform" begin
|
||||||
include("transform/initcond_test.jl")
|
include("transform/initcond_test.jl")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON
|
using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON
|
||||||
|
|
||||||
@testset "build_model" begin
|
@testset "usage" 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 in 1:4
|
||||||
line.normal_flow_limit[t] = 10.0
|
line.normal_flow_limit[t] = 10.0
|
||||||
|
|||||||
Reference in New Issue
Block a user