Compare commits

...

10 Commits

27 changed files with 548 additions and 473 deletions

View File

@@ -11,6 +11,17 @@ All notable changes to this project will be documented in this file.
[semver]: https://semver.org/spec/v2.0.0.html
[pkjjl]: https://pkgdocs.julialang.org/v1/compatibility/#compat-pre-1.0
## [Unreleased]
### Added
- Add multiple reserve products
### Changed
- To support multiple reserve products, the input data format has been modified as follows:
- In `Generators`, replace `Provides spinning reserves?` by `Reserve eligibility`
- In `Parameters`, remove `Reserve shortfall penalty`
- Revise `Reserves` section
## [0.2.2] - 2021-07-21
### Fixed
- Fix small bug in validation scripts related to startup costs

View File

@@ -2,7 +2,7 @@ name = "UnitCommitment"
uuid = "64606440-39ea-11e9-0f29-3303a1d3d877"
authors = ["Santos Xavier, Alinson <axavier@anl.gov>"]
repo = "https://github.com/ANL-CEEESA/UnitCommitment.jl"
version = "0.2.2"
version = "0.3.0"
[deps]
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
@@ -17,6 +17,7 @@ MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
[compat]

View File

@@ -28,15 +28,13 @@ Each section is described in detail below. For a complete example, see [case14](
### 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?
| :----------------------------- | :------------------------------------------------ | :------: | :------------:
| `Time horizon (h)` | Length of the planning horizon (in hours). | Required | N
| `Time step (min)` | Length of each time step (in minutes). Must be a divisor of 60 (e.g. 60, 30, 20, 15, etc). | `60` | N
| `Power balance penalty ($/MW)` | Penalty for system-wide shortage or surplus in production (in $/MW). This is charged per time step. For example, if there is a shortage of 1 MW for three time steps, three times this amount will be charged. | `1000.0` | Y
| `Reserve shortfall penalty ($/MW)` | Penalty for system-wide shortage in meeting reserve requirements (in $/MW). This is charged per time step. Negative value implies reserve constraints must always be satisfied. | `-1` | Y
| `Flexiramp penalty ($/MW)` | Penalty for system-wide shortage in meeting flexible ramping product requirements (in $/MW). This is charged per time step. | `500` | Y
#### Example
@@ -45,8 +43,6 @@ This section describes system-wide parameters, such as power balance and reserve
"Parameters": {
"Time horizon (h)": 4,
"Power balance penalty ($/MW)": 1000.0,
"Reserve shortfall penalty ($/MW)": -1.0,
"Flexiramp penalty ($/MW)": 100.0
}
}
```
@@ -98,9 +94,7 @@ This section describes all generators in the system, including thermal units, re
| `Initial status (h)` | If set to a positive number, indicates the amount of time (in hours) the generator has been on at the beginning of the simulation, and if set to a negative number, the amount of time the generator has been off. For example, if `Initial status (h)` is `-2`, this means that the generator was off since `-02:00` (h:min). The simulation starts at time `00:00`. If `Initial status (h)` is `3`, this means that the generator was on since `-03:00`. A value of zero is not acceptable. | Required | N
| `Initial power (MW)` | Amount of power the generator at time step `-1`, immediately before the planning horizon starts. | Required | N
| `Must run?` | If `true`, the generator should be committed, even if that is not economical (Boolean). | `false` | Y
| `Provides spinning reserves?` | If `true`, this generator may provide spinning reserves (Boolean). | `true` | Y
| `Provides flexible capacity?` | If `true`, this generator may provide flexible ramping product (Boolean). | `true` | Y
| `Reserve eligibility` | List of reserve products this generator is eligibe to provide. By default, the generator is not eligible to provide any reserves. | `[]` | N
#### Production costs and limits
@@ -139,14 +133,13 @@ Note that this curve also specifies the production limits. Specifically, the fir
"Minimum uptime (h)": 4,
"Initial status (h)": 12,
"Must run?": false,
"Provides spinning reserves?": true,
"Provides flexible capacity?": false,
"Reserve eligibility": ["r1"],
},
"gen2": {
"Bus": "b5",
"Production cost curve (MW)": [0.0, [10.0, 8.0, 0.0, 3.0]],
"Production cost curve ($)": [0.0, 0.0],
"Provides spinning reserves?": true,
"Reserve eligibility": ["r1", "r2"],
}
}
}
@@ -216,42 +209,34 @@ This section describes the hourly amount of reserves required.
| Key | Description | Default | Time series?
| :-------------------- | :------------------------------------------------- | --------- | :----:
| `Spinning (MW)` | Minimum amount of system-wide spinning reserves (in MW). Only generators which are online may provide this reserve. | `0.0` | Y
| `Up-flexiramp (MW)` | Minimum amount of system-wide upward flexible ramping product (in MW). Only generators which are online may provide this reserve. | `0.0` | Y
| `Down-flexiramp (MW)` | Minimum amount of system-wide downward flexible ramping product (in MW). Only generators which are online may provide this reserve. | `0.0` | Y
| `Type` | Type of reserve product. Must be either "spinning" or "flexiramp". | Required | N
| `Amount (MW)` | Amount of reserves required. | Required | Y
| `Shortfall penalty ($/MW)` | Penalty for shortage in meeting the reserve requirements (in $/MW). This is charged per time step. Negative value implies reserve constraints must always be satisfied. | `-1` | Y
#### Example 1
```json
{
"Reserves": {
"Spinning (MW)": [
57.30552,
53.88429,
51.31838,
50.46307
]
}
}
```
#### Example 2
```json
{
"Reserves": {
"Up-flexiramp (MW)": [
20.31042,
23.65273,
27.41784,
25.34057
],
"Down-flexiramp (MW)": [
19.41546,
21.45377,
23.53402,
24.80973
]
"r1": {
"Type": "spinning",
"Amount (MW)": [
57.30552,
53.88429,
51.31838,
50.46307
],
"Shortfall penalty ($/MW)": 5.0
},
"r2": {
"Type": "flexiramp",
"Amount (MW)": [
20.31042,
23.65273,
27.41784,
25.34057
],
}
}
}
```
@@ -314,10 +299,7 @@ The output data format is also JSON-based, but it is not currently documented si
Current limitations
-------------------
* All reserves are system-wide. Zonal reserves are not currently supported.
* Upward and downward flexible ramping products can only be acquired under the WanHob2016 formulation, which does not support spinning reserves.
* Network topology remains the same for all time periods
* Only N-1 transmission contingencies are supported. Generator contingencies are not currently supported.
* Time-varying minimum production amounts are not currently compatible with ramp/startup/shutdown limits.
* Flexible ramping products can only be acquired under the `WanHob2016` formulation, which does not support spinning reserves.

View File

@@ -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
`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
`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

Binary file not shown.

View File

@@ -8,7 +8,7 @@ using DataStructures
using GZip
import Base: getindex, time
const INSTANCES_URL = "https://axavier.org/UnitCommitment.jl/0.2/instances"
const INSTANCES_URL = "https://axavier.org/UnitCommitment.jl/0.3/instances"
"""
read_benchmark(name::AbstractString)::UnitCommitmentInstance
@@ -85,6 +85,7 @@ function _from_json(json; repair = true)
contingencies = Contingency[]
lines = TransmissionLine[]
loads = PriceSensitiveLoad[]
reserves = Reserve[]
function scalar(x; default = nothing)
x !== nothing || return default
@@ -105,6 +106,7 @@ function _from_json(json; repair = true)
name_to_bus = Dict{String,Bus}()
name_to_line = Dict{String,TransmissionLine}()
name_to_unit = Dict{String,Unit}()
name_to_reserve = Dict{String,Reserve}()
function timeseries(x; default = nothing)
x !== nothing || return default
@@ -140,6 +142,24 @@ function _from_json(json; repair = true)
push!(buses, bus)
end
# Read reserves
if "Reserves" in keys(json)
for (reserve_name, dict) in json["Reserves"]
r = Reserve(
name = reserve_name,
type = lowercase(dict["Type"]),
amount = timeseries(dict["Amount (MW)"]),
units = [],
shortfall_penalty = scalar(
dict["Shortfall penalty (\$/MW)"],
default = -1,
),
)
name_to_reserve[reserve_name] = r
push!(reserves, r)
end
end
# Read units
for (unit_name, dict) in json["Generators"]
bus = name_to_bus[dict["Bus"]]
@@ -177,6 +197,13 @@ function _from_json(json; repair = true)
)
end
# Read reserve eligibility
unit_reserves = Reserve[]
if "Reserve eligibility" in keys(dict)
unit_reserves =
[name_to_reserve[n] for n in dict["Reserve eligibility"]]
end
# Read and validate initial conditions
initial_power = scalar(dict["Initial power (MW)"], default = nothing)
initial_status = scalar(dict["Initial status (h)"], default = nothing)
@@ -210,36 +237,17 @@ function _from_json(json; repair = true)
scalar(dict["Shutdown limit (MW)"], default = 1e6),
initial_status,
initial_power,
timeseries(
dict["Provides spinning reserves?"],
default = [true for t in 1:T],
),
timeseries(
dict["Provides flexible capacity?"],
default = [true for t in 1:T],
),
startup_categories,
unit_reserves,
)
push!(bus.units, unit)
for r in unit_reserves
push!(r.units, unit)
end
name_to_unit[unit_name] = unit
push!(units, unit)
end
# Read spinning, up-flexiramp, and down-flexiramp reserve requirements
reserves = Reserves(zeros(T), zeros(T), zeros(T))
if "Reserves" in keys(json)
reserves.spinning =
timeseries(json["Reserves"]["Spinning (MW)"], default = zeros(T))
reserves.upflexiramp = timeseries(
json["Reserves"]["Up-flexiramp (MW)"],
default = zeros(T),
)
reserves.dwflexiramp = timeseries(
json["Reserves"]["Down-flexiramp (MW)"],
default = zeros(T),
)
end
# Read transmission lines
if "Transmission lines" in keys(json)
for (line_name, dict) in json["Transmission lines"]
@@ -312,6 +320,7 @@ function _from_json(json; repair = true)
price_sensitive_loads_by_name = Dict(ps.name => ps for ps in loads),
price_sensitive_loads = loads,
reserves = reserves,
reserves_by_name = name_to_reserve,
shortfall_penalty = shortfall_penalty,
flexiramp_shortfall_penalty = flexiramp_shortfall_penalty,
time = T,

View File

@@ -20,6 +20,14 @@ mutable struct StartupCategory
cost::Float64
end
Base.@kwdef mutable struct Reserve
name::String
type::String
amount::Vector{Float64}
units::Vector
shortfall_penalty::Float64
end
mutable struct Unit
name::String
bus::Bus
@@ -36,9 +44,8 @@ mutable struct Unit
shutdown_limit::Float64
initial_status::Union{Int,Nothing}
initial_power::Union{Float64,Nothing}
provides_spinning_reserves::Vector{Bool}
provides_flexiramp_reserves::Vector{Bool}
startup_categories::Vector{StartupCategory}
reserves::Vector{Reserve}
end
mutable struct TransmissionLine
@@ -53,12 +60,6 @@ mutable struct TransmissionLine
flow_limit_penalty::Vector{Float64}
end
mutable struct Reserves
spinning::Vector{Float64}
upflexiramp::Vector{Float64}
dwflexiramp::Vector{Float64}
end
mutable struct Contingency
name::String
lines::Vector{TransmissionLine}
@@ -82,7 +83,8 @@ Base.@kwdef mutable struct UnitCommitmentInstance
power_balance_penalty::Vector{Float64}
price_sensitive_loads_by_name::Dict{AbstractString,PriceSensitiveLoad}
price_sensitive_loads::Vector{PriceSensitiveLoad}
reserves::Reserves
reserves::Vector{Reserve}
reserves_by_name::Dict{AbstractString,Reserve}
shortfall_penalty::Vector{Float64}
flexiramp_shortfall_penalty::Vector{Float64}
time::Int

BIN
src/model/.DS_Store vendored

Binary file not shown.

View File

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

View File

@@ -19,10 +19,10 @@ function _add_ramp_eqs!(
RD = g.ramp_down_limit
SU = g.startup_limit
SD = g.shutdown_limit
reserve = model[:reserve]
eq_ramp_down = _init(model, :eq_ramp_down)
eq_ramp_up = _init(model, :eq_ramp_up)
is_initially_on = (g.initial_status > 0)
reserve = _total_reserves(model, g)
# Gar1962.ProdVars
prod_above = model[:prod_above]
@@ -41,7 +41,7 @@ function _add_ramp_eqs!(
model,
g.min_power[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
)
end
@@ -51,7 +51,7 @@ function _add_ramp_eqs!(
prod_above[gn, t] +
(
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ?
reserve[gn, t] : 0.0
reserve[t] : 0.0
)
min_prod_last_period =
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] +
(
RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ?
reserve[gn, t-1] : 0.0
reserve[t-1] : 0.0
)
min_prod_this_period =
g.min_power[t] * is_on[gn, t] + prod_above[gn, t]

View File

@@ -23,7 +23,7 @@ function _add_ramp_eqs!(
gn = g.name
eq_str_ramp_down = _init(model, :eq_str_ramp_down)
eq_str_ramp_up = _init(model, :eq_str_ramp_up)
reserve = model[:reserve]
reserve = _total_reserves(model, g)
# Gar1962.ProdVars
prod_above = model[:prod_above]
@@ -48,10 +48,8 @@ function _add_ramp_eqs!(
# end
max_prod_this_period =
prod_above[gn, t] + (
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ?
reserve[gn, t] : 0.0
)
prod_above[gn, t] +
(RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0)
min_prod_last_period = 0.0
if t > 1 && time_invariant
min_prod_last_period = prod_above[gn, t-1]
@@ -88,7 +86,7 @@ function _add_ramp_eqs!(
max_prod_last_period =
min_prod_last_period + (
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]
on_last_period = 0.0

View File

@@ -26,7 +26,7 @@ function _add_production_limit_eqs!(
eq_prod_limit = _init(model, :eq_prod_limit)
is_on = model[:is_on]
prod_above = model[:prod_above]
reserve = model[:reserve]
reserve = _total_reserves(model, g)
gn = g.name
for t in 1:model[:instance].time
# Objective function terms for production costs
@@ -44,7 +44,7 @@ function _add_production_limit_eqs!(
end
eq_prod_limit[gn, t] = @constraint(
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

View File

@@ -22,7 +22,7 @@ function _add_ramp_eqs!(
gn = g.name
eq_ramp_down = _init(model, :eq_ramp_down)
eq_ramp_up = _init(model, :eq_str_ramp_up)
reserve = model[:reserve]
reserve = _total_reserves(model, g)
# Gar1962.ProdVars
prod_above = model[:prod_above]
@@ -43,7 +43,7 @@ function _add_ramp_eqs!(
model,
g.min_power[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
)
end
@@ -61,7 +61,7 @@ function _add_ramp_eqs!(
prod_above[gn, t] +
(
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ?
reserve[gn, t] : 0.0
reserve[t] : 0.0
)
min_prod_last_period =
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(
model,
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
)
end
@@ -105,7 +105,7 @@ function _add_ramp_eqs!(
prod_above[gn, 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 =
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(
model,
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
)
end

View File

@@ -12,7 +12,7 @@ function _add_ramp_eqs!(
# TODO: Move upper case constants to model[:instance]
RESERVES_WHEN_SHUT_DOWN = true
gn = g.name
reserve = model[:reserve]
reserve = _total_reserves(model, g)
eq_str_prod_limit = _init(model, :eq_str_prod_limit)
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,
prod_above[gn, t] +
g.min_power[t] * is_on[gn, t] +
reserve[gn, t] <=
reserve[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
@@ -71,7 +71,7 @@ function _add_ramp_eqs!(
model,
prod_above[gn, t] +
g.min_power[t] * is_on[gn, t] +
reserve[gn, t] <=
reserve[t] <=
Pbar * is_on[gn, t] - sum(
(Pbar - (SU + i * RU)) * switch_on[gn, t-i] for
i in 0:min(UT - 1, TRU, t - 1)
@@ -88,7 +88,7 @@ function _add_ramp_eqs!(
model,
prod_above[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 - (SD + i * RD)) * switch_off[gn, t+1+i] for
i in 0:KSD

View File

@@ -2,38 +2,12 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
function _add_flexiramp_vars!(model::JuMP.Model, g::Unit)::Nothing
upflexiramp = _init(model, :upflexiramp)
upflexiramp_shortfall = _init(model, :upflexiramp_shortfall)
mfg = _init(model, :mfg)
dwflexiramp = _init(model, :dwflexiramp)
dwflexiramp_shortfall = _init(model, :dwflexiramp_shortfall)
for t in 1:model[:instance].time
# maximum feasible generation, \bar{g_{its}} in Wang & Hobbs (2016)
mfg[g.name, t] = @variable(model, lower_bound = 0)
if g.provides_flexiramp_reserves[t]
upflexiramp[g.name, t] = @variable(model) # up-flexiramp, ur_{it} in Wang & Hobbs (2016)
dwflexiramp[g.name, t] = @variable(model) # down-flexiramp, dr_{it} in Wang & Hobbs (2016)
else
upflexiramp[g.name, t] = 0.0
dwflexiramp[g.name, t] = 0.0
end
upflexiramp_shortfall[t] =
(model[:instance].flexiramp_shortfall_penalty[t] >= 0) ?
@variable(model, lower_bound = 0) : 0.0
dwflexiramp_shortfall[t] =
(model[:instance].flexiramp_shortfall_penalty[t] >= 0) ?
@variable(model, lower_bound = 0) : 0.0
end
return
end
function _add_ramp_eqs!(
model::JuMP.Model,
g::Unit,
formulation_prod_vars::Gar1962.ProdVars,
formulation_ramping::WanHob2016.Ramping,
formulation_status_vars::Gar1962.StatusVars,
::Gar1962.ProdVars,
::WanHob2016.Ramping,
::Gar1962.StatusVars,
)::Nothing
is_initially_on = (g.initial_status > 0)
SU = g.startup_limit
@@ -51,49 +25,144 @@ function _add_ramp_eqs!(
dwflexiramp = model[:dwflexiramp]
mfg = model[:mfg]
for t in 1:model[:instance].time
@constraint(
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)
if t != model[:instance].time
if length(g.reserves) > 1
error("Each generator may only provide one flexiramp reserve")
end
for r in g.reserves
if r.type !== "flexiramp"
error(
"This formulation only supports flexiramp reserves, not $(r.type)",
)
end
rn = r.name
for t in 1:model[:instance].time
@constraint(
model,
minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <=
prod_above[gn, t] - dwflexiramp[gn, t] +
(is_on[gn, t] * minp[t])
) # first inequality of Eq. (20) in Wang & Hobbs (2016)
@constraint(
model,
prod_above[gn, t] - dwflexiramp[gn, t] +
(is_on[gn, t] * minp[t]) <=
mfg[gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
) # second inequality of Eq. (20) in Wang & Hobbs (2016)
@constraint(
model,
minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <=
prod_above[gn, t] +
upflexiramp[gn, t] +
(is_on[gn, t] * minp[t])
) # first inequality of Eq. (21) in Wang & Hobbs (2016)
@constraint(
model,
prod_above[gn, t] +
upflexiramp[gn, t] +
(is_on[gn, t] * minp[t]) <=
mfg[gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
) # second inequality of Eq. (21) in Wang & Hobbs (2016)
if t != 1
prod_above[gn, t] + (is_on[gn, t] * minp[t]) <= mfg[rn, gn, t]
) # Eq. (19) in Wang & Hobbs (2016)
@constraint(model, mfg[rn, gn, t] <= is_on[gn, t] * maxp[t]) # Eq. (22) in Wang & Hobbs (2016)
if t != model[:instance].time
@constraint(
model,
mfg[gn, t] <=
minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <=
prod_above[gn, t] - dwflexiramp[rn, gn, t] +
(is_on[gn, t] * minp[t])
) # first inequality of Eq. (20) in Wang & Hobbs (2016)
@constraint(
model,
prod_above[gn, t] - dwflexiramp[rn, gn, t] +
(is_on[gn, t] * minp[t]) <=
mfg[rn, gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
) # second inequality of Eq. (20) in Wang & Hobbs (2016)
@constraint(
model,
minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <=
prod_above[gn, t] +
upflexiramp[rn, gn, t] +
(is_on[gn, t] * minp[t])
) # first inequality of Eq. (21) in Wang & Hobbs (2016)
@constraint(
model,
prod_above[gn, t] +
upflexiramp[rn, gn, t] +
(is_on[gn, t] * minp[t]) <=
mfg[rn, gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
) # second inequality of Eq. (21) in Wang & Hobbs (2016)
if t != 1
@constraint(
model,
mfg[rn, gn, t] <=
prod_above[gn, t-1] +
(is_on[gn, t-1] * minp[t]) +
(RU * is_on[gn, t-1]) +
(SU * (is_on[gn, t] - is_on[gn, t-1])) +
maxp[t] * (1 - is_on[gn, t])
) # Eq. (23) in Wang & Hobbs (2016)
@constraint(
model,
(prod_above[gn, t-1] + (is_on[gn, t-1] * minp[t])) -
(prod_above[gn, t] + (is_on[gn, t] * minp[t])) <=
RD * is_on[gn, t] +
SD * (is_on[gn, t-1] - is_on[gn, t]) +
maxp[t] * (1 - is_on[gn, t-1])
) # Eq. (25) in Wang & Hobbs (2016)
else
@constraint(
model,
mfg[rn, gn, t] <=
initial_power +
(RU * is_initially_on) +
(SU * (is_on[gn, t] - is_initially_on)) +
maxp[t] * (1 - is_on[gn, t])
) # Eq. (23) in Wang & Hobbs (2016) for the first time period
@constraint(
model,
initial_power -
(prod_above[gn, t] + (is_on[gn, t] * minp[t])) <=
RD * is_on[gn, t] +
SD * (is_initially_on - is_on[gn, t]) +
maxp[t] * (1 - is_initially_on)
) # Eq. (25) in Wang & Hobbs (2016) for the first time period
end
@constraint(
model,
mfg[rn, gn, t] <=
(SD * (is_on[gn, t] - is_on[gn, t+1])) +
(maxp[t] * is_on[gn, t+1])
) # Eq. (24) in Wang & Hobbs (2016)
@constraint(
model,
-RD * is_on[gn, t+1] -
SD * (is_on[gn, t] - is_on[gn, t+1]) -
maxp[t] * (1 - is_on[gn, t]) <= upflexiramp[rn, gn, t]
) # first inequality of Eq. (26) in Wang & Hobbs (2016)
@constraint(
model,
upflexiramp[rn, gn, t] <=
RU * is_on[gn, t] +
SU * (is_on[gn, t+1] - is_on[gn, t]) +
maxp[t] * (1 - is_on[gn, t+1])
) # second inequality of Eq. (26) in Wang & Hobbs (2016)
@constraint(
model,
-RU * is_on[gn, t] - SU * (is_on[gn, t+1] - is_on[gn, t]) -
maxp[t] * (1 - is_on[gn, t+1]) <= dwflexiramp[rn, gn, t]
) # first inequality of Eq. (27) in Wang & Hobbs (2016)
@constraint(
model,
dwflexiramp[rn, gn, t] <=
RD * is_on[gn, t+1] +
SD * (is_on[gn, t] - is_on[gn, t+1]) +
maxp[t] * (1 - is_on[gn, t])
) # second inequality of Eq. (27) in Wang & Hobbs (2016)
@constraint(
model,
-maxp[t] * is_on[gn, t] + minp[t] * is_on[gn, t+1] <=
upflexiramp[rn, gn, t]
) # first inequality of Eq. (28) in Wang & Hobbs (2016)
@constraint(
model,
upflexiramp[rn, 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[rn, gn, t]
) # first inequality of Eq. (29) in Wang & Hobbs (2016)
@constraint(
model,
dwflexiramp[rn, gn, t] <=
(maxp[t] * is_on[gn, t]) - (minp[t] * is_on[gn, t+1])
) # second inequality of Eq. (29) in Wang & Hobbs (2016)
else
@constraint(
model,
mfg[rn, gn, t] <=
prod_above[gn, t-1] +
(is_on[gn, t-1] * minp[t]) +
(RU * is_on[gn, t-1]) +
(SU * (is_on[gn, t] - is_on[gn, t-1])) +
maxp[t] * (1 - is_on[gn, t])
) # Eq. (23) in Wang & Hobbs (2016)
) # Eq. (23) in Wang & Hobbs (2016) for the last time period
@constraint(
model,
(prod_above[gn, t-1] + (is_on[gn, t-1] * minp[t])) -
@@ -101,85 +170,8 @@ function _add_ramp_eqs!(
RD * is_on[gn, t] +
SD * (is_on[gn, t-1] - is_on[gn, t]) +
maxp[t] * (1 - is_on[gn, t-1])
) # Eq. (25) in Wang & Hobbs (2016)
else
@constraint(
model,
mfg[gn, t] <=
initial_power +
(RU * is_initially_on) +
(SU * (is_on[gn, t] - is_initially_on)) +
maxp[t] * (1 - is_on[gn, t])
) # Eq. (23) in Wang & Hobbs (2016) for the first time period
@constraint(
model,
initial_power -
(prod_above[gn, t] + (is_on[gn, t] * minp[t])) <=
RD * is_on[gn, t] +
SD * (is_initially_on - is_on[gn, t]) +
maxp[t] * (1 - is_initially_on)
) # Eq. (25) in Wang & Hobbs (2016) for the first time period
) # Eq. (25) in Wang & Hobbs (2016) for the last time period
end
@constraint(
model,
mfg[gn, t] <=
(SD * (is_on[gn, t] - is_on[gn, t+1])) +
(maxp[t] * is_on[gn, t+1])
) # Eq. (24) in Wang & Hobbs (2016)
@constraint(
model,
-RD * is_on[gn, t+1] - SD * (is_on[gn, t] - is_on[gn, t+1]) -
maxp[t] * (1 - is_on[gn, t]) <= upflexiramp[gn, t]
) # first inequality of Eq. (26) in Wang & Hobbs (2016)
@constraint(
model,
upflexiramp[gn, t] <=
RU * is_on[gn, t] +
SU * (is_on[gn, t+1] - is_on[gn, t]) +
maxp[t] * (1 - is_on[gn, t+1])
) # second inequality of Eq. (26) in Wang & Hobbs (2016)
@constraint(
model,
-RU * is_on[gn, t] - SU * (is_on[gn, t+1] - is_on[gn, t]) -
maxp[t] * (1 - is_on[gn, t+1]) <= dwflexiramp[gn, t]
) # first inequality of Eq. (27) in Wang & Hobbs (2016)
@constraint(
model,
dwflexiramp[gn, t] <=
RD * is_on[gn, t+1] +
SD * (is_on[gn, t] - is_on[gn, t+1]) +
maxp[t] * (1 - is_on[gn, t])
) # second inequality of Eq. (27) in Wang & Hobbs (2016)
@constraint(
model,
-maxp[t] * is_on[gn, t] + minp[t] * is_on[gn, t+1] <=
upflexiramp[gn, t]
) # 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, -maxp[t] * is_on[gn, t+1] <= dwflexiramp[gn, t]) # first inequality of Eq. (29) in Wang & Hobbs (2016)
@constraint(
model,
dwflexiramp[gn, t] <=
(maxp[t] * is_on[gn, t]) - (minp[t] * is_on[gn, t+1])
) # second inequality of Eq. (29) in Wang & Hobbs (2016)
else
@constraint(
model,
mfg[gn, t] <=
prod_above[gn, t-1] +
(is_on[gn, t-1] * minp[t]) +
(RU * is_on[gn, t-1]) +
(SU * (is_on[gn, t] - is_on[gn, t-1])) +
maxp[t] * (1 - is_on[gn, t])
) # Eq. (23) in Wang & Hobbs (2016) for the last time period
@constraint(
model,
(prod_above[gn, t-1] + (is_on[gn, t-1] * minp[t])) -
(prod_above[gn, t] + (is_on[gn, t] * minp[t])) <=
RD * is_on[gn, t] +
SD * (is_on[gn, t-1] - is_on[gn, t]) +
maxp[t] * (1 - is_on[gn, t-1])
) # Eq. (25) in Wang & Hobbs (2016) for the last time period
end
end
end

View File

@@ -4,8 +4,8 @@
function _add_system_wide_eqs!(model::JuMP.Model)::Nothing
_add_net_injection_eqs!(model)
_add_reserve_eqs!(model)
_add_flexiramp_eqs!(model)
_add_spinning_reserve_eqs!(model)
_add_flexiramp_reserve_eqs!(model)
return
end
@@ -28,74 +28,69 @@ function _add_net_injection_eqs!(model::JuMP.Model)::Nothing
return
end
function _add_reserve_eqs!(model::JuMP.Model)::Nothing
eq_min_reserve = _init(model, :eq_min_reserve)
function _add_spinning_reserve_eqs!(model::JuMP.Model)::Nothing
instance = model[:instance]
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)
shortfall_penalty = instance.shortfall_penalty[t]
eq_min_reserve[t] = @constraint(
model,
sum(model[:reserve][g.name, t] for g in instance.units) +
(shortfall_penalty >= 0 ? model[:reserve_shortfall][t] : 0.0) >=
instance.reserves.spinning[t]
)
# Account for shortfall contribution to objective
if shortfall_penalty >= 0
add_to_expression!(
model[:obj],
shortfall_penalty,
model[:reserve_shortfall][t],
eq_min_spinning_reserve = _init(model, :eq_min_spinning_reserve)
for r in instance.reserves
r.type == "spinning" || continue
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_spinning_reserve[r.name, t] = @constraint(
model,
sum(model[:reserve][r.name, g.name, t] for g in r.units) +
model[:reserve_shortfall][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_shortfall][r.name, t],
)
end
end
end
return
end
function _add_flexiramp_eqs!(model::JuMP.Model)::Nothing
function _add_flexiramp_reserve_eqs!(model::JuMP.Model)::Nothing
# Note: The flexpramp requirements in Wang & Hobbs (2016) are imposed as hard constraints
# through Eq. (17) and Eq. (18). The constraints eq_min_upflexiramp[t] and eq_min_dwflexiramp[t]
# through Eq. (17) and Eq. (18). The constraints eq_min_upflexiramp and eq_min_dwflexiramp
# provided below are modified versions of Eq. (17) and Eq. (18), respectively, in that
# they include slack variables for flexiramp shortfall, which are penalized in the
# objective function.
eq_min_upflexiramp = _init(model, :eq_min_upflexiramp)
eq_min_dwflexiramp = _init(model, :eq_min_dwflexiramp)
instance = model[:instance]
for t in 1:instance.time
flexiramp_shortfall_penalty = instance.flexiramp_shortfall_penalty[t]
# Eq. (17) in Wang & Hobbs (2016)
eq_min_upflexiramp[t] = @constraint(
model,
sum(model[:upflexiramp][g.name, t] for g in instance.units) +
(
flexiramp_shortfall_penalty >= 0 ?
model[:upflexiramp_shortfall][t] : 0.0
) >= instance.reserves.upflexiramp[t]
)
# Eq. (18) in Wang & Hobbs (2016)
eq_min_dwflexiramp[t] = @constraint(
model,
sum(model[:dwflexiramp][g.name, t] for g in instance.units) +
(
flexiramp_shortfall_penalty >= 0 ?
model[:dwflexiramp_shortfall][t] : 0.0
) >= instance.reserves.dwflexiramp[t]
)
# Account for flexiramp shortfall contribution to objective
if flexiramp_shortfall_penalty >= 0
add_to_expression!(
model[:obj],
flexiramp_shortfall_penalty,
(
model[:upflexiramp_shortfall][t] +
model[:dwflexiramp_shortfall][t]
),
for r in instance.reserves
r.type == "flexiramp" || continue
for t in 1:instance.time
# Eq. (17) in Wang & Hobbs (2016)
eq_min_upflexiramp[r.name, t] = @constraint(
model,
sum(model[:upflexiramp][r.name, g.name, t] for g in r.units) + model[:upflexiramp_shortfall][r.name, t] >= r.amount[t]
)
# Eq. (18) in Wang & Hobbs (2016)
eq_min_dwflexiramp[r.name, t] = @constraint(
model,
sum(model[:dwflexiramp][r.name, g.name, t] for g in r.units) + model[:dwflexiramp_shortfall][r.name, t] >= r.amount[t]
)
# Account for flexiramp shortfall contribution to objective
if r.shortfall_penalty >= 0
add_to_expression!(
model[:obj],
r.shortfall_penalty,
(
model[:upflexiramp_shortfall][r.name, t] +
model[:dwflexiramp_shortfall][r.name, t]
),
)
end
end
end
return

View File

@@ -12,8 +12,8 @@ function _add_unit!(model::JuMP.Model, g::Unit, formulation::Formulation)
# Variables
_add_production_vars!(model, g, formulation.prod_vars)
_add_reserve_vars!(model, g)
_add_flexiramp_vars!(model, g)
_add_spinning_reserve_vars!(model, g)
_add_flexiramp_reserve_vars!(model, g)
_add_startup_shutdown_vars!(model, g)
_add_status_vars!(model, g, formulation.status_vars)
@@ -43,26 +43,48 @@ end
_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_spinning_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing
reserve = _init(model, :reserve)
reserve_shortfall = _init(model, :reserve_shortfall)
for t in 1:model[:instance].time
if g.provides_spinning_reserves[t]
reserve[g.name, t] = @variable(model, lower_bound = 0)
else
reserve[g.name, t] = 0.0
for r in g.reserves
r.type == "spinning" || continue
for t in 1:model[:instance].time
reserve[r.name, g.name, t] = @variable(model, lower_bound = 0)
if (r.name, t) keys(reserve_shortfall)
reserve_shortfall[r.name, t] = @variable(model, lower_bound = 0)
if r.shortfall_penalty < 0
set_upper_bound(reserve_shortfall[r.name, t], 0.0)
end
end
end
reserve_shortfall[t] =
(model[:instance].shortfall_penalty[t] >= 0) ?
@variable(model, lower_bound = 0) : 0.0
end
return
end
function _add_reserve_eqs!(model::JuMP.Model, g::Unit)::Nothing
reserve = model[:reserve]
for t in 1:model[:instance].time
add_to_expression!(expr_reserve[g.bus.name, t], reserve[g.name, t], 1.0)
function _add_flexiramp_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing
upflexiramp = _init(model, :upflexiramp)
upflexiramp_shortfall = _init(model, :upflexiramp_shortfall)
mfg = _init(model, :mfg)
dwflexiramp = _init(model, :dwflexiramp)
dwflexiramp_shortfall = _init(model, :dwflexiramp_shortfall)
for r in g.reserves
r.type == "flexiramp" || continue
for t in 1:model[:instance].time
# maximum feasible generation, \bar{g_{its}} in Wang & Hobbs (2016)
mfg[r.name, g.name, t] = @variable(model, lower_bound = 0)
upflexiramp[r.name, g.name, t] = @variable(model) # up-flexiramp, ur_{it} in Wang & Hobbs (2016)
dwflexiramp[r.name, g.name, t] = @variable(model) # down-flexiramp, dr_{it} in Wang & Hobbs (2016)
if (r.name, t) keys(upflexiramp_shortfall)
upflexiramp_shortfall[r.name, t] =
@variable(model, lower_bound = 0)
dwflexiramp_shortfall[r.name, t] =
@variable(model, lower_bound = 0)
if r.shortfall_penalty < 0
set_upper_bound(upflexiramp_shortfall[r.name, t], 0.0)
set_upper_bound(dwflexiramp_shortfall[r.name, t], 0.0)
end
end
end
end
return
end
@@ -82,7 +104,7 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
eq_startup_limit = _init(model, :eq_startup_limit)
is_on = model[:is_on]
prod_above = model[:prod_above]
reserve = model[:reserve]
reserve = _total_reserves(model, g)
switch_off = model[:switch_off]
switch_on = model[:switch_on]
T = model[:instance].time
@@ -90,7 +112,7 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
# Startup limit
eq_startup_limit[g.name, t] = @constraint(
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] -
max(0, g.max_power[t] - g.startup_limit) * switch_on[g.name, t]
)
@@ -118,7 +140,7 @@ function _add_ramp_eqs!(
formulation::RampingFormulation,
)::Nothing
prod_above = model[:prod_above]
reserve = model[:reserve]
reserve = _total_reserves(model, g)
eq_ramp_up = _init(model, :eq_ramp_up)
eq_ramp_down = _init(model, :eq_ramp_down)
for t in 1:model[:instance].time
@@ -127,14 +149,14 @@ function _add_ramp_eqs!(
if _is_initially_on(g) == 1
eq_ramp_up[g.name, t] = @constraint(
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
)
end
else
eq_ramp_up[g.name, t] = @constraint(
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
)
end
@@ -217,3 +239,15 @@ function _add_net_injection_eqs!(model::JuMP.Model, g::Unit)::Nothing
)
end
end
function _total_reserves(model, g)::Vector
T = model[:instance].time
reserve = [0.0 for _ in 1:T]
spinning_reserves = [r for r in g.reserves if r.type == "spinning"]
if !isempty(spinning_reserves)
reserve += [
sum(model[:reserve][r.name, g.name, t] for r in spinning_reserves) for t in 1:model[:instance].time
]
end
return reserve
end

View File

@@ -18,15 +18,28 @@ function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing
is_on_value = round(solution["Is on"][g.name][t])
prod_value =
round(solution["Production (MW)"][g.name][t], digits = 5)
reserve_value =
round(solution["Reserve (MW)"][g.name][t], digits = 5)
JuMP.fix(is_on[g.name, t], is_on_value, force = true)
JuMP.fix(
prod_above[g.name, t],
prod_value - is_on_value * g.min_power[t],
force = true,
)
JuMP.fix(reserve[g.name, t], reserve_value, force = true)
end
end
for r in instance.reserves
r.type == "spinning" || continue
for g in r.units
for t in 1:T
reserve_value = round(
solution["Spinning reserve (MW)"][r.name][g.name][t],
digits = 5,
)
JuMP.fix(
reserve[r.name, g.name, t],
reserve_value,
force = true,
)
end
end
end
return

View File

@@ -50,37 +50,6 @@ function solution(model::JuMP.Model)::OrderedDict
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)
if instance.reserves.upflexiramp != zeros(T) ||
instance.reserves.dwflexiramp != zeros(T)
# Report flexiramp solutions only if either of the up-flexiramp and
# down-flexiramp requirements is not a default array of zeros
sol["Up-flexiramp (MW)"] =
timeseries(model[:upflexiramp], instance.units)
sol["Up-flexiramp shortfall (MW)"] = OrderedDict(
t =>
(instance.flexiramp_shortfall_penalty[t] >= 0) ?
round(value(model[:upflexiramp_shortfall][t]), digits = 5) :
0.0 for t in 1:instance.time
)
sol["Down-flexiramp (MW)"] =
timeseries(model[:dwflexiramp], instance.units)
sol["Down-flexiramp shortfall (MW)"] = OrderedDict(
t =>
(instance.flexiramp_shortfall_penalty[t] >= 0) ?
round(value(model[:dwflexiramp_shortfall][t]), digits = 5) :
0.0 for t in 1:instance.time
)
else
# Report spinning reserve solutions only if both up-flexiramp and
# down-flexiramp requirements are arrays of zeros.
sol["Reserve (MW)"] = timeseries(model[:reserve], instance.units)
sol["Reserve shortfall (MW)"] = OrderedDict(
t =>
(instance.shortfall_penalty[t] >= 0) ?
round(value(model[:reserve_shortfall][t]), digits = 5) :
0.0 for t in 1:instance.time
)
end
sol["Net injection (MW)"] =
timeseries(model[:net_injection], instance.buses)
sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses)
@@ -91,5 +60,47 @@ function solution(model::JuMP.Model)::OrderedDict
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"
)
return sol
end

View File

@@ -24,13 +24,14 @@ function slice(
modified = deepcopy(instance)
modified.time = length(range)
modified.power_balance_penalty = modified.power_balance_penalty[range]
modified.reserves.spinning = modified.reserves.spinning[range]
for r in modified.reserves
r.amount = r.amount[range]
end
for u in modified.units
u.max_power = u.max_power[range]
u.min_power = u.min_power[range]
u.must_run = u.must_run[range]
u.min_power_cost = u.min_power_cost[range]
u.provides_spinning_reserves = u.provides_spinning_reserves[range]
for s in u.cost_segments
s.mw = s.mw[range]
s.cost = s.cost[range]

View File

@@ -40,12 +40,19 @@ function validate(
return true
end
function _validate_units(instance, solution; tol = 0.01)
function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01)
err_count = 0
for unit in instance.units
production = solution["Production (MW)"][unit.name]
reserve = solution["Reserve (MW)"][unit.name]
reserve = [0.0 for _ in 1:instance.time]
spinning_reserves = [r for r in unit.reserves if r.type == "spinning"]
if !isempty(spinning_reserves)
reserve += sum(
solution["Spinning reserve (MW)"][r.name][unit.name] for
r in spinning_reserves
)
end
actual_production_cost = solution["Production cost (\$)"][unit.name]
actual_startup_cost = solution["Startup cost (\$)"][unit.name]
is_on = bin(solution["Is on"][unit.name])
@@ -99,13 +106,18 @@ function _validate_units(instance, solution; tol = 0.01)
end
# Verify reserve eligibility
if !unit.provides_spinning_reserves[t] && reserve[t] > tol
@error @sprintf(
"Unit %s is not eligible to provide spinning reserves at time %d",
unit.name,
t
)
err_count += 1
for r in instance.reserves
if r.type == "spinning"
if unit r.units &&
(unit in keys(solution["Spinning reserve (MW)"][r.name]))
@error @sprintf(
"Unit %s is not eligible to provide reserve %s",
unit.name,
r.name,
)
err_count += 1
end
end
end
# If unit is on, must produce at least its minimum power
@@ -137,9 +149,11 @@ function _validate_units(instance, solution; tol = 0.01)
# If unit is off, must produce zero
if !is_on[t] && production[t] + reserve[t] > tol
@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,
t
t,
production[t],
reserve[t],
)
err_count += 1
end
@@ -321,67 +335,65 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
err_count += 1
end
# Verify flexiramp solutions only if either of the up-flexiramp and
# down-flexiramp requirements is not a default array of zeros
if instance.reserves.upflexiramp != zeros(instance.time) ||
instance.reserves.dwflexiramp != zeros(instance.time)
upflexiramp = sum(
solution["Up-flexiramp (MW)"][g.name][t] for
g in instance.units
)
upflexiramp_shortfall =
(instance.flexiramp_shortfall_penalty[t] >= 0) ?
solution["Up-flexiramp shortfall (MW)"][t] : 0
if upflexiramp + upflexiramp_shortfall <
instance.reserves.upflexiramp[t] - tol
@error @sprintf(
"Insufficient up-flexiramp at time %d (%.2f + %.2f should be %.2f)",
t,
upflexiramp,
upflexiramp_shortfall,
instance.reserves.upflexiramp[t],
# Verify reserves
for r in instance.reserves
if r.type == "spinning"
provided = sum(
solution["Spinning reserve (MW)"][r.name][g.name][t] for
g in r.units
)
err_count += 1
end
shortfall =
solution["Spinning reserve shortfall (MW)"][r.name][t]
required = r.amount[t]
dwflexiramp = sum(
solution["Down-flexiramp (MW)"][g.name][t] for
g in instance.units
)
dwflexiramp_shortfall =
(instance.flexiramp_shortfall_penalty[t] >= 0) ?
solution["Down-flexiramp shortfall (MW)"][t] : 0
if dwflexiramp + dwflexiramp_shortfall <
instance.reserves.dwflexiramp[t] - tol
@error @sprintf(
"Insufficient down-flexiramp at time %d (%.2f + %.2f should be %.2f)",
t,
dwflexiramp,
dwflexiramp_shortfall,
instance.reserves.dwflexiramp[t],
if provided + shortfall < required - tol
@error @sprintf(
"Insufficient reserve %s at time %d (%.2f + %.2f < %.2f)",
r.name,
t,
provided,
shortfall,
required,
)
end
elseif r.type == "flexiramp"
upflexiramp = sum(
solution["Up-flexiramp (MW)"][r.name][g.name][t] for
g in r.units
)
err_count += 1
end
# Verify spinning reserve solutions only if both up-flexiramp and
# down-flexiramp requirements are arrays of zeros.
else
reserve =
sum(solution["Reserve (MW)"][g.name][t] for g in instance.units)
reserve_shortfall =
(instance.shortfall_penalty[t] >= 0) ?
solution["Reserve shortfall (MW)"][t] : 0
upflexiramp_shortfall =
solution["Up-flexiramp shortfall (MW)"][r.name][t]
if reserve + reserve_shortfall < instance.reserves.spinning[t] - tol
@error @sprintf(
"Insufficient spinning reserves at time %d (%.2f + %.2f should be %.2f)",
t,
reserve,
reserve_shortfall,
instance.reserves.spinning[t],
if upflexiramp + upflexiramp_shortfall < r.amount[t] - tol
@error @sprintf(
"Insufficient up-flexiramp at time %d (%.2f + %.2f < %.2f)",
t,
upflexiramp,
upflexiramp_shortfall,
r.amount[t],
)
err_count += 1
end
dwflexiramp = sum(
solution["Down-flexiramp (MW)"][r.name][g.name][t] for
g in r.units
)
err_count += 1
dwflexiramp_shortfall =
solution["Down-flexiramp shortfall (MW)"][r.name][t]
if dwflexiramp + dwflexiramp_shortfall < r.amount[t] - tol
@error @sprintf(
"Insufficient down-flexiramp at time %d (%.2f + %.2f < %.2f)",
t,
dwflexiramp,
dwflexiramp_shortfall,
r.amount[t],
)
err_count += 1
end
else
error("Unknown reserve type: $(r.type)")
end
end
end

Binary file not shown.

View File

@@ -37,6 +37,11 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test instance.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353]
@test instance.buses_by_name["b9"].name == "b9"
@test instance.reserves[1].name == "r1"
@test instance.reserves[1].type == "spinning"
@test instance.reserves[1].amount == [100.0, 100.0, 100.0, 100.0]
@test instance.reserves_by_name["r1"].name == "r1"
unit = instance.units[1]
@test unit.name == "g1"
@test unit.bus.name == "b1"
@@ -48,7 +53,6 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test unit.min_power_cost == [1400.0 for t in 1:4]
@test unit.min_uptime == 1
@test unit.min_downtime == 1
@test unit.provides_spinning_reserves == [true for t in 1:4]
for t in 1:1
@test unit.cost_segments[1].mw[t] == 10.0
@test unit.cost_segments[2].mw[t] == 20.0
@@ -64,11 +68,13 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test unit.startup_categories[1].cost == 1000.0
@test unit.startup_categories[2].cost == 1500.0
@test unit.startup_categories[3].cost == 2000.0
@test length(unit.reserves) == 0
@test instance.units_by_name["g1"].name == "g1"
unit = instance.units[2]
@test unit.name == "g2"
@test unit.must_run == [false for t in 1:4]
@test length(unit.reserves) == 1
unit = instance.units[3]
@test unit.name == "g3"
@@ -81,7 +87,6 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test unit.min_power_cost == [0.0 for t in 1:4]
@test unit.min_uptime == 1
@test unit.min_downtime == 1
@test unit.provides_spinning_reserves == [true for t in 1:4]
for t in 1:4
@test unit.cost_segments[1].mw[t] 33
@test unit.cost_segments[2].mw[t] 33
@@ -90,8 +95,8 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@test unit.cost_segments[2].cost[t] 38.04
@test unit.cost_segments[3].cost[t] 44.77853
end
@test instance.reserves.spinning == zeros(4)
@test length(unit.reserves) == 1
@test unit.reserves[1].name == "r1"
@test instance.contingencies[1].lines == [instance.lines[1]]
@test instance.contingencies[1].units == []

View File

@@ -5,6 +5,7 @@
using UnitCommitment
using JuMP
using Cbc
using JSON
import UnitCommitment:
ArrCon2000,
CarArr2006,
@@ -19,42 +20,65 @@ import UnitCommitment:
function _test(
formulation::Formulation;
instances::Array{String} = ["test/case14"],
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0),
instances = ["test/case14"],
dump::Bool = false,
)::Nothing
for instance_name in instances
instance = UnitCommitment.read_benchmark(instance_name)
model = UnitCommitment.build_model(
instance = instance,
formulation = formulation,
optimizer = optimizer,
)
UnitCommitment.optimize!(
model,
XavQiuWanThi2019.Method(two_phase_gap = false, gap_limit = 0.1),
optimizer = Cbc.Optimizer,
variable_names = true,
)
set_silent(model)
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
@test UnitCommitment.validate(instance, solution)
end
return
end
@testset "formulations" begin
_test(Formulation())
_test(Formulation(ramping = ArrCon2000.Ramping()))
# _test(Formulation(ramping = DamKucRajAta2016.Ramping()))
_test(
Formulation(
ramping = MorLatRam2013.Ramping(),
startup_costs = MorLatRam2013.StartupCosts(),
),
)
_test(Formulation(ramping = PanGua2016.Ramping()))
_test(Formulation(pwl_costs = Gar1962.PwlCosts()))
_test(Formulation(pwl_costs = CarArr2006.PwlCosts()))
_test(Formulation(pwl_costs = KnuOstWat2018.PwlCosts()))
_test(
Formulation(ramping = WanHob2016.Ramping()),
instances = ["test/case14-flex"],
)
@testset "default" begin
_test(Formulation())
end
@testset "ArrCon2000" begin
_test(Formulation(ramping = ArrCon2000.Ramping()))
end
@testset "DamKucRajAta2016" begin
_test(Formulation(ramping = DamKucRajAta2016.Ramping()))
end
@testset "MorLatRam2013" begin
_test(
Formulation(
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
@testset "WanHob2016" begin
_test(
Formulation(ramping = WanHob2016.Ramping()),
instances = ["test/case14-flex"],
)
end
end

View File

@@ -19,10 +19,12 @@ UnitCommitment._setup_logger(level = Base.CoreLogging.Error)
@testset "model" begin
include("model/formulations_test.jl")
end
@testset "XavQiuWanThi19" begin
include("solution/methods/XavQiuWanThi19/filter_test.jl")
include("solution/methods/XavQiuWanThi19/find_test.jl")
include("solution/methods/XavQiuWanThi19/sensitivity_test.jl")
@testset "solution" begin
@testset "XavQiuWanThi19" begin
include("solution/methods/XavQiuWanThi19/filter_test.jl")
include("solution/methods/XavQiuWanThi19/find_test.jl")
include("solution/methods/XavQiuWanThi19/sensitivity_test.jl")
end
end
@testset "transform" begin
include("transform/initcond_test.jl")

View File

@@ -11,13 +11,12 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
# Should update all time-dependent fields
@test modified.time == 2
@test length(modified.power_balance_penalty) == 2
@test length(modified.reserves.spinning) == 2
@test length(modified.reserves_by_name["r1"].amount) == 2
for u in modified.units
@test length(u.max_power) == 2
@test length(u.min_power) == 2
@test length(u.must_run) == 2
@test length(u.min_power_cost) == 2
@test length(u.provides_spinning_reserves) == 2
for s in u.cost_segments
@test length(s.mw) == 2
@test length(s.cost) == 2

View File

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