mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 00:08:52 -06:00
Compare commits
12 Commits
update-dep
...
feature/mi
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e30645084 | |||
| 678e6aa2f5 | |||
| fd25580967 | |||
| dc693896a3 | |||
| ddebcc6ddb | |||
| 3282e5bc3a | |||
| 15de1901c8 | |||
| bf2dc4ddc4 | |||
| 5ca566f147 | |||
| 3220650e39 | |||
| ca0d250dfa | |||
| 2bd68b49a5 |
11
CHANGELOG.md
11
CHANGELOG.md
@@ -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
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -2,7 +2,7 @@
|
|||||||
# 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 Manifest.toml test/Manifest.toml deps/formatter/build deps/formatter/Manifest.toml
|
rm -rfv build Manifest.toml test/Manifest.toml deps/formatter/build deps/formatter/Manifest.toml
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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>"
|
||||||
|
|||||||
@@ -24,19 +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
|
|
||||||
| `Flexiramp penalty ($/MW)` | Penalty for system-wide shortage in meeting flexible ramping product requirements (in $/MW). This is charged per time step. | `500` | Y
|
|
||||||
|
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
@@ -45,8 +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,
|
|
||||||
"Flexiramp penalty ($/MW)": 100.0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -98,9 +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
|
||||||
| `Provides flexible capacity?` | If `true`, this generator may provide flexible ramping product (Boolean). | `true` | Y
|
|
||||||
|
|
||||||
|
|
||||||
#### Production costs and limits
|
#### Production costs and limits
|
||||||
|
|
||||||
@@ -139,14 +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"],
|
||||||
"Provides flexible capacity?": false,
|
|
||||||
},
|
},
|
||||||
"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"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,42 +211,34 @@ 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
|
||||||
| `Up-flexiramp (MW)` | Minimum amount of system-wide upward flexible ramping product (in MW). Only generators which are online may provide this reserve. | `0.0` | Y
|
| `Amount (MW)` | Amount of reserves required. | Required | Y
|
||||||
| `Down-flexiramp (MW)` | Minimum amount of system-wide downward flexible ramping product (in MW). Only generators which are online may provide this reserve. | `0.0` | Y
|
| `Shortfall penalty ($/MW)` | Penalty for shortage in meeting the reserve requirements (in $/MW). This is charged per time step. Negative value implies reserve constraints must always be satisfied. | `-1` | Y
|
||||||
|
|
||||||
#### Example 1
|
#### Example 1
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"Reserves": {
|
"Reserves": {
|
||||||
"Spinning (MW)": [
|
"r1": {
|
||||||
|
"Type": "spinning",
|
||||||
|
"Amount (MW)": [
|
||||||
57.30552,
|
57.30552,
|
||||||
53.88429,
|
53.88429,
|
||||||
51.31838,
|
51.31838,
|
||||||
50.46307
|
50.46307
|
||||||
]
|
],
|
||||||
}
|
"Shortfall penalty ($/MW)": 5.0
|
||||||
}
|
},
|
||||||
```
|
"r2": {
|
||||||
|
"Type": "flexiramp",
|
||||||
#### Example 2
|
"Amount (MW)": [
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Reserves": {
|
|
||||||
"Up-flexiramp (MW)": [
|
|
||||||
20.31042,
|
20.31042,
|
||||||
23.65273,
|
23.65273,
|
||||||
27.41784,
|
27.41784,
|
||||||
25.34057
|
25.34057
|
||||||
],
|
],
|
||||||
"Down-flexiramp (MW)": [
|
}
|
||||||
19.41546,
|
|
||||||
21.45377,
|
|
||||||
23.53402,
|
|
||||||
24.80973
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -314,10 +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.
|
|
||||||
* Upward and downward flexible ramping products can only be acquired under the WanHob2016 formulation, which does not support spinning reserves.
|
|
||||||
* Network topology remains the same for all time periods
|
* 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.
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
4
docs/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Sphinx==3.5.4
|
||||||
|
sphinx-book-theme==0.1.0
|
||||||
|
myst-parser==0.14.0
|
||||||
|
Jinja2==3.0.2
|
||||||
@@ -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:
|
||||||
|
|||||||
Binary file not shown.
@@ -20,6 +20,7 @@ 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")
|
||||||
|
|||||||
38
src/instance/migrate.jl
Normal file
38
src/instance/migrate.jl
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
using DataStructures
|
||||||
|
using JSON
|
||||||
|
|
||||||
|
function _migrate(json)
|
||||||
|
version = json["Parameters"]["Version"]
|
||||||
|
if version === nothing
|
||||||
|
error(
|
||||||
|
"The provided input file cannot be loaded because it does not " *
|
||||||
|
"specify what version of UnitCommitment.jl it was written for. " *
|
||||||
|
"Please modify the \"Parameters\" section of the file and include " *
|
||||||
|
"a \"Version\" entry. For example: {\"Parameters\":{\"Version\":\"0.3\"}}",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
version = VersionNumber(version)
|
||||||
|
version >= v"0.3" || _migrate_to_v03(json)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
function _migrate_to_v03(json)
|
||||||
|
# Migrate reserves
|
||||||
|
if json["Reserves"] !== nothing &&
|
||||||
|
json["Reserves"]["Spinning (MW)"] !== nothing
|
||||||
|
amount = json["Reserves"]["Spinning (MW)"]
|
||||||
|
json["Reserves"] = DefaultOrderedDict(nothing)
|
||||||
|
json["Reserves"]["r1"] = DefaultOrderedDict(nothing)
|
||||||
|
json["Reserves"]["r1"]["Type"] = "spinning"
|
||||||
|
json["Reserves"]["r1"]["Amount (MW)"] = amount
|
||||||
|
for (gen_name, gen) in json["Generators"]
|
||||||
|
if gen["Provides spinning reserves?"] == true
|
||||||
|
gen["Reserve eligibility"] = ["r1"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,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
|
||||||
@@ -140,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"]]
|
||||||
@@ -177,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)
|
||||||
@@ -210,36 +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],
|
|
||||||
),
|
|
||||||
timeseries(
|
|
||||||
dict["Provides flexible capacity?"],
|
|
||||||
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 spinning, up-flexiramp, and down-flexiramp reserve requirements
|
|
||||||
reserves = Reserves(zeros(T), zeros(T), zeros(T))
|
|
||||||
if "Reserves" in keys(json)
|
|
||||||
reserves.spinning =
|
|
||||||
timeseries(json["Reserves"]["Spinning (MW)"], default = zeros(T))
|
|
||||||
reserves.upflexiramp = timeseries(
|
|
||||||
json["Reserves"]["Up-flexiramp (MW)"],
|
|
||||||
default = zeros(T),
|
|
||||||
)
|
|
||||||
reserves.dwflexiramp = timeseries(
|
|
||||||
json["Reserves"]["Down-flexiramp (MW)"],
|
|
||||||
default = zeros(T),
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Read transmission lines
|
# 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"]
|
||||||
@@ -312,6 +321,7 @@ 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,
|
flexiramp_shortfall_penalty = flexiramp_shortfall_penalty,
|
||||||
time = T,
|
time = T,
|
||||||
|
|||||||
@@ -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,9 +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}
|
|
||||||
provides_flexiramp_reserves::Vector{Bool}
|
|
||||||
startup_categories::Vector{StartupCategory}
|
startup_categories::Vector{StartupCategory}
|
||||||
|
reserves::Vector{Reserve}
|
||||||
end
|
end
|
||||||
|
|
||||||
mutable struct TransmissionLine
|
mutable struct TransmissionLine
|
||||||
@@ -53,12 +60,6 @@ mutable struct TransmissionLine
|
|||||||
flow_limit_penalty::Vector{Float64}
|
flow_limit_penalty::Vector{Float64}
|
||||||
end
|
end
|
||||||
|
|
||||||
mutable struct Reserves
|
|
||||||
spinning::Vector{Float64}
|
|
||||||
upflexiramp::Vector{Float64}
|
|
||||||
dwflexiramp::Vector{Float64}
|
|
||||||
end
|
|
||||||
|
|
||||||
mutable struct Contingency
|
mutable struct Contingency
|
||||||
name::String
|
name::String
|
||||||
lines::Vector{TransmissionLine}
|
lines::Vector{TransmissionLine}
|
||||||
@@ -82,7 +83,8 @@ 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}
|
flexiramp_shortfall_penalty::Vector{Float64}
|
||||||
time::Int
|
time::Int
|
||||||
|
|||||||
BIN
src/model/.DS_Store
vendored
BIN
src/model/.DS_Store
vendored
Binary file not shown.
@@ -32,22 +32,6 @@ function build_model(;
|
|||||||
formulation = Formulation(),
|
formulation = Formulation(),
|
||||||
variable_names::Bool = false,
|
variable_names::Bool = false,
|
||||||
)::JuMP.Model
|
)::JuMP.Model
|
||||||
if formulation.ramping == WanHob2016.Ramping() &&
|
|
||||||
instance.reserves.spinning >= ones(instance.time) .* 1e-6
|
|
||||||
error(
|
|
||||||
"Spinning reserves are not supported by the WanHob2016 ramping formulation",
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
if formulation.ramping !== WanHob2016.Ramping() && (
|
|
||||||
instance.reserves.upflexiramp >= ones(instance.time) .* 1e-6 ||
|
|
||||||
instance.reserves.dwflexiramp >= ones(instance.time) .* 1e-6
|
|
||||||
)
|
|
||||||
error(
|
|
||||||
"Flexiramp is supported only by the WanHob2016 ramping formulation",
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
@info "Building model..."
|
@info "Building model..."
|
||||||
time_model = @elapsed begin
|
time_model = @elapsed begin
|
||||||
model = Model()
|
model = Model()
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ function _add_ramp_eqs!(
|
|||||||
RD = g.ramp_down_limit
|
RD = g.ramp_down_limit
|
||||||
SU = g.startup_limit
|
SU = g.startup_limit
|
||||||
SD = g.shutdown_limit
|
SD = g.shutdown_limit
|
||||||
reserve = model[:reserve]
|
|
||||||
eq_ramp_down = _init(model, :eq_ramp_down)
|
eq_ramp_down = _init(model, :eq_ramp_down)
|
||||||
eq_ramp_up = _init(model, :eq_ramp_up)
|
eq_ramp_up = _init(model, :eq_ramp_up)
|
||||||
is_initially_on = (g.initial_status > 0)
|
is_initially_on = (g.initial_status > 0)
|
||||||
|
reserve = _total_reserves(model, g)
|
||||||
|
|
||||||
# Gar1962.ProdVars
|
# Gar1962.ProdVars
|
||||||
prod_above = model[:prod_above]
|
prod_above = model[:prod_above]
|
||||||
@@ -41,7 +41,7 @@ function _add_ramp_eqs!(
|
|||||||
model,
|
model,
|
||||||
g.min_power[t] +
|
g.min_power[t] +
|
||||||
prod_above[gn, t] +
|
prod_above[gn, t] +
|
||||||
(RESERVES_WHEN_RAMP_UP ? reserve[gn, t] : 0.0) <=
|
(RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) <=
|
||||||
g.initial_power + RU
|
g.initial_power + RU
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -51,7 +51,7 @@ function _add_ramp_eqs!(
|
|||||||
prod_above[gn, t] +
|
prod_above[gn, t] +
|
||||||
(
|
(
|
||||||
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ?
|
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ?
|
||||||
reserve[gn, t] : 0.0
|
reserve[t] : 0.0
|
||||||
)
|
)
|
||||||
min_prod_last_period =
|
min_prod_last_period =
|
||||||
g.min_power[t-1] * is_on[gn, t-1] + prod_above[gn, t-1]
|
g.min_power[t-1] * is_on[gn, t-1] + prod_above[gn, t-1]
|
||||||
@@ -82,7 +82,7 @@ function _add_ramp_eqs!(
|
|||||||
prod_above[gn, t-1] +
|
prod_above[gn, t-1] +
|
||||||
(
|
(
|
||||||
RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ?
|
RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ?
|
||||||
reserve[gn, t-1] : 0.0
|
reserve[t-1] : 0.0
|
||||||
)
|
)
|
||||||
min_prod_this_period =
|
min_prod_this_period =
|
||||||
g.min_power[t] * is_on[gn, t] + prod_above[gn, t]
|
g.min_power[t] * is_on[gn, t] + prod_above[gn, t]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function _add_ramp_eqs!(
|
|||||||
gn = g.name
|
gn = g.name
|
||||||
eq_str_ramp_down = _init(model, :eq_str_ramp_down)
|
eq_str_ramp_down = _init(model, :eq_str_ramp_down)
|
||||||
eq_str_ramp_up = _init(model, :eq_str_ramp_up)
|
eq_str_ramp_up = _init(model, :eq_str_ramp_up)
|
||||||
reserve = model[:reserve]
|
reserve = _total_reserves(model, g)
|
||||||
|
|
||||||
# Gar1962.ProdVars
|
# Gar1962.ProdVars
|
||||||
prod_above = model[:prod_above]
|
prod_above = model[:prod_above]
|
||||||
@@ -48,10 +48,8 @@ function _add_ramp_eqs!(
|
|||||||
# end
|
# end
|
||||||
|
|
||||||
max_prod_this_period =
|
max_prod_this_period =
|
||||||
prod_above[gn, t] + (
|
prod_above[gn, t] +
|
||||||
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ?
|
(RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0)
|
||||||
reserve[gn, t] : 0.0
|
|
||||||
)
|
|
||||||
min_prod_last_period = 0.0
|
min_prod_last_period = 0.0
|
||||||
if t > 1 && time_invariant
|
if t > 1 && time_invariant
|
||||||
min_prod_last_period = prod_above[gn, t-1]
|
min_prod_last_period = prod_above[gn, t-1]
|
||||||
@@ -88,7 +86,7 @@ function _add_ramp_eqs!(
|
|||||||
max_prod_last_period =
|
max_prod_last_period =
|
||||||
min_prod_last_period + (
|
min_prod_last_period + (
|
||||||
t > 1 && (RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN) ?
|
t > 1 && (RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN) ?
|
||||||
reserve[gn, t-1] : 0.0
|
reserve[t-1] : 0.0
|
||||||
)
|
)
|
||||||
min_prod_this_period = prod_above[gn, t]
|
min_prod_this_period = prod_above[gn, t]
|
||||||
on_last_period = 0.0
|
on_last_period = 0.0
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function _add_production_limit_eqs!(
|
|||||||
eq_prod_limit = _init(model, :eq_prod_limit)
|
eq_prod_limit = _init(model, :eq_prod_limit)
|
||||||
is_on = model[:is_on]
|
is_on = model[:is_on]
|
||||||
prod_above = model[:prod_above]
|
prod_above = model[:prod_above]
|
||||||
reserve = model[:reserve]
|
reserve = _total_reserves(model, g)
|
||||||
gn = g.name
|
gn = g.name
|
||||||
for t in 1:model[:instance].time
|
for t in 1:model[:instance].time
|
||||||
# Objective function terms for production costs
|
# Objective function terms for production costs
|
||||||
@@ -44,7 +44,7 @@ function _add_production_limit_eqs!(
|
|||||||
end
|
end
|
||||||
eq_prod_limit[gn, t] = @constraint(
|
eq_prod_limit[gn, t] = @constraint(
|
||||||
model,
|
model,
|
||||||
prod_above[gn, t] + reserve[gn, t] <= power_diff * is_on[gn, t]
|
prod_above[gn, t] + reserve[t] <= power_diff * is_on[gn, t]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function _add_ramp_eqs!(
|
|||||||
gn = g.name
|
gn = g.name
|
||||||
eq_ramp_down = _init(model, :eq_ramp_down)
|
eq_ramp_down = _init(model, :eq_ramp_down)
|
||||||
eq_ramp_up = _init(model, :eq_str_ramp_up)
|
eq_ramp_up = _init(model, :eq_str_ramp_up)
|
||||||
reserve = model[:reserve]
|
reserve = _total_reserves(model, g)
|
||||||
|
|
||||||
# Gar1962.ProdVars
|
# Gar1962.ProdVars
|
||||||
prod_above = model[:prod_above]
|
prod_above = model[:prod_above]
|
||||||
@@ -43,7 +43,7 @@ function _add_ramp_eqs!(
|
|||||||
model,
|
model,
|
||||||
g.min_power[t] +
|
g.min_power[t] +
|
||||||
prod_above[gn, t] +
|
prod_above[gn, t] +
|
||||||
(RESERVES_WHEN_RAMP_UP ? reserve[gn, t] : 0.0) <=
|
(RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) <=
|
||||||
g.initial_power + RU
|
g.initial_power + RU
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -61,7 +61,7 @@ function _add_ramp_eqs!(
|
|||||||
prod_above[gn, t] +
|
prod_above[gn, t] +
|
||||||
(
|
(
|
||||||
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ?
|
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ?
|
||||||
reserve[gn, t] : 0.0
|
reserve[t] : 0.0
|
||||||
)
|
)
|
||||||
min_prod_last_period =
|
min_prod_last_period =
|
||||||
g.min_power[t-1] * is_on[gn, t-1] + prod_above[gn, t-1]
|
g.min_power[t-1] * is_on[gn, t-1] + prod_above[gn, t-1]
|
||||||
@@ -77,7 +77,7 @@ function _add_ramp_eqs!(
|
|||||||
eq_ramp_up[gn, t] = @constraint(
|
eq_ramp_up[gn, t] = @constraint(
|
||||||
model,
|
model,
|
||||||
prod_above[gn, t] +
|
prod_above[gn, t] +
|
||||||
(RESERVES_WHEN_RAMP_UP ? reserve[gn, t] : 0.0) -
|
(RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) -
|
||||||
prod_above[gn, t-1] <= RU
|
prod_above[gn, t-1] <= RU
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -105,7 +105,7 @@ function _add_ramp_eqs!(
|
|||||||
prod_above[gn, t-1] +
|
prod_above[gn, t-1] +
|
||||||
(
|
(
|
||||||
RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ?
|
RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ?
|
||||||
reserve[gn, t-1] : 0.0
|
reserve[t-1] : 0.0
|
||||||
)
|
)
|
||||||
min_prod_this_period =
|
min_prod_this_period =
|
||||||
g.min_power[t] * is_on[gn, t] + prod_above[gn, t]
|
g.min_power[t] * is_on[gn, t] + prod_above[gn, t]
|
||||||
@@ -121,7 +121,7 @@ function _add_ramp_eqs!(
|
|||||||
eq_ramp_down[gn, t] = @constraint(
|
eq_ramp_down[gn, t] = @constraint(
|
||||||
model,
|
model,
|
||||||
prod_above[gn, t-1] +
|
prod_above[gn, t-1] +
|
||||||
(RESERVES_WHEN_RAMP_DOWN ? reserve[gn, t-1] : 0.0) -
|
(RESERVES_WHEN_RAMP_DOWN ? reserve[t-1] : 0.0) -
|
||||||
prod_above[gn, t] <= RD
|
prod_above[gn, t] <= RD
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function _add_ramp_eqs!(
|
|||||||
# TODO: Move upper case constants to model[:instance]
|
# TODO: Move upper case constants to model[:instance]
|
||||||
RESERVES_WHEN_SHUT_DOWN = true
|
RESERVES_WHEN_SHUT_DOWN = true
|
||||||
gn = g.name
|
gn = g.name
|
||||||
reserve = model[:reserve]
|
reserve = _total_reserves(model, g)
|
||||||
eq_str_prod_limit = _init(model, :eq_str_prod_limit)
|
eq_str_prod_limit = _init(model, :eq_str_prod_limit)
|
||||||
eq_prod_limit_ramp_up_extra_period =
|
eq_prod_limit_ramp_up_extra_period =
|
||||||
_init(model, :eq_prod_limit_ramp_up_extra_period)
|
_init(model, :eq_prod_limit_ramp_up_extra_period)
|
||||||
@@ -56,7 +56,7 @@ function _add_ramp_eqs!(
|
|||||||
model,
|
model,
|
||||||
prod_above[gn, t] +
|
prod_above[gn, t] +
|
||||||
g.min_power[t] * is_on[gn, t] +
|
g.min_power[t] * is_on[gn, t] +
|
||||||
reserve[gn, t] <=
|
reserve[t] <=
|
||||||
Pbar * is_on[gn, t] -
|
Pbar * is_on[gn, t] -
|
||||||
(t < T ? (Pbar - SD) * switch_off[gn, t+1] : 0.0) - sum(
|
(t < T ? (Pbar - SD) * switch_off[gn, t+1] : 0.0) - sum(
|
||||||
(Pbar - (SU + i * RU)) * switch_on[gn, t-i] for
|
(Pbar - (SU + i * RU)) * switch_on[gn, t-i] for
|
||||||
@@ -71,7 +71,7 @@ function _add_ramp_eqs!(
|
|||||||
model,
|
model,
|
||||||
prod_above[gn, t] +
|
prod_above[gn, t] +
|
||||||
g.min_power[t] * is_on[gn, t] +
|
g.min_power[t] * is_on[gn, t] +
|
||||||
reserve[gn, t] <=
|
reserve[t] <=
|
||||||
Pbar * is_on[gn, t] - sum(
|
Pbar * is_on[gn, t] - sum(
|
||||||
(Pbar - (SU + i * RU)) * switch_on[gn, t-i] for
|
(Pbar - (SU + i * RU)) * switch_on[gn, t-i] for
|
||||||
i in 0:min(UT - 1, TRU, t - 1)
|
i in 0:min(UT - 1, TRU, t - 1)
|
||||||
@@ -88,7 +88,7 @@ function _add_ramp_eqs!(
|
|||||||
model,
|
model,
|
||||||
prod_above[gn, t] +
|
prod_above[gn, t] +
|
||||||
g.min_power[t] * is_on[gn, t] +
|
g.min_power[t] * is_on[gn, t] +
|
||||||
(RESERVES_WHEN_SHUT_DOWN ? reserve[gn, t] : 0.0) <=
|
(RESERVES_WHEN_SHUT_DOWN ? reserve[t] : 0.0) <=
|
||||||
Pbar * is_on[gn, t] - sum(
|
Pbar * is_on[gn, t] - sum(
|
||||||
(Pbar - (SD + i * RD)) * switch_off[gn, t+1+i] for
|
(Pbar - (SD + i * RD)) * switch_off[gn, t+1+i] for
|
||||||
i in 0:KSD
|
i in 0:KSD
|
||||||
|
|||||||
@@ -2,38 +2,12 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
function _add_flexiramp_vars!(model::JuMP.Model, g::Unit)::Nothing
|
|
||||||
upflexiramp = _init(model, :upflexiramp)
|
|
||||||
upflexiramp_shortfall = _init(model, :upflexiramp_shortfall)
|
|
||||||
mfg = _init(model, :mfg)
|
|
||||||
dwflexiramp = _init(model, :dwflexiramp)
|
|
||||||
dwflexiramp_shortfall = _init(model, :dwflexiramp_shortfall)
|
|
||||||
for t in 1:model[:instance].time
|
|
||||||
# maximum feasible generation, \bar{g_{its}} in Wang & Hobbs (2016)
|
|
||||||
mfg[g.name, t] = @variable(model, lower_bound = 0)
|
|
||||||
if g.provides_flexiramp_reserves[t]
|
|
||||||
upflexiramp[g.name, t] = @variable(model) # up-flexiramp, ur_{it} in Wang & Hobbs (2016)
|
|
||||||
dwflexiramp[g.name, t] = @variable(model) # down-flexiramp, dr_{it} in Wang & Hobbs (2016)
|
|
||||||
else
|
|
||||||
upflexiramp[g.name, t] = 0.0
|
|
||||||
dwflexiramp[g.name, t] = 0.0
|
|
||||||
end
|
|
||||||
upflexiramp_shortfall[t] =
|
|
||||||
(model[:instance].flexiramp_shortfall_penalty[t] >= 0) ?
|
|
||||||
@variable(model, lower_bound = 0) : 0.0
|
|
||||||
dwflexiramp_shortfall[t] =
|
|
||||||
(model[:instance].flexiramp_shortfall_penalty[t] >= 0) ?
|
|
||||||
@variable(model, lower_bound = 0) : 0.0
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
function _add_ramp_eqs!(
|
function _add_ramp_eqs!(
|
||||||
model::JuMP.Model,
|
model::JuMP.Model,
|
||||||
g::Unit,
|
g::Unit,
|
||||||
formulation_prod_vars::Gar1962.ProdVars,
|
::Gar1962.ProdVars,
|
||||||
formulation_ramping::WanHob2016.Ramping,
|
::WanHob2016.Ramping,
|
||||||
formulation_status_vars::Gar1962.StatusVars,
|
::Gar1962.StatusVars,
|
||||||
)::Nothing
|
)::Nothing
|
||||||
is_initially_on = (g.initial_status > 0)
|
is_initially_on = (g.initial_status > 0)
|
||||||
SU = g.startup_limit
|
SU = g.startup_limit
|
||||||
@@ -51,43 +25,53 @@ function _add_ramp_eqs!(
|
|||||||
dwflexiramp = model[:dwflexiramp]
|
dwflexiramp = model[:dwflexiramp]
|
||||||
mfg = model[:mfg]
|
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
|
for t in 1:model[:instance].time
|
||||||
@constraint(
|
@constraint(
|
||||||
model,
|
model,
|
||||||
prod_above[gn, t] + (is_on[gn, t] * minp[t]) <= mfg[gn, t]
|
prod_above[gn, t] + (is_on[gn, t] * minp[t]) <= mfg[rn, gn, t]
|
||||||
) # Eq. (19) in Wang & Hobbs (2016)
|
) # Eq. (19) in Wang & Hobbs (2016)
|
||||||
@constraint(model, mfg[gn, t] <= is_on[gn, t] * maxp[t]) # Eq. (22) 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
|
if t != model[:instance].time
|
||||||
@constraint(
|
@constraint(
|
||||||
model,
|
model,
|
||||||
minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <=
|
minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <=
|
||||||
prod_above[gn, t] - dwflexiramp[gn, t] +
|
prod_above[gn, t] - dwflexiramp[rn, gn, t] +
|
||||||
(is_on[gn, t] * minp[t])
|
(is_on[gn, t] * minp[t])
|
||||||
) # first inequality of Eq. (20) in Wang & Hobbs (2016)
|
) # first inequality of Eq. (20) in Wang & Hobbs (2016)
|
||||||
@constraint(
|
@constraint(
|
||||||
model,
|
model,
|
||||||
prod_above[gn, t] - dwflexiramp[gn, t] +
|
prod_above[gn, t] - dwflexiramp[rn, gn, t] +
|
||||||
(is_on[gn, t] * minp[t]) <=
|
(is_on[gn, t] * minp[t]) <=
|
||||||
mfg[gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
|
mfg[rn, gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
|
||||||
) # second inequality of Eq. (20) in Wang & Hobbs (2016)
|
) # second inequality of Eq. (20) in Wang & Hobbs (2016)
|
||||||
@constraint(
|
@constraint(
|
||||||
model,
|
model,
|
||||||
minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <=
|
minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <=
|
||||||
prod_above[gn, t] +
|
prod_above[gn, t] +
|
||||||
upflexiramp[gn, t] +
|
upflexiramp[rn, gn, t] +
|
||||||
(is_on[gn, t] * minp[t])
|
(is_on[gn, t] * minp[t])
|
||||||
) # first inequality of Eq. (21) in Wang & Hobbs (2016)
|
) # first inequality of Eq. (21) in Wang & Hobbs (2016)
|
||||||
@constraint(
|
@constraint(
|
||||||
model,
|
model,
|
||||||
prod_above[gn, t] +
|
prod_above[gn, t] +
|
||||||
upflexiramp[gn, t] +
|
upflexiramp[rn, gn, t] +
|
||||||
(is_on[gn, t] * minp[t]) <=
|
(is_on[gn, t] * minp[t]) <=
|
||||||
mfg[gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
|
mfg[rn, gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
|
||||||
) # second inequality of Eq. (21) in Wang & Hobbs (2016)
|
) # second inequality of Eq. (21) in Wang & Hobbs (2016)
|
||||||
if t != 1
|
if t != 1
|
||||||
@constraint(
|
@constraint(
|
||||||
model,
|
model,
|
||||||
mfg[gn, t] <=
|
mfg[rn, gn, t] <=
|
||||||
prod_above[gn, t-1] +
|
prod_above[gn, t-1] +
|
||||||
(is_on[gn, t-1] * minp[t]) +
|
(is_on[gn, t-1] * minp[t]) +
|
||||||
(RU * is_on[gn, t-1]) +
|
(RU * is_on[gn, t-1]) +
|
||||||
@@ -105,7 +89,7 @@ function _add_ramp_eqs!(
|
|||||||
else
|
else
|
||||||
@constraint(
|
@constraint(
|
||||||
model,
|
model,
|
||||||
mfg[gn, t] <=
|
mfg[rn, gn, t] <=
|
||||||
initial_power +
|
initial_power +
|
||||||
(RU * is_initially_on) +
|
(RU * is_initially_on) +
|
||||||
(SU * (is_on[gn, t] - is_initially_on)) +
|
(SU * (is_on[gn, t] - is_initially_on)) +
|
||||||
@@ -122,18 +106,19 @@ function _add_ramp_eqs!(
|
|||||||
end
|
end
|
||||||
@constraint(
|
@constraint(
|
||||||
model,
|
model,
|
||||||
mfg[gn, t] <=
|
mfg[rn, gn, t] <=
|
||||||
(SD * (is_on[gn, t] - is_on[gn, t+1])) +
|
(SD * (is_on[gn, t] - is_on[gn, t+1])) +
|
||||||
(maxp[t] * is_on[gn, t+1])
|
(maxp[t] * is_on[gn, t+1])
|
||||||
) # Eq. (24) in Wang & Hobbs (2016)
|
) # Eq. (24) in Wang & Hobbs (2016)
|
||||||
@constraint(
|
@constraint(
|
||||||
model,
|
model,
|
||||||
-RD * is_on[gn, t+1] - SD * (is_on[gn, t] - is_on[gn, t+1]) -
|
-RD * is_on[gn, t+1] -
|
||||||
maxp[t] * (1 - is_on[gn, t]) <= upflexiramp[gn, t]
|
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)
|
) # first inequality of Eq. (26) in Wang & Hobbs (2016)
|
||||||
@constraint(
|
@constraint(
|
||||||
model,
|
model,
|
||||||
upflexiramp[gn, t] <=
|
upflexiramp[rn, gn, t] <=
|
||||||
RU * is_on[gn, t] +
|
RU * is_on[gn, t] +
|
||||||
SU * (is_on[gn, t+1] - is_on[gn, t]) +
|
SU * (is_on[gn, t+1] - is_on[gn, t]) +
|
||||||
maxp[t] * (1 - is_on[gn, t+1])
|
maxp[t] * (1 - is_on[gn, t+1])
|
||||||
@@ -141,11 +126,11 @@ function _add_ramp_eqs!(
|
|||||||
@constraint(
|
@constraint(
|
||||||
model,
|
model,
|
||||||
-RU * is_on[gn, t] - SU * (is_on[gn, t+1] - is_on[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]) <= dwflexiramp[gn, t]
|
maxp[t] * (1 - is_on[gn, t+1]) <= dwflexiramp[rn, gn, t]
|
||||||
) # first inequality of Eq. (27) in Wang & Hobbs (2016)
|
) # first inequality of Eq. (27) in Wang & Hobbs (2016)
|
||||||
@constraint(
|
@constraint(
|
||||||
model,
|
model,
|
||||||
dwflexiramp[gn, t] <=
|
dwflexiramp[rn, gn, t] <=
|
||||||
RD * is_on[gn, t+1] +
|
RD * is_on[gn, t+1] +
|
||||||
SD * (is_on[gn, t] - is_on[gn, t+1]) +
|
SD * (is_on[gn, t] - is_on[gn, t+1]) +
|
||||||
maxp[t] * (1 - is_on[gn, t])
|
maxp[t] * (1 - is_on[gn, t])
|
||||||
@@ -153,19 +138,25 @@ function _add_ramp_eqs!(
|
|||||||
@constraint(
|
@constraint(
|
||||||
model,
|
model,
|
||||||
-maxp[t] * is_on[gn, t] + minp[t] * is_on[gn, t+1] <=
|
-maxp[t] * is_on[gn, t] + minp[t] * is_on[gn, t+1] <=
|
||||||
upflexiramp[gn, t]
|
upflexiramp[rn, gn, t]
|
||||||
) # first inequality of Eq. (28) in Wang & Hobbs (2016)
|
) # first inequality of Eq. (28) in Wang & Hobbs (2016)
|
||||||
@constraint(model, upflexiramp[gn, t] <= maxp[t] * is_on[gn, t+1]) # second inequality of Eq. (28) in Wang & Hobbs (2016)
|
|
||||||
@constraint(model, -maxp[t] * is_on[gn, t+1] <= dwflexiramp[gn, t]) # first inequality of Eq. (29) in Wang & Hobbs (2016)
|
|
||||||
@constraint(
|
@constraint(
|
||||||
model,
|
model,
|
||||||
dwflexiramp[gn, t] <=
|
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])
|
(maxp[t] * is_on[gn, t]) - (minp[t] * is_on[gn, t+1])
|
||||||
) # second inequality of Eq. (29) in Wang & Hobbs (2016)
|
) # second inequality of Eq. (29) in Wang & Hobbs (2016)
|
||||||
else
|
else
|
||||||
@constraint(
|
@constraint(
|
||||||
model,
|
model,
|
||||||
mfg[gn, t] <=
|
mfg[rn, gn, t] <=
|
||||||
prod_above[gn, t-1] +
|
prod_above[gn, t-1] +
|
||||||
(is_on[gn, t-1] * minp[t]) +
|
(is_on[gn, t-1] * minp[t]) +
|
||||||
(RU * is_on[gn, t-1]) +
|
(RU * is_on[gn, t-1]) +
|
||||||
@@ -182,4 +173,5 @@ function _add_ramp_eqs!(
|
|||||||
) # Eq. (25) in Wang & Hobbs (2016) for the last time period
|
) # Eq. (25) in Wang & Hobbs (2016) for the last time period
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,8 +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_eqs!(model)
|
_add_flexiramp_reserve_eqs!(model)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -28,75 +28,70 @@ 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]
|
||||||
|
eq_min_spinning_reserve = _init(model, :eq_min_spinning_reserve)
|
||||||
|
for r in instance.reserves
|
||||||
|
r.type == "spinning" || continue
|
||||||
for t in 1:instance.time
|
for t in 1:instance.time
|
||||||
# Equation (68) in Kneuven et al. (2020)
|
# Equation (68) in Kneuven et al. (2020)
|
||||||
# As in Morales-España et al. (2013a)
|
# As in Morales-España et al. (2013a)
|
||||||
# Akin to the alternative formulation with max_power_avail
|
# Akin to the alternative formulation with max_power_avail
|
||||||
# from Carrión and Arroyo (2006) and Ostrowski et al. (2012)
|
# from Carrión and Arroyo (2006) and Ostrowski et al. (2012)
|
||||||
shortfall_penalty = instance.shortfall_penalty[t]
|
eq_min_spinning_reserve[r.name, t] = @constraint(
|
||||||
eq_min_reserve[t] = @constraint(
|
|
||||||
model,
|
model,
|
||||||
sum(model[:reserve][g.name, t] for g in instance.units) +
|
sum(model[:reserve][r.name, g.name, t] for g in r.units) +
|
||||||
(shortfall_penalty >= 0 ? model[:reserve_shortfall][t] : 0.0) >=
|
model[:reserve_shortfall][r.name, t] >= r.amount[t]
|
||||||
instance.reserves.spinning[t]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Account for shortfall contribution to objective
|
# Account for shortfall contribution to objective
|
||||||
if shortfall_penalty >= 0
|
if r.shortfall_penalty >= 0
|
||||||
add_to_expression!(
|
add_to_expression!(
|
||||||
model[:obj],
|
model[:obj],
|
||||||
shortfall_penalty,
|
r.shortfall_penalty,
|
||||||
model[:reserve_shortfall][t],
|
model[:reserve_shortfall][r.name, t],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
function _add_flexiramp_eqs!(model::JuMP.Model)::Nothing
|
function _add_flexiramp_reserve_eqs!(model::JuMP.Model)::Nothing
|
||||||
# Note: The flexpramp requirements in Wang & Hobbs (2016) are imposed as hard constraints
|
# Note: The flexpramp requirements in Wang & Hobbs (2016) are imposed as hard constraints
|
||||||
# through Eq. (17) and Eq. (18). The constraints eq_min_upflexiramp[t] and eq_min_dwflexiramp[t]
|
# through Eq. (17) and Eq. (18). The constraints eq_min_upflexiramp and eq_min_dwflexiramp
|
||||||
# provided below are modified versions of Eq. (17) and Eq. (18), respectively, in that
|
# 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
|
# they include slack variables for flexiramp shortfall, which are penalized in the
|
||||||
# objective function.
|
# objective function.
|
||||||
eq_min_upflexiramp = _init(model, :eq_min_upflexiramp)
|
eq_min_upflexiramp = _init(model, :eq_min_upflexiramp)
|
||||||
eq_min_dwflexiramp = _init(model, :eq_min_dwflexiramp)
|
eq_min_dwflexiramp = _init(model, :eq_min_dwflexiramp)
|
||||||
instance = model[:instance]
|
instance = model[:instance]
|
||||||
|
for r in instance.reserves
|
||||||
|
r.type == "flexiramp" || continue
|
||||||
for t in 1:instance.time
|
for t in 1:instance.time
|
||||||
flexiramp_shortfall_penalty = instance.flexiramp_shortfall_penalty[t]
|
|
||||||
# Eq. (17) in Wang & Hobbs (2016)
|
# Eq. (17) in Wang & Hobbs (2016)
|
||||||
eq_min_upflexiramp[t] = @constraint(
|
eq_min_upflexiramp[r.name, t] = @constraint(
|
||||||
model,
|
model,
|
||||||
sum(model[:upflexiramp][g.name, t] for g in instance.units) +
|
sum(model[:upflexiramp][r.name, g.name, t] for g in r.units) + model[:upflexiramp_shortfall][r.name, t] >= r.amount[t]
|
||||||
(
|
|
||||||
flexiramp_shortfall_penalty >= 0 ?
|
|
||||||
model[:upflexiramp_shortfall][t] : 0.0
|
|
||||||
) >= instance.reserves.upflexiramp[t]
|
|
||||||
)
|
)
|
||||||
# Eq. (18) in Wang & Hobbs (2016)
|
# Eq. (18) in Wang & Hobbs (2016)
|
||||||
eq_min_dwflexiramp[t] = @constraint(
|
eq_min_dwflexiramp[r.name, t] = @constraint(
|
||||||
model,
|
model,
|
||||||
sum(model[:dwflexiramp][g.name, t] for g in instance.units) +
|
sum(model[:dwflexiramp][r.name, g.name, t] for g in r.units) + model[:dwflexiramp_shortfall][r.name, t] >= r.amount[t]
|
||||||
(
|
|
||||||
flexiramp_shortfall_penalty >= 0 ?
|
|
||||||
model[:dwflexiramp_shortfall][t] : 0.0
|
|
||||||
) >= instance.reserves.dwflexiramp[t]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Account for flexiramp shortfall contribution to objective
|
# Account for flexiramp shortfall contribution to objective
|
||||||
if flexiramp_shortfall_penalty >= 0
|
if r.shortfall_penalty >= 0
|
||||||
add_to_expression!(
|
add_to_expression!(
|
||||||
model[:obj],
|
model[:obj],
|
||||||
flexiramp_shortfall_penalty,
|
r.shortfall_penalty,
|
||||||
(
|
(
|
||||||
model[:upflexiramp_shortfall][t] +
|
model[:upflexiramp_shortfall][r.name, t] +
|
||||||
model[:dwflexiramp_shortfall][t]
|
model[:dwflexiramp_shortfall][r.name, t]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,8 +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_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)
|
||||||
|
|
||||||
@@ -43,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 r in g.reserves
|
||||||
|
r.type == "spinning" || continue
|
||||||
for t in 1:model[:instance].time
|
for t in 1:model[:instance].time
|
||||||
if g.provides_spinning_reserves[t]
|
reserve[r.name, g.name, t] = @variable(model, lower_bound = 0)
|
||||||
reserve[g.name, t] = @variable(model, lower_bound = 0)
|
if (r.name, t) ∉ keys(reserve_shortfall)
|
||||||
else
|
reserve_shortfall[r.name, t] = @variable(model, lower_bound = 0)
|
||||||
reserve[g.name, t] = 0.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)
|
||||||
|
upflexiramp_shortfall = _init(model, :upflexiramp_shortfall)
|
||||||
|
mfg = _init(model, :mfg)
|
||||||
|
dwflexiramp = _init(model, :dwflexiramp)
|
||||||
|
dwflexiramp_shortfall = _init(model, :dwflexiramp_shortfall)
|
||||||
|
for r in g.reserves
|
||||||
|
r.type == "flexiramp" || continue
|
||||||
for t in 1:model[:instance].time
|
for t in 1:model[:instance].time
|
||||||
add_to_expression!(expr_reserve[g.bus.name, t], reserve[g.name, t], 1.0)
|
# 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
|
||||||
@@ -82,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
|
||||||
@@ -90,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]
|
||||||
)
|
)
|
||||||
@@ -118,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
|
||||||
@@ -127,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
|
||||||
@@ -217,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -50,37 +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)
|
||||||
if instance.reserves.upflexiramp != zeros(T) ||
|
|
||||||
instance.reserves.dwflexiramp != zeros(T)
|
|
||||||
# Report flexiramp solutions only if either of the up-flexiramp and
|
|
||||||
# down-flexiramp requirements is not a default array of zeros
|
|
||||||
sol["Up-flexiramp (MW)"] =
|
|
||||||
timeseries(model[:upflexiramp], instance.units)
|
|
||||||
sol["Up-flexiramp shortfall (MW)"] = OrderedDict(
|
|
||||||
t =>
|
|
||||||
(instance.flexiramp_shortfall_penalty[t] >= 0) ?
|
|
||||||
round(value(model[:upflexiramp_shortfall][t]), digits = 5) :
|
|
||||||
0.0 for t in 1:instance.time
|
|
||||||
)
|
|
||||||
sol["Down-flexiramp (MW)"] =
|
|
||||||
timeseries(model[:dwflexiramp], instance.units)
|
|
||||||
sol["Down-flexiramp shortfall (MW)"] = OrderedDict(
|
|
||||||
t =>
|
|
||||||
(instance.flexiramp_shortfall_penalty[t] >= 0) ?
|
|
||||||
round(value(model[:dwflexiramp_shortfall][t]), digits = 5) :
|
|
||||||
0.0 for t in 1:instance.time
|
|
||||||
)
|
|
||||||
else
|
|
||||||
# Report spinning reserve solutions only if both up-flexiramp and
|
|
||||||
# down-flexiramp requirements are arrays of zeros.
|
|
||||||
sol["Reserve (MW)"] = timeseries(model[:reserve], instance.units)
|
|
||||||
sol["Reserve shortfall (MW)"] = OrderedDict(
|
|
||||||
t =>
|
|
||||||
(instance.shortfall_penalty[t] >= 0) ?
|
|
||||||
round(value(model[:reserve_shortfall][t]), digits = 5) :
|
|
||||||
0.0 for t in 1:instance.time
|
|
||||||
)
|
|
||||||
end
|
|
||||||
sol["Net injection (MW)"] =
|
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)
|
||||||
@@ -91,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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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,14 +106,19 @@ 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
|
||||||
|
if r.type == "spinning"
|
||||||
|
if unit ∉ r.units &&
|
||||||
|
(unit in keys(solution["Spinning reserve (MW)"][r.name]))
|
||||||
@error @sprintf(
|
@error @sprintf(
|
||||||
"Unit %s is not eligible to provide spinning reserves at time %d",
|
"Unit %s is not eligible to provide reserve %s",
|
||||||
unit.name,
|
unit.name,
|
||||||
t
|
r.name,
|
||||||
)
|
)
|
||||||
err_count += 1
|
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
|
||||||
if is_on[t] && (production[t] < unit.min_power[t] - tol)
|
if is_on[t] && (production[t] < unit.min_power[t] - tol)
|
||||||
@@ -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,67 +335,65 @@ function _validate_reserve_and_demand(instance, solution, tol = 0.01)
|
|||||||
err_count += 1
|
err_count += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
# Verify flexiramp solutions only if either of the up-flexiramp and
|
# Verify reserves
|
||||||
# down-flexiramp requirements is not a default array of zeros
|
for r in instance.reserves
|
||||||
if instance.reserves.upflexiramp != zeros(instance.time) ||
|
if r.type == "spinning"
|
||||||
instance.reserves.dwflexiramp != zeros(instance.time)
|
provided = sum(
|
||||||
|
solution["Spinning reserve (MW)"][r.name][g.name][t] for
|
||||||
|
g in r.units
|
||||||
|
)
|
||||||
|
shortfall =
|
||||||
|
solution["Spinning reserve shortfall (MW)"][r.name][t]
|
||||||
|
required = r.amount[t]
|
||||||
|
|
||||||
|
if provided + shortfall < required - tol
|
||||||
|
@error @sprintf(
|
||||||
|
"Insufficient reserve %s at time %d (%.2f + %.2f < %.2f)",
|
||||||
|
r.name,
|
||||||
|
t,
|
||||||
|
provided,
|
||||||
|
shortfall,
|
||||||
|
required,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
elseif r.type == "flexiramp"
|
||||||
upflexiramp = sum(
|
upflexiramp = sum(
|
||||||
solution["Up-flexiramp (MW)"][g.name][t] for
|
solution["Up-flexiramp (MW)"][r.name][g.name][t] for
|
||||||
g in instance.units
|
g in r.units
|
||||||
)
|
)
|
||||||
upflexiramp_shortfall =
|
upflexiramp_shortfall =
|
||||||
(instance.flexiramp_shortfall_penalty[t] >= 0) ?
|
solution["Up-flexiramp shortfall (MW)"][r.name][t]
|
||||||
solution["Up-flexiramp shortfall (MW)"][t] : 0
|
|
||||||
|
|
||||||
if upflexiramp + upflexiramp_shortfall <
|
if upflexiramp + upflexiramp_shortfall < r.amount[t] - tol
|
||||||
instance.reserves.upflexiramp[t] - tol
|
|
||||||
@error @sprintf(
|
@error @sprintf(
|
||||||
"Insufficient up-flexiramp at time %d (%.2f + %.2f should be %.2f)",
|
"Insufficient up-flexiramp at time %d (%.2f + %.2f < %.2f)",
|
||||||
t,
|
t,
|
||||||
upflexiramp,
|
upflexiramp,
|
||||||
upflexiramp_shortfall,
|
upflexiramp_shortfall,
|
||||||
instance.reserves.upflexiramp[t],
|
r.amount[t],
|
||||||
)
|
)
|
||||||
err_count += 1
|
err_count += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
dwflexiramp = sum(
|
dwflexiramp = sum(
|
||||||
solution["Down-flexiramp (MW)"][g.name][t] for
|
solution["Down-flexiramp (MW)"][r.name][g.name][t] for
|
||||||
g in instance.units
|
g in r.units
|
||||||
)
|
)
|
||||||
dwflexiramp_shortfall =
|
dwflexiramp_shortfall =
|
||||||
(instance.flexiramp_shortfall_penalty[t] >= 0) ?
|
solution["Down-flexiramp shortfall (MW)"][r.name][t]
|
||||||
solution["Down-flexiramp shortfall (MW)"][t] : 0
|
|
||||||
|
|
||||||
if dwflexiramp + dwflexiramp_shortfall <
|
if dwflexiramp + dwflexiramp_shortfall < r.amount[t] - tol
|
||||||
instance.reserves.dwflexiramp[t] - tol
|
|
||||||
@error @sprintf(
|
@error @sprintf(
|
||||||
"Insufficient down-flexiramp at time %d (%.2f + %.2f should be %.2f)",
|
"Insufficient down-flexiramp at time %d (%.2f + %.2f < %.2f)",
|
||||||
t,
|
t,
|
||||||
dwflexiramp,
|
dwflexiramp,
|
||||||
dwflexiramp_shortfall,
|
dwflexiramp_shortfall,
|
||||||
instance.reserves.dwflexiramp[t],
|
r.amount[t],
|
||||||
)
|
)
|
||||||
err_count += 1
|
err_count += 1
|
||||||
end
|
end
|
||||||
# Verify spinning reserve solutions only if both up-flexiramp and
|
|
||||||
# down-flexiramp requirements are arrays of zeros.
|
|
||||||
else
|
else
|
||||||
reserve =
|
error("Unknown reserve type: $(r.type)")
|
||||||
sum(solution["Reserve (MW)"][g.name][t] for g in instance.units)
|
|
||||||
reserve_shortfall =
|
|
||||||
(instance.shortfall_penalty[t] >= 0) ?
|
|
||||||
solution["Reserve shortfall (MW)"][t] : 0
|
|
||||||
|
|
||||||
if reserve + reserve_shortfall < instance.reserves.spinning[t] - tol
|
|
||||||
@error @sprintf(
|
|
||||||
"Insufficient spinning reserves at time %d (%.2f + %.2f should be %.2f)",
|
|
||||||
t,
|
|
||||||
reserve,
|
|
||||||
reserve_shortfall,
|
|
||||||
instance.reserves.spinning[t],
|
|
||||||
)
|
|
||||||
err_count += 1
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
BIN
test/fixtures/case118-initcond.json.gz
vendored
BIN
test/fixtures/case118-initcond.json.gz
vendored
Binary file not shown.
BIN
test/fixtures/case14-flex.json.gz
vendored
Normal file
BIN
test/fixtures/case14-flex.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/case14-sub-hourly.json.gz
vendored
Normal file
BIN
test/fixtures/case14-sub-hourly.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/case14.json.gz
vendored
Normal file
BIN
test/fixtures/case14.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/ucjl-0.2.json.gz
vendored
Normal file
BIN
test/fixtures/ucjl-0.2.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/ucjl-0.3.json.gz
vendored
Normal file
BIN
test/fixtures/ucjl-0.3.json.gz
vendored
Normal file
Binary file not shown.
@@ -4,12 +4,9 @@
|
|||||||
|
|
||||||
using UnitCommitment
|
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])
|
||||||
|
|||||||
18
test/instance/migrate_test.jl
Normal file
18
test/instance/migrate_test.jl
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||||
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||||
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
|
using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
||||||
|
|
||||||
|
@testset "read v0.2" begin
|
||||||
|
instance = UnitCommitment.read("$FIXTURES/ucjl-0.2.json.gz")
|
||||||
|
@test length(instance.reserves_by_name["r1"].amount) == 4
|
||||||
|
@test instance.units_by_name["g2"].reserves[1].name == "r1"
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "read v0.3" begin
|
||||||
|
instance = UnitCommitment.read("$FIXTURES/ucjl-0.3.json.gz")
|
||||||
|
@test length(instance.units) == 6
|
||||||
|
@test length(instance.buses) == 14
|
||||||
|
@test length(instance.lines) == 20
|
||||||
|
end
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
using UnitCommitment, LinearAlgebra, Cbc, JuMP, JSON, GZip
|
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"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
using UnitCommitment
|
using UnitCommitment
|
||||||
using JuMP
|
using JuMP
|
||||||
using Cbc
|
using Cbc
|
||||||
|
using JSON
|
||||||
import UnitCommitment:
|
import UnitCommitment:
|
||||||
ArrCon2000,
|
ArrCon2000,
|
||||||
CarArr2006,
|
CarArr2006,
|
||||||
@@ -19,42 +20,65 @@ import UnitCommitment:
|
|||||||
|
|
||||||
function _test(
|
function _test(
|
||||||
formulation::Formulation;
|
formulation::Formulation;
|
||||||
instances::Array{String} = ["test/case14"],
|
instances = ["case14"],
|
||||||
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0),
|
dump::Bool = false,
|
||||||
)::Nothing
|
)::Nothing
|
||||||
for instance_name in instances
|
for instance_name in instances
|
||||||
instance = UnitCommitment.read_benchmark(instance_name)
|
instance = UnitCommitment.read("$(FIXTURES)/$(instance_name).json.gz")
|
||||||
model = UnitCommitment.build_model(
|
model = UnitCommitment.build_model(
|
||||||
instance = instance,
|
instance = instance,
|
||||||
formulation = formulation,
|
formulation = formulation,
|
||||||
optimizer = 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
|
||||||
end
|
end
|
||||||
|
|
||||||
@testset "formulations" begin
|
@testset "formulations" begin
|
||||||
|
@testset "default" begin
|
||||||
_test(Formulation())
|
_test(Formulation())
|
||||||
|
end
|
||||||
|
@testset "ArrCon2000" begin
|
||||||
_test(Formulation(ramping = ArrCon2000.Ramping()))
|
_test(Formulation(ramping = ArrCon2000.Ramping()))
|
||||||
|
end
|
||||||
# _test(Formulation(ramping = DamKucRajAta2016.Ramping()))
|
@testset "DamKucRajAta2016" begin
|
||||||
|
_test(Formulation(ramping = DamKucRajAta2016.Ramping()))
|
||||||
|
end
|
||||||
|
@testset "MorLatRam2013" begin
|
||||||
_test(
|
_test(
|
||||||
Formulation(
|
Formulation(
|
||||||
ramping = MorLatRam2013.Ramping(),
|
ramping = MorLatRam2013.Ramping(),
|
||||||
startup_costs = MorLatRam2013.StartupCosts(),
|
startup_costs = MorLatRam2013.StartupCosts(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
end
|
||||||
|
@testset "PanGua2016" begin
|
||||||
_test(Formulation(ramping = PanGua2016.Ramping()))
|
_test(Formulation(ramping = PanGua2016.Ramping()))
|
||||||
|
end
|
||||||
|
@testset "Gar1962" begin
|
||||||
_test(Formulation(pwl_costs = Gar1962.PwlCosts()))
|
_test(Formulation(pwl_costs = Gar1962.PwlCosts()))
|
||||||
|
end
|
||||||
|
@testset "CarArr2006" begin
|
||||||
_test(Formulation(pwl_costs = CarArr2006.PwlCosts()))
|
_test(Formulation(pwl_costs = CarArr2006.PwlCosts()))
|
||||||
|
end
|
||||||
|
@testset "KnuOstWat2018" begin
|
||||||
_test(Formulation(pwl_costs = KnuOstWat2018.PwlCosts()))
|
_test(Formulation(pwl_costs = KnuOstWat2018.PwlCosts()))
|
||||||
|
end
|
||||||
|
@testset "WanHob2016" begin
|
||||||
_test(
|
_test(
|
||||||
Formulation(ramping = WanHob2016.Ramping()),
|
Formulation(ramping = WanHob2016.Ramping()),
|
||||||
instances = ["test/case14-flex"],
|
instances = ["case14-flex"],
|
||||||
)
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ using UnitCommitment
|
|||||||
push!(Base.LOAD_PATH, @__DIR__)
|
push!(Base.LOAD_PATH, @__DIR__)
|
||||||
UnitCommitment._setup_logger(level = Base.CoreLogging.Error)
|
UnitCommitment._setup_logger(level = Base.CoreLogging.Error)
|
||||||
|
|
||||||
|
FIXTURES = "$(@__DIR__)/fixtures"
|
||||||
|
|
||||||
@testset "UnitCommitment" begin
|
@testset "UnitCommitment" begin
|
||||||
include("usage.jl")
|
include("usage.jl")
|
||||||
@testset "import" begin
|
@testset "import" begin
|
||||||
@@ -15,15 +17,18 @@ UnitCommitment._setup_logger(level = Base.CoreLogging.Error)
|
|||||||
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 "solution" begin
|
||||||
@testset "XavQiuWanThi19" begin
|
@testset "XavQiuWanThi19" begin
|
||||||
include("solution/methods/XavQiuWanThi19/filter_test.jl")
|
include("solution/methods/XavQiuWanThi19/filter_test.jl")
|
||||||
include("solution/methods/XavQiuWanThi19/find_test.jl")
|
include("solution/methods/XavQiuWanThi19/find_test.jl")
|
||||||
include("solution/methods/XavQiuWanThi19/sensitivity_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")
|
||||||
include("transform/slice_test.jl")
|
include("transform/slice_test.jl")
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user