mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 00:08:52 -06:00
Compare commits
18 Commits
bugfix/for
...
feature/st
| Author | SHA1 | Date | |
|---|---|---|---|
| d751c2af88 | |||
| 1397ae438e | |||
| 7308ff6477 | |||
|
|
8b6cbe8c1b | ||
|
|
c2557a64d1 | ||
|
|
5afb2363af | ||
|
|
860c47b7e3 | ||
|
|
37b21853be | ||
|
|
c8c7350096 | ||
|
|
7302fabe37 | ||
|
|
4ed13d6e95 | ||
| b1498c50b3 | |||
|
|
000215e991 | ||
| 7a1b6f0f55 | |||
| 719143ea40 | |||
| 07d7e04728 | |||
| 4daf38906d | |||
|
|
b2eaa0e48b |
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -9,8 +9,8 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
julia-version: ['1.3', '1.4', '1.5', '1.6']
|
julia-version: ['1.4', '1.5', '1.6']
|
||||||
julia-arch: [x64, x86]
|
julia-arch: [x64]
|
||||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||||
exclude:
|
exclude:
|
||||||
- os: macOS-latest
|
- os: macOS-latest
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ All notable changes to this project will be documented in this file.
|
|||||||
[semver]: https://semver.org/spec/v2.0.0.html
|
[semver]: https://semver.org/spec/v2.0.0.html
|
||||||
[pkjjl]: https://pkgdocs.julialang.org/v1/compatibility/#compat-pre-1.0
|
[pkjjl]: https://pkgdocs.julialang.org/v1/compatibility/#compat-pre-1.0
|
||||||
|
|
||||||
|
## [0.2.2] - 2021-07-21
|
||||||
|
### Fixed
|
||||||
|
- Fix small bug in validation scripts related to startup costs
|
||||||
|
- Fix duplicated startup constraints (@mtanneau, #12)
|
||||||
|
|
||||||
## [0.2.1] - 2021-06-02
|
## [0.2.1] - 2021-06-02
|
||||||
### Added
|
### Added
|
||||||
- Add multiple ramping formulations (ArrCon2000, MorLatRam2013, DamKucRajAta2016, PanGua2016)
|
- Add multiple ramping formulations (ArrCon2000, MorLatRam2013, DamKucRajAta2016, PanGua2016)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name = "UnitCommitment"
|
|||||||
uuid = "64606440-39ea-11e9-0f29-3303a1d3d877"
|
uuid = "64606440-39ea-11e9-0f29-3303a1d3d877"
|
||||||
authors = ["Santos Xavier, Alinson <axavier@anl.gov>"]
|
authors = ["Santos Xavier, Alinson <axavier@anl.gov>"]
|
||||||
repo = "https://github.com/ANL-CEEESA/UnitCommitment.jl"
|
repo = "https://github.com/ANL-CEEESA/UnitCommitment.jl"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
|
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
|
||||||
@@ -31,6 +31,7 @@ julia = "1"
|
|||||||
[extras]
|
[extras]
|
||||||
Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76"
|
Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76"
|
||||||
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
||||||
|
Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b"
|
||||||
|
|
||||||
[targets]
|
[targets]
|
||||||
test = ["Cbc", "Test"]
|
test = ["Cbc", "Test", "Gurobi"]
|
||||||
|
|||||||
@@ -28,13 +28,14 @@ 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 penalties, optimization parameters, such as the length of the planning horizon and the time.
|
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.
|
||||||
|
|
||||||
| Key | Description | Default | Time series?
|
| Key | Description | Default | Time series?
|
||||||
| :----------------------------- | :------------------------------------------------ | :------: | :------------:
|
| :----------------------------- | :------------------------------------------------ | :------: | :------------:
|
||||||
| `Time horizon (h)` | Length of the planning horizon (in hours). | Required | N
|
| `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
|
| `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
|
| `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
|
||||||
|
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
@@ -42,7 +43,8 @@ This section describes system-wide parameters, such as power balance penalties,
|
|||||||
{
|
{
|
||||||
"Parameters": {
|
"Parameters": {
|
||||||
"Time horizon (h)": 4,
|
"Time horizon (h)": 4,
|
||||||
"Power balance penalty ($/MW)": 1000.0
|
"Power balance penalty ($/MW)": 1000.0,
|
||||||
|
"Reserve shortfall penalty ($/MW)": -1.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ for g in instance.units
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
### Modifying the model
|
### Fixing variables, modifying objective function and adding constraints
|
||||||
|
|
||||||
Since we now have a direct reference to the JuMP decision variables, it is possible to fix variables, change the coefficients in the objective function, or even add new constraints to the model before solving it. The script below shows how can this be accomplished. For more information on modifying an existing model, [see the JuMP documentation](https://jump.dev/JuMP.jl/stable/manual/variables/).
|
Since we now have a direct reference to the JuMP decision variables, it is possible to fix variables, change the coefficients in the objective function, or even add new constraints to the model before solving it. The script below shows how can this be accomplished. For more information on modifying an existing model, [see the JuMP documentation](https://jump.dev/JuMP.jl/stable/manual/variables/).
|
||||||
|
|
||||||
@@ -190,6 +190,54 @@ JuMP.set_objective_coefficient(
|
|||||||
UnitCommitment.optimize!(model)
|
UnitCommitment.optimize!(model)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Adding new component to a bus
|
||||||
|
|
||||||
|
The following snippet shows how to add a new grid component to a particular bus. For each time step, we create decision variables for the new grid component, add these variables to the objective function, then attach the component to a particular bus by modifying some existing model constraints.
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using Cbc
|
||||||
|
using JuMP
|
||||||
|
using UnitCommitment
|
||||||
|
|
||||||
|
# Load instance and build base model
|
||||||
|
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
||||||
|
model = UnitCommitment.build_model(
|
||||||
|
instance=instance,
|
||||||
|
optimizer=Cbc.Optimizer,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the number of time steps in the original instance
|
||||||
|
T = instance.time
|
||||||
|
|
||||||
|
# Create decision variables for the new grid component.
|
||||||
|
# In this example, we assume that the new component can
|
||||||
|
# inject up to 10 MW of power at each time step, so we
|
||||||
|
# create new continuous variables 0 ≤ x[t] ≤ 10.
|
||||||
|
@variable(model, x[1:T], lower_bound=0.0, upper_bound=10.0)
|
||||||
|
|
||||||
|
# For each time step
|
||||||
|
for t in 1:T
|
||||||
|
|
||||||
|
# Add production costs to the objective function.
|
||||||
|
# In this example, we assume a cost of $5/MW.
|
||||||
|
set_objective_coefficient(model, x[t], 5.0)
|
||||||
|
|
||||||
|
# Attach the new component to bus b1, by modifying the
|
||||||
|
# constraint `eq_net_injection`.
|
||||||
|
set_normalized_coefficient(
|
||||||
|
model[:eq_net_injection]["b1", t],
|
||||||
|
x[t],
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Solve the model
|
||||||
|
UnitCommitment.optimize!(model)
|
||||||
|
|
||||||
|
# Show optimal values for the x variables
|
||||||
|
@show value.(x)
|
||||||
|
```
|
||||||
|
|
||||||
References
|
References
|
||||||
----------
|
----------
|
||||||
* [KnOsWa20] **Bernard Knueven, James Ostrowski and Jean-Paul Watson.** "On Mixed-Integer Programming Formulations for the Unit Commitment Problem". INFORMS Journal on Computing (2020). [DOI: 10.1287/ijoc.2019.0944](https://doi.org/10.1287/ijoc.2019.0944)
|
* [KnOsWa20] **Bernard Knueven, James Ostrowski and Jean-Paul Watson.** "On Mixed-Integer Programming Formulations for the Unit Commitment Problem". INFORMS Journal on Computing (2020). [DOI: 10.1287/ijoc.2019.0944](https://doi.org/10.1287/ijoc.2019.0944)
|
||||||
|
|||||||
Binary file not shown.
@@ -98,6 +98,10 @@ function _from_json(json; repair = true)
|
|||||||
json["Parameters"]["Power balance penalty (\$/MW)"],
|
json["Parameters"]["Power balance penalty (\$/MW)"],
|
||||||
default = [1000.0 for t in 1:T],
|
default = [1000.0 for t in 1:T],
|
||||||
)
|
)
|
||||||
|
shortfall_penalty = timeseries(
|
||||||
|
json["Parameters"]["Reserve shortfall penalty (\$/MW)"],
|
||||||
|
default = [-1.0 for t in 1:T],
|
||||||
|
)
|
||||||
|
|
||||||
# Read buses
|
# Read buses
|
||||||
for (bus_name, dict) in json["Buses"]
|
for (bus_name, dict) in json["Buses"]
|
||||||
@@ -264,6 +268,7 @@ function _from_json(json; repair = true)
|
|||||||
instance = UnitCommitmentInstance(
|
instance = UnitCommitmentInstance(
|
||||||
T,
|
T,
|
||||||
power_balance_penalty,
|
power_balance_penalty,
|
||||||
|
shortfall_penalty,
|
||||||
units,
|
units,
|
||||||
buses,
|
buses,
|
||||||
lines,
|
lines,
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ end
|
|||||||
mutable struct UnitCommitmentInstance
|
mutable struct UnitCommitmentInstance
|
||||||
time::Int
|
time::Int
|
||||||
power_balance_penalty::Vector{Float64}
|
power_balance_penalty::Vector{Float64}
|
||||||
|
"Penalty for failing to meet reserve requirement."
|
||||||
|
shortfall_penalty::Vector{Float64}
|
||||||
units::Vector{Unit}
|
units::Vector{Unit}
|
||||||
buses::Vector{Bus}
|
buses::Vector{Bus}
|
||||||
lines::Vector{TransmissionLine}
|
lines::Vector{TransmissionLine}
|
||||||
|
|||||||
96
src/model/formulations/GenMorRam2017/startstop.jl
Normal file
96
src/model/formulations/GenMorRam2017/startstop.jl
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
"""
|
||||||
|
_add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
|
||||||
|
|
||||||
|
Startup and shutdown limits from Gentile et al. (2017).
|
||||||
|
Eqns. (20), (23a), and (23b) in Knueven et al. (2020).
|
||||||
|
|
||||||
|
Creates constraints `eq_startstop_limit`, `eq_startup_limit`, and `eq_shutdown_limit`
|
||||||
|
using variables `Gar1962.StatusVars`, `prod_above` from `Gar1962.ProdVars`, and `reserve`.
|
||||||
|
|
||||||
|
Constraints
|
||||||
|
---
|
||||||
|
* `eq_startstop_limit`
|
||||||
|
* `eq_startup_limit`
|
||||||
|
* `eq_shutdown_limit`
|
||||||
|
"""
|
||||||
|
function _add_startup_shutdown_limit_eqs!(
|
||||||
|
model::JuMP.Model,
|
||||||
|
g::Unit,
|
||||||
|
formulation_prod_vars::Gar1962.ProdVars,
|
||||||
|
formulation_status_vars::Gar1962.StatusVars,
|
||||||
|
)::Nothing
|
||||||
|
# TODO: Move upper case constants to model[:instance]
|
||||||
|
RESERVES_WHEN_START_UP = true
|
||||||
|
RESERVES_WHEN_RAMP_UP = true
|
||||||
|
RESERVES_WHEN_RAMP_DOWN = true
|
||||||
|
RESERVES_WHEN_SHUT_DOWN = true
|
||||||
|
|
||||||
|
eq_startstop_limit = _init(model, :eq_startstop_limit)
|
||||||
|
eq_shutdown_limit = _init(model, :eq_shutdown_limit)
|
||||||
|
eq_startup_limit = _init(model, :eq_startup_limit)
|
||||||
|
|
||||||
|
is_on = model[:is_on]
|
||||||
|
prod_above = model[:prod_above]
|
||||||
|
reserve = model[:reserve]
|
||||||
|
switch_off = model[:switch_off]
|
||||||
|
switch_on = model[:switch_on]
|
||||||
|
|
||||||
|
T = model[:instance].time
|
||||||
|
gi = g.name
|
||||||
|
|
||||||
|
if g.initial_power > g.shutdown_limit
|
||||||
|
eqs.shutdown_limit[gi, 0] =
|
||||||
|
@constraint(mip, vars.switch_off[gi, 1] <= 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
for t in 1:T
|
||||||
|
## 2020-10-09 amk: added eqn (20) and check of g.min_uptime
|
||||||
|
# Not present in (23) in Kneueven et al.
|
||||||
|
if g.min_uptime > 1
|
||||||
|
# Equation (20) in Knueven et al. (2020)
|
||||||
|
eqs.startstop_limit[gi, t] = @constraint(
|
||||||
|
model,
|
||||||
|
prod_above[gi, t] + reserve[gi, t] <=
|
||||||
|
(g.max_power[t] - g.min_power[t]) * is_on[gi, t] -
|
||||||
|
max(0, g.max_power[t] - g.startup_limit) * switch_on[gi, t] - (
|
||||||
|
t < T ?
|
||||||
|
max(0, g.max_power[t] - g.shutdown_limit) *
|
||||||
|
switch_off[gi, t+1] : 0.0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
## Startup limits
|
||||||
|
# Equation (23a) in Knueven et al. (2020)
|
||||||
|
eqs.startup_limit[gi, t] = @constraint(
|
||||||
|
model,
|
||||||
|
prod_above[gi, t] + reserve[gi, t] <=
|
||||||
|
(g.max_power[t] - g.min_power[t]) * is_on[gi, t] -
|
||||||
|
max(0, g.max_power[t] - g.startup_limit) * switch_on[gi, t] - (
|
||||||
|
t < T ?
|
||||||
|
max(0, g.startup_limit - g.shutdown_limit) *
|
||||||
|
switch_off[gi, t+1] : 0.0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
## Shutdown limits
|
||||||
|
if t < T
|
||||||
|
# Equation (23b) in Knueven et al. (2020)
|
||||||
|
eqs.shutdown_limit[gi, t] = @constraint(
|
||||||
|
model,
|
||||||
|
prod_above[gi, t] + reserve[gi, t] <=
|
||||||
|
(g.max_power[t] - g.min_power[t]) * xis_on[gi, t] - (
|
||||||
|
t < T ?
|
||||||
|
max(0, g.max_power[t] - g.shutdown_limit) *
|
||||||
|
switch_off[gi, t+1] : 0.0
|
||||||
|
) -
|
||||||
|
max(0, g.shutdown_limit - g.startup_limit) *
|
||||||
|
switch_on[gi, t]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -12,14 +12,14 @@ function _add_startup_cost_eqs!(
|
|||||||
S = length(g.startup_categories)
|
S = length(g.startup_categories)
|
||||||
startup = model[:startup]
|
startup = model[:startup]
|
||||||
for t in 1:model[:instance].time
|
for t in 1:model[:instance].time
|
||||||
for s in 1:S
|
# If unit is switching on, we must choose a startup category
|
||||||
# If unit is switching on, we must choose a startup category
|
eq_startup_choose[g.name, t] = @constraint(
|
||||||
eq_startup_choose[g.name, t, s] = @constraint(
|
model,
|
||||||
model,
|
model[:switch_on][g.name, t] ==
|
||||||
model[:switch_on][g.name, t] ==
|
sum(startup[g.name, t, s] for s in 1:S)
|
||||||
sum(startup[g.name, t, s] for s in 1:S)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
for s in 1:S
|
||||||
# If unit has not switched off in the last `delay` time periods, startup category is forbidden.
|
# If unit has not switched off in the last `delay` time periods, startup category is forbidden.
|
||||||
# The last startup category is always allowed.
|
# The last startup category is always allowed.
|
||||||
if s < S
|
if s < S
|
||||||
|
|||||||
89
src/model/formulations/MorLatRam2013/startstop.jl
Normal file
89
src/model/formulations/MorLatRam2013/startstop.jl
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
"""
|
||||||
|
_add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
|
||||||
|
|
||||||
|
Startup and shutdown limits from Morales-España et al. (2013a).
|
||||||
|
Eqns. (20), (21a), and (21b) in Knueven et al. (2020).
|
||||||
|
|
||||||
|
Uses variable `prod_above` from `Gar1962.ProdVars`, the variables in `Gar1962.StatusVars`, and `reserve`
|
||||||
|
to generate constraints below.
|
||||||
|
|
||||||
|
Constraints
|
||||||
|
---
|
||||||
|
* :eq_startstop_limit
|
||||||
|
* :eq_startup_limit
|
||||||
|
* :eq_shutdown_limit
|
||||||
|
"""
|
||||||
|
function _add_startup_shutdown_limit_eqs!(
|
||||||
|
model::JuMP.Model,
|
||||||
|
g::Unit,
|
||||||
|
formulation_prod_vars::Gar1962.ProdVars,
|
||||||
|
formulation_status_vars::Gar1962.StatusVars,
|
||||||
|
)::Nothing
|
||||||
|
# TODO: Move upper case constants to model[:instance]
|
||||||
|
RESERVES_WHEN_START_UP = true
|
||||||
|
RESERVES_WHEN_RAMP_UP = true
|
||||||
|
RESERVES_WHEN_RAMP_DOWN = true
|
||||||
|
RESERVES_WHEN_SHUT_DOWN = true
|
||||||
|
|
||||||
|
eq_startstop_limit = _init(model, :eq_startstop_limit)
|
||||||
|
eq_shutdown_limit = _init(model, :eq_shutdown_limit)
|
||||||
|
eq_startup_limit = _init(model, :eq_startup_limit)
|
||||||
|
|
||||||
|
is_on = model[:is_on]
|
||||||
|
prod_above = model[:prod_above]
|
||||||
|
reserve = model[:reserve]
|
||||||
|
switch_off = model[:switch_off]
|
||||||
|
switch_on = model[:switch_on]
|
||||||
|
|
||||||
|
T = model[:instance].time
|
||||||
|
gi = g.name
|
||||||
|
for t in 1:T
|
||||||
|
## 2020-10-09 amk: added eqn (20) and check of g.min_uptime
|
||||||
|
if g.min_uptime > 1 && t < T
|
||||||
|
# Equation (20) in Knueven et al. (2020)
|
||||||
|
# UT > 1 required, to guarantee that vars.switch_on[gi, t] and vars.switch_off[gi, t+1] are not both = 1 at the same time
|
||||||
|
eq_startstop_limit[gi, t] = @constraint(
|
||||||
|
model,
|
||||||
|
prod_above[gi, t] + reserve[gi, t] <=
|
||||||
|
(g.max_power[t] - g.min_power[t]) * is_on[gi, t] -
|
||||||
|
max(0, g.max_power[t] - g.startup_limit) * switch_on[gi, t] -
|
||||||
|
max(0, g.max_power[t] - g.shutdown_limit) * switch_off[gi, t+1]
|
||||||
|
)
|
||||||
|
else
|
||||||
|
## Startup limits
|
||||||
|
# Equation (21a) in Knueven et al. (2020)
|
||||||
|
# Proposed by Morales-España et al. (2013a)
|
||||||
|
eqs_startup_limit[gi, t] = @constraint(
|
||||||
|
model,
|
||||||
|
prod_above[gi, t] + reserve[gi, t] <=
|
||||||
|
(g.max_power[t] - g.min_power[t]) * is_on[gi, t] -
|
||||||
|
max(0, g.max_power[t] - g.startup_limit) * switch_on[gi, t]
|
||||||
|
)
|
||||||
|
|
||||||
|
## Shutdown limits
|
||||||
|
if t < T
|
||||||
|
# Equation (21b) in Knueven et al. (2020)
|
||||||
|
# TODO different from what was in previous model, due to reserve variable
|
||||||
|
# ax: ideally should have reserve_up and reserve_down variables
|
||||||
|
# i.e., the generator should be able to increase/decrease production as specified
|
||||||
|
# (this is a heuristic for a "robust" solution,
|
||||||
|
# in case there is an outage or a surge, and flow has to be redirected)
|
||||||
|
# amk: if shutdown_limit is the max prod of generator in time period before shutting down,
|
||||||
|
# then it makes sense to count reserves, because otherwise, if reserves ≠ 0,
|
||||||
|
# then the generator will actually produce more than the limit
|
||||||
|
eqs.shutdown_limit[gi, t] = @constraint(
|
||||||
|
model,
|
||||||
|
prod_above[gi, t] +
|
||||||
|
(RESERVES_WHEN_SHUT_DOWN ? reserve[gi, t] : 0.0) <=
|
||||||
|
(g.max_power[t] - g.min_power[t]) * is_on[gi, t] -
|
||||||
|
max(0, g.max_power[t] - g.shutdown_limit) *
|
||||||
|
switch_off[gi, t+1]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,15 +4,11 @@
|
|||||||
|
|
||||||
function _add_bus!(model::JuMP.Model, b::Bus)::Nothing
|
function _add_bus!(model::JuMP.Model, b::Bus)::Nothing
|
||||||
net_injection = _init(model, :expr_net_injection)
|
net_injection = _init(model, :expr_net_injection)
|
||||||
reserve = _init(model, :expr_reserve)
|
|
||||||
curtail = _init(model, :curtail)
|
curtail = _init(model, :curtail)
|
||||||
for t in 1:model[:instance].time
|
for t in 1:model[:instance].time
|
||||||
# Fixed load
|
# Fixed load
|
||||||
net_injection[b.name, t] = AffExpr(-b.load[t])
|
net_injection[b.name, t] = AffExpr(-b.load[t])
|
||||||
|
|
||||||
# Reserves
|
|
||||||
reserve[b.name, t] = AffExpr()
|
|
||||||
|
|
||||||
# Load curtailment
|
# Load curtailment
|
||||||
curtail[b.name, t] =
|
curtail[b.name, t] =
|
||||||
@variable(model, lower_bound = 0, upper_bound = b.load[t])
|
@variable(model, lower_bound = 0, upper_bound = b.load[t])
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ end
|
|||||||
function _add_net_injection_eqs!(model::JuMP.Model)::Nothing
|
function _add_net_injection_eqs!(model::JuMP.Model)::Nothing
|
||||||
T = model[:instance].time
|
T = model[:instance].time
|
||||||
net_injection = _init(model, :net_injection)
|
net_injection = _init(model, :net_injection)
|
||||||
eq_net_injection_def = _init(model, :eq_net_injection_def)
|
eq_net_injection = _init(model, :eq_net_injection)
|
||||||
eq_power_balance = _init(model, :eq_power_balance)
|
eq_power_balance = _init(model, :eq_power_balance)
|
||||||
for t in 1:T, b in model[:instance].buses
|
for t in 1:T, b in model[:instance].buses
|
||||||
n = net_injection[b.name, t] = @variable(model)
|
n = net_injection[b.name, t] = @variable(model)
|
||||||
eq_net_injection_def[t, b.name] =
|
eq_net_injection[b.name, t] =
|
||||||
@constraint(model, n == model[:expr_net_injection][b.name, t])
|
@constraint(model, -n + model[:expr_net_injection][b.name, t] == 0)
|
||||||
end
|
end
|
||||||
for t in 1:T
|
for t in 1:T
|
||||||
eq_power_balance[t] = @constraint(
|
eq_power_balance[t] = @constraint(
|
||||||
@@ -29,13 +29,28 @@ end
|
|||||||
|
|
||||||
function _add_reserve_eqs!(model::JuMP.Model)::Nothing
|
function _add_reserve_eqs!(model::JuMP.Model)::Nothing
|
||||||
eq_min_reserve = _init(model, :eq_min_reserve)
|
eq_min_reserve = _init(model, :eq_min_reserve)
|
||||||
for t in 1:model[:instance].time
|
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(
|
eq_min_reserve[t] = @constraint(
|
||||||
model,
|
model,
|
||||||
sum(
|
sum(model[:reserve][g.name, t] for g in instance.units) +
|
||||||
model[:expr_reserve][b.name, t] for b in model[:instance].buses
|
(shortfall_penalty >= 0 ? model[:reserve_shortfall][t] : 0.0) >=
|
||||||
) >= model[:instance].reserves.spinning[t]
|
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],
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ function _add_unit!(model::JuMP.Model, g::Unit, formulation::Formulation)
|
|||||||
formulation.status_vars,
|
formulation.status_vars,
|
||||||
)
|
)
|
||||||
_add_startup_cost_eqs!(model, g, formulation.startup_costs)
|
_add_startup_cost_eqs!(model, g, formulation.startup_costs)
|
||||||
_add_startup_shutdown_limit_eqs!(model, g)
|
_add_startup_shutdown_limit_eqs!(
|
||||||
|
model,
|
||||||
|
g,
|
||||||
|
formulation.prod_vars,
|
||||||
|
formulation.status_vars,
|
||||||
|
)
|
||||||
_add_status_eqs!(model, g, formulation.status_vars)
|
_add_status_eqs!(model, g, formulation.status_vars)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -44,12 +49,16 @@ _is_initially_on(g::Unit)::Float64 = (g.initial_status > 0 ? 1.0 : 0.0)
|
|||||||
|
|
||||||
function _add_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing
|
function _add_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing
|
||||||
reserve = _init(model, :reserve)
|
reserve = _init(model, :reserve)
|
||||||
|
reserve_shortfall = _init(model, :reserve_shortfall)
|
||||||
for t in 1:model[:instance].time
|
for t in 1:model[:instance].time
|
||||||
if g.provides_spinning_reserves[t]
|
if g.provides_spinning_reserves[t]
|
||||||
reserve[g.name, t] = @variable(model, lower_bound = 0)
|
reserve[g.name, t] = @variable(model, lower_bound = 0)
|
||||||
else
|
else
|
||||||
reserve[g.name, t] = 0.0
|
reserve[g.name, t] = 0.0
|
||||||
end
|
end
|
||||||
|
reserve_shortfall[t] =
|
||||||
|
(model[:instance].shortfall_penalty[t] >= 0) ?
|
||||||
|
@variable(model, lower_bound = 0) : 0.0
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -72,7 +81,22 @@ function _add_startup_shutdown_vars!(model::JuMP.Model, g::Unit)::Nothing
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
|
"""
|
||||||
|
_add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
|
||||||
|
|
||||||
|
Creates startup/shutdown limit constraints below based on variables `Gar1962.StatusVars`, `prod_above` from `Gar1962.ProdVars`, and `reserve`.
|
||||||
|
|
||||||
|
Constraints
|
||||||
|
---
|
||||||
|
* :eq_startup_limit
|
||||||
|
* :eq_shutdown_limit
|
||||||
|
"""
|
||||||
|
function _add_startup_shutdown_limit_eqs!(
|
||||||
|
model::JuMP.Model,
|
||||||
|
g::Unit,
|
||||||
|
formulation_prod_vars::Gar1962.ProdVars,
|
||||||
|
formulation_status_vars::Gar1962.StatusVars,
|
||||||
|
)::Nothing
|
||||||
eq_shutdown_limit = _init(model, :eq_shutdown_limit)
|
eq_shutdown_limit = _init(model, :eq_shutdown_limit)
|
||||||
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]
|
||||||
@@ -210,11 +234,5 @@ function _add_net_injection_eqs!(model::JuMP.Model, g::Unit)::Nothing
|
|||||||
model[:is_on][g.name, t],
|
model[:is_on][g.name, t],
|
||||||
g.min_power[t],
|
g.min_power[t],
|
||||||
)
|
)
|
||||||
# Add to reserves expression
|
|
||||||
add_to_expression!(
|
|
||||||
model[:expr_reserve][g.bus.name, t],
|
|
||||||
model[:reserve][g.name, t],
|
|
||||||
1.0,
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ function solution(model::JuMP.Model)::OrderedDict
|
|||||||
sol["Switch on"] = timeseries(model[:switch_on], instance.units)
|
sol["Switch on"] = timeseries(model[:switch_on], instance.units)
|
||||||
sol["Switch off"] = timeseries(model[:switch_off], instance.units)
|
sol["Switch off"] = timeseries(model[:switch_off], instance.units)
|
||||||
sol["Reserve (MW)"] = timeseries(model[:reserve], instance.units)
|
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
|
||||||
|
)
|
||||||
sol["Net injection (MW)"] =
|
sol["Net injection (MW)"] =
|
||||||
timeseries(model[:net_injection], instance.buses)
|
timeseries(model[:net_injection], instance.buses)
|
||||||
sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses)
|
sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses)
|
||||||
|
|||||||
@@ -208,12 +208,8 @@ function _validate_units(instance, solution; tol = 0.01)
|
|||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if t == time_down + 1
|
if (t == time_down + 1) && (unit.initial_status < 0)
|
||||||
initial_down = unit.min_downtime
|
time_down -= unit.initial_status
|
||||||
if unit.initial_status < 0
|
|
||||||
initial_down = -unit.initial_status
|
|
||||||
end
|
|
||||||
time_down += initial_down
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Calculate startup costs
|
# Calculate startup costs
|
||||||
@@ -246,14 +242,6 @@ function _validate_units(instance, solution; tol = 0.01)
|
|||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if t == time_up + 1
|
|
||||||
initial_up = unit.min_uptime
|
|
||||||
if unit.initial_status > 0
|
|
||||||
initial_up = unit.initial_status
|
|
||||||
end
|
|
||||||
time_up += initial_up
|
|
||||||
end
|
|
||||||
|
|
||||||
if (t == time_up + 1) && (unit.initial_status > 0)
|
if (t == time_up + 1) && (unit.initial_status > 0)
|
||||||
time_up += unit.initial_status
|
time_up += unit.initial_status
|
||||||
end
|
end
|
||||||
@@ -336,11 +324,16 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
|
|||||||
# Verify spinning reserves
|
# Verify spinning reserves
|
||||||
reserve =
|
reserve =
|
||||||
sum(solution["Reserve (MW)"][g.name][t] for g in instance.units)
|
sum(solution["Reserve (MW)"][g.name][t] for g in instance.units)
|
||||||
if reserve < instance.reserves.spinning[t] - tol
|
reserve_shortfall =
|
||||||
|
(instance.shortfall_penalty[t] >= 0) ?
|
||||||
|
solution["Reserve shortfall (MW)"][t] : 0
|
||||||
|
|
||||||
|
if reserve + reserve_shortfall < instance.reserves.spinning[t] - tol
|
||||||
@error @sprintf(
|
@error @sprintf(
|
||||||
"Insufficient spinning reserves at time %d (%.2f should be %.2f)",
|
"Insufficient spinning reserves at time %d (%.2f + %.2f should be %.2f)",
|
||||||
t,
|
t,
|
||||||
reserve,
|
reserve,
|
||||||
|
reserve_shortfall,
|
||||||
instance.reserves.spinning[t],
|
instance.reserves.spinning[t],
|
||||||
)
|
)
|
||||||
err_count += 1
|
err_count += 1
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
using UnitCommitment
|
using UnitCommitment
|
||||||
|
using JuMP
|
||||||
import UnitCommitment:
|
import UnitCommitment:
|
||||||
ArrCon2000,
|
ArrCon2000,
|
||||||
CarArr2006,
|
CarArr2006,
|
||||||
@@ -11,17 +12,55 @@ import UnitCommitment:
|
|||||||
Gar1962,
|
Gar1962,
|
||||||
KnuOstWat2018,
|
KnuOstWat2018,
|
||||||
MorLatRam2013,
|
MorLatRam2013,
|
||||||
PanGua2016
|
PanGua2016,
|
||||||
|
XavQiuWanThi2019
|
||||||
|
|
||||||
function _test(formulation::Formulation)::Nothing
|
if ENABLE_LARGE_TESTS
|
||||||
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
using Gurobi
|
||||||
UnitCommitment.build_model(instance = instance, formulation = formulation) # should not crash
|
end
|
||||||
|
|
||||||
|
function _small_test(formulation::Formulation)::Nothing
|
||||||
|
instances = ["matpower/case118/2017-02-01", "test/case14"]
|
||||||
|
for instance in instances
|
||||||
|
# Should not crash
|
||||||
|
UnitCommitment.build_model(
|
||||||
|
instance = UnitCommitment.read_benchmark(instance),
|
||||||
|
formulation = formulation,
|
||||||
|
)
|
||||||
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function _large_test(formulation::Formulation)::Nothing
|
||||||
|
instances = ["pglib-uc/ca/Scenario400_reserves_1"]
|
||||||
|
for instance in instances
|
||||||
|
instance = UnitCommitment.read_benchmark(instance)
|
||||||
|
model = UnitCommitment.build_model(
|
||||||
|
instance = instance,
|
||||||
|
formulation = formulation,
|
||||||
|
optimizer = Gurobi.Optimizer,
|
||||||
|
)
|
||||||
|
UnitCommitment.optimize!(
|
||||||
|
model,
|
||||||
|
XavQiuWanThi2019.Method(two_phase_gap = false, gap_limit = 0.1),
|
||||||
|
)
|
||||||
|
solution = UnitCommitment.solution(model)
|
||||||
|
@test UnitCommitment.validate(instance, solution)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
function _test(formulation::Formulation)::Nothing
|
||||||
|
_small_test(formulation)
|
||||||
|
if ENABLE_LARGE_TESTS
|
||||||
|
_large_test(formulation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@testset "formulations" begin
|
@testset "formulations" begin
|
||||||
|
_test(Formulation())
|
||||||
_test(Formulation(ramping = ArrCon2000.Ramping()))
|
_test(Formulation(ramping = ArrCon2000.Ramping()))
|
||||||
_test(Formulation(ramping = DamKucRajAta2016.Ramping()))
|
# _test(Formulation(ramping = DamKucRajAta2016.Ramping()))
|
||||||
_test(
|
_test(
|
||||||
Formulation(
|
Formulation(
|
||||||
ramping = MorLatRam2013.Ramping(),
|
ramping = MorLatRam2013.Ramping(),
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ using UnitCommitment
|
|||||||
|
|
||||||
UnitCommitment._setup_logger()
|
UnitCommitment._setup_logger()
|
||||||
|
|
||||||
|
const ENABLE_LARGE_TESTS = ("UCJL_LARGE_TESTS" in keys(ENV))
|
||||||
|
|
||||||
@testset "UnitCommitment" begin
|
@testset "UnitCommitment" begin
|
||||||
include("usage.jl")
|
include("usage.jl")
|
||||||
@testset "import" begin
|
@testset "import" begin
|
||||||
|
|||||||
Reference in New Issue
Block a user