Compare commits

...

35 Commits

Author SHA1 Message Date
6e30645084 Allow v0.3 to read v0.2 instance files 2022-07-12 11:57:55 -05:00
678e6aa2f5 Update docs 2022-07-11 12:16:06 -05:00
fd25580967 Reformat source code 2022-07-11 10:58:42 -05:00
dc693896a3 Merge branch 'dev' into feature/reserves 2022-06-20 17:17:27 -05:00
ddebcc6ddb Merge branch 'dev' into feature/reserves 2022-06-20 14:31:02 -05:00
3282e5bc3a Fix all tests 2022-06-20 14:21:02 -05:00
15de1901c8 Remove temporary files 2022-06-14 14:55:59 -05:00
bf2dc4ddc4 Remove instances from repository; download on the fly 2022-06-14 14:38:44 -05:00
5c3c8f0d63 GitHub Actions: Remove older non-LTS Julia versions 2022-04-16 11:53:12 -05:00
cce6a874b9 Bump JuMP version to 1.0 2022-04-16 11:52:21 -05:00
1ce1cddaf3 Remove Gurobi from test dependencies; remove large tests 2022-04-16 11:43:09 -05:00
46d754dbcf GitHub Actions: Add Julia 1.7 2022-04-16 11:34:25 -05:00
b7d9083335 Makefile: Update clean target 2022-04-16 11:34:14 -05:00
86ae1d0429 juliaw: Make it compatible with Julia 1.7 2022-04-16 11:33:57 -05:00
58a7567c16 Randomization: Explicitly use MersenneTwister; allow other RNGs 2022-04-16 11:14:06 -05:00
2367e5a348 Fix formatting 2022-04-16 10:27:46 -05:00
74b8a8ae2c Fix formatting 2022-04-16 10:23:58 -05:00
3260fa29ad Remove temporary files 2022-04-16 10:16:53 -05:00
3b1d2d1845 Add author: Ogün Yurdakul 2022-04-16 10:15:32 -05:00
db106f1a38 Make juliaw executable 2022-04-16 10:12:09 -05:00
16b0fec6cd Make tests completely silent; remove set_gap warnings on Cbc 2022-04-16 10:11:33 -05:00
cda1e368fe Remove some redundant comments 2022-04-16 09:55:28 -05:00
099fb4e3cb Add case14-flex test case 2022-04-16 09:52:08 -05:00
oyurdakul
b4bc50c865 new formatting 2022-04-01 15:22:42 +02:00
oyurdakul
febb4f1aad new formatting 2022-04-01 15:17:14 +02:00
oyurdakul
8988b00b07 modified validation, error scripts 2022-03-23 02:39:24 +01:00
oyurdakul
0046c4ca2a change the validation of reserves 2022-03-22 19:01:20 +01:00
72f659b9ff Merge branch 'dev' into add-flexiramp 2022-03-01 16:32:52 -06:00
360308ef4a Reformat source code 2022-03-01 16:26:51 -06:00
03268dd3df Merge branch 'dev' into add-flexiramp 2022-03-01 16:26:42 -06:00
oyurdakul
a3a71ff5a9 add flexiramp 2022-02-03 09:45:06 +01:00
5ca566f147 Remove old reserves 2022-01-20 16:23:22 -06:00
3220650e39 Implement new reserves 2022-01-20 10:18:19 -06:00
ca0d250dfa Parse new reserves 2022-01-19 10:03:22 -06:00
2bd68b49a5 Reserves: Update docs 2022-01-19 09:23:21 -06:00
55 changed files with 840 additions and 309 deletions

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
julia-version: ['1.4', '1.5', '1.6'] julia-version: ['1.6', '1.7']
julia-arch: [x64] julia-arch: [x64]
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, windows-latest, macOS-latest]
exclude: exclude:

33
.gitignore vendored
View File

@@ -1,21 +1,38 @@
*.bak *.bak
*.gz *.gz
*.lastrun
*.so
*.mps
*.ipynb *.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* .ipy*
.vscode
Icon
Manifest.toml
Network Trash Folder
TODO.md
Temporary Items
benchmark/results benchmark/results
benchmark/runs benchmark/runs
benchmark/tables benchmark/tables
benchmark/tmp.json benchmark/tmp.json
build build
docs/_build
instances/**/*.json instances/**/*.json
instances/_source instances/_source
local local
notebooks notebooks
TODO.md
docs/_build
.vscode
Manifest.toml
*/Manifest.toml

View File

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

View File

@@ -2,10 +2,10 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
VERSION := 0.2 VERSION := 0.3
clean: clean:
rm -rfv build rm -rfv build Manifest.toml test/Manifest.toml deps/formatter/build deps/formatter/Manifest.toml
docs: docs:
cd docs; make clean; make dirhtml cd docs; make clean; make dirhtml

View File

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

View File

@@ -95,6 +95,7 @@ UnitCommitment.write("/tmp/output.json", solution)
## Authors ## Authors
* **Alinson S. Xavier** (Argonne National Laboratory) * **Alinson S. Xavier** (Argonne National Laboratory)
* **Aleksandr M. Kazachkov** (University of Florida) * **Aleksandr M. Kazachkov** (University of Florida)
* **Ogün Yurdakul** (Technische Universität Berlin)
* **Feng Qiu** (Argonne National Laboratory) * **Feng Qiu** (Argonne National Laboratory)
## Acknowledgments ## Acknowledgments

View File

@@ -1,7 +1,7 @@
project = "UnitCommitment.jl" project = "UnitCommitment.jl"
copyright = "2020-2021, UChicago Argonne, LLC" copyright = "2020-2022, UChicago Argonne, LLC"
author = "" author = ""
release = "0.2" release = "0.3"
extensions = ["myst_parser"] extensions = ["myst_parser"]
templates_path = ["_templates"] templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
@@ -10,7 +10,7 @@ html_static_path = ["_static"]
html_css_files = ["custom.css"] html_css_files = ["custom.css"]
html_theme_options = { html_theme_options = {
"repository_url": "https://github.com/ANL-CEEESA/UnitCommitment.jl/", "repository_url": "https://github.com/ANL-CEEESA/UnitCommitment.jl/",
"use_repository_button": True, "use_repository_button": False,
"extra_navbar": "", "extra_navbar": "",
} }
html_title = f"UnitCommitment.jl<br/><small>{release}</small>" html_title = f"UnitCommitment.jl<br/><small>{release}</small>"

View File

@@ -24,18 +24,19 @@ Instances are specified by JSON files containing the following main sections:
* 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][example] for a complete example.
[example]: https://axavier.org/UnitCommitment.jl/0.3/instances/matpower/case118/2017-01-01.json.gz
### Parameters ### Parameters
This section describes system-wide parameters, such as power balance and reserve shortfall penalties, and optimization parameters, such as the length of the planning horizon and the time. This section describes system-wide parameters, such as power balance penalty, and optimization parameters, such as the length of the planning horizon and the time.
| Key | Description | Default | Time series? | Key | Description | Default | Time series?
| :----------------------------- | :------------------------------------------------ | :------: | :------------: | :----------------------------- | :------------------------------------------------ | :------: | :------------:
| `Time horizon (h)` | Length of the planning horizon (in hours). | Required | N | `Time horizon (h)` | Length of the planning horizon (in hours). | Required | N
| `Time step (min)` | Length of each time step (in minutes). Must be a divisor of 60 (e.g. 60, 30, 20, 15, etc). | `60` | N | `Time step (min)` | Length of each time step (in minutes). Must be a divisor of 60 (e.g. 60, 30, 20, 15, etc). | `60` | N
| `Power balance penalty ($/MW)` | Penalty for system-wide shortage or surplus in production (in $/MW). This is charged per time step. For example, if there is a shortage of 1 MW for three time steps, three times this amount will be charged. | `1000.0` | Y | `Power balance penalty ($/MW)` | Penalty for system-wide shortage or surplus in production (in $/MW). This is charged per time step. For example, if there is a shortage of 1 MW for three time steps, three times this amount will be charged. | `1000.0` | Y
| `Reserve shortfall penalty ($/MW)` | Penalty for system-wide shortage in meeting reserve requirements (in $/MW). This is charged per time step. Negative value implies reserve constraints must always be satisfied. | `-1` | Y
#### Example #### Example
@@ -44,7 +45,6 @@ This section describes system-wide parameters, such as power balance and reserve
"Parameters": { "Parameters": {
"Time horizon (h)": 4, "Time horizon (h)": 4,
"Power balance penalty ($/MW)": 1000.0, "Power balance penalty ($/MW)": 1000.0,
"Reserve shortfall penalty ($/MW)": -1.0
} }
} }
``` ```
@@ -96,7 +96,7 @@ This section describes all generators in the system, including thermal units, re
| `Initial status (h)` | If set to a positive number, indicates the amount of time (in hours) the generator has been on at the beginning of the simulation, and if set to a negative number, the amount of time the generator has been off. For example, if `Initial status (h)` is `-2`, this means that the generator was off since `-02:00` (h:min). The simulation starts at time `00:00`. If `Initial status (h)` is `3`, this means that the generator was on since `-03:00`. A value of zero is not acceptable. | Required | N | `Initial 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 | `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 | `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 and limits
@@ -135,13 +135,13 @@ Note that this curve also specifies the production limits. Specifically, the fir
"Minimum uptime (h)": 4, "Minimum uptime (h)": 4,
"Initial status (h)": 12, "Initial status (h)": 12,
"Must run?": false, "Must run?": false,
"Provides spinning reserves?": true, "Reserve eligibility": ["r1"],
}, },
"gen2": { "gen2": {
"Bus": "b5", "Bus": "b5",
"Production cost curve (MW)": [0.0, [10.0, 8.0, 0.0, 3.0]], "Production cost curve (MW)": [0.0, [10.0, 8.0, 0.0, 3.0]],
"Production cost curve ($)": [0.0, 0.0], "Production cost curve ($)": [0.0, 0.0],
"Provides spinning reserves?": true, "Reserve eligibility": ["r1", "r2"],
} }
} }
} }
@@ -206,24 +206,39 @@ This section describes the characteristics of transmission system, such as its t
### Reserves ### 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? | 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 ```json
{ {
"Reserves": { "Reserves": {
"Spinning (MW)": [ "r1": {
57.30552, "Type": "spinning",
53.88429, "Amount (MW)": [
51.31838, 57.30552,
50.46307 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 +301,8 @@ The output data format is also JSON-based, but it is not currently documented si
Current limitations Current limitations
------------------- -------------------
* All reserves are system-wide. Zonal reserves are not currently supported.
* Network topology remains the same for all time periods * Network topology remains the same for all time periods
* Only N-1 transmission contingencies are supported. Generator contingencies are not currently supported. * 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. * Time-varying minimum production amounts are not currently compatible with ramp/startup/shutdown limits.
* Flexible ramping products can only be acquired under the `WanHob2016` formulation, which does not support spinning reserves.

View File

@@ -21,6 +21,7 @@
### Authors ### Authors
* **Alinson S. Xavier** (Argonne National Laboratory) * **Alinson S. Xavier** (Argonne National Laboratory)
* **Aleksandr M. Kazachkov** (University of Florida) * **Aleksandr M. Kazachkov** (University of Florida)
* **Ogün Yurdakul** (Technische Universität Berlin)
* **Feng Qiu** (Argonne National Laboratory) * **Feng Qiu** (Argonne National Laboratory)
### Acknowledgments ### Acknowledgments
@@ -35,7 +36,7 @@
If you use UnitCommitment.jl in your research (instances, models or algorithms), we kindly request that you cite the package as follows: 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". Zenodo (2020). [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). If you use the instances, we additionally request that you cite the original sources, as described in the [instances page](instances.md).

View File

@@ -9,7 +9,7 @@ suffix: .
Instances 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} ```{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. 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.
@@ -33,7 +33,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. * **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 364 variations (`2017-01-01` to `2017-12-30`) corresponding different days of the year.
### MATPOWER/UW-PSTCA ### MATPOWER/UW-PSTCA
@@ -41,11 +41,11 @@ A variety of smaller IEEE test cases, [compiled by University of Washington](htt
| Name | Buses | Generators | Lines | Contingencies | References | | Name | Buses | Generators | Lines | Contingencies | References |
|------|-------|------------|-------|---------------|--------| |------|-------|------------|-------|---------------|--------|
| `matpower/case14/2017-02-01` | 14 | 5 | 20 | 19 | [MTPWR, PSTCA] | `matpower/case14/2017-01-01` | 14 | 5 | 20 | 19 | [MTPWR, PSTCA]
| `matpower/case30/2017-02-01` | 30 | 6 | 41 | 38 | [MTPWR, PSTCA] | `matpower/case30/2017-01-01` | 30 | 6 | 41 | 38 | [MTPWR, PSTCA]
| `matpower/case57/2017-02-01` | 57 | 7 | 80 | 79 | [MTPWR, PSTCA] | `matpower/case57/2017-01-01` | 57 | 7 | 80 | 79 | [MTPWR, PSTCA]
| `matpower/case118/2017-02-01` | 118 | 54 | 186 | 177 | [MTPWR, PSTCA] | `matpower/case118/2017-01-01` | 118 | 54 | 186 | 177 | [MTPWR, PSTCA]
| `matpower/case300/2017-02-01` | 300 | 69 | 411 | 320 | [MTPWR, PSTCA] | `matpower/case300/2017-01-01` | 300 | 69 | 411 | 320 | [MTPWR, PSTCA]
### MATPOWER/Polish ### MATPOWER/Polish
@@ -54,14 +54,14 @@ Test cases based on the Polish 400, 220 and 110 kV networks, originally provided
| Name | Buses | Generators | Lines | Contingencies | References | | Name | Buses | Generators | Lines | Contingencies | References |
|------|-------|------------|-------|---------------|--------| |------|-------|------------|-------|---------------|--------|
| `matpower/case2383wp/2017-02-01` | 2383 | 323 | 2896 | 2240 | [MTPWR] | `matpower/case2383wp/2017-01-01` | 2383 | 323 | 2896 | 2240 | [MTPWR]
| `matpower/case2736sp/2017-02-01` | 2736 | 289 | 3504 | 3159 | [MTPWR] | `matpower/case2736sp/2017-01-01` | 2736 | 289 | 3504 | 3159 | [MTPWR]
| `matpower/case2737sop/2017-02-01` | 2737 | 267 | 3506 | 3161 | [MTPWR] | `matpower/case2737sop/2017-01-01` | 2737 | 267 | 3506 | 3161 | [MTPWR]
| `matpower/case2746wop/2017-02-01` | 2746 | 443 | 3514 | 3155 | [MTPWR] | `matpower/case2746wop/2017-01-01` | 2746 | 443 | 3514 | 3155 | [MTPWR]
| `matpower/case2746wp/2017-02-01` | 2746 | 457 | 3514 | 3156 | [MTPWR] | `matpower/case2746wp/2017-01-01` | 2746 | 457 | 3514 | 3156 | [MTPWR]
| `matpower/case3012wp/2017-02-01` | 3012 | 496 | 3572 | 2854 | [MTPWR] | `matpower/case3012wp/2017-01-01` | 3012 | 496 | 3572 | 2854 | [MTPWR]
| `matpower/case3120sp/2017-02-01` | 3120 | 483 | 3693 | 2950 | [MTPWR] | `matpower/case3120sp/2017-01-01` | 3120 | 483 | 3693 | 2950 | [MTPWR]
| `matpower/case3375wp/2017-02-01` | 3374 | 590 | 4161 | 3245 | [MTPWR] | `matpower/case3375wp/2017-01-01` | 3374 | 590 | 4161 | 3245 | [MTPWR]
### MATPOWER/PEGASE ### MATPOWER/PEGASE
@@ -69,11 +69,11 @@ Test cases from the [Pan European Grid Advanced Simulation and State Estimation
| Name | Buses | Generators | Lines | Contingencies | References | | Name | Buses | Generators | Lines | Contingencies | References |
|------|-------|------------|-------|---------------|--------| |------|-------|------------|-------|---------------|--------|
| `matpower/case89pegase/2017-02-01` | 89 | 12 | 210 | 192 | [JoFlMa16, FlPaCa13, MTPWR] | `matpower/case89pegase/2017-01-01` | 89 | 12 | 210 | 192 | [JoFlMa16, FlPaCa13, MTPWR]
| `matpower/case1354pegase/2017-02-01` | 1354 | 260 | 1991 | 1288 | [JoFlMa16, FlPaCa13, MTPWR] | `matpower/case1354pegase/2017-01-01` | 1354 | 260 | 1991 | 1288 | [JoFlMa16, FlPaCa13, MTPWR]
| `matpower/case2869pegase/2017-02-01` | 2869 | 510 | 4582 | 3579 | [JoFlMa16, FlPaCa13, MTPWR] | `matpower/case2869pegase/2017-01-01` | 2869 | 510 | 4582 | 3579 | [JoFlMa16, FlPaCa13, MTPWR]
| `matpower/case9241pegase/2017-02-01` | 9241 | 1445 | 16049 | 13932 | [JoFlMa16, FlPaCa13, MTPWR] | `matpower/case9241pegase/2017-01-01` | 9241 | 1445 | 16049 | 13932 | [JoFlMa16, FlPaCa13, MTPWR]
| `matpower/case13659pegase/2017-02-01` | 13659 | 4092 | 20467 | 13932 | [JoFlMa16, FlPaCa13, MTPWR] | `matpower/case13659pegase/2017-01-01` | 13659 | 4092 | 20467 | 13932 | [JoFlMa16, FlPaCa13, MTPWR]
### MATPOWER/RTE ### MATPOWER/RTE
@@ -81,14 +81,14 @@ Test cases from the R&D Division at [Reseau de Transport d'Electricite](https://
| Name | Buses | Generators | Lines | Contingencies | References | | Name | Buses | Generators | Lines | Contingencies | References |
|------|-------|------------|-------|---------------|--------| |------|-------|------------|-------|---------------|--------|
| `matpower/case1888rte/2017-02-01` | 1888 | 296 | 2531 | 1484 | [MTPWR, JoFlMa16] | `matpower/case1888rte/2017-01-01` | 1888 | 296 | 2531 | 1484 | [MTPWR, JoFlMa16]
| `matpower/case1951rte/2017-02-01` | 1951 | 390 | 2596 | 1497 | [MTPWR, JoFlMa16] | `matpower/case1951rte/2017-01-01` | 1951 | 390 | 2596 | 1497 | [MTPWR, JoFlMa16]
| `matpower/case2848rte/2017-02-01` | 2848 | 544 | 3776 | 2242 | [MTPWR, JoFlMa16] | `matpower/case2848rte/2017-01-01` | 2848 | 544 | 3776 | 2242 | [MTPWR, JoFlMa16]
| `matpower/case2868rte/2017-02-01` | 2868 | 596 | 3808 | 2260 | [MTPWR, JoFlMa16] | `matpower/case2868rte/2017-01-01` | 2868 | 596 | 3808 | 2260 | [MTPWR, JoFlMa16]
| `matpower/case6468rte/2017-02-01` | 6468 | 1262 | 9000 | 6094 | [MTPWR, JoFlMa16] | `matpower/case6468rte/2017-01-01` | 6468 | 1262 | 9000 | 6094 | [MTPWR, JoFlMa16]
| `matpower/case6470rte/2017-02-01` | 6470 | 1306 | 9005 | 6085 | [MTPWR, JoFlMa16] | `matpower/case6470rte/2017-01-01` | 6470 | 1306 | 9005 | 6085 | [MTPWR, JoFlMa16]
| `matpower/case6495rte/2017-02-01` | 6495 | 1352 | 9019 | 6060 | [MTPWR, JoFlMa16] | `matpower/case6495rte/2017-01-01` | 6495 | 1352 | 9019 | 6060 | [MTPWR, JoFlMa16]
| `matpower/case6515rte/2017-02-01` | 6515 | 1368 | 9037 | 6063 | [MTPWR, JoFlMa16] | `matpower/case6515rte/2017-01-01` | 6515 | 1368 | 9037 | 6063 | [MTPWR, JoFlMa16]
PGLIB-UC Instances PGLIB-UC Instances
@@ -288,7 +288,7 @@ Tejada19
References 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". Zenodo (2020). [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) * [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)

View File

@@ -23,7 +23,7 @@ Name | Symbol | Description | Unit
`switch_off[g,t]` | $w_{g}(t)$ | True if generator `g` switches off at time `t`. | Binary `switch_off[g,t]` | $w_{g}(t)$ | True if generator `g` switches off at time `t`. | Binary
`prod_above[g,t]` |$p'_{g}(t)$ | Amount of power produced by generator `g` above its minimum power output at time `t`. For example, if the minimum power of generator `g` is 100 MW and `g` is producing 115 MW of power at time `t`, then `prod_above[g,t]` equals `15.0`. | MW `prod_above[g,t]` |$p'_{g}(t)$ | Amount of power produced by generator `g` above its minimum power output at time `t`. For example, if the minimum power of generator `g` is 100 MW and `g` is producing 115 MW of power at time `t`, then `prod_above[g,t]` equals `15.0`. | MW
`segprod[g,t,k]` | $p^k_g(t)$ | Amount of power from piecewise linear segment `k` produced by generator `g` at time `t`. For example, if cost curve for generator `g` is defined by the points `(100, 1400)`, `(110, 1600)`, `(130, 2200)` and `(135, 2400)`, and if the generator is producing 115 MW of power at time `t`, then `segprod[g,t,:]` equals `[10.0, 5.0, 0.0]`.| MW `segprod[g,t,k]` | $p^k_g(t)$ | Amount of power from piecewise linear segment `k` produced by generator `g` at time `t`. For example, if cost curve for generator `g` is defined by the points `(100, 1400)`, `(110, 1600)`, `(130, 2200)` and `(135, 2400)`, and if the generator is producing 115 MW of power at time `t`, then `segprod[g,t,:]` equals `[10.0, 5.0, 0.0]`.| MW
`reserve[g,t]` | $r_g(t)$ | Amount of reserves provided by generator `g` at time `t`. | MW `reserve[r,g,t]` | $r_g(t)$ | Amount of reserve `r` provided by unit `g` at time `t`. | MW
`startup[g,t,s]` | $\delta^s_g(t)$ | True if generator `g` switches on at time `t` incurring start-up costs from start-up category `s`. | Binary `startup[g,t,s]` | $\delta^s_g(t)$ | True if generator `g` switches on at time `t` incurring start-up costs from start-up category `s`. | Binary

4
docs/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
Sphinx==3.5.4
sphinx-book-theme==0.1.0
myst-parser==0.14.0
Jinja2==3.0.2

View File

@@ -12,10 +12,10 @@ Usage
Installation 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 ```text
pkg> add UnitCommitment@0.2 pkg> add UnitCommitment@0.3
``` ```
To test that the package has been correctly installed, run: To test that the package has been correctly installed, run:

9
juliaw Normal file → Executable file
View File

@@ -47,7 +47,14 @@ project = TOML.parsefile("Project.toml")
manifest = TOML.parsefile("Manifest.toml") manifest = TOML.parsefile("Manifest.toml")
deps = Symbol[] deps = Symbol[]
for dep in keys(project["deps"]) 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]") println(" - \$(dep) [skip]")
else else
println(" - \$(dep)") println(" - \$(dep)")

View File

@@ -16,9 +16,11 @@ include("model/formulations/KnuOstWat2018/structs.jl")
include("model/formulations/MorLatRam2013/structs.jl") include("model/formulations/MorLatRam2013/structs.jl")
include("model/formulations/PanGua2016/structs.jl") include("model/formulations/PanGua2016/structs.jl")
include("solution/methods/XavQiuWanThi2019/structs.jl") include("solution/methods/XavQiuWanThi2019/structs.jl")
include("model/formulations/WanHob2016/structs.jl")
include("import/egret.jl") include("import/egret.jl")
include("instance/read.jl") include("instance/read.jl")
include("instance/migrate.jl")
include("model/build.jl") include("model/build.jl")
include("model/formulations/ArrCon2000/ramp.jl") include("model/formulations/ArrCon2000/ramp.jl")
include("model/formulations/base/bus.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/ramp.jl")
include("model/formulations/MorLatRam2013/scosts.jl") include("model/formulations/MorLatRam2013/scosts.jl")
include("model/formulations/PanGua2016/ramp.jl") include("model/formulations/PanGua2016/ramp.jl")
include("model/formulations/WanHob2016/ramp.jl")
include("model/jumpext.jl") include("model/jumpext.jl")
include("solution/fix.jl") include("solution/fix.jl")
include("solution/methods/XavQiuWanThi2019/enforce.jl") include("solution/methods/XavQiuWanThi2019/enforce.jl")

38
src/instance/migrate.jl Normal file
View 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

View File

@@ -8,7 +8,7 @@ using DataStructures
using GZip using GZip
import Base: getindex, time 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_benchmark(name::AbstractString)::UnitCommitmentInstance
@@ -80,11 +80,13 @@ function _read_json(path::String)::OrderedDict
end end
function _from_json(json; repair = true) function _from_json(json; repair = true)
_migrate(json)
units = Unit[] units = Unit[]
buses = Bus[] buses = Bus[]
contingencies = Contingency[] contingencies = Contingency[]
lines = TransmissionLine[] lines = TransmissionLine[]
loads = PriceSensitiveLoad[] loads = PriceSensitiveLoad[]
reserves = Reserve[]
function scalar(x; default = nothing) function scalar(x; default = nothing)
x !== nothing || return default x !== nothing || return default
@@ -105,6 +107,7 @@ function _from_json(json; repair = true)
name_to_bus = Dict{String,Bus}() name_to_bus = Dict{String,Bus}()
name_to_line = Dict{String,TransmissionLine}() name_to_line = Dict{String,TransmissionLine}()
name_to_unit = Dict{String,Unit}() name_to_unit = Dict{String,Unit}()
name_to_reserve = Dict{String,Reserve}()
function timeseries(x; default = nothing) function timeseries(x; default = nothing)
x !== nothing || return default x !== nothing || return default
@@ -117,6 +120,11 @@ function _from_json(json; repair = true)
json["Parameters"]["Power balance penalty (\$/MW)"], json["Parameters"]["Power balance penalty (\$/MW)"],
default = [1000.0 for t in 1:T], default = [1000.0 for t in 1:T],
) )
# 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( shortfall_penalty = timeseries(
json["Parameters"]["Reserve shortfall penalty (\$/MW)"], json["Parameters"]["Reserve shortfall penalty (\$/MW)"],
default = [-1.0 for t in 1:T], default = [-1.0 for t in 1:T],
@@ -135,6 +143,24 @@ function _from_json(json; repair = true)
push!(buses, bus) push!(buses, bus)
end 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 # Read units
for (unit_name, dict) in json["Generators"] for (unit_name, dict) in json["Generators"]
bus = name_to_bus[dict["Bus"]] bus = name_to_bus[dict["Bus"]]
@@ -172,6 +198,13 @@ function _from_json(json; repair = true)
) )
end 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 # Read and validate initial conditions
initial_power = scalar(dict["Initial power (MW)"], default = nothing) initial_power = scalar(dict["Initial power (MW)"], default = nothing)
initial_status = scalar(dict["Initial status (h)"], default = nothing) initial_status = scalar(dict["Initial status (h)"], default = nothing)
@@ -205,24 +238,17 @@ function _from_json(json; repair = true)
scalar(dict["Shutdown limit (MW)"], default = 1e6), scalar(dict["Shutdown limit (MW)"], default = 1e6),
initial_status, initial_status,
initial_power, initial_power,
timeseries(
dict["Provides spinning reserves?"],
default = [true for t in 1:T],
),
startup_categories, startup_categories,
unit_reserves,
) )
push!(bus.units, unit) push!(bus.units, unit)
for r in unit_reserves
push!(r.units, unit)
end
name_to_unit[unit_name] = unit name_to_unit[unit_name] = unit
push!(units, unit) push!(units, unit)
end end
# 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 # Read transmission lines
if "Transmission lines" in keys(json) if "Transmission lines" in keys(json)
for (line_name, dict) in json["Transmission lines"] for (line_name, dict) in json["Transmission lines"]
@@ -295,7 +321,9 @@ function _from_json(json; repair = true)
price_sensitive_loads_by_name = Dict(ps.name => ps for ps in loads), price_sensitive_loads_by_name = Dict(ps.name => ps for ps in loads),
price_sensitive_loads = loads, price_sensitive_loads = loads,
reserves = reserves, reserves = reserves,
reserves_by_name = name_to_reserve,
shortfall_penalty = shortfall_penalty, shortfall_penalty = shortfall_penalty,
flexiramp_shortfall_penalty = flexiramp_shortfall_penalty,
time = T, time = T,
units_by_name = Dict(g.name => g for g in units), units_by_name = Dict(g.name => g for g in units),
units = units, units = units,

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ function _add_ramp_eqs!(
gn = g.name gn = g.name
eq_ramp_down = _init(model, :eq_ramp_down) eq_ramp_down = _init(model, :eq_ramp_down)
eq_ramp_up = _init(model, :eq_str_ramp_up) eq_ramp_up = _init(model, :eq_str_ramp_up)
reserve = model[:reserve] reserve = _total_reserves(model, g)
# Gar1962.ProdVars # Gar1962.ProdVars
prod_above = model[:prod_above] prod_above = model[:prod_above]
@@ -43,7 +43,7 @@ function _add_ramp_eqs!(
model, model,
g.min_power[t] + g.min_power[t] +
prod_above[gn, t] + prod_above[gn, t] +
(RESERVES_WHEN_RAMP_UP ? reserve[gn, t] : 0.0) <= (RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) <=
g.initial_power + RU g.initial_power + RU
) )
end end
@@ -61,7 +61,7 @@ function _add_ramp_eqs!(
prod_above[gn, t] + prod_above[gn, t] +
( (
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ? RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ?
reserve[gn, t] : 0.0 reserve[t] : 0.0
) )
min_prod_last_period = min_prod_last_period =
g.min_power[t-1] * is_on[gn, t-1] + prod_above[gn, t-1] g.min_power[t-1] * is_on[gn, t-1] + prod_above[gn, t-1]
@@ -77,7 +77,7 @@ function _add_ramp_eqs!(
eq_ramp_up[gn, t] = @constraint( eq_ramp_up[gn, t] = @constraint(
model, model,
prod_above[gn, t] + prod_above[gn, t] +
(RESERVES_WHEN_RAMP_UP ? reserve[gn, t] : 0.0) - (RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) -
prod_above[gn, t-1] <= RU prod_above[gn, t-1] <= RU
) )
end end
@@ -105,7 +105,7 @@ function _add_ramp_eqs!(
prod_above[gn, t-1] + prod_above[gn, t-1] +
( (
RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ? RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ?
reserve[gn, t-1] : 0.0 reserve[t-1] : 0.0
) )
min_prod_this_period = min_prod_this_period =
g.min_power[t] * is_on[gn, t] + prod_above[gn, t] g.min_power[t] * is_on[gn, t] + prod_above[gn, t]
@@ -121,7 +121,7 @@ function _add_ramp_eqs!(
eq_ramp_down[gn, t] = @constraint( eq_ramp_down[gn, t] = @constraint(
model, model,
prod_above[gn, t-1] + prod_above[gn, t-1] +
(RESERVES_WHEN_RAMP_DOWN ? reserve[gn, t-1] : 0.0) - (RESERVES_WHEN_RAMP_DOWN ? reserve[t-1] : 0.0) -
prod_above[gn, t] <= RD prod_above[gn, t] <= RD
) )
end end

View File

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

View 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

View File

@@ -0,0 +1,17 @@
# 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

View File

@@ -4,7 +4,8 @@
function _add_system_wide_eqs!(model::JuMP.Model)::Nothing function _add_system_wide_eqs!(model::JuMP.Model)::Nothing
_add_net_injection_eqs!(model) _add_net_injection_eqs!(model)
_add_reserve_eqs!(model) _add_spinning_reserve_eqs!(model)
_add_flexiramp_reserve_eqs!(model)
return return
end end
@@ -27,29 +28,69 @@ function _add_net_injection_eqs!(model::JuMP.Model)::Nothing
return return
end end
function _add_reserve_eqs!(model::JuMP.Model)::Nothing function _add_spinning_reserve_eqs!(model::JuMP.Model)::Nothing
eq_min_reserve = _init(model, :eq_min_reserve)
instance = model[:instance] instance = model[:instance]
for t in 1:instance.time eq_min_spinning_reserve = _init(model, :eq_min_spinning_reserve)
# Equation (68) in Kneuven et al. (2020) for r in instance.reserves
# As in Morales-España et al. (2013a) r.type == "spinning" || continue
# Akin to the alternative formulation with max_power_avail for t in 1:instance.time
# from Carrión and Arroyo (2006) and Ostrowski et al. (2012) # Equation (68) in Kneuven et al. (2020)
shortfall_penalty = instance.shortfall_penalty[t] # As in Morales-España et al. (2013a)
eq_min_reserve[t] = @constraint( # Akin to the alternative formulation with max_power_avail
model, # from Carrión and Arroyo (2006) and Ostrowski et al. (2012)
sum(model[:reserve][g.name, t] for g in instance.units) + eq_min_spinning_reserve[r.name, t] = @constraint(
(shortfall_penalty >= 0 ? model[:reserve_shortfall][t] : 0.0) >= model,
instance.reserves.spinning[t] 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 shortfall_penalty >= 0
add_to_expression!(
model[:obj],
shortfall_penalty,
model[:reserve_shortfall][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
end end
return return

View File

@@ -12,7 +12,8 @@ function _add_unit!(model::JuMP.Model, g::Unit, formulation::Formulation)
# Variables # Variables
_add_production_vars!(model, g, formulation.prod_vars) _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_startup_shutdown_vars!(model, g)
_add_status_vars!(model, g, formulation.status_vars) _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) _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 = _init(model, :reserve)
reserve_shortfall = _init(model, :reserve_shortfall) reserve_shortfall = _init(model, :reserve_shortfall)
for t in 1:model[:instance].time for r in g.reserves
if g.provides_spinning_reserves[t] r.type == "spinning" || continue
reserve[g.name, t] = @variable(model, lower_bound = 0) for t in 1:model[:instance].time
else reserve[r.name, g.name, t] = @variable(model, lower_bound = 0)
reserve[g.name, t] = 0.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 end
reserve_shortfall[t] =
(model[:instance].shortfall_penalty[t] >= 0) ?
@variable(model, lower_bound = 0) : 0.0
end end
return return
end end
function _add_reserve_eqs!(model::JuMP.Model, g::Unit)::Nothing function _add_flexiramp_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing
reserve = model[:reserve] upflexiramp = _init(model, :upflexiramp)
for t in 1:model[:instance].time upflexiramp_shortfall = _init(model, :upflexiramp_shortfall)
add_to_expression!(expr_reserve[g.bus.name, t], reserve[g.name, t], 1.0) 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 end
return return
end 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) eq_startup_limit = _init(model, :eq_startup_limit)
is_on = model[:is_on] is_on = model[:is_on]
prod_above = model[:prod_above] prod_above = model[:prod_above]
reserve = model[:reserve] reserve = _total_reserves(model, g)
switch_off = model[:switch_off] switch_off = model[:switch_off]
switch_on = model[:switch_on] switch_on = model[:switch_on]
T = model[:instance].time T = model[:instance].time
@@ -89,7 +112,7 @@ function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
# Startup limit # Startup limit
eq_startup_limit[g.name, t] = @constraint( eq_startup_limit[g.name, t] = @constraint(
model, model,
prod_above[g.name, t] + reserve[g.name, t] <= prod_above[g.name, t] + reserve[t] <=
(g.max_power[t] - g.min_power[t]) * is_on[g.name, t] - (g.max_power[t] - g.min_power[t]) * is_on[g.name, t] -
max(0, g.max_power[t] - g.startup_limit) * switch_on[g.name, t] max(0, g.max_power[t] - g.startup_limit) * switch_on[g.name, t]
) )
@@ -117,7 +140,7 @@ function _add_ramp_eqs!(
formulation::RampingFormulation, formulation::RampingFormulation,
)::Nothing )::Nothing
prod_above = model[:prod_above] prod_above = model[:prod_above]
reserve = model[:reserve] reserve = _total_reserves(model, g)
eq_ramp_up = _init(model, :eq_ramp_up) eq_ramp_up = _init(model, :eq_ramp_up)
eq_ramp_down = _init(model, :eq_ramp_down) eq_ramp_down = _init(model, :eq_ramp_down)
for t in 1:model[:instance].time for t in 1:model[:instance].time
@@ -126,14 +149,14 @@ function _add_ramp_eqs!(
if _is_initially_on(g) == 1 if _is_initially_on(g) == 1
eq_ramp_up[g.name, t] = @constraint( eq_ramp_up[g.name, t] = @constraint(
model, model,
prod_above[g.name, t] + reserve[g.name, t] <= prod_above[g.name, t] + reserve[t] <=
(g.initial_power - g.min_power[t]) + g.ramp_up_limit (g.initial_power - g.min_power[t]) + g.ramp_up_limit
) )
end end
else else
eq_ramp_up[g.name, t] = @constraint( eq_ramp_up[g.name, t] = @constraint(
model, model,
prod_above[g.name, t] + reserve[g.name, t] <= prod_above[g.name, t] + reserve[t] <=
prod_above[g.name, t-1] + g.ramp_up_limit prod_above[g.name, t-1] + g.ramp_up_limit
) )
end end
@@ -216,3 +239,15 @@ function _add_net_injection_eqs!(model::JuMP.Model, g::Unit)::Nothing
) )
end end
end end
function _total_reserves(model, g)::Vector
T = model[:instance].time
reserve = [0.0 for _ in 1:T]
spinning_reserves = [r for r in g.reserves if r.type == "spinning"]
if !isempty(spinning_reserves)
reserve += [
sum(model[:reserve][r.name, g.name, t] for r in spinning_reserves) for t in 1:model[:instance].time
]
end
return reserve
end

View File

@@ -18,15 +18,28 @@ function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing
is_on_value = round(solution["Is on"][g.name][t]) is_on_value = round(solution["Is on"][g.name][t])
prod_value = prod_value =
round(solution["Production (MW)"][g.name][t], digits = 5) 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(is_on[g.name, t], is_on_value, force = true)
JuMP.fix( JuMP.fix(
prod_above[g.name, t], prod_above[g.name, t],
prod_value - is_on_value * g.min_power[t], prod_value - is_on_value * g.min_power[t],
force = true, 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
end end
return return

View File

@@ -3,13 +3,12 @@
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Nothing 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) function set_gap(gap)
try JuMP.set_optimizer_attribute(model, "MIPGap", gap)
JuMP.set_optimizer_attribute(model, "MIPGap", gap) @info @sprintf("MIP gap tolerance set to %f", gap)
@info @sprintf("MIP gap tolerance set to %f", gap)
catch
@warn "Could not change MIP gap tolerance"
end
end end
initial_time = time() initial_time = time()
large_gap = false large_gap = false
@@ -17,8 +16,6 @@ function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Nothing
if has_transmission && method.two_phase_gap if has_transmission && method.two_phase_gap
set_gap(1e-2) set_gap(1e-2)
large_gap = true large_gap = true
else
set_gap(method.gap_limit)
end end
while true while true
time_elapsed = time() - initial_time time_elapsed = time() - initial_time

View File

@@ -13,7 +13,7 @@ Lazy constraint solution method described in:
module XavQiuWanThi2019 module XavQiuWanThi2019
import ..SolutionMethod import ..SolutionMethod
""" """
struct Method mutable struct Method
time_limit::Float64 time_limit::Float64
gap_limit::Float64 gap_limit::Float64
two_phase_gap::Bool two_phase_gap::Bool
@@ -27,7 +27,7 @@ Fields
- `time_limit`: - `time_limit`:
the time limit over the entire optimization procedure. the time limit over the entire optimization procedure.
- `gap_limit`: - `gap_limit`:
the desired relative optimality gap. the desired relative optimality gap. Only used when `two_phase_gap=true`.
- `two_phase_gap`: - `two_phase_gap`:
if true, solve the problem with large gap tolerance first, then reduce if true, solve the problem with large gap tolerance first, then reduce
the gap tolerance when no further violated constraints are found. the gap tolerance when no further violated constraints are found.
@@ -39,7 +39,7 @@ Fields
formulation per time period. formulation per time period.
""" """
struct Method <: SolutionMethod mutable struct Method <: SolutionMethod
time_limit::Float64 time_limit::Float64
gap_limit::Float64 gap_limit::Float64
two_phase_gap::Bool two_phase_gap::Bool

View File

@@ -50,13 +50,6 @@ function solution(model::JuMP.Model)::OrderedDict
sol["Is on"] = timeseries(model[:is_on], instance.units) sol["Is on"] = timeseries(model[:is_on], instance.units)
sol["Switch on"] = timeseries(model[:switch_on], instance.units) sol["Switch on"] = timeseries(model[:switch_on], instance.units)
sol["Switch off"] = timeseries(model[:switch_off], instance.units) sol["Switch off"] = timeseries(model[:switch_off], instance.units)
sol["Reserve (MW)"] = timeseries(model[:reserve], instance.units)
sol["Reserve shortfall (MW)"] = OrderedDict(
t =>
(instance.shortfall_penalty[t] >= 0) ?
round(value(model[:reserve_shortfall][t]), digits = 5) : 0.0 for
t in 1:instance.time
)
sol["Net injection (MW)"] = sol["Net injection (MW)"] =
timeseries(model[:net_injection], instance.buses) timeseries(model[:net_injection], instance.buses)
sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses) sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses)
@@ -67,5 +60,47 @@ function solution(model::JuMP.Model)::OrderedDict
sol["Price-sensitive loads (MW)"] = sol["Price-sensitive loads (MW)"] =
timeseries(model[:loads], instance.price_sensitive_loads) timeseries(model[:loads], instance.price_sensitive_loads)
end end
sol["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 return sol
end end

View File

@@ -118,11 +118,12 @@ Base.@kwdef struct Randomization
end end
function _randomize_costs( function _randomize_costs(
rng,
instance::UnitCommitmentInstance, instance::UnitCommitmentInstance,
distribution, distribution,
)::Nothing )::Nothing
for unit in instance.units for unit in instance.units
α = rand(distribution) α = rand(rng, distribution)
unit.min_power_cost *= α unit.min_power_cost *= α
for k in unit.cost_segments for k in unit.cost_segments
k.cost *= α k.cost *= α
@@ -135,10 +136,11 @@ function _randomize_costs(
end end
function _randomize_load_share( function _randomize_load_share(
rng,
instance::UnitCommitmentInstance, instance::UnitCommitmentInstance,
distribution, distribution,
)::Nothing )::Nothing
α = rand(distribution, length(instance.buses)) α = rand(rng, distribution, length(instance.buses))
for t in 1:instance.time for t in 1:instance.time
total = sum(bus.load[t] for bus in instance.buses) total = sum(bus.load[t] for bus in instance.buses)
den = sum( den = sum(
@@ -153,6 +155,7 @@ function _randomize_load_share(
end end
function _randomize_load_profile( function _randomize_load_profile(
rng,
instance::UnitCommitmentInstance, instance::UnitCommitmentInstance,
params::Randomization, params::Randomization,
)::Nothing )::Nothing
@@ -161,12 +164,13 @@ function _randomize_load_profile(
for t in 2:instance.time for t in 2:instance.time
idx = (t - 1) % length(params.load_profile_mu) + 1 idx = (t - 1) % length(params.load_profile_mu) + 1
gamma = rand( gamma = rand(
rng,
Normal(params.load_profile_mu[idx], params.load_profile_sigma[idx]), Normal(params.load_profile_mu[idx], params.load_profile_sigma[idx]),
) )
push!(system_load, system_load[t-1] * gamma) push!(system_load, system_load[t-1] * gamma)
end end
capacity = sum(maximum(u.max_power) for u in instance.units) 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 system_load = system_load ./ maximum(system_load) .* peak_load
# Scale bus loads to match the new system load # Scale bus loads to match the new system load
@@ -186,22 +190,24 @@ end
function randomize!( function randomize!(
instance::UnitCommitment.UnitCommitmentInstance, instance::UnitCommitment.UnitCommitmentInstance,
method::XavQiuAhm2021.Randomization, method::XavQiuAhm2021.Randomization,
rng = MersenneTwister(),
)::Nothing )::Nothing
Randomize costs and loads based on the method described in XavQiuAhm2021. Randomize costs and loads based on the method described in XavQiuAhm2021.
""" """
function randomize!( function randomize!(
instance::UnitCommitment.UnitCommitmentInstance, instance::UnitCommitment.UnitCommitmentInstance,
method::XavQiuAhm2021.Randomization, method::XavQiuAhm2021.Randomization;
rng = MersenneTwister(),
)::Nothing )::Nothing
if method.randomize_costs if method.randomize_costs
XavQiuAhm2021._randomize_costs(instance, method.cost) XavQiuAhm2021._randomize_costs(rng, instance, method.cost)
end end
if method.randomize_load_share if method.randomize_load_share
XavQiuAhm2021._randomize_load_share(instance, method.load_share) XavQiuAhm2021._randomize_load_share(rng, instance, method.load_share)
end end
if method.randomize_load_profile if method.randomize_load_profile
XavQiuAhm2021._randomize_load_profile(instance, method) XavQiuAhm2021._randomize_load_profile(rng, instance, method)
end end
return return
end end

View File

@@ -24,13 +24,14 @@ function slice(
modified = deepcopy(instance) modified = deepcopy(instance)
modified.time = length(range) modified.time = length(range)
modified.power_balance_penalty = modified.power_balance_penalty[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 for u in modified.units
u.max_power = u.max_power[range] u.max_power = u.max_power[range]
u.min_power = u.min_power[range] u.min_power = u.min_power[range]
u.must_run = u.must_run[range] u.must_run = u.must_run[range]
u.min_power_cost = u.min_power_cost[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 for s in u.cost_segments
s.mw = s.mw[range] s.mw = s.mw[range]
s.cost = s.cost[range] s.cost = s.cost[range]

View File

@@ -5,20 +5,11 @@
import Logging: min_enabled_level, shouldlog, handle_message import Logging: min_enabled_level, shouldlog, handle_message
using Base.CoreLogging, Logging, Printf using Base.CoreLogging, Logging, Printf
struct TimeLogger <: AbstractLogger Base.@kwdef struct TimeLogger <: AbstractLogger
initial_time::Float64 initial_time::Float64
file::Union{Nothing,IOStream} file::Union{Nothing,IOStream} = nothing
screen_log_level::Any screen_log_level::Any = CoreLogging.Info
io_log_level::Any io_log_level::Any = CoreLogging.Info
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)
end end
min_enabled_level(logger::TimeLogger) = logger.io_log_level min_enabled_level(logger::TimeLogger) = logger.io_log_level
@@ -61,7 +52,9 @@ function handle_message(
end end
end end
function _setup_logger() function _setup_logger(; level = CoreLogging.Info)
initial_time = time() initial_time = time()
return global_logger(TimeLogger(initial_time = initial_time)) return global_logger(
TimeLogger(initial_time = initial_time, screen_log_level = level),
)
end end

View File

@@ -40,12 +40,19 @@ function validate(
return true return true
end end
function _validate_units(instance, solution; tol = 0.01) function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01)
err_count = 0 err_count = 0
for unit in instance.units for unit in instance.units
production = solution["Production (MW)"][unit.name] production = solution["Production (MW)"][unit.name]
reserve = solution["Reserve (MW)"][unit.name] reserve = [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_production_cost = solution["Production cost (\$)"][unit.name]
actual_startup_cost = solution["Startup cost (\$)"][unit.name] actual_startup_cost = solution["Startup cost (\$)"][unit.name]
is_on = bin(solution["Is on"][unit.name]) is_on = bin(solution["Is on"][unit.name])
@@ -99,13 +106,18 @@ function _validate_units(instance, solution; tol = 0.01)
end end
# Verify reserve eligibility # Verify reserve eligibility
if !unit.provides_spinning_reserves[t] && reserve[t] > tol for r in instance.reserves
@error @sprintf( if r.type == "spinning"
"Unit %s is not eligible to provide spinning reserves at time %d", if unit r.units &&
unit.name, (unit in keys(solution["Spinning reserve (MW)"][r.name]))
t @error @sprintf(
) "Unit %s is not eligible to provide reserve %s",
err_count += 1 unit.name,
r.name,
)
err_count += 1
end
end
end end
# If unit is on, must produce at least its minimum power # 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 unit is off, must produce zero
if !is_on[t] && production[t] + reserve[t] > tol if !is_on[t] && production[t] + reserve[t] > tol
@error @sprintf( @error @sprintf(
"Unit %s produces power at time %d while off", "Unit %s produces power at time %d while off (%.2f + %.2f > 0)",
unit.name, unit.name,
t t,
production[t],
reserve[t],
) )
err_count += 1 err_count += 1
end end
@@ -321,22 +335,66 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
err_count += 1 err_count += 1
end end
# Verify spinning reserves # Verify reserves
reserve = for r in instance.reserves
sum(solution["Reserve (MW)"][g.name][t] for g in instance.units) if r.type == "spinning"
reserve_shortfall = provided = sum(
(instance.shortfall_penalty[t] >= 0) ? solution["Spinning reserve (MW)"][r.name][g.name][t] for
solution["Reserve shortfall (MW)"][t] : 0 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 if provided + shortfall < required - tol
@error @sprintf( @error @sprintf(
"Insufficient spinning reserves at time %d (%.2f + %.2f should be %.2f)", "Insufficient reserve %s at time %d (%.2f + %.2f < %.2f)",
t, r.name,
reserve, t,
reserve_shortfall, provided,
instance.reserves.spinning[t], shortfall,
) required,
err_count += 1 )
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
end end

View File

@@ -3,7 +3,6 @@ Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
GZip = "92fee26a-97fe-5a0c-ad85-20a5f3185b63" GZip = "92fee26a-97fe-5a0c-ad85-20a5f3185b63"
Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572" JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
@@ -20,7 +19,7 @@ DataStructures = "0.18"
Distributions = "0.25" Distributions = "0.25"
GZip = "0.5" GZip = "0.5"
JSON = "0.21" JSON = "0.21"
JuMP = "0.21" JuMP = "1"
MathOptInterface = "0.9" MathOptInterface = "1"
PackageCompiler = "1" PackageCompiler = "1"
julia = "1" julia = "1"

Binary file not shown.

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

Binary file not shown.

BIN
test/fixtures/case14.json.gz vendored Normal file

Binary file not shown.

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

Binary file not shown.

View File

@@ -4,12 +4,9 @@
using UnitCommitment using UnitCommitment
basedir = @__DIR__
@testset "read_egret_solution" begin @testset "read_egret_solution" begin
solution = UnitCommitment.read_egret_solution( solution =
"$basedir/../fixtures/egret_output.json.gz", UnitCommitment.read_egret_solution("$FIXTURES/egret_output.json.gz")
)
for attr in ["Is on", "Production (MW)", "Production cost (\$)"] for attr in ["Is on", "Production (MW)", "Production cost (\$)"]
@test attr in keys(solution) @test attr in keys(solution)
@test "115_STEAM_1" in keys(solution[attr]) @test "115_STEAM_1" in keys(solution[attr])

View 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

View File

@@ -5,7 +5,7 @@
using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@testset "read_benchmark" begin @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.lines) == 20
@test length(instance.buses) == 14 @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[9].load == [35.36638, 33.25495, 31.67138, 31.14353]
@test instance.buses_by_name["b9"].name == "b9" @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] unit = instance.units[1]
@test unit.name == "g1" @test unit.name == "g1"
@test unit.bus.name == "b1" @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_power_cost == [1400.0 for t in 1:4]
@test unit.min_uptime == 1 @test unit.min_uptime == 1
@test unit.min_downtime == 1 @test unit.min_downtime == 1
@test unit.provides_spinning_reserves == [true for t in 1:4]
for t in 1:1 for t in 1:1
@test unit.cost_segments[1].mw[t] == 10.0 @test unit.cost_segments[1].mw[t] == 10.0
@test unit.cost_segments[2].mw[t] == 20.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[1].cost == 1000.0
@test unit.startup_categories[2].cost == 1500.0 @test unit.startup_categories[2].cost == 1500.0
@test unit.startup_categories[3].cost == 2000.0 @test unit.startup_categories[3].cost == 2000.0
@test length(unit.reserves) == 0
@test instance.units_by_name["g1"].name == "g1" @test instance.units_by_name["g1"].name == "g1"
unit = instance.units[2] unit = instance.units[2]
@test unit.name == "g2" @test unit.name == "g2"
@test unit.must_run == [false for t in 1:4] @test unit.must_run == [false for t in 1:4]
@test length(unit.reserves) == 1
unit = instance.units[3] unit = instance.units[3]
@test unit.name == "g3" @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_power_cost == [0.0 for t in 1:4]
@test unit.min_uptime == 1 @test unit.min_uptime == 1
@test unit.min_downtime == 1 @test unit.min_downtime == 1
@test unit.provides_spinning_reserves == [true for t in 1:4]
for t in 1:4 for t in 1:4
@test unit.cost_segments[1].mw[t] 33 @test unit.cost_segments[1].mw[t] 33
@test unit.cost_segments[2].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[2].cost[t] 38.04
@test unit.cost_segments[3].cost[t] 44.77853 @test unit.cost_segments[3].cost[t] 44.77853
end end
@test length(unit.reserves) == 1
@test instance.reserves.spinning == zeros(4) @test unit.reserves[1].name == "r1"
@test instance.contingencies[1].lines == [instance.lines[1]] @test instance.contingencies[1].lines == [instance.lines[1]]
@test instance.contingencies[1].units == [] @test instance.contingencies[1].units == []
@@ -107,7 +112,7 @@ using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
end end
@testset "read_benchmark sub-hourly" begin @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 @test instance.time == 4
unit = instance.units[1] unit = instance.units[1]
@test unit.name == "g1" @test unit.name == "g1"

View File

@@ -4,6 +4,8 @@
using UnitCommitment using UnitCommitment
using JuMP using JuMP
using Cbc
using JSON
import UnitCommitment: import UnitCommitment:
ArrCon2000, ArrCon2000,
CarArr2006, CarArr2006,
@@ -13,62 +15,70 @@ import UnitCommitment:
KnuOstWat2018, KnuOstWat2018,
MorLatRam2013, MorLatRam2013,
PanGua2016, PanGua2016,
XavQiuWanThi2019 XavQiuWanThi2019,
WanHob2016
if ENABLE_LARGE_TESTS function _test(
using Gurobi formulation::Formulation;
end instances = ["case14"],
dump::Bool = false,
function _small_test(formulation::Formulation)::Nothing )::Nothing
instances = ["matpower/case118/2017-02-01", "test/case14"] for instance_name in instances
for instance in instances instance = UnitCommitment.read("$(FIXTURES)/$(instance_name).json.gz")
# 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)
model = UnitCommitment.build_model( model = UnitCommitment.build_model(
instance = instance, instance = instance,
formulation = formulation, formulation = formulation,
optimizer = Gurobi.Optimizer, optimizer = Cbc.Optimizer,
) variable_names = true,
UnitCommitment.optimize!(
model,
XavQiuWanThi2019.Method(two_phase_gap = false, gap_limit = 0.1),
) )
set_silent(model)
UnitCommitment.optimize!(model)
solution = UnitCommitment.solution(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) @test UnitCommitment.validate(instance, solution)
end end
return return
end end
function _test(formulation::Formulation)::Nothing @testset "formulations" begin
_small_test(formulation) @testset "default" begin
if ENABLE_LARGE_TESTS _test(Formulation())
_large_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
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

View File

@@ -6,9 +6,9 @@ using Test
using UnitCommitment using UnitCommitment
push!(Base.LOAD_PATH, @__DIR__) 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 @testset "UnitCommitment" begin
include("usage.jl") include("usage.jl")
@@ -17,14 +17,17 @@ const ENABLE_LARGE_TESTS = ("UCJL_LARGE_TESTS" in keys(ENV))
end end
@testset "instance" begin @testset "instance" begin
include("instance/read_test.jl") include("instance/read_test.jl")
include("instance/migrate_test.jl")
end end
@testset "model" begin @testset "model" begin
include("model/formulations_test.jl") include("model/formulations_test.jl")
end end
@testset "XavQiuWanThi19" begin @testset "solution" begin
include("solution/methods/XavQiuWanThi19/filter_test.jl") @testset "XavQiuWanThi19" begin
include("solution/methods/XavQiuWanThi19/find_test.jl") include("solution/methods/XavQiuWanThi19/filter_test.jl")
include("solution/methods/XavQiuWanThi19/sensitivity_test.jl") include("solution/methods/XavQiuWanThi19/find_test.jl")
include("solution/methods/XavQiuWanThi19/sensitivity_test.jl")
end
end end
@testset "transform" begin @testset "transform" begin
include("transform/initcond_test.jl") include("transform/initcond_test.jl")

View File

@@ -6,7 +6,7 @@ using UnitCommitment, Test, LinearAlgebra
import UnitCommitment: _Violation, _offer, _query import UnitCommitment: _Violation, _offer, _query
@testset "_ViolationFilter" begin @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) filter = UnitCommitment._ViolationFilter(max_per_line = 1, max_total = 2)
_offer( _offer(

View File

@@ -6,7 +6,7 @@ using UnitCommitment, Test, LinearAlgebra
import UnitCommitment: _Violation, _offer, _query import UnitCommitment: _Violation, _offer, _query
@testset "find_violations" begin @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 for line in instance.lines, t in 1:instance.time
line.normal_flow_limit[t] = 1.0 line.normal_flow_limit[t] = 1.0
line.emergency_flow_limit[t] = 1.0 line.emergency_flow_limit[t] = 1.0

View File

@@ -5,7 +5,7 @@
using UnitCommitment, Test, LinearAlgebra using UnitCommitment, Test, LinearAlgebra
@testset "_susceptance_matrix" begin @testset "_susceptance_matrix" begin
instance = UnitCommitment.read_benchmark("test/case14") instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
actual = UnitCommitment._susceptance_matrix(instance.lines) actual = UnitCommitment._susceptance_matrix(instance.lines)
@test size(actual) == (20, 20) @test size(actual) == (20, 20)
expected = Diagonal([ expected = Diagonal([
@@ -34,7 +34,7 @@ using UnitCommitment, Test, LinearAlgebra
end end
@testset "_reduced_incidence_matrix" begin @testset "_reduced_incidence_matrix" begin
instance = UnitCommitment.read_benchmark("test/case14") instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
actual = UnitCommitment._reduced_incidence_matrix( actual = UnitCommitment._reduced_incidence_matrix(
lines = instance.lines, lines = instance.lines,
buses = instance.buses, buses = instance.buses,
@@ -81,7 +81,7 @@ end
end end
@testset "_injection_shift_factors" begin @testset "_injection_shift_factors" begin
instance = UnitCommitment.read_benchmark("test/case14") instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
actual = UnitCommitment._injection_shift_factors( actual = UnitCommitment._injection_shift_factors(
lines = instance.lines, lines = instance.lines,
buses = instance.buses, buses = instance.buses,
@@ -112,7 +112,7 @@ end
end end
@testset "_line_outage_factors" begin @testset "_line_outage_factors" begin
instance = UnitCommitment.read_benchmark("test/case14") instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
isf_before = UnitCommitment._injection_shift_factors( isf_before = UnitCommitment._injection_shift_factors(
lines = instance.lines, lines = instance.lines,
buses = instance.buses, buses = instance.buses,

View File

@@ -4,12 +4,9 @@
using UnitCommitment, Cbc, JuMP using UnitCommitment, Cbc, JuMP
basedir = @__DIR__
@testset "generate_initial_conditions!" begin @testset "generate_initial_conditions!" begin
# Load instance # Load instance
instance = instance = UnitCommitment.read("$FIXTURES/case118-initcond.json.gz")
UnitCommitment.read("$basedir/../fixtures/case118-initcond.json.gz")
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
# All units should have unknown initial conditions # All units should have unknown initial conditions

View File

@@ -6,6 +6,7 @@ import Random
import UnitCommitment: XavQiuAhm2021 import UnitCommitment: XavQiuAhm2021
using Distributions using Distributions
using Random
using UnitCommitment, Cbc, JuMP using UnitCommitment, Cbc, JuMP
get_instance() = UnitCommitment.read_benchmark("matpower/case118/2017-02-01") get_instance() = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
@@ -27,10 +28,10 @@ test_approx(x, y) = @test isapprox(x, y, atol = 1e-3)
prev_system_load = system_load(instance) prev_system_load = system_load(instance)
test_approx(bus.load[1] / prev_system_load[1], 0.012) test_approx(bus.load[1] / prev_system_load[1], 0.012)
Random.seed!(42)
randomize!( randomize!(
instance, instance,
XavQiuAhm2021.Randomization(randomize_load_profile = false), XavQiuAhm2021.Randomization(randomize_load_profile = false),
rng = MersenneTwister(42),
) )
# Check randomized costs # Check randomized costs
@@ -53,8 +54,11 @@ test_approx(x, y) = @test isapprox(x, y, atol = 1e-3)
@test round.(system_load(instance), digits = 1)[1:8] @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] [3059.5, 2983.2, 2937.5, 2953.9, 3073.1, 3356.4, 4068.5, 4018.8]
Random.seed!(42) randomize!(
randomize!(instance, XavQiuAhm2021.Randomization()) instance,
XavQiuAhm2021.Randomization(),
rng = MersenneTwister(42),
)
# Check randomized load profile # Check randomized load profile
@test round.(system_load(instance), digits = 1)[1:8] @test round.(system_load(instance), digits = 1)[1:8]

View File

@@ -5,19 +5,18 @@
using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
@testset "slice" begin @testset "slice" begin
instance = UnitCommitment.read_benchmark("test/case14") instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
modified = UnitCommitment.slice(instance, 1:2) modified = UnitCommitment.slice(instance, 1:2)
# Should update all time-dependent fields # Should update all time-dependent fields
@test modified.time == 2 @test modified.time == 2
@test length(modified.power_balance_penalty) == 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 for u in modified.units
@test length(u.max_power) == 2 @test length(u.max_power) == 2
@test length(u.min_power) == 2 @test length(u.min_power) == 2
@test length(u.must_run) == 2 @test length(u.must_run) == 2
@test length(u.min_power_cost) == 2 @test length(u.min_power_cost) == 2
@test length(u.provides_spinning_reserves) == 2
for s in u.cost_segments for s in u.cost_segments
@test length(s.mw) == 2 @test length(s.mw) == 2
@test length(s.cost) == 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.demand) == 2
@test length(ps.revenue) == 2 @test length(ps.revenue) == 2
end end
# Should be able to build model without errors # Should be able to build model without errors
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
model = UnitCommitment.build_model( model = UnitCommitment.build_model(

View File

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

View File

@@ -4,11 +4,9 @@
using UnitCommitment, JSON, GZip, DataStructures using UnitCommitment, JSON, GZip, DataStructures
basedir = @__DIR__
function parse_case14() function parse_case14()
return JSON.parse( return JSON.parse(
GZip.gzopen("$basedir/../../instances/test/case14.json.gz"), GZip.gzopen("$FIXTURES/case14.json.gz"),
dicttype = () -> DefaultOrderedDict(nothing), dicttype = () -> DefaultOrderedDict(nothing),
) )
end end