mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 00:08:52 -06:00
Compare commits
41 Commits
gh-actions
...
v0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 6573bb7ea2 | |||
| 1769f2a932 | |||
| 4dc39363e8 | |||
| 5fef01cd99 | |||
| 18daaf5358 | |||
| b68b4ff9e4 | |||
| 6e30645084 | |||
| 678e6aa2f5 | |||
| fd25580967 | |||
| dc693896a3 | |||
| ddebcc6ddb | |||
| 3282e5bc3a | |||
| 15de1901c8 | |||
| bf2dc4ddc4 | |||
| 5c3c8f0d63 | |||
| cce6a874b9 | |||
| 1ce1cddaf3 | |||
| 46d754dbcf | |||
| b7d9083335 | |||
| 86ae1d0429 | |||
| 58a7567c16 | |||
| 2367e5a348 | |||
| 74b8a8ae2c | |||
| 3260fa29ad | |||
| 3b1d2d1845 | |||
| db106f1a38 | |||
| 16b0fec6cd | |||
| cda1e368fe | |||
| 099fb4e3cb | |||
|
|
b4bc50c865 | ||
|
|
febb4f1aad | ||
|
|
8988b00b07 | ||
|
|
0046c4ca2a | ||
| 72f659b9ff | |||
| 360308ef4a | |||
| 03268dd3df | |||
|
|
a3a71ff5a9 | ||
| 5ca566f147 | |||
| 3220650e39 | |||
| ca0d250dfa | |||
| 2bd68b49a5 |
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
julia-version: ['1.4', '1.5', '1.6']
|
||||
julia-version: ['1.6', '1.7']
|
||||
julia-arch: [x64]
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
exclude:
|
||||
|
||||
33
.gitignore
vendored
33
.gitignore
vendored
@@ -1,21 +1,38 @@
|
||||
*.bak
|
||||
*.gz
|
||||
*.lastrun
|
||||
*.so
|
||||
*.mps
|
||||
*.ipynb
|
||||
*.lastrun
|
||||
*.mps
|
||||
*.so
|
||||
*/Manifest.toml
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
.AppleDouble
|
||||
.DS_Store
|
||||
.DocumentRevisions-V100
|
||||
.LSOverride
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
._*
|
||||
.apdisk
|
||||
.com.apple.timemachine.donotpresent
|
||||
.fseventsd
|
||||
.ipy*
|
||||
.vscode
|
||||
Icon
|
||||
Manifest.toml
|
||||
Network Trash Folder
|
||||
TODO.md
|
||||
Temporary Items
|
||||
benchmark/results
|
||||
benchmark/runs
|
||||
benchmark/tables
|
||||
benchmark/tmp.json
|
||||
build
|
||||
docs/_build
|
||||
instances/**/*.json
|
||||
instances/_source
|
||||
local
|
||||
notebooks
|
||||
TODO.md
|
||||
docs/_build
|
||||
.vscode
|
||||
Manifest.toml
|
||||
*/Manifest.toml
|
||||
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -11,6 +11,21 @@ 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
|
||||
|
||||
## [0.3.0] - 2022-07-18
|
||||
### Added
|
||||
- Add support for multiple reserve products and zonal reserves.
|
||||
- Add flexiramp reserve products, following WanHob2016's formulation (@oyurdakul, #21).
|
||||
- Add 365 variations for each MATPOWER instance, corresponding to each day of the year.
|
||||
|
||||
### Changed
|
||||
- To support multiple/zonal reserves, 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
|
||||
- To allow new versions of UnitCommitment.jl to read old instance files, a new required field `Version` has been added to the `Parameters` section. To load v0.2 files in v0.3, please add `{"Parameters":{"Version":"0.2"}}` to the file.
|
||||
- Benchmark test cases are now downloaded on-the-fly as needed, instead of being stored in our GitHub repository. Test cases can also be directly downloaded from: https://axavier.org/UnitCommitment.jl/
|
||||
|
||||
|
||||
## [0.2.2] - 2021-07-21
|
||||
### Fixed
|
||||
- Fix small bug in validation scripts related to startup costs
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright © 2020, UChicago Argonne, LLC
|
||||
Copyright © 2020-2022, UChicago Argonne, LLC
|
||||
|
||||
All Rights Reserved
|
||||
|
||||
|
||||
8
Makefile
8
Makefile
@@ -2,14 +2,14 @@
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
VERSION := 0.2
|
||||
VERSION := 0.3
|
||||
|
||||
clean:
|
||||
rm -rfv build
|
||||
rm -rfv build Manifest.toml test/Manifest.toml deps/formatter/build deps/formatter/Manifest.toml
|
||||
|
||||
docs:
|
||||
cd docs; make clean; make dirhtml
|
||||
rsync -avP --delete-after docs/_build/dirhtml/ ../docs/$(VERSION)/
|
||||
cd docs; julia --project=. make.jl; cd ..
|
||||
rsync -avP --delete-after docs/build/ ../docs/$(VERSION)/
|
||||
|
||||
format:
|
||||
cd deps/formatter; ../../juliaw format.jl
|
||||
|
||||
@@ -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"
|
||||
@@ -24,7 +24,7 @@ DataStructures = "0.18"
|
||||
Distributions = "0.25"
|
||||
GZip = "0.5"
|
||||
JSON = "0.21"
|
||||
JuMP = "0.21"
|
||||
MathOptInterface = "0.9"
|
||||
JuMP = "1"
|
||||
MathOptInterface = "1"
|
||||
PackageCompiler = "1"
|
||||
julia = "1"
|
||||
|
||||
16
README.md
16
README.md
@@ -87,14 +87,16 @@ UnitCommitment.write("/tmp/output.json", solution)
|
||||
|
||||
## Documentation
|
||||
|
||||
1. [Usage](https://anl-ceeesa.github.io/UnitCommitment.jl/0.2/usage/)
|
||||
2. [Data Format](https://anl-ceeesa.github.io/UnitCommitment.jl/0.2/format/)
|
||||
3. [Instances](https://anl-ceeesa.github.io/UnitCommitment.jl/0.2/instances/)
|
||||
4. [JuMP Model](https://anl-ceeesa.github.io/UnitCommitment.jl/0.2/model/)
|
||||
1. [Usage](https://anl-ceeesa.github.io/UnitCommitment.jl/0.3/usage/)
|
||||
2. [Data Format](https://anl-ceeesa.github.io/UnitCommitment.jl/0.3/format/)
|
||||
3. [Instances](https://anl-ceeesa.github.io/UnitCommitment.jl/0.3/instances/)
|
||||
4. [JuMP Model](https://anl-ceeesa.github.io/UnitCommitment.jl/0.3/model/)
|
||||
5. [API Reference](https://anl-ceeesa.github.io/UnitCommitment.jl/0.3/api/)
|
||||
|
||||
## Authors
|
||||
* **Alinson S. Xavier** (Argonne National Laboratory)
|
||||
* **Aleksandr M. Kazachkov** (University of Florida)
|
||||
* **Ogün Yurdakul** (Technische Universität Berlin)
|
||||
* **Feng Qiu** (Argonne National Laboratory)
|
||||
|
||||
## Acknowledgments
|
||||
@@ -109,15 +111,15 @@ UnitCommitment.write("/tmp/output.json", solution)
|
||||
|
||||
If you use UnitCommitment.jl in your research (instances, models or algorithms), we kindly request that you cite the package as follows:
|
||||
|
||||
* **Alinson S. Xavier, Aleksandr M. Kazachkov, Feng Qiu**. "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment". Zenodo (2020). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874).
|
||||
* **Alinson S. Xavier, Aleksandr M. Kazachkov, Ogün Yurdakul, Feng Qiu**. "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment (Version 0.3)". Zenodo (2022). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874).
|
||||
|
||||
If you use the instances, we additionally request that you cite the original sources, as described in the [instances page](docs/instances.md).
|
||||
If you use the instances, we additionally request that you cite the original sources, as described in the documentation.
|
||||
|
||||
## License
|
||||
|
||||
```text
|
||||
UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment
|
||||
Copyright © 2020-2021, UChicago Argonne, LLC. All Rights Reserved.
|
||||
Copyright © 2020-2022, UChicago Argonne, LLC. All Rights Reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
provided that the following conditions are met:
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
4
docs/Project.toml
Normal file
4
docs/Project.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[deps]
|
||||
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
|
||||
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||
UnitCommitment = "64606440-39ea-11e9-0f29-3303a1d3d877"
|
||||
49
docs/_static/custom.css
vendored
49
docs/_static/custom.css
vendored
@@ -1,49 +0,0 @@
|
||||
h1.site-logo {
|
||||
font-size: 30px !important;
|
||||
}
|
||||
|
||||
h1.site-logo small {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
|
||||
h1.site-logo {
|
||||
font-size: 30px !important;
|
||||
}
|
||||
|
||||
h1.site-logo small {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
|
||||
tbody, thead, pre {
|
||||
border: 1px solid rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
table td, th {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
table p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
table td code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table tr,
|
||||
table th {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
table tr:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
box-shadow: inherit !important;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.text-align\:center {
|
||||
text-align: center;
|
||||
}
|
||||
16
docs/conf.py
16
docs/conf.py
@@ -1,16 +0,0 @@
|
||||
project = "UnitCommitment.jl"
|
||||
copyright = "2020-2021, UChicago Argonne, LLC"
|
||||
author = ""
|
||||
release = "0.2"
|
||||
extensions = ["myst_parser"]
|
||||
templates_path = ["_templates"]
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
html_theme = "sphinx_book_theme"
|
||||
html_static_path = ["_static"]
|
||||
html_css_files = ["custom.css"]
|
||||
html_theme_options = {
|
||||
"repository_url": "https://github.com/ANL-CEEESA/UnitCommitment.jl/",
|
||||
"use_repository_button": True,
|
||||
"extra_navbar": "",
|
||||
}
|
||||
html_title = f"UnitCommitment.jl<br/><small>{release}</small>"
|
||||
16
docs/make.jl
Normal file
16
docs/make.jl
Normal file
@@ -0,0 +1,16 @@
|
||||
using Documenter, UnitCommitment
|
||||
|
||||
makedocs(
|
||||
sitename="UnitCommitment.jl",
|
||||
pages=[
|
||||
"Home" => "index.md",
|
||||
"usage.md",
|
||||
"format.md",
|
||||
"instances.md",
|
||||
"model.md",
|
||||
"api.md",
|
||||
],
|
||||
format = Documenter.HTML(
|
||||
assets=["assets/custom.css"],
|
||||
)
|
||||
)
|
||||
48
docs/src/api.md
Normal file
48
docs/src/api.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# API Reference
|
||||
|
||||
## Read data, build model & optimize
|
||||
|
||||
```@docs
|
||||
UnitCommitment.read
|
||||
UnitCommitment.read_benchmark
|
||||
UnitCommitment.build_model
|
||||
UnitCommitment.optimize!
|
||||
UnitCommitment.solution
|
||||
UnitCommitment.validate
|
||||
UnitCommitment.write
|
||||
```
|
||||
|
||||
## Modify instance
|
||||
|
||||
```@docs
|
||||
UnitCommitment.slice
|
||||
UnitCommitment.randomize!(::UnitCommitment.UnitCommitmentInstance)
|
||||
UnitCommitment.generate_initial_conditions!
|
||||
```
|
||||
|
||||
## Formulations
|
||||
|
||||
```@docs
|
||||
UnitCommitment.Formulation
|
||||
UnitCommitment.ShiftFactorsFormulation
|
||||
UnitCommitment.ArrCon2000
|
||||
UnitCommitment.CarArr2006
|
||||
UnitCommitment.DamKucRajAta2016
|
||||
UnitCommitment.Gar1962
|
||||
UnitCommitment.KnuOstWat2018
|
||||
UnitCommitment.MorLatRam2013
|
||||
UnitCommitment.PanGua2016
|
||||
UnitCommitment.WanHob2016
|
||||
```
|
||||
|
||||
## Solution Methods
|
||||
|
||||
```@docs
|
||||
UnitCommitment.XavQiuWanThi2019.Method
|
||||
```
|
||||
|
||||
## Randomization Methods
|
||||
|
||||
```@docs
|
||||
UnitCommitment.XavQiuAhm2021.Randomization
|
||||
```
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
36
docs/src/assets/custom.css
Normal file
36
docs/src/assets/custom.css
Normal file
@@ -0,0 +1,36 @@
|
||||
@media screen and (min-width: 1056px) {
|
||||
#documenter .docs-main {
|
||||
max-width: 65rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tbody, thead, pre {
|
||||
border: 1px solid rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
table td, th {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
table p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
table td code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table tr,
|
||||
table th {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
table tr:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: transparent;
|
||||
color: rgb(232, 62, 140);
|
||||
}
|
||||
@@ -1,50 +1,40 @@
|
||||
```{sectnum}
|
||||
---
|
||||
start: 2
|
||||
depth: 2
|
||||
suffix: .
|
||||
---
|
||||
```
|
||||
|
||||
|
||||
Data Format
|
||||
===========
|
||||
|
||||
|
||||
Input Data Format
|
||||
-----------------
|
||||
|
||||
Instances are specified by JSON files containing the following main sections:
|
||||
|
||||
* Parameters
|
||||
* Buses
|
||||
* Generators
|
||||
* Price-sensitive loads
|
||||
* Transmission lines
|
||||
* Reserves
|
||||
* Contingencies
|
||||
* [Parameters](#Parameters)
|
||||
* [Buses](#Buses)
|
||||
* [Generators](#Generators)
|
||||
* [Price-sensitive loads](#Price-sensitive-loads)
|
||||
* [Transmission lines](#Transmission-lines)
|
||||
* [Reserves](#Reserves)
|
||||
* [Contingencies](#Contingencies)
|
||||
|
||||
Each section is described in detail below. For a complete example, see [case14](https://github.com/ANL-CEEESA/UnitCommitment.jl/tree/dev/instances/matpower/case14).
|
||||
Each section is described in detail below. See [case118/2017-01-01.json.gz](https://axavier.org/UnitCommitment.jl/0.3/instances/matpower/case118/2017-01-01.json.gz) for a complete example.
|
||||
|
||||
### 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?
|
||||
| :----------------------------- | :------------------------------------------------ | :------: | :------------:
|
||||
| `Version` | Version of UnitCommitment.jl this file was written for. Required to ensure that the file remains readable in future versions of the package. If you are following this page to construct the file, this field should equal `0.3`. | 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
|
||||
| `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
|
||||
```json
|
||||
{
|
||||
"Parameters": {
|
||||
"Version": "0.3",
|
||||
"Time horizon (h)": 4,
|
||||
"Power balance penalty ($/MW)": 1000.0,
|
||||
"Reserve shortfall penalty ($/MW)": -1.0
|
||||
"Power balance penalty ($/MW)": 1000.0
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -96,18 +86,20 @@ 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
|
||||
| `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
|
||||
|
||||
Production costs are represented as piecewise-linear curves. Figure 1 shows an example cost curve with three segments, where it costs \$1400, \$1600, \$2200 and \$2400 to generate, respectively, 100, 110, 130 and 135 MW of power. To model this generator, `Production cost curve (MW)` should be set to `[100, 110, 130, 135]`, and `Production cost curve ($)` should be set to `[1400, 1600, 2200, 2400]`.
|
||||
Note that this curve also specifies the production limits. Specifically, the first point identifies the minimum power output when the unit is operational, while the last point identifies the maximum power output.
|
||||
|
||||
```@raw html
|
||||
<center>
|
||||
<img src="../_static/cost_curve.png" style="max-width: 500px"/>
|
||||
<img src="../assets/cost_curve.png" style="max-width: 500px"/>
|
||||
<div><b>Figure 1.</b> Piecewise-linear production cost curve.</div>
|
||||
<br/>
|
||||
</center>
|
||||
```
|
||||
|
||||
#### Additional remarks:
|
||||
|
||||
@@ -135,13 +127,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,
|
||||
"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"],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,7 +163,7 @@ This section describes components in the system which may increase or reduce the
|
||||
}
|
||||
```
|
||||
|
||||
### Transmission Lines
|
||||
### Transmission lines
|
||||
|
||||
This section describes the characteristics of transmission system, such as its topology and the susceptance of each transmission line.
|
||||
|
||||
@@ -206,24 +198,39 @@ This section describes the characteristics of transmission system, such as its t
|
||||
|
||||
### Reserves
|
||||
|
||||
This section describes the hourly amount of operating reserves required.
|
||||
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
|
||||
| `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
|
||||
#### Example 1
|
||||
|
||||
```json
|
||||
{
|
||||
"Reserves": {
|
||||
"Spinning (MW)": [
|
||||
57.30552,
|
||||
53.88429,
|
||||
51.31838,
|
||||
50.46307
|
||||
]
|
||||
"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
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -286,9 +293,8 @@ 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.
|
||||
* 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.
|
||||
|
||||
@@ -6,24 +6,23 @@
|
||||
|
||||
* **Data Format:** The package proposes an extensible and fully-documented JSON-based data specification format for SCUC, developed in collaboration with Independent System Operators (ISOs), which describes the most important aspects of the problem. The format supports all the most common generator characteristics (including ramping, piecewise-linear production cost curves and time-dependent startup costs), as well as operating reserves, price-sensitive loads, transmission networks and contingencies.
|
||||
* **Benchmark Instances:** The package provides a diverse collection of large-scale benchmark instances collected from the literature, converted into a common data format, and extended using data-driven methods to make them more challenging and realistic.
|
||||
* **Model Implementation**: The package provides a Julia/JuMP implementations of state-of-the-art formulations and solution methods for SCUC, including multiple ramping formulations ([ArrCon2000][ArrCon2000], [MorLatRam2013][MorLatRam2013], [DamKucRajAta2016][DamKucRajAta2016], [PanGua2016][PanGua2016]), multiple piecewise-linear costs formulations ([Gar1962][Gar1962], [CarArr2006][CarArr2006], [KnuOstWat2018][KnuOstWat2018]) and contingency screening methods ([XavQiuWanThi2019][XavQiuWanThi2019]). Our goal is to keep these implementations up-to-date as new methods are proposed in the literature.
|
||||
* **Model Implementation**: The package provides a Julia/JuMP implementations of state-of-the-art formulations and solution methods for SCUC, including multiple ramping formulations ([ArrCon2000](https://doi.org/10.1109/59.871739), [MorLatRam2013](https://doi.org/10.1109/TPWRS.2013.2251373), [DamKucRajAta2016](https://doi.org/10.1007/s10107-015-0919-9), [PanGua2016](https://doi.org/10.1287/opre.2016.1520)), multiple piecewise-linear costs formulations ([Gar1962](https://doi.org/10.1109/AIEEPAS.1962.4501405), [CarArr2006](https://doi.org/10.1109/TPWRS.2006.876672), [KnuOstWat2018](https://doi.org/10.1109/TPWRS.2017.2783850)) and contingency screening methods ([XavQiuWanThi2019](https://doi.org/10.1109/TPWRS.2019.2892620)). Our goal is to keep these implementations up-to-date as new methods are proposed in the literature.
|
||||
* **Benchmark Tools:** The package provides automated benchmark scripts to accurately evaluate the performance impact of proposed code changes.
|
||||
|
||||
[ArrCon2000]: https://doi.org/10.1109/59.871739
|
||||
[CarArr2006]: https://doi.org/10.1109/TPWRS.2006.876672
|
||||
[DamKucRajAta2016]: https://doi.org/10.1007/s10107-015-0919-9
|
||||
[Gar1962]: https://doi.org/10.1109/AIEEPAS.1962.4501405
|
||||
[KnuOstWat2018]: https://doi.org/10.1109/TPWRS.2017.2783850
|
||||
[MorLatRam2013]: https://doi.org/10.1109/TPWRS.2013.2251373
|
||||
[PanGua2016]: https://doi.org/10.1287/opre.2016.1520
|
||||
[XavQiuWanThi2019]: https://doi.org/10.1109/TPWRS.2019.2892620
|
||||
## Table of Contents
|
||||
|
||||
### Authors
|
||||
```@contents
|
||||
Pages = ["usage.md", "format.md", "instances.md", "model.md", "api.md"]
|
||||
Depth = 3
|
||||
```
|
||||
|
||||
## Authors
|
||||
* **Alinson S. Xavier** (Argonne National Laboratory)
|
||||
* **Aleksandr M. Kazachkov** (University of Florida)
|
||||
* **Ogün Yurdakul** (Technische Universität Berlin)
|
||||
* **Feng Qiu** (Argonne National Laboratory)
|
||||
|
||||
### Acknowledgments
|
||||
## Acknowledgments
|
||||
|
||||
* We would like to thank **Yonghong Chen** (Midcontinent Independent System Operator), **Feng Pan** (Pacific Northwest National Laboratory) for valuable feedback on early versions of this package.
|
||||
|
||||
@@ -31,19 +30,19 @@
|
||||
|
||||
* Based upon work supported by the **U.S. Department of Energy Advanced Grid Modeling Program** under Grant DE-OE0000875.
|
||||
|
||||
### Citing
|
||||
## Citing
|
||||
|
||||
If you use UnitCommitment.jl in your research (instances, models or algorithms), we kindly request that you cite the package as follows:
|
||||
|
||||
* **Alinson S. Xavier, Aleksandr M. Kazachkov, Feng Qiu**, "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment". Zenodo (2020). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874).
|
||||
* **Alinson S. Xavier, Aleksandr M. Kazachkov, Ogün Yurdakul, Feng Qiu**, "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment (Version 0.3)". Zenodo (2022). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874).
|
||||
|
||||
If you use the instances, we additionally request that you cite the original sources, as described in the [instances page](instances.md).
|
||||
|
||||
### License
|
||||
## License
|
||||
|
||||
```text
|
||||
UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment
|
||||
Copyright © 2020, UChicago Argonne, LLC. All Rights Reserved.
|
||||
Copyright © 2020-2022, UChicago Argonne, LLC. All Rights Reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
provided that the following conditions are met:
|
||||
@@ -67,16 +66,3 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING N
|
||||
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
|
||||
## Site contents
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 2
|
||||
---
|
||||
usage.md
|
||||
format.md
|
||||
instances.md
|
||||
model.md
|
||||
```
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
```{sectnum}
|
||||
---
|
||||
start: 3
|
||||
depth: 2
|
||||
suffix: .
|
||||
---
|
||||
```
|
||||
|
||||
Instances
|
||||
=========
|
||||
|
||||
UnitCommitment.jl provides a large collection of benchmark instances collected from the literature and converted to a [common data format](format.md). In some cases, as indicated below, the original instances have been extended, with realistic parameters, using data-driven methods. If you use these instances in your research, we request that you cite UnitCommitment.jl, as well as the original sources, as listed below. Benchmark instances can be loaded with `UnitCommitment.read_benchmark(name)`, as explained in the [usage section](usage.md).
|
||||
UnitCommitment.jl provides a large collection of benchmark instances collected from the literature and converted to a [common data format](format.md). In some cases, as indicated below, the original instances have been extended, with realistic parameters, using data-driven methods. If you use these instances in your research, we request that you cite UnitCommitment.jl, as well as the original sources, as listed below. Benchmark instances can be loaded with `UnitCommitment.read_benchmark(name)`, as explained in the [usage section](usage.md). Instance files can also be [directly downloaded from our website](https://axavier.org/UnitCommitment.jl/0.3/instances/).
|
||||
|
||||
```{warning}
|
||||
The instances included in UC.jl are still under development and may change in the future. If you use these instances in your research, for reproducibility, you should specify what version of UC.jl they came from.
|
||||
```
|
||||
!!! warning
|
||||
|
||||
The instances included in UC.jl are still under development and may change in the future. If you use these instances in your research, for reproducibility, you should specify what version of UC.jl they came from.
|
||||
|
||||
|
||||
MATPOWER
|
||||
@@ -33,7 +25,7 @@ Because most MATPOWER test cases were originally designed for power flow studies
|
||||
|
||||
* **Contingencies** were set to include all N-1 transmission line contingencies that do not generate islands or isolated buses. More specifically, there is one contingency for each transmission line, as long as that transmission line is not a bridge in the network graph.
|
||||
|
||||
For each MATPOWER test case, UC.jl provides two variations (`2017-02-01` and `2017-08-01`) corresponding respectively to a winter and to a summer test case.
|
||||
For each MATPOWER test case, UC.jl provides 365 variations (`2017-01-01` to `2017-12-31`) corresponding different days of the year.
|
||||
|
||||
### MATPOWER/UW-PSTCA
|
||||
|
||||
@@ -41,11 +33,11 @@ A variety of smaller IEEE test cases, [compiled by University of Washington](htt
|
||||
|
||||
| Name | Buses | Generators | Lines | Contingencies | References |
|
||||
|------|-------|------------|-------|---------------|--------|
|
||||
| `matpower/case14/2017-02-01` | 14 | 5 | 20 | 19 | [MTPWR, PSTCA]
|
||||
| `matpower/case30/2017-02-01` | 30 | 6 | 41 | 38 | [MTPWR, PSTCA]
|
||||
| `matpower/case57/2017-02-01` | 57 | 7 | 80 | 79 | [MTPWR, PSTCA]
|
||||
| `matpower/case118/2017-02-01` | 118 | 54 | 186 | 177 | [MTPWR, PSTCA]
|
||||
| `matpower/case300/2017-02-01` | 300 | 69 | 411 | 320 | [MTPWR, PSTCA]
|
||||
| `matpower/case14/2017-01-01` | 14 | 5 | 20 | 19 | [MTPWR, PSTCA]
|
||||
| `matpower/case30/2017-01-01` | 30 | 6 | 41 | 38 | [MTPWR, PSTCA]
|
||||
| `matpower/case57/2017-01-01` | 57 | 7 | 80 | 79 | [MTPWR, PSTCA]
|
||||
| `matpower/case118/2017-01-01` | 118 | 54 | 186 | 177 | [MTPWR, PSTCA]
|
||||
| `matpower/case300/2017-01-01` | 300 | 69 | 411 | 320 | [MTPWR, PSTCA]
|
||||
|
||||
|
||||
### MATPOWER/Polish
|
||||
@@ -54,14 +46,14 @@ Test cases based on the Polish 400, 220 and 110 kV networks, originally provided
|
||||
|
||||
| Name | Buses | Generators | Lines | Contingencies | References |
|
||||
|------|-------|------------|-------|---------------|--------|
|
||||
| `matpower/case2383wp/2017-02-01` | 2383 | 323 | 2896 | 2240 | [MTPWR]
|
||||
| `matpower/case2736sp/2017-02-01` | 2736 | 289 | 3504 | 3159 | [MTPWR]
|
||||
| `matpower/case2737sop/2017-02-01` | 2737 | 267 | 3506 | 3161 | [MTPWR]
|
||||
| `matpower/case2746wop/2017-02-01` | 2746 | 443 | 3514 | 3155 | [MTPWR]
|
||||
| `matpower/case2746wp/2017-02-01` | 2746 | 457 | 3514 | 3156 | [MTPWR]
|
||||
| `matpower/case3012wp/2017-02-01` | 3012 | 496 | 3572 | 2854 | [MTPWR]
|
||||
| `matpower/case3120sp/2017-02-01` | 3120 | 483 | 3693 | 2950 | [MTPWR]
|
||||
| `matpower/case3375wp/2017-02-01` | 3374 | 590 | 4161 | 3245 | [MTPWR]
|
||||
| `matpower/case2383wp/2017-01-01` | 2383 | 323 | 2896 | 2240 | [MTPWR]
|
||||
| `matpower/case2736sp/2017-01-01` | 2736 | 289 | 3504 | 3159 | [MTPWR]
|
||||
| `matpower/case2737sop/2017-01-01` | 2737 | 267 | 3506 | 3161 | [MTPWR]
|
||||
| `matpower/case2746wop/2017-01-01` | 2746 | 443 | 3514 | 3155 | [MTPWR]
|
||||
| `matpower/case2746wp/2017-01-01` | 2746 | 457 | 3514 | 3156 | [MTPWR]
|
||||
| `matpower/case3012wp/2017-01-01` | 3012 | 496 | 3572 | 2854 | [MTPWR]
|
||||
| `matpower/case3120sp/2017-01-01` | 3120 | 483 | 3693 | 2950 | [MTPWR]
|
||||
| `matpower/case3375wp/2017-01-01` | 3374 | 590 | 4161 | 3245 | [MTPWR]
|
||||
|
||||
### MATPOWER/PEGASE
|
||||
|
||||
@@ -69,11 +61,11 @@ Test cases from the [Pan European Grid Advanced Simulation and State Estimation
|
||||
|
||||
| Name | Buses | Generators | Lines | Contingencies | References |
|
||||
|------|-------|------------|-------|---------------|--------|
|
||||
| `matpower/case89pegase/2017-02-01` | 89 | 12 | 210 | 192 | [JoFlMa16, FlPaCa13, MTPWR]
|
||||
| `matpower/case1354pegase/2017-02-01` | 1354 | 260 | 1991 | 1288 | [JoFlMa16, FlPaCa13, MTPWR]
|
||||
| `matpower/case2869pegase/2017-02-01` | 2869 | 510 | 4582 | 3579 | [JoFlMa16, FlPaCa13, MTPWR]
|
||||
| `matpower/case9241pegase/2017-02-01` | 9241 | 1445 | 16049 | 13932 | [JoFlMa16, FlPaCa13, MTPWR]
|
||||
| `matpower/case13659pegase/2017-02-01` | 13659 | 4092 | 20467 | 13932 | [JoFlMa16, FlPaCa13, MTPWR]
|
||||
| `matpower/case89pegase/2017-01-01` | 89 | 12 | 210 | 192 | [JoFlMa16, FlPaCa13, MTPWR]
|
||||
| `matpower/case1354pegase/2017-01-01` | 1354 | 260 | 1991 | 1288 | [JoFlMa16, FlPaCa13, MTPWR]
|
||||
| `matpower/case2869pegase/2017-01-01` | 2869 | 510 | 4582 | 3579 | [JoFlMa16, FlPaCa13, MTPWR]
|
||||
| `matpower/case9241pegase/2017-01-01` | 9241 | 1445 | 16049 | 13932 | [JoFlMa16, FlPaCa13, MTPWR]
|
||||
| `matpower/case13659pegase/2017-01-01` | 13659 | 4092 | 20467 | 13932 | [JoFlMa16, FlPaCa13, MTPWR]
|
||||
|
||||
### MATPOWER/RTE
|
||||
|
||||
@@ -81,14 +73,14 @@ Test cases from the R&D Division at [Reseau de Transport d'Electricite](https://
|
||||
|
||||
| Name | Buses | Generators | Lines | Contingencies | References |
|
||||
|------|-------|------------|-------|---------------|--------|
|
||||
| `matpower/case1888rte/2017-02-01` | 1888 | 296 | 2531 | 1484 | [MTPWR, JoFlMa16]
|
||||
| `matpower/case1951rte/2017-02-01` | 1951 | 390 | 2596 | 1497 | [MTPWR, JoFlMa16]
|
||||
| `matpower/case2848rte/2017-02-01` | 2848 | 544 | 3776 | 2242 | [MTPWR, JoFlMa16]
|
||||
| `matpower/case2868rte/2017-02-01` | 2868 | 596 | 3808 | 2260 | [MTPWR, JoFlMa16]
|
||||
| `matpower/case6468rte/2017-02-01` | 6468 | 1262 | 9000 | 6094 | [MTPWR, JoFlMa16]
|
||||
| `matpower/case6470rte/2017-02-01` | 6470 | 1306 | 9005 | 6085 | [MTPWR, JoFlMa16]
|
||||
| `matpower/case6495rte/2017-02-01` | 6495 | 1352 | 9019 | 6060 | [MTPWR, JoFlMa16]
|
||||
| `matpower/case6515rte/2017-02-01` | 6515 | 1368 | 9037 | 6063 | [MTPWR, JoFlMa16]
|
||||
| `matpower/case1888rte/2017-01-01` | 1888 | 296 | 2531 | 1484 | [MTPWR, JoFlMa16]
|
||||
| `matpower/case1951rte/2017-01-01` | 1951 | 390 | 2596 | 1497 | [MTPWR, JoFlMa16]
|
||||
| `matpower/case2848rte/2017-01-01` | 2848 | 544 | 3776 | 2242 | [MTPWR, JoFlMa16]
|
||||
| `matpower/case2868rte/2017-01-01` | 2868 | 596 | 3808 | 2260 | [MTPWR, JoFlMa16]
|
||||
| `matpower/case6468rte/2017-01-01` | 6468 | 1262 | 9000 | 6094 | [MTPWR, JoFlMa16]
|
||||
| `matpower/case6470rte/2017-01-01` | 6470 | 1306 | 9005 | 6085 | [MTPWR, JoFlMa16]
|
||||
| `matpower/case6495rte/2017-01-01` | 6495 | 1352 | 9019 | 6060 | [MTPWR, JoFlMa16]
|
||||
| `matpower/case6515rte/2017-01-01` | 6515 | 1368 | 9037 | 6063 | [MTPWR, JoFlMa16]
|
||||
|
||||
|
||||
PGLIB-UC Instances
|
||||
@@ -288,7 +280,7 @@ Tejada19
|
||||
References
|
||||
----------
|
||||
|
||||
* [UCJL] **Alinson S. Xavier, Aleksandr M. Kazachkov, Feng Qiu.** "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment". Zenodo (2020). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874)
|
||||
* [UCJL] **Alinson S. Xavier, Aleksandr M. Kazachkov, Ogün Yurdakul, Feng Qiu.** "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment (Version 0.3)". Zenodo (2022). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874)
|
||||
|
||||
* [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)
|
||||
|
||||
@@ -296,14 +288,9 @@ References
|
||||
|
||||
* [BaBlEh19] **Clayton Barrows, Aaron Bloom, Ali Ehlen, Jussi Ikaheimo, Jennie Jorgenson, Dheepak Krishnamurthy, Jessica Lau et al.** "The IEEE Reliability Test System: A Proposed 2019 Update." IEEE Transactions on Power Systems (2019). [DOI: 10.1109/TPWRS.2019.2925557](https://doi.org/10.1109/TPWRS.2019.2925557)
|
||||
|
||||
* [JoFlMa16] **C. Josz, S. Fliscounakis, J. Maeght, and P. Panciatici.** "AC Power Flow
|
||||
Data in MATPOWER and QCQP Format: iTesla, RTE Snapshots, and PEGASE". [ArXiv (2016)](https://arxiv.org/abs/1603.01533).
|
||||
* [JoFlMa16] **C. Josz, S. Fliscounakis, J. Maeght, and P. Panciatici.** "AC Power Flow Data in MATPOWER and QCQP Format: iTesla, RTE Snapshots, and PEGASE". [ArXiv (2016)](https://arxiv.org/abs/1603.01533).
|
||||
|
||||
* [FlPaCa13] **S. Fliscounakis, P. Panciatici, F. Capitanescu, and L. Wehenkel.**
|
||||
"Contingency ranking with respect to overloads in very large power
|
||||
systems taking into account uncertainty, preventive and corrective
|
||||
actions", Power Systems, IEEE Trans. on, (28)4:4909-4917, 2013.
|
||||
[DOI: 10.1109/TPWRS.2013.2251015](https://doi.org/10.1109/TPWRS.2013.2251015)
|
||||
* [FlPaCa13] **S. Fliscounakis, P. Panciatici, F. Capitanescu, and L. Wehenkel.** "Contingency ranking with respect to overloads in very large power systems taking into account uncertainty, preventive and corrective actions", Power Systems, IEEE Trans. on, (28)4:4909-4917, 2013. [DOI: 10.1109/TPWRS.2013.2251015](https://doi.org/10.1109/TPWRS.2013.2251015)
|
||||
|
||||
* [MTPWR] **D. Zimmerman, C. E. Murillo-Sandnchez and R. J. Thomas.** "Matpower: Steady-state operations, planning, and analysis tools forpower systems research and education", IEEE Transactions on PowerSystems, vol. 26, no. 1, pp. 12 –19, Feb. 2011. [DOI: 10.1109/TPWRS.2010.2051168](https://doi.org/10.1109/TPWRS.2010.2051168)
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
```{sectnum}
|
||||
---
|
||||
start: 4
|
||||
depth: 2
|
||||
suffix: .
|
||||
---
|
||||
```
|
||||
|
||||
JuMP Model
|
||||
==========
|
||||
|
||||
@@ -17,20 +9,20 @@ Decision variables
|
||||
### Generators
|
||||
|
||||
Name | Symbol | Description | Unit
|
||||
-----|:--------:|-------------|:------:
|
||||
:-----|:--------:|:-------------|:------:
|
||||
`is_on[g,t]` | $u_{g}(t)$ | True if generator `g` is on at time `t`. | Binary
|
||||
`switch_on[g,t]` | $v_{g}(t)$ | True is generator `g` switches on 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
|
||||
`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
|
||||
|
||||
|
||||
### Buses
|
||||
|
||||
Name | Symbol | Description | Unit
|
||||
-----|:------:|-------------|:------:
|
||||
:-----|:------:|:-------------|:------:
|
||||
`net_injection[b,t]` | $n_b(t)$ | Net injection at bus `b` at time `t`. | MW
|
||||
`curtail[b,t]` | $s^+_b(t)$ | Amount of load curtailed at bus `b` at time `t` | MW
|
||||
|
||||
@@ -38,69 +30,24 @@ Name | Symbol | Description | Unit
|
||||
### Price-sensitive loads
|
||||
|
||||
Name | Symbol | Description | Unit
|
||||
-----|:------:|-------------|:------:
|
||||
:-----|:------:|:-------------|:------:
|
||||
`loads[s,t]` | $d_{s}(t)$ | Amount of power served to price-sensitive load `s` at time `t`. | MW
|
||||
|
||||
### Transmission lines
|
||||
|
||||
Name | Symbol | Description | Unit
|
||||
-----|:------:|-------------|:------:
|
||||
:-----|:------:|:-------------|:------:
|
||||
`flow[l,t]` | $f_l(t)$ | Power flow on line `l` at time `t`. | MW
|
||||
`overflow[l,t]` | $f^+_l(t)$ | Amount of flow above the limit for line `l` at time `t`. | MW
|
||||
|
||||
```{warning}
|
||||
!!! warning
|
||||
|
||||
Since transmission and N-1 security constraints are enforced in a lazy way, most of the `flow[l,t]` variables are never added to the model. Accessing `model[:flow][l,t]` without first checking that the variable exists will likely generate an error.
|
||||
```
|
||||
Since transmission and N-1 security constraints are enforced in a lazy way, most of the `flow[l,t]` variables are never added to the model. Accessing `model[:flow][l,t]` without first checking that the variable exists will likely generate an error.
|
||||
|
||||
Objective function
|
||||
------------------
|
||||
|
||||
$$
|
||||
\begin{align}
|
||||
\text{minimize} \;\; &
|
||||
\sum_{t \in \mathcal{T}}
|
||||
\sum_{g \in \mathcal{G}}
|
||||
C^\text{min}_g(t) u_g(t) \\
|
||||
&
|
||||
+ \sum_{t \in \mathcal{T}}
|
||||
\sum_{g \in \mathcal{G}}
|
||||
\sum_{g \in \mathcal{K}_g}
|
||||
C^k_g(t) p^k_g(t) \\
|
||||
&
|
||||
+ \sum_{t \in \mathcal{T}}
|
||||
\sum_{g \in \mathcal{G}}
|
||||
\sum_{s \in \mathcal{S}_g}
|
||||
C^s_{g}(t) \delta^s_g(t) \\
|
||||
&
|
||||
+ \sum_{t \in \mathcal{T}}
|
||||
\sum_{l \in \mathcal{L}}
|
||||
C^\text{overflow}_{l}(t) f^+_l(t) \\
|
||||
&
|
||||
+ \sum_{t \in \mathcal{T}}
|
||||
\sum_{b \in \mathcal{B}}
|
||||
C^\text{curtail}(t) s^+_b(t) \\
|
||||
&
|
||||
- \sum_{t \in \mathcal{T}}
|
||||
\sum_{s \in \mathcal{PS}}
|
||||
R_{s}(t) d_{s}(t) \\
|
||||
|
||||
\end{align}
|
||||
$$
|
||||
where
|
||||
- $\mathcal{B}$ is the set of buses
|
||||
- $\mathcal{G}$ is the set of generators
|
||||
- $\mathcal{L}$ is the set of transmission lines
|
||||
- $\mathcal{PS}$ is the set of price-sensitive loads
|
||||
- $\mathcal{S}_g$ is the set of start-up categories for generator $g$
|
||||
- $\mathcal{T}$ is the set of time steps
|
||||
- $C^\text{curtail}(t)$ is the curtailment penalty (in \$/MW)
|
||||
- $C^\text{min}_g(t)$ is the cost of keeping generator $g$ on and producing at minimum power during time $t$ (in \$)
|
||||
- $C^\text{overflow}_{l}(t)$ is the flow limit penalty for line $l$ at time $t$ (in \$/MW)
|
||||
- $C^k_g(t)$ is the cost for generator $g$ to produce 1 MW of power at time $t$ under piecewise linear segment $k$
|
||||
- $C^s_{g}(t)$ is the cost of starting up generator $g$ at time $t$ under start-up category $s$ (in \$)
|
||||
- $R_{s}(t)$ is the revenue obtained from serving price-sensitive load $s$ at time $t$ (in \$/MW)
|
||||
|
||||
TODO
|
||||
|
||||
Constraints
|
||||
-----------
|
||||
@@ -1,21 +1,13 @@
|
||||
```{sectnum}
|
||||
---
|
||||
start: 1
|
||||
depth: 2
|
||||
suffix: .
|
||||
---
|
||||
```
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
UnitCommitment.jl was tested and developed with [Julia 1.6](https://julialang.org/). To install Julia, please follow the [installation guide on the official Julia website](https://julialang.org/downloads/platform.html). To install UnitCommitment.jl, run the Julia interpreter, type `]` to open the package manager, then type:
|
||||
UnitCommitment.jl was tested and developed with [Julia 1.7](https://julialang.org/). To install Julia, please follow the [installation guide on the official Julia website](https://julialang.org/downloads/). To install UnitCommitment.jl, run the Julia interpreter, type `]` to open the package manager, then type:
|
||||
|
||||
```text
|
||||
pkg> add UnitCommitment@0.2
|
||||
pkg> add UnitCommitment@0.3
|
||||
```
|
||||
|
||||
To test that the package has been correctly installed, run:
|
||||
@@ -126,9 +118,9 @@ model = UnitCommitment.build_model(
|
||||
UnitCommitment.optimize!(model)
|
||||
```
|
||||
|
||||
```{warning}
|
||||
The function `generate_initial_conditions!` may return different initial conditions after each call, even if the same instance and the same optimizer is provided. The particular algorithm may also change in a future version of UC.jl. For these reasons, it is recommended that you generate initial conditions exactly once for each instance and store them for later use.
|
||||
```
|
||||
!!! warning
|
||||
|
||||
The function `generate_initial_conditions!` may return different initial conditions after each call, even if the same instance and the same optimizer is provided. The particular algorithm may also change in a future version of UC.jl. For these reasons, it is recommended that you generate initial conditions exactly once for each instance and store them for later use.
|
||||
|
||||
### Verifying solutions
|
||||
|
||||
9
juliaw
Normal file → Executable file
9
juliaw
Normal file → Executable file
@@ -47,7 +47,14 @@ project = TOML.parsefile("Project.toml")
|
||||
manifest = TOML.parsefile("Manifest.toml")
|
||||
deps = Symbol[]
|
||||
for dep in keys(project["deps"])
|
||||
if "path" in keys(manifest[dep][1])
|
||||
if dep in keys(manifest)
|
||||
# Up to Julia 1.6
|
||||
dep_entry = manifest[dep][1]
|
||||
else
|
||||
# Julia 1.7+
|
||||
dep_entry = manifest["deps"][dep][1]
|
||||
end
|
||||
if "path" in keys(dep_entry)
|
||||
println(" - \$(dep) [skip]")
|
||||
else
|
||||
println(" - \$(dep)")
|
||||
|
||||
@@ -16,9 +16,11 @@ include("model/formulations/KnuOstWat2018/structs.jl")
|
||||
include("model/formulations/MorLatRam2013/structs.jl")
|
||||
include("model/formulations/PanGua2016/structs.jl")
|
||||
include("solution/methods/XavQiuWanThi2019/structs.jl")
|
||||
include("model/formulations/WanHob2016/structs.jl")
|
||||
|
||||
include("import/egret.jl")
|
||||
include("instance/read.jl")
|
||||
include("instance/migrate.jl")
|
||||
include("model/build.jl")
|
||||
include("model/formulations/ArrCon2000/ramp.jl")
|
||||
include("model/formulations/base/bus.jl")
|
||||
@@ -36,6 +38,7 @@ include("model/formulations/KnuOstWat2018/pwlcosts.jl")
|
||||
include("model/formulations/MorLatRam2013/ramp.jl")
|
||||
include("model/formulations/MorLatRam2013/scosts.jl")
|
||||
include("model/formulations/PanGua2016/ramp.jl")
|
||||
include("model/formulations/WanHob2016/ramp.jl")
|
||||
include("model/jumpext.jl")
|
||||
include("solution/fix.jl")
|
||||
include("solution/methods/XavQiuWanThi2019/enforce.jl")
|
||||
|
||||
38
src/instance/migrate.jl
Normal file
38
src/instance/migrate.jl
Normal file
@@ -0,0 +1,38 @@
|
||||
# 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.
|
||||
|
||||
using DataStructures
|
||||
using JSON
|
||||
|
||||
function _migrate(json)
|
||||
version = json["Parameters"]["Version"]
|
||||
if version === nothing
|
||||
error(
|
||||
"The provided input file cannot be loaded because it does not " *
|
||||
"specify what version of UnitCommitment.jl it was written for. " *
|
||||
"Please modify the \"Parameters\" section of the file and include " *
|
||||
"a \"Version\" entry. For example: {\"Parameters\":{\"Version\":\"0.3\"}}",
|
||||
)
|
||||
end
|
||||
version = VersionNumber(version)
|
||||
version >= v"0.3" || _migrate_to_v03(json)
|
||||
return
|
||||
end
|
||||
|
||||
function _migrate_to_v03(json)
|
||||
# Migrate reserves
|
||||
if json["Reserves"] !== nothing &&
|
||||
json["Reserves"]["Spinning (MW)"] !== nothing
|
||||
amount = json["Reserves"]["Spinning (MW)"]
|
||||
json["Reserves"] = DefaultOrderedDict(nothing)
|
||||
json["Reserves"]["r1"] = DefaultOrderedDict(nothing)
|
||||
json["Reserves"]["r1"]["Type"] = "spinning"
|
||||
json["Reserves"]["r1"]["Amount (MW)"] = amount
|
||||
for (gen_name, gen) in json["Generators"]
|
||||
if gen["Provides spinning reserves?"] == true
|
||||
gen["Reserve eligibility"] = ["r1"]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -8,20 +8,18 @@ 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
|
||||
|
||||
Read one of the benchmark unit commitment instances included in the package.
|
||||
See "Instances" section of the documentation for the entire list of benchmark
|
||||
instances available.
|
||||
Read one of the benchmark instances included in the package. See
|
||||
[Instances](instances.md) for the entire list of benchmark instances available.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
import UnitCommitment
|
||||
instance = UnitCommitment.read_benchmark("matpower/case3375wp/2017-02-01")
|
||||
# Example
|
||||
```julia
|
||||
instance = UnitCommitment.read_benchmark("matpower/case3375wp/2017-02-01")
|
||||
```
|
||||
"""
|
||||
function read_benchmark(
|
||||
name::AbstractString;
|
||||
@@ -48,13 +46,13 @@ end
|
||||
"""
|
||||
read(path::AbstractString)::UnitCommitmentInstance
|
||||
|
||||
Read a unit commitment instance from a file. The file may be gzipped.
|
||||
Read instance from a file. The file may be gzipped.
|
||||
|
||||
Example
|
||||
-------
|
||||
# Example
|
||||
|
||||
import UnitCommitment
|
||||
instance = UnitCommitment.read("/path/to/input.json.gz")
|
||||
```julia
|
||||
instance = UnitCommitment.read("/path/to/input.json.gz")
|
||||
```
|
||||
"""
|
||||
function read(path::AbstractString)::UnitCommitmentInstance
|
||||
if endswith(path, ".gz")
|
||||
@@ -80,11 +78,13 @@ function _read_json(path::String)::OrderedDict
|
||||
end
|
||||
|
||||
function _from_json(json; repair = true)
|
||||
_migrate(json)
|
||||
units = Unit[]
|
||||
buses = Bus[]
|
||||
contingencies = Contingency[]
|
||||
lines = TransmissionLine[]
|
||||
loads = PriceSensitiveLoad[]
|
||||
reserves = Reserve[]
|
||||
|
||||
function scalar(x; default = nothing)
|
||||
x !== nothing || return default
|
||||
@@ -105,6 +105,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
|
||||
@@ -117,6 +118,11 @@ function _from_json(json; repair = true)
|
||||
json["Parameters"]["Power balance penalty (\$/MW)"],
|
||||
default = [1000.0 for t in 1:T],
|
||||
)
|
||||
# Penalty price for shortage in meeting system-wide flexiramp requirements
|
||||
flexiramp_shortfall_penalty = timeseries(
|
||||
json["Parameters"]["Flexiramp penalty (\$/MW)"],
|
||||
default = [500.0 for t in 1:T],
|
||||
)
|
||||
shortfall_penalty = timeseries(
|
||||
json["Parameters"]["Reserve shortfall penalty (\$/MW)"],
|
||||
default = [-1.0 for t in 1:T],
|
||||
@@ -135,6 +141,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"]]
|
||||
@@ -172,6 +196,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)
|
||||
@@ -205,24 +236,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],
|
||||
),
|
||||
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 reserves
|
||||
reserves = Reserves(zeros(T))
|
||||
if "Reserves" in keys(json)
|
||||
reserves.spinning =
|
||||
timeseries(json["Reserves"]["Spinning (MW)"], default = zeros(T))
|
||||
end
|
||||
|
||||
# Read transmission lines
|
||||
if "Transmission lines" in keys(json)
|
||||
for (line_name, dict) in json["Transmission lines"]
|
||||
@@ -295,7 +319,9 @@ 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,
|
||||
units_by_name = Dict(g.name => g for g in units),
|
||||
units = units,
|
||||
|
||||
@@ -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,8 +44,8 @@ mutable struct Unit
|
||||
shutdown_limit::Float64
|
||||
initial_status::Union{Int,Nothing}
|
||||
initial_power::Union{Float64,Nothing}
|
||||
provides_spinning_reserves::Vector{Bool}
|
||||
startup_categories::Vector{StartupCategory}
|
||||
reserves::Vector{Reserve}
|
||||
end
|
||||
|
||||
mutable struct TransmissionLine
|
||||
@@ -52,10 +60,6 @@ mutable struct TransmissionLine
|
||||
flow_limit_penalty::Vector{Float64}
|
||||
end
|
||||
|
||||
mutable struct Reserves
|
||||
spinning::Vector{Float64}
|
||||
end
|
||||
|
||||
mutable struct Contingency
|
||||
name::String
|
||||
lines::Vector{TransmissionLine}
|
||||
@@ -79,8 +83,10 @@ 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
|
||||
units_by_name::Dict{AbstractString,Unit}
|
||||
units::Vector{Unit}
|
||||
|
||||
@@ -9,22 +9,59 @@ import JuMP: value, fix, set_name
|
||||
function build_model(;
|
||||
instance::UnitCommitmentInstance,
|
||||
optimizer = nothing,
|
||||
formulation = Formulation(),
|
||||
variable_names::Bool = false,
|
||||
)::JuMP.Model
|
||||
|
||||
Build the JuMP model corresponding to the given unit commitment instance.
|
||||
|
||||
Arguments
|
||||
=========
|
||||
---------
|
||||
|
||||
- `instance`:
|
||||
the instance.
|
||||
- `optimizer`:
|
||||
the optimizer factory that should be attached to this model (e.g. Cbc.Optimizer).
|
||||
If not provided, no optimizer will be attached.
|
||||
- `formulation`:
|
||||
the MIP formulation to use. By default, uses a formulation that combines
|
||||
modeling components from different publications that provides good
|
||||
performance across a wide variety of instances. An alternative formulation
|
||||
may also be provided.
|
||||
- `variable_names`:
|
||||
If true, set variable and constraint names. Important if the model is going
|
||||
if true, set variable and constraint names. Important if the model is going
|
||||
to be exported to an MPS file. For large models, this can take significant
|
||||
time, so it's disabled by default.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
```julia
|
||||
# Read benchmark instance
|
||||
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
||||
|
||||
# Construct model (using state-of-the-art defaults)
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = Cbc.Optimizer,
|
||||
)
|
||||
|
||||
# Construct model (using customized formulation)
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = Cbc.Optimizer,
|
||||
formulation = Formulation(
|
||||
pwl_costs = KnuOstWat2018.PwlCosts(),
|
||||
ramping = MorLatRam2013.Ramping(),
|
||||
startup_costs = MorLatRam2013.StartupCosts(),
|
||||
transmission = ShiftFactorsFormulation(
|
||||
isf_cutoff = 0.005,
|
||||
lodf_cutoff = 0.001,
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
"""
|
||||
function build_model(;
|
||||
instance::UnitCommitmentInstance,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
177
src/model/formulations/WanHob2016/ramp.jl
Normal file
177
src/model/formulations/WanHob2016/ramp.jl
Normal file
@@ -0,0 +1,177 @@
|
||||
# UnitCommitmentFL.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.
|
||||
|
||||
function _add_ramp_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
::Gar1962.ProdVars,
|
||||
::WanHob2016.Ramping,
|
||||
::Gar1962.StatusVars,
|
||||
)::Nothing
|
||||
is_initially_on = (g.initial_status > 0)
|
||||
SU = g.startup_limit
|
||||
SD = g.shutdown_limit
|
||||
RU = g.ramp_up_limit
|
||||
RD = g.ramp_down_limit
|
||||
gn = g.name
|
||||
minp = g.min_power
|
||||
maxp = g.max_power
|
||||
initial_power = g.initial_power
|
||||
|
||||
is_on = model[:is_on]
|
||||
prod_above = model[:prod_above]
|
||||
upflexiramp = model[:upflexiramp]
|
||||
dwflexiramp = model[:dwflexiramp]
|
||||
mfg = model[:mfg]
|
||||
|
||||
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,
|
||||
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,
|
||||
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) 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
|
||||
end
|
||||
18
src/model/formulations/WanHob2016/structs.jl
Normal file
18
src/model/formulations/WanHob2016/structs.jl
Normal file
@@ -0,0 +1,18 @@
|
||||
# UnitCommitmentFL.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.
|
||||
|
||||
"""
|
||||
Formulation described in:
|
||||
|
||||
B. Wang and B. F. Hobbs, "Real-Time Markets for Flexiramp: A Stochastic
|
||||
Unit Commitment-Based Analysis," in IEEE Transactions on Power Systems,
|
||||
vol. 31, no. 2, pp. 846-860, March 2016, doi: 10.1109/TPWRS.2015.2411268.
|
||||
"""
|
||||
module WanHob2016
|
||||
|
||||
import ..RampingFormulation
|
||||
|
||||
struct Ramping <: RampingFormulation end
|
||||
|
||||
end
|
||||
@@ -9,6 +9,27 @@ abstract type StartupCostsFormulation end
|
||||
abstract type StatusVarsFormulation end
|
||||
abstract type ProductionVarsFormulation end
|
||||
|
||||
"""
|
||||
struct Formulation
|
||||
prod_vars::ProductionVarsFormulation
|
||||
pwl_costs::PiecewiseLinearCostsFormulation
|
||||
ramping::RampingFormulation
|
||||
startup_costs::StartupCostsFormulation
|
||||
status_vars::StatusVarsFormulation
|
||||
transmission::TransmissionFormulation
|
||||
end
|
||||
|
||||
Struct provided to `build_model` that holds various formulation components.
|
||||
|
||||
# Fields
|
||||
|
||||
- `prod_vars`: Formulation for the production decision variables
|
||||
- `pwl_costs`: Formulation for the piecewise linear costs
|
||||
- `ramping`: Formulation for ramping constraints
|
||||
- `startup_costs`: Formulation for time-dependent start-up costs
|
||||
- `status_vars`: Formulation for the status variables (e.g. `is_on`, `is_off`)
|
||||
- `transmission`: Formulation for transmission and N-1 security constraints
|
||||
"""
|
||||
struct Formulation
|
||||
prod_vars::ProductionVarsFormulation
|
||||
pwl_costs::PiecewiseLinearCostsFormulation
|
||||
@@ -38,10 +59,10 @@ end
|
||||
|
||||
"""
|
||||
struct ShiftFactorsFormulation <: TransmissionFormulation
|
||||
isf_cutoff::Float64
|
||||
lodf_cutoff::Float64
|
||||
precomputed_isf::Union{Nothing,Matrix{Float64}}
|
||||
precomputed_lodf::Union{Nothing,Matrix{Float64}}
|
||||
isf_cutoff::Float64 = 0.005
|
||||
lodf_cutoff::Float64 = 0.001
|
||||
precomputed_isf=nothing
|
||||
precomputed_lodf=nothing
|
||||
end
|
||||
|
||||
Transmission formulation based on Injection Shift Factors (ISF) and Line
|
||||
@@ -49,15 +70,15 @@ Outage Distribution Factors (LODF). Constraints are enforced in a lazy way.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
- `precomputed_isf::Union{Matrix{Float64},Nothing} = nothing`:
|
||||
- `precomputed_isf`:
|
||||
the injection shift factors matrix. If not provided, it will be computed.
|
||||
- `precomputed_lodf::Union{Matrix{Float64},Nothing} = nothing`:
|
||||
- `precomputed_lodf`:
|
||||
the line outage distribution factors matrix. If not provided, it will be
|
||||
computed.
|
||||
- `isf_cutoff::Float64 = 0.005`:
|
||||
- `isf_cutoff`:
|
||||
the cutoff that should be applied to the ISF matrix. Entries with magnitude
|
||||
smaller than this value will be set to zero.
|
||||
- `lodf_cutoff::Float64 = 0.001`:
|
||||
- `lodf_cutoff`:
|
||||
the cutoff that should be applied to the LODF matrix. Entries with magnitude
|
||||
smaller than this value will be set to zero.
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
function _add_system_wide_eqs!(model::JuMP.Model)::Nothing
|
||||
_add_net_injection_eqs!(model)
|
||||
_add_reserve_eqs!(model)
|
||||
_add_spinning_reserve_eqs!(model)
|
||||
_add_flexiramp_reserve_eqs!(model)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -27,29 +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_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 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 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
|
||||
|
||||
@@ -12,7 +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_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)
|
||||
|
||||
@@ -42,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
|
||||
@@ -81,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
|
||||
@@ -89,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]
|
||||
)
|
||||
@@ -117,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
|
||||
@@ -126,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
|
||||
@@ -216,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Nothing
|
||||
if !occursin("Gurobi", JuMP.solver_name(model))
|
||||
method.two_phase_gap = false
|
||||
end
|
||||
function set_gap(gap)
|
||||
try
|
||||
JuMP.set_optimizer_attribute(model, "MIPGap", gap)
|
||||
@info @sprintf("MIP gap tolerance set to %f", gap)
|
||||
catch
|
||||
@warn "Could not change MIP gap tolerance"
|
||||
end
|
||||
JuMP.set_optimizer_attribute(model, "MIPGap", gap)
|
||||
@info @sprintf("MIP gap tolerance set to %f", gap)
|
||||
end
|
||||
initial_time = time()
|
||||
large_gap = false
|
||||
@@ -17,8 +16,6 @@ function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Nothing
|
||||
if has_transmission && method.two_phase_gap
|
||||
set_gap(1e-2)
|
||||
large_gap = true
|
||||
else
|
||||
set_gap(method.gap_limit)
|
||||
end
|
||||
while true
|
||||
time_elapsed = time() - initial_time
|
||||
|
||||
@@ -2,18 +2,10 @@
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
Lazy constraint solution method described in:
|
||||
|
||||
Xavier, A. S., Qiu, F., Wang, F., & Thimmapuram, P. R. (2019). Transmission
|
||||
constraint filtering in large-scale security-constrained unit commitment.
|
||||
IEEE Transactions on Power Systems, 34(3), 2457-2460.
|
||||
DOI: https://doi.org/10.1109/TPWRS.2019.2892620
|
||||
"""
|
||||
module XavQiuWanThi2019
|
||||
import ..SolutionMethod
|
||||
"""
|
||||
struct Method
|
||||
mutable struct Method
|
||||
time_limit::Float64
|
||||
gap_limit::Float64
|
||||
two_phase_gap::Bool
|
||||
@@ -21,13 +13,20 @@ import ..SolutionMethod
|
||||
max_violations_per_period::Int
|
||||
end
|
||||
|
||||
Lazy constraint solution method described in:
|
||||
|
||||
Xavier, A. S., Qiu, F., Wang, F., & Thimmapuram, P. R. (2019). Transmission
|
||||
constraint filtering in large-scale security-constrained unit commitment.
|
||||
IEEE Transactions on Power Systems, 34(3), 2457-2460.
|
||||
DOI: https://doi.org/10.1109/TPWRS.2019.2892620
|
||||
|
||||
Fields
|
||||
------
|
||||
|
||||
- `time_limit`:
|
||||
the time limit over the entire optimization procedure.
|
||||
- `gap_limit`:
|
||||
the desired relative optimality gap.
|
||||
the desired relative optimality gap. Only used when `two_phase_gap=true`.
|
||||
- `two_phase_gap`:
|
||||
if true, solve the problem with large gap tolerance first, then reduce
|
||||
the gap tolerance when no further violated constraints are found.
|
||||
@@ -39,7 +38,7 @@ Fields
|
||||
formulation per time period.
|
||||
|
||||
"""
|
||||
struct Method <: SolutionMethod
|
||||
mutable struct Method <: SolutionMethod
|
||||
time_limit::Float64
|
||||
gap_limit::Float64
|
||||
two_phase_gap::Bool
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
function optimize!(model::JuMP.Model)::Nothing
|
||||
optimize!(model::JuMP.Model)::Nothing
|
||||
|
||||
Solve the given unit commitment model. Unlike JuMP.optimize!, this uses more
|
||||
Solve the given unit commitment model. Unlike `JuMP.optimize!`, this uses more
|
||||
advanced methods to accelerate the solution process and to enforce transmission
|
||||
and N-1 security constraints.
|
||||
"""
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
solution(model::JuMP.Model)::OrderedDict
|
||||
|
||||
Extracts the optimal solution from the UC.jl model. The model must be solved beforehand.
|
||||
|
||||
# Example
|
||||
|
||||
```julia
|
||||
UnitCommitment.optimize!(model)
|
||||
solution = UnitCommitment.solution(model)
|
||||
```
|
||||
"""
|
||||
function solution(model::JuMP.Model)::OrderedDict
|
||||
instance, T = model[:instance], model[:instance].time
|
||||
function timeseries(vars, collection)
|
||||
@@ -50,13 +62,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)
|
||||
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)"] =
|
||||
timeseries(model[:net_injection], instance.buses)
|
||||
sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses)
|
||||
@@ -67,5 +72,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
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
write(filename::AbstractString, solution::AbstractDict)::Nothing
|
||||
|
||||
Write the given solution to a JSON file.
|
||||
|
||||
# Example
|
||||
|
||||
```julia
|
||||
solution = UnitCommitment.solution(model)
|
||||
UnitCommitment.write("/tmp/output.json", solution)
|
||||
```
|
||||
"""
|
||||
function write(filename::AbstractString, solution::AbstractDict)::Nothing
|
||||
open(filename, "w") do file
|
||||
return JSON.print(file, solution, 2)
|
||||
|
||||
@@ -2,13 +2,6 @@
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
Methods described in:
|
||||
|
||||
Xavier, Álinson S., Feng Qiu, and Shabbir Ahmed. "Learning to solve
|
||||
large-scale security-constrained unit commitment problems." INFORMS
|
||||
Journal on Computing 33.2 (2021): 739-756. DOI: 10.1287/ijoc.2020.0976
|
||||
"""
|
||||
module XavQiuAhm2021
|
||||
|
||||
using Distributions
|
||||
@@ -55,6 +48,13 @@ load profile, as follows:
|
||||
The default parameters were obtained based on an analysis of publicly available
|
||||
bid and hourly data from PJM, corresponding to the month of January, 2017. For
|
||||
more details, see Section 4.2 of the paper.
|
||||
|
||||
# References
|
||||
|
||||
- **Xavier, Álinson S., Feng Qiu, and Shabbir Ahmed.** *"Learning to solve
|
||||
large-scale security-constrained unit commitment problems."* INFORMS Journal
|
||||
on Computing 33.2 (2021): 739-756. DOI: 10.1287/ijoc.2020.0976
|
||||
|
||||
"""
|
||||
Base.@kwdef struct Randomization
|
||||
cost = Uniform(0.95, 1.05)
|
||||
@@ -118,11 +118,12 @@ Base.@kwdef struct Randomization
|
||||
end
|
||||
|
||||
function _randomize_costs(
|
||||
rng,
|
||||
instance::UnitCommitmentInstance,
|
||||
distribution,
|
||||
)::Nothing
|
||||
for unit in instance.units
|
||||
α = rand(distribution)
|
||||
α = rand(rng, distribution)
|
||||
unit.min_power_cost *= α
|
||||
for k in unit.cost_segments
|
||||
k.cost *= α
|
||||
@@ -135,10 +136,11 @@ function _randomize_costs(
|
||||
end
|
||||
|
||||
function _randomize_load_share(
|
||||
rng,
|
||||
instance::UnitCommitmentInstance,
|
||||
distribution,
|
||||
)::Nothing
|
||||
α = rand(distribution, length(instance.buses))
|
||||
α = rand(rng, distribution, length(instance.buses))
|
||||
for t in 1:instance.time
|
||||
total = sum(bus.load[t] for bus in instance.buses)
|
||||
den = sum(
|
||||
@@ -153,6 +155,7 @@ function _randomize_load_share(
|
||||
end
|
||||
|
||||
function _randomize_load_profile(
|
||||
rng,
|
||||
instance::UnitCommitmentInstance,
|
||||
params::Randomization,
|
||||
)::Nothing
|
||||
@@ -161,12 +164,13 @@ function _randomize_load_profile(
|
||||
for t in 2:instance.time
|
||||
idx = (t - 1) % length(params.load_profile_mu) + 1
|
||||
gamma = rand(
|
||||
rng,
|
||||
Normal(params.load_profile_mu[idx], params.load_profile_sigma[idx]),
|
||||
)
|
||||
push!(system_load, system_load[t-1] * gamma)
|
||||
end
|
||||
capacity = sum(maximum(u.max_power) for u in instance.units)
|
||||
peak_load = rand(params.peak_load) * capacity
|
||||
peak_load = rand(rng, params.peak_load) * capacity
|
||||
system_load = system_load ./ maximum(system_load) .* peak_load
|
||||
|
||||
# Scale bus loads to match the new system load
|
||||
@@ -186,24 +190,53 @@ end
|
||||
function randomize!(
|
||||
instance::UnitCommitment.UnitCommitmentInstance,
|
||||
method::XavQiuAhm2021.Randomization,
|
||||
rng = MersenneTwister(),
|
||||
)::Nothing
|
||||
|
||||
Randomize costs and loads based on the method described in XavQiuAhm2021.
|
||||
"""
|
||||
function randomize!(
|
||||
instance::UnitCommitment.UnitCommitmentInstance,
|
||||
method::XavQiuAhm2021.Randomization,
|
||||
method::XavQiuAhm2021.Randomization;
|
||||
rng = MersenneTwister(),
|
||||
)::Nothing
|
||||
if method.randomize_costs
|
||||
XavQiuAhm2021._randomize_costs(instance, method.cost)
|
||||
XavQiuAhm2021._randomize_costs(rng, instance, method.cost)
|
||||
end
|
||||
if method.randomize_load_share
|
||||
XavQiuAhm2021._randomize_load_share(instance, method.load_share)
|
||||
XavQiuAhm2021._randomize_load_share(rng, instance, method.load_share)
|
||||
end
|
||||
if method.randomize_load_profile
|
||||
XavQiuAhm2021._randomize_load_profile(instance, method)
|
||||
XavQiuAhm2021._randomize_load_profile(rng, instance, method)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
"""
|
||||
function randomize!(
|
||||
instance::UnitCommitmentInstance;
|
||||
method = UnitCommitment.XavQiuAhm2021.Randomization();
|
||||
rng = MersenneTwister(),
|
||||
)::Nothing
|
||||
|
||||
Randomizes instance parameters according to the provided randomization method.
|
||||
|
||||
# Example
|
||||
|
||||
```julia
|
||||
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
||||
UnitCommitment.randomize!(instance)
|
||||
model = UnitCommitment.build_model(; instance)
|
||||
```
|
||||
|
||||
"""
|
||||
function randomize!(
|
||||
instance::UnitCommitment.UnitCommitmentInstance;
|
||||
method = XavQiuAhm2021.Randomization(),
|
||||
rng = MersenneTwister(),
|
||||
)::Nothing
|
||||
randomize!(instance, method; rng)
|
||||
return
|
||||
end
|
||||
|
||||
export randomize!
|
||||
|
||||
@@ -12,10 +12,11 @@ conditions are also not modified.
|
||||
Example
|
||||
-------
|
||||
|
||||
# Build a 2-hour UC instance
|
||||
instance = UnitCommitment.read_benchmark("test/case14")
|
||||
modified = UnitCommitment.slice(instance, 1:2)
|
||||
|
||||
```julia
|
||||
# Build a 2-hour UC instance
|
||||
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
||||
modified = UnitCommitment.slice(instance, 1:2)
|
||||
```
|
||||
"""
|
||||
function slice(
|
||||
instance::UnitCommitmentInstance,
|
||||
@@ -24,13 +25,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]
|
||||
|
||||
@@ -5,20 +5,11 @@
|
||||
import Logging: min_enabled_level, shouldlog, handle_message
|
||||
using Base.CoreLogging, Logging, Printf
|
||||
|
||||
struct TimeLogger <: AbstractLogger
|
||||
Base.@kwdef struct TimeLogger <: AbstractLogger
|
||||
initial_time::Float64
|
||||
file::Union{Nothing,IOStream}
|
||||
screen_log_level::Any
|
||||
io_log_level::Any
|
||||
end
|
||||
|
||||
function TimeLogger(;
|
||||
initial_time::Float64,
|
||||
file::Union{Nothing,IOStream} = nothing,
|
||||
screen_log_level = CoreLogging.Info,
|
||||
io_log_level = CoreLogging.Info,
|
||||
)::TimeLogger
|
||||
return TimeLogger(initial_time, file, screen_log_level, io_log_level)
|
||||
file::Union{Nothing,IOStream} = nothing
|
||||
screen_log_level::Any = CoreLogging.Info
|
||||
io_log_level::Any = CoreLogging.Info
|
||||
end
|
||||
|
||||
min_enabled_level(logger::TimeLogger) = logger.io_log_level
|
||||
@@ -61,7 +52,9 @@ function handle_message(
|
||||
end
|
||||
end
|
||||
|
||||
function _setup_logger()
|
||||
function _setup_logger(; level = CoreLogging.Info)
|
||||
initial_time = time()
|
||||
return global_logger(TimeLogger(initial_time = initial_time))
|
||||
return global_logger(
|
||||
TimeLogger(initial_time = initial_time, screen_log_level = level),
|
||||
)
|
||||
end
|
||||
|
||||
@@ -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,22 +335,66 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Verify spinning reserves
|
||||
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
|
||||
# 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
|
||||
)
|
||||
shortfall =
|
||||
solution["Spinning reserve shortfall (MW)"][r.name][t]
|
||||
required = r.amount[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],
|
||||
)
|
||||
err_count += 1
|
||||
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
|
||||
)
|
||||
upflexiramp_shortfall =
|
||||
solution["Up-flexiramp shortfall (MW)"][r.name][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
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76"
|
||||
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
|
||||
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
|
||||
GZip = "92fee26a-97fe-5a0c-ad85-20a5f3185b63"
|
||||
Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b"
|
||||
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
||||
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
|
||||
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
|
||||
@@ -20,7 +19,7 @@ DataStructures = "0.18"
|
||||
Distributions = "0.25"
|
||||
GZip = "0.5"
|
||||
JSON = "0.21"
|
||||
JuMP = "0.21"
|
||||
MathOptInterface = "0.9"
|
||||
JuMP = "1"
|
||||
MathOptInterface = "1"
|
||||
PackageCompiler = "1"
|
||||
julia = "1"
|
||||
|
||||
BIN
test/fixtures/case118-initcond.json.gz
vendored
BIN
test/fixtures/case118-initcond.json.gz
vendored
Binary file not shown.
BIN
test/fixtures/case14-flex.json.gz
vendored
Normal file
BIN
test/fixtures/case14-flex.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/case14-sub-hourly.json.gz
vendored
Normal file
BIN
test/fixtures/case14-sub-hourly.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/case14.json.gz
vendored
Normal file
BIN
test/fixtures/case14.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/ucjl-0.2.json.gz
vendored
Normal file
BIN
test/fixtures/ucjl-0.2.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/ucjl-0.3.json.gz
vendored
Normal file
BIN
test/fixtures/ucjl-0.3.json.gz
vendored
Normal file
Binary file not shown.
@@ -4,12 +4,9 @@
|
||||
|
||||
using UnitCommitment
|
||||
|
||||
basedir = @__DIR__
|
||||
|
||||
@testset "read_egret_solution" begin
|
||||
solution = UnitCommitment.read_egret_solution(
|
||||
"$basedir/../fixtures/egret_output.json.gz",
|
||||
)
|
||||
solution =
|
||||
UnitCommitment.read_egret_solution("$FIXTURES/egret_output.json.gz")
|
||||
for attr in ["Is on", "Production (MW)", "Production cost (\$)"]
|
||||
@test attr in keys(solution)
|
||||
@test "115_STEAM_1" in keys(solution[attr])
|
||||
|
||||
18
test/instance/migrate_test.jl
Normal file
18
test/instance/migrate_test.jl
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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.
|
||||
|
||||
using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
||||
|
||||
@testset "read v0.2" begin
|
||||
instance = UnitCommitment.read("$FIXTURES/ucjl-0.2.json.gz")
|
||||
@test length(instance.reserves_by_name["r1"].amount) == 4
|
||||
@test instance.units_by_name["g2"].reserves[1].name == "r1"
|
||||
end
|
||||
|
||||
@testset "read v0.3" begin
|
||||
instance = UnitCommitment.read("$FIXTURES/ucjl-0.3.json.gz")
|
||||
@test length(instance.units) == 6
|
||||
@test length(instance.buses) == 14
|
||||
@test length(instance.lines) == 20
|
||||
end
|
||||
@@ -5,7 +5,7 @@
|
||||
using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
||||
|
||||
@testset "read_benchmark" begin
|
||||
instance = UnitCommitment.read_benchmark("test/case14")
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
|
||||
@test length(instance.lines) == 20
|
||||
@test length(instance.buses) == 14
|
||||
@@ -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 == []
|
||||
@@ -107,7 +112,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
||||
end
|
||||
|
||||
@testset "read_benchmark sub-hourly" begin
|
||||
instance = UnitCommitment.read_benchmark("test/case14-sub-hourly")
|
||||
instance = UnitCommitment.read("$FIXTURES/case14-sub-hourly.json.gz")
|
||||
@test instance.time == 4
|
||||
unit = instance.units[1]
|
||||
@test unit.name == "g1"
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
using UnitCommitment
|
||||
using JuMP
|
||||
using Cbc
|
||||
using JSON
|
||||
import UnitCommitment:
|
||||
ArrCon2000,
|
||||
CarArr2006,
|
||||
@@ -13,62 +15,70 @@ import UnitCommitment:
|
||||
KnuOstWat2018,
|
||||
MorLatRam2013,
|
||||
PanGua2016,
|
||||
XavQiuWanThi2019
|
||||
XavQiuWanThi2019,
|
||||
WanHob2016
|
||||
|
||||
if ENABLE_LARGE_TESTS
|
||||
using Gurobi
|
||||
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
|
||||
end
|
||||
|
||||
function _large_test(formulation::Formulation)::Nothing
|
||||
instances = ["pglib-uc/ca/Scenario400_reserves_1"]
|
||||
for instance in instances
|
||||
instance = UnitCommitment.read_benchmark(instance)
|
||||
function _test(
|
||||
formulation::Formulation;
|
||||
instances = ["case14"],
|
||||
dump::Bool = false,
|
||||
)::Nothing
|
||||
for instance_name in instances
|
||||
instance = UnitCommitment.read("$(FIXTURES)/$(instance_name).json.gz")
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
formulation = formulation,
|
||||
optimizer = Gurobi.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
|
||||
|
||||
function _test(formulation::Formulation)::Nothing
|
||||
_small_test(formulation)
|
||||
if ENABLE_LARGE_TESTS
|
||||
_large_test(formulation)
|
||||
@testset "formulations" begin
|
||||
@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 = ["case14-flex"],
|
||||
)
|
||||
end
|
||||
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()))
|
||||
end
|
||||
|
||||
@@ -6,9 +6,9 @@ using Test
|
||||
using UnitCommitment
|
||||
|
||||
push!(Base.LOAD_PATH, @__DIR__)
|
||||
UnitCommitment._setup_logger()
|
||||
UnitCommitment._setup_logger(level = Base.CoreLogging.Error)
|
||||
|
||||
const ENABLE_LARGE_TESTS = ("UCJL_LARGE_TESTS" in keys(ENV))
|
||||
FIXTURES = "$(@__DIR__)/fixtures"
|
||||
|
||||
@testset "UnitCommitment" begin
|
||||
include("usage.jl")
|
||||
@@ -17,14 +17,17 @@ const ENABLE_LARGE_TESTS = ("UCJL_LARGE_TESTS" in keys(ENV))
|
||||
end
|
||||
@testset "instance" begin
|
||||
include("instance/read_test.jl")
|
||||
include("instance/migrate_test.jl")
|
||||
end
|
||||
@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")
|
||||
|
||||
@@ -6,7 +6,7 @@ using UnitCommitment, Test, LinearAlgebra
|
||||
import UnitCommitment: _Violation, _offer, _query
|
||||
|
||||
@testset "_ViolationFilter" begin
|
||||
instance = UnitCommitment.read_benchmark("test/case14")
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
filter = UnitCommitment._ViolationFilter(max_per_line = 1, max_total = 2)
|
||||
|
||||
_offer(
|
||||
|
||||
@@ -6,7 +6,7 @@ using UnitCommitment, Test, LinearAlgebra
|
||||
import UnitCommitment: _Violation, _offer, _query
|
||||
|
||||
@testset "find_violations" begin
|
||||
instance = UnitCommitment.read_benchmark("test/case14")
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
for line in instance.lines, t in 1:instance.time
|
||||
line.normal_flow_limit[t] = 1.0
|
||||
line.emergency_flow_limit[t] = 1.0
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
using UnitCommitment, Test, LinearAlgebra
|
||||
|
||||
@testset "_susceptance_matrix" begin
|
||||
instance = UnitCommitment.read_benchmark("test/case14")
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
actual = UnitCommitment._susceptance_matrix(instance.lines)
|
||||
@test size(actual) == (20, 20)
|
||||
expected = Diagonal([
|
||||
@@ -34,7 +34,7 @@ using UnitCommitment, Test, LinearAlgebra
|
||||
end
|
||||
|
||||
@testset "_reduced_incidence_matrix" begin
|
||||
instance = UnitCommitment.read_benchmark("test/case14")
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
actual = UnitCommitment._reduced_incidence_matrix(
|
||||
lines = instance.lines,
|
||||
buses = instance.buses,
|
||||
@@ -81,7 +81,7 @@ end
|
||||
end
|
||||
|
||||
@testset "_injection_shift_factors" begin
|
||||
instance = UnitCommitment.read_benchmark("test/case14")
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
actual = UnitCommitment._injection_shift_factors(
|
||||
lines = instance.lines,
|
||||
buses = instance.buses,
|
||||
@@ -112,7 +112,7 @@ end
|
||||
end
|
||||
|
||||
@testset "_line_outage_factors" begin
|
||||
instance = UnitCommitment.read_benchmark("test/case14")
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
isf_before = UnitCommitment._injection_shift_factors(
|
||||
lines = instance.lines,
|
||||
buses = instance.buses,
|
||||
|
||||
@@ -4,12 +4,9 @@
|
||||
|
||||
using UnitCommitment, Cbc, JuMP
|
||||
|
||||
basedir = @__DIR__
|
||||
|
||||
@testset "generate_initial_conditions!" begin
|
||||
# Load instance
|
||||
instance =
|
||||
UnitCommitment.read("$basedir/../fixtures/case118-initcond.json.gz")
|
||||
instance = UnitCommitment.read("$FIXTURES/case118-initcond.json.gz")
|
||||
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||
|
||||
# All units should have unknown initial conditions
|
||||
|
||||
@@ -6,6 +6,7 @@ import Random
|
||||
import UnitCommitment: XavQiuAhm2021
|
||||
|
||||
using Distributions
|
||||
using Random
|
||||
using UnitCommitment, Cbc, JuMP
|
||||
|
||||
get_instance() = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
||||
@@ -27,10 +28,12 @@ test_approx(x, y) = @test isapprox(x, y, atol = 1e-3)
|
||||
prev_system_load = system_load(instance)
|
||||
test_approx(bus.load[1] / prev_system_load[1], 0.012)
|
||||
|
||||
Random.seed!(42)
|
||||
randomize!(
|
||||
instance,
|
||||
XavQiuAhm2021.Randomization(randomize_load_profile = false),
|
||||
method = XavQiuAhm2021.Randomization(
|
||||
randomize_load_profile = false,
|
||||
),
|
||||
rng = MersenneTwister(42),
|
||||
)
|
||||
|
||||
# Check randomized costs
|
||||
@@ -53,8 +56,11 @@ test_approx(x, y) = @test isapprox(x, y, atol = 1e-3)
|
||||
@test round.(system_load(instance), digits = 1)[1:8] ≈
|
||||
[3059.5, 2983.2, 2937.5, 2953.9, 3073.1, 3356.4, 4068.5, 4018.8]
|
||||
|
||||
Random.seed!(42)
|
||||
randomize!(instance, XavQiuAhm2021.Randomization())
|
||||
randomize!(
|
||||
instance,
|
||||
XavQiuAhm2021.Randomization(),
|
||||
rng = MersenneTwister(42),
|
||||
)
|
||||
|
||||
# Check randomized load profile
|
||||
@test round.(system_load(instance), digits = 1)[1:8] ≈
|
||||
|
||||
@@ -5,19 +5,18 @@
|
||||
using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
||||
|
||||
@testset "slice" begin
|
||||
instance = UnitCommitment.read_benchmark("test/case14")
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
modified = UnitCommitment.slice(instance, 1:2)
|
||||
|
||||
# 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
|
||||
@@ -35,7 +34,6 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
||||
@test length(ps.demand) == 2
|
||||
@test length(ps.revenue) == 2
|
||||
end
|
||||
|
||||
# Should be able to build model without errors
|
||||
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||
model = UnitCommitment.build_model(
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON
|
||||
|
||||
@testset "build_model" begin
|
||||
instance = UnitCommitment.read_benchmark("test/case14")
|
||||
@testset "usage" begin
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
for line in instance.lines, t in 1:4
|
||||
line.normal_flow_limit[t] = 10.0
|
||||
end
|
||||
|
||||
@@ -4,11 +4,9 @@
|
||||
|
||||
using UnitCommitment, JSON, GZip, DataStructures
|
||||
|
||||
basedir = @__DIR__
|
||||
|
||||
function parse_case14()
|
||||
return JSON.parse(
|
||||
GZip.gzopen("$basedir/../../instances/test/case14.json.gz"),
|
||||
GZip.gzopen("$FIXTURES/case14.json.gz"),
|
||||
dicttype = () -> DefaultOrderedDict(nothing),
|
||||
)
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user