mirror of
https://github.com/ANL-CEEESA/UnitCommitment.jl.git
synced 2025-12-06 00:08:52 -06:00
Compare commits
51 Commits
feature/mi
...
new-tests
| Author | SHA1 | Date | |
|---|---|---|---|
|
1afd71b97b
|
|||
|
6db2ca76e8
|
|||
|
4adb3344ac
|
|||
|
|
316d0bdf5a | ||
|
|
33f8ec26d5 | ||
|
|
41790db448 | ||
|
|
baf529a15d | ||
|
|
b71a1c3d5f | ||
|
|
bea42d174c | ||
|
|
896ef0f3e3 | ||
|
|
cb7f9e3b27 | ||
| 319a787904 | |||
|
b1c963f217
|
|||
|
19534a128f
|
|||
|
|
51f6aa9a80 | ||
|
|
f2c0388cac | ||
|
|
3564358a63 | ||
|
|
b2ed0f67c1 | ||
|
|
2a6c206e08 | ||
|
|
30a4284119 | ||
|
|
71ed55cb40 | ||
|
|
0b95df25ec | ||
|
|
5f5c8b66eb | ||
| 52f1ff9a27 | |||
|
414128cc0b
|
|||
|
20939dc4b7
|
|||
|
d8741f04a0
|
|||
|
3b6d810884
|
|||
|
204c5d900f
|
|||
|
cb9334c0a3
|
|||
|
31e0613134
|
|||
|
4827c29230
|
|||
|
19e84bac07
|
|||
|
d7d2a3fcf6
|
|||
|
784ebfa199
|
|||
|
d2e11eee42
|
|||
|
34ca6952fb
|
|||
|
|
bc3aee38f8 | ||
|
|
415732f0ec | ||
|
|
5c91dc2ac9 | ||
|
|
ad4a754d63 | ||
|
|
481f5a904c | ||
|
|
7e8a2ee026 | ||
|
|
c95b01dadf | ||
|
|
8fc84412eb | ||
| 6573bb7ea2 | |||
| 1769f2a932 | |||
| 4dc39363e8 | |||
| 5fef01cd99 | |||
| 18daaf5358 | |||
| b68b4ff9e4 |
33
.github/workflows/test.yml
vendored
33
.github/workflows/test.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Tests
|
||||
name: Build & Test
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
@@ -6,19 +6,30 @@ on:
|
||||
- cron: '45 10 * * *'
|
||||
jobs:
|
||||
test:
|
||||
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
julia-version: ['1.6', '1.7']
|
||||
julia-arch: [x64]
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
exclude:
|
||||
- os: macOS-latest
|
||||
julia-arch: x86
|
||||
version: ['1.6', '1.7', '1.8', '1.9']
|
||||
os:
|
||||
- ubuntu-latest
|
||||
arch:
|
||||
- x64
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: julia-actions/setup-julia@latest
|
||||
- uses: julia-actions/setup-julia@v1
|
||||
with:
|
||||
version: ${{ matrix.julia-version }}
|
||||
- uses: julia-actions/julia-buildpkg@latest
|
||||
- uses: julia-actions/julia-runtest@latest
|
||||
version: ${{ matrix.version }}
|
||||
arch: ${{ matrix.arch }}
|
||||
- name: Run tests
|
||||
shell: julia --color=yes --project=test {0}
|
||||
run: |
|
||||
using Pkg
|
||||
Pkg.develop(path=".")
|
||||
Pkg.update()
|
||||
using UnitCommitmentT
|
||||
try
|
||||
runtests()
|
||||
catch
|
||||
exit(1)
|
||||
end
|
||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -11,15 +11,19 @@ All notable changes to this project will be documented in this file.
|
||||
[semver]: https://semver.org/spec/v2.0.0.html
|
||||
[pkjjl]: https://pkgdocs.julialang.org/v1/compatibility/#compat-pre-1.0
|
||||
|
||||
## [Unreleased]
|
||||
## [0.3.0] - 2022-07-18
|
||||
### Added
|
||||
- Add multiple reserve products
|
||||
- Add support for multiple reserve products and zonal reserves.
|
||||
- Add flexiramp reserve products, following WanHob2016's formulation (@oyurdakul, #21).
|
||||
- Add 365 variations for each MATPOWER instance, corresponding to each day of the year.
|
||||
|
||||
### Changed
|
||||
- To support multiple reserve products, the input data format has been modified as follows:
|
||||
- To support multiple/zonal reserves, the input data format has been modified as follows:
|
||||
- In `Generators`, replace `Provides spinning reserves?` by `Reserve eligibility`
|
||||
- In `Parameters`, remove `Reserve shortfall penalty`
|
||||
- Revise `Reserves` section
|
||||
- To allow new versions of UnitCommitment.jl to read old instance files, a new required field `Version` has been added to the `Parameters` section. To load v0.2 files in v0.3, please add `{"Parameters":{"Version":"0.2"}}` to the file.
|
||||
- Benchmark test cases are now downloaded on-the-fly as needed, instead of being stored in our GitHub repository. Test cases can also be directly downloaded from: https://axavier.org/UnitCommitment.jl/
|
||||
|
||||
|
||||
## [0.2.2] - 2021-07-21
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright © 2020, UChicago Argonne, LLC
|
||||
Copyright © 2020-2022, UChicago Argonne, LLC
|
||||
|
||||
All Rights Reserved
|
||||
|
||||
|
||||
18
Makefile
18
Makefile
@@ -4,20 +4,8 @@
|
||||
|
||||
VERSION := 0.3
|
||||
|
||||
clean:
|
||||
rm -rfv build Manifest.toml test/Manifest.toml deps/formatter/build deps/formatter/Manifest.toml
|
||||
|
||||
docs:
|
||||
cd docs; make clean; make dirhtml
|
||||
rsync -avP --delete-after docs/_build/dirhtml/ ../docs/$(VERSION)/
|
||||
cd docs; julia --project=. make.jl; cd ..
|
||||
rsync -avP --delete-after docs/build/ ../docs/$(VERSION)/
|
||||
|
||||
format:
|
||||
cd deps/formatter; ../../juliaw format.jl
|
||||
|
||||
test: test/Manifest.toml
|
||||
./juliaw test/runtests.jl
|
||||
|
||||
test/Manifest.toml: test/Project.toml
|
||||
julia --project=test -e "using Pkg; Pkg.instantiate()"
|
||||
|
||||
.PHONY: docs test format install-deps
|
||||
.PHONY: docs
|
||||
|
||||
@@ -17,7 +17,6 @@ MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
|
||||
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
|
||||
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
|
||||
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
||||
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
|
||||
|
||||
[compat]
|
||||
|
||||
18
README.md
18
README.md
@@ -87,20 +87,22 @@ UnitCommitment.write("/tmp/output.json", solution)
|
||||
|
||||
## Documentation
|
||||
|
||||
1. [Usage](https://anl-ceeesa.github.io/UnitCommitment.jl/0.2/usage/)
|
||||
2. [Data Format](https://anl-ceeesa.github.io/UnitCommitment.jl/0.2/format/)
|
||||
3. [Instances](https://anl-ceeesa.github.io/UnitCommitment.jl/0.2/instances/)
|
||||
4. [JuMP Model](https://anl-ceeesa.github.io/UnitCommitment.jl/0.2/model/)
|
||||
1. [Usage](https://anl-ceeesa.github.io/UnitCommitment.jl/0.3/usage/)
|
||||
2. [Data Format](https://anl-ceeesa.github.io/UnitCommitment.jl/0.3/format/)
|
||||
3. [Instances](https://anl-ceeesa.github.io/UnitCommitment.jl/0.3/instances/)
|
||||
4. [JuMP Model](https://anl-ceeesa.github.io/UnitCommitment.jl/0.3/model/)
|
||||
5. [API Reference](https://anl-ceeesa.github.io/UnitCommitment.jl/0.3/api/)
|
||||
|
||||
## Authors
|
||||
* **Alinson S. Xavier** (Argonne National Laboratory)
|
||||
* **Aleksandr M. Kazachkov** (University of Florida)
|
||||
* **Ogün Yurdakul** (Technische Universität Berlin)
|
||||
* **Jun He** (Purdue University)
|
||||
* **Feng Qiu** (Argonne National Laboratory)
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
* We would like to **Yonghong Chen** (Midcontinent Independent System Operator), **Feng Pan** (Pacific Northwest National Laboratory) for valuable feedback on early versions of this package.
|
||||
* We would like to thank **Yonghong Chen** (Midcontinent Independent System Operator), **Feng Pan** (Pacific Northwest National Laboratory) for valuable feedback on early versions of this package.
|
||||
|
||||
* Based upon work supported by **Laboratory Directed Research and Development** (LDRD) funding from Argonne National Laboratory, provided by the Director, Office of Science, of the U.S. Department of Energy under Contract No. DE-AC02-06CH11357
|
||||
|
||||
@@ -110,15 +112,15 @@ UnitCommitment.write("/tmp/output.json", solution)
|
||||
|
||||
If you use UnitCommitment.jl in your research (instances, models or algorithms), we kindly request that you cite the package as follows:
|
||||
|
||||
* **Alinson S. Xavier, Aleksandr M. Kazachkov, Feng Qiu**. "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment". Zenodo (2020). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874).
|
||||
* **Alinson S. Xavier, Aleksandr M. Kazachkov, Ogün Yurdakul, Feng Qiu**. "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment (Version 0.3)". Zenodo (2022). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874).
|
||||
|
||||
If you use the instances, we additionally request that you cite the original sources, as described in the [instances page](docs/instances.md).
|
||||
If you use the instances, we additionally request that you cite the original sources, as described in the documentation.
|
||||
|
||||
## License
|
||||
|
||||
```text
|
||||
UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment
|
||||
Copyright © 2020-2021, UChicago Argonne, LLC. All Rights Reserved.
|
||||
Copyright © 2020-2022, UChicago Argonne, LLC. All Rights Reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
provided that the following conditions are met:
|
||||
|
||||
5
deps/formatter/Project.toml
vendored
5
deps/formatter/Project.toml
vendored
@@ -1,5 +0,0 @@
|
||||
[deps]
|
||||
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
|
||||
|
||||
[compat]
|
||||
JuliaFormatter = "0.14.4"
|
||||
9
deps/formatter/format.jl
vendored
9
deps/formatter/format.jl
vendored
@@ -1,9 +0,0 @@
|
||||
using JuliaFormatter
|
||||
format(
|
||||
[
|
||||
"../../src",
|
||||
"../../test",
|
||||
"../../benchmark/run.jl",
|
||||
],
|
||||
verbose=true,
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
5
docs/Project.toml
Normal file
5
docs/Project.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[deps]
|
||||
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
|
||||
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
|
||||
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||
UnitCommitment = "64606440-39ea-11e9-0f29-3303a1d3d877"
|
||||
49
docs/_static/custom.css
vendored
49
docs/_static/custom.css
vendored
@@ -1,49 +0,0 @@
|
||||
h1.site-logo {
|
||||
font-size: 30px !important;
|
||||
}
|
||||
|
||||
h1.site-logo small {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
|
||||
h1.site-logo {
|
||||
font-size: 30px !important;
|
||||
}
|
||||
|
||||
h1.site-logo small {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
|
||||
tbody, thead, pre {
|
||||
border: 1px solid rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
table td, th {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
table p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
table td code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table tr,
|
||||
table th {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
table tr:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
box-shadow: inherit !important;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.text-align\:center {
|
||||
text-align: center;
|
||||
}
|
||||
16
docs/conf.py
16
docs/conf.py
@@ -1,16 +0,0 @@
|
||||
project = "UnitCommitment.jl"
|
||||
copyright = "2020-2022, UChicago Argonne, LLC"
|
||||
author = ""
|
||||
release = "0.3"
|
||||
extensions = ["myst_parser"]
|
||||
templates_path = ["_templates"]
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
html_theme = "sphinx_book_theme"
|
||||
html_static_path = ["_static"]
|
||||
html_css_files = ["custom.css"]
|
||||
html_theme_options = {
|
||||
"repository_url": "https://github.com/ANL-CEEESA/UnitCommitment.jl/",
|
||||
"use_repository_button": False,
|
||||
"extra_navbar": "",
|
||||
}
|
||||
html_title = f"UnitCommitment.jl<br/><small>{release}</small>"
|
||||
16
docs/make.jl
Normal file
16
docs/make.jl
Normal file
@@ -0,0 +1,16 @@
|
||||
using Documenter, UnitCommitment, JuMP
|
||||
|
||||
makedocs(
|
||||
sitename="UnitCommitment.jl",
|
||||
pages=[
|
||||
"Home" => "index.md",
|
||||
"usage.md",
|
||||
"format.md",
|
||||
"instances.md",
|
||||
"model.md",
|
||||
"api.md",
|
||||
],
|
||||
format = Documenter.HTML(
|
||||
assets=["assets/custom.css"],
|
||||
)
|
||||
)
|
||||
@@ -1,4 +0,0 @@
|
||||
Sphinx==3.5.4
|
||||
sphinx-book-theme==0.1.0
|
||||
myst-parser==0.14.0
|
||||
Jinja2==3.0.2
|
||||
62
docs/src/api.md
Normal file
62
docs/src/api.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# API Reference
|
||||
|
||||
## Read data, build model & optimize
|
||||
|
||||
```@docs
|
||||
UnitCommitment.read
|
||||
UnitCommitment.read_benchmark
|
||||
UnitCommitment.build_model
|
||||
UnitCommitment.optimize!
|
||||
UnitCommitment.solution
|
||||
UnitCommitment.validate
|
||||
UnitCommitment.write
|
||||
```
|
||||
|
||||
## Locational Marginal Prices
|
||||
|
||||
### Conventional LMPs
|
||||
```@docs
|
||||
UnitCommitment.compute_lmp(::JuMP.Model,::UnitCommitment.ConventionalLMP)
|
||||
```
|
||||
|
||||
### Approximated Extended LMPs
|
||||
```@docs
|
||||
UnitCommitment.AELMP
|
||||
UnitCommitment.compute_lmp(::JuMP.Model,::UnitCommitment.AELMP)
|
||||
```
|
||||
|
||||
|
||||
## Modify instance
|
||||
|
||||
```@docs
|
||||
UnitCommitment.slice
|
||||
UnitCommitment.randomize!(::UnitCommitment.UnitCommitmentInstance)
|
||||
UnitCommitment.generate_initial_conditions!
|
||||
```
|
||||
|
||||
## Formulations
|
||||
|
||||
```@docs
|
||||
UnitCommitment.Formulation
|
||||
UnitCommitment.ShiftFactorsFormulation
|
||||
UnitCommitment.ArrCon2000
|
||||
UnitCommitment.CarArr2006
|
||||
UnitCommitment.DamKucRajAta2016
|
||||
UnitCommitment.Gar1962
|
||||
UnitCommitment.KnuOstWat2018
|
||||
UnitCommitment.MorLatRam2013
|
||||
UnitCommitment.PanGua2016
|
||||
UnitCommitment.WanHob2016
|
||||
```
|
||||
|
||||
## Solution Methods
|
||||
|
||||
```@docs
|
||||
UnitCommitment.XavQiuWanThi2019.Method
|
||||
```
|
||||
|
||||
## Randomization Methods
|
||||
|
||||
```@docs
|
||||
UnitCommitment.XavQiuAhm2021.Randomization
|
||||
```
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
36
docs/src/assets/custom.css
Normal file
36
docs/src/assets/custom.css
Normal file
@@ -0,0 +1,36 @@
|
||||
@media screen and (min-width: 1056px) {
|
||||
#documenter .docs-main {
|
||||
max-width: 65rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tbody, thead, pre {
|
||||
border: 1px solid rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
table td, th {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
table p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
table td code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table tr,
|
||||
table th {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
table tr:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: transparent;
|
||||
color: rgb(232, 62, 140);
|
||||
}
|
||||
@@ -1,32 +1,20 @@
|
||||
```{sectnum}
|
||||
---
|
||||
start: 2
|
||||
depth: 2
|
||||
suffix: .
|
||||
---
|
||||
```
|
||||
|
||||
|
||||
Data Format
|
||||
===========
|
||||
|
||||
|
||||
Input Data Format
|
||||
-----------------
|
||||
|
||||
Instances are specified by JSON files containing the following main sections:
|
||||
|
||||
* Parameters
|
||||
* Buses
|
||||
* Generators
|
||||
* Price-sensitive loads
|
||||
* Transmission lines
|
||||
* Reserves
|
||||
* Contingencies
|
||||
* [Parameters](#Parameters)
|
||||
* [Buses](#Buses)
|
||||
* [Generators](#Generators)
|
||||
* [Price-sensitive loads](#Price-sensitive-loads)
|
||||
* [Transmission lines](#Transmission-lines)
|
||||
* [Reserves](#Reserves)
|
||||
* [Contingencies](#Contingencies)
|
||||
|
||||
Each section is described in detail below. 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
|
||||
Each section is described in detail below. See [case118/2017-01-01.json.gz](https://axavier.org/UnitCommitment.jl/0.3/instances/matpower/case118/2017-01-01.json.gz) for a complete example.
|
||||
|
||||
### Parameters
|
||||
|
||||
@@ -34,6 +22,7 @@ This section describes system-wide parameters, such as power balance penalty, an
|
||||
|
||||
| Key | Description | Default | Time series?
|
||||
| :----------------------------- | :------------------------------------------------ | :------: | :------------:
|
||||
| `Version` | Version of UnitCommitment.jl this file was written for. Required to ensure that the file remains readable in future versions of the package. If you are following this page to construct the file, this field should equal `0.3`. | Required | N
|
||||
| `Time horizon (h)` | Length of the planning horizon (in hours). | Required | N
|
||||
| `Time step (min)` | Length of each time step (in minutes). Must be a divisor of 60 (e.g. 60, 30, 20, 15, etc). | `60` | N
|
||||
| `Power balance penalty ($/MW)` | Penalty for system-wide shortage or surplus in production (in $/MW). This is charged per time step. For example, if there is a shortage of 1 MW for three time steps, three times this amount will be charged. | `1000.0` | Y
|
||||
@@ -43,8 +32,9 @@ This section describes system-wide parameters, such as power balance penalty, an
|
||||
```json
|
||||
{
|
||||
"Parameters": {
|
||||
"Version": "0.3",
|
||||
"Time horizon (h)": 4,
|
||||
"Power balance penalty ($/MW)": 1000.0,
|
||||
"Power balance penalty ($/MW)": 1000.0
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -80,11 +70,17 @@ This section describes the characteristics of each bus in the system.
|
||||
|
||||
### Generators
|
||||
|
||||
This section describes all generators in the system, including thermal units, renewable units and virtual units.
|
||||
This section describes all generators in the system. Two types of units can be specified:
|
||||
|
||||
- **Thermal units:** Units that produce power by converting heat into electrical energy, such as coal and oil power plants. These units use a more complex model, with binary decision variables, and various constraints to enforce ramp rates and minimum up/down time.
|
||||
- **Profiled units:** Simplified model for units that do not require the constraints mentioned above, only a maximum and minimum power output for each time period. Typically used for renewables and hydro.
|
||||
|
||||
#### Thermal Units
|
||||
|
||||
| Key | Description | Default | Time series?
|
||||
| :------------------------ | :------------------------------------------------| ------- | :-----------:
|
||||
| `Bus` | Identifier of the bus where this generator is located (string). | Required | N
|
||||
| `Type` | Type of the generator (string). For thermal generators, this must be `Thermal`. | Required | N
|
||||
| `Production cost curve (MW)` and `Production cost curve ($)` | Parameters describing the piecewise-linear production costs. See below for more details. | Required | Y
|
||||
| `Startup costs ($)` and `Startup delays (h)` | Parameters describing how much it costs to start the generator after it has been shut down for a certain amount of time. If `Startup costs ($)` and `Startup delays (h)` are set to `[300.0, 400.0]` and `[1, 4]`, for example, and the generator is shut down at time `00:00` (h:min), then it costs \$300 to start up the generator at any time between `01:00` and `03:59`, and \$400 to start the generator at time `04:00` or any time after that. The number of startup cost points is unlimited, and may be different for each generator. Startup delays must be strictly increasing and the first entry must equal `Minimum downtime (h)`. | `[0.0]` and `[1]` | N
|
||||
| `Minimum uptime (h)` | Minimum amount of time the generator must stay operational after starting up (in hours). For example, if the generator starts up at time `00:00` (h:min) and `Minimum uptime (h)` is set to 4, then the generator can only shut down at time `04:00`. | `1` | N
|
||||
@@ -97,17 +93,30 @@ This section describes all generators in the system, including thermal units, re
|
||||
| `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
|
||||
| `Reserve eligibility` | List of reserve products this generator is eligibe to provide. By default, the generator is not eligible to provide any reserves. | `[]` | N
|
||||
| `Commitment status` | List of commitment status over the time horizon. At time `t`, if `true`, the generator must be commited at that time period; if `false`, the generator must not be commited at that time period. If `null` at time `t`, the generator's commitment status is then decided by the model. By default, the status is a list of `null` values. | `null` | Y
|
||||
|
||||
#### Profiled Units
|
||||
|
||||
| Key | Description | Default | Time series?
|
||||
| :---------------- | :------------------------------------------------ | :------: | :------------:
|
||||
| `Bus` | Identifier of the bus where this generator is located (string). | Required | N
|
||||
| `Type` | Type of the generator (string). For profiled generators, this must be `Profiled`. | Required | N
|
||||
| `Cost ($/MW)` | Cost incurred for serving each MW of power by this generator. | Required | Y
|
||||
| `Minimum power (MW)` | Minimum amount of power this generator may supply. | `0.0` | Y
|
||||
| `Maximum power (MW)` | Maximum amount of power this generator may supply. | Required | Y
|
||||
|
||||
#### Production costs and limits
|
||||
|
||||
Production costs are represented as piecewise-linear curves. Figure 1 shows an example cost curve with three segments, where it costs \$1400, \$1600, \$2200 and \$2400 to generate, respectively, 100, 110, 130 and 135 MW of power. To model this generator, `Production cost curve (MW)` should be set to `[100, 110, 130, 135]`, and `Production cost curve ($)` should be set to `[1400, 1600, 2200, 2400]`.
|
||||
Note that this curve also specifies the production limits. Specifically, the first point identifies the minimum power output when the unit is operational, while the last point identifies the maximum power output.
|
||||
|
||||
```@raw html
|
||||
<center>
|
||||
<img src="../_static/cost_curve.png" style="max-width: 500px"/>
|
||||
<img src="../assets/cost_curve.png" style="max-width: 500px"/>
|
||||
<div><b>Figure 1.</b> Piecewise-linear production cost curve.</div>
|
||||
<br/>
|
||||
</center>
|
||||
```
|
||||
|
||||
#### Additional remarks:
|
||||
|
||||
@@ -123,6 +132,7 @@ Note that this curve also specifies the production limits. Specifically, the fir
|
||||
"Generators": {
|
||||
"gen1": {
|
||||
"Bus": "b1",
|
||||
"Type": "Thermal",
|
||||
"Production cost curve (MW)": [100.0, 110.0, 130.0, 135.0],
|
||||
"Production cost curve ($)": [1400.0, 1600.0, 2200.0, 2400.0],
|
||||
"Startup costs ($)": [300.0, 400.0],
|
||||
@@ -134,14 +144,26 @@ Note that this curve also specifies the production limits. Specifically, the fir
|
||||
"Minimum downtime (h)": 4,
|
||||
"Minimum uptime (h)": 4,
|
||||
"Initial status (h)": 12,
|
||||
"Initial power (MW)": 115,
|
||||
"Must run?": false,
|
||||
"Reserve eligibility": ["r1"],
|
||||
"Reserve eligibility": ["r1"]
|
||||
},
|
||||
"gen2": {
|
||||
"Bus": "b5",
|
||||
"Type": "Thermal",
|
||||
"Production cost curve (MW)": [0.0, [10.0, 8.0, 0.0, 3.0]],
|
||||
"Production cost curve ($)": [0.0, 0.0],
|
||||
"Initial status (h)": -100,
|
||||
"Initial power (MW)": 0,
|
||||
"Reserve eligibility": ["r1", "r2"],
|
||||
"Commitment status": [true, false, null, true]
|
||||
},
|
||||
"gen3": {
|
||||
"Bus": "b6",
|
||||
"Type": "Profiled",
|
||||
"Minimum power (MW)": 10.0,
|
||||
"Maximum power (MW)": 120.0,
|
||||
"Cost ($/MW)": 100.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,7 +193,7 @@ This section describes components in the system which may increase or reduce the
|
||||
}
|
||||
```
|
||||
|
||||
### Transmission Lines
|
||||
### Transmission lines
|
||||
|
||||
This section describes the characteristics of transmission system, such as its topology and the susceptance of each transmission line.
|
||||
|
||||
@@ -6,25 +6,24 @@
|
||||
|
||||
* **Data Format:** The package proposes an extensible and fully-documented JSON-based data specification format for SCUC, developed in collaboration with Independent System Operators (ISOs), which describes the most important aspects of the problem. The format supports all the most common generator characteristics (including ramping, piecewise-linear production cost curves and time-dependent startup costs), as well as operating reserves, price-sensitive loads, transmission networks and contingencies.
|
||||
* **Benchmark Instances:** The package provides a diverse collection of large-scale benchmark instances collected from the literature, converted into a common data format, and extended using data-driven methods to make them more challenging and realistic.
|
||||
* **Model Implementation**: The package provides a Julia/JuMP implementations of state-of-the-art formulations and solution methods for SCUC, including multiple ramping formulations ([ArrCon2000][ArrCon2000], [MorLatRam2013][MorLatRam2013], [DamKucRajAta2016][DamKucRajAta2016], [PanGua2016][PanGua2016]), multiple piecewise-linear costs formulations ([Gar1962][Gar1962], [CarArr2006][CarArr2006], [KnuOstWat2018][KnuOstWat2018]) and contingency screening methods ([XavQiuWanThi2019][XavQiuWanThi2019]). Our goal is to keep these implementations up-to-date as new methods are proposed in the literature.
|
||||
* **Model Implementation**: The package provides a Julia/JuMP implementations of state-of-the-art formulations and solution methods for SCUC, including multiple ramping formulations ([ArrCon2000](https://doi.org/10.1109/59.871739), [MorLatRam2013](https://doi.org/10.1109/TPWRS.2013.2251373), [DamKucRajAta2016](https://doi.org/10.1007/s10107-015-0919-9), [PanGua2016](https://doi.org/10.1287/opre.2016.1520)), multiple piecewise-linear costs formulations ([Gar1962](https://doi.org/10.1109/AIEEPAS.1962.4501405), [CarArr2006](https://doi.org/10.1109/TPWRS.2006.876672), [KnuOstWat2018](https://doi.org/10.1109/TPWRS.2017.2783850)) and contingency screening methods ([XavQiuWanThi2019](https://doi.org/10.1109/TPWRS.2019.2892620)). Our goal is to keep these implementations up-to-date as new methods are proposed in the literature.
|
||||
* **Benchmark Tools:** The package provides automated benchmark scripts to accurately evaluate the performance impact of proposed code changes.
|
||||
|
||||
[ArrCon2000]: https://doi.org/10.1109/59.871739
|
||||
[CarArr2006]: https://doi.org/10.1109/TPWRS.2006.876672
|
||||
[DamKucRajAta2016]: https://doi.org/10.1007/s10107-015-0919-9
|
||||
[Gar1962]: https://doi.org/10.1109/AIEEPAS.1962.4501405
|
||||
[KnuOstWat2018]: https://doi.org/10.1109/TPWRS.2017.2783850
|
||||
[MorLatRam2013]: https://doi.org/10.1109/TPWRS.2013.2251373
|
||||
[PanGua2016]: https://doi.org/10.1287/opre.2016.1520
|
||||
[XavQiuWanThi2019]: https://doi.org/10.1109/TPWRS.2019.2892620
|
||||
## Table of Contents
|
||||
|
||||
### Authors
|
||||
```@contents
|
||||
Pages = ["usage.md", "format.md", "instances.md", "model.md", "api.md"]
|
||||
Depth = 3
|
||||
```
|
||||
|
||||
## Authors
|
||||
* **Alinson S. Xavier** (Argonne National Laboratory)
|
||||
* **Aleksandr M. Kazachkov** (University of Florida)
|
||||
* **Ogün Yurdakul** (Technische Universität Berlin)
|
||||
* **Jun He** (Purdue University)
|
||||
* **Feng Qiu** (Argonne National Laboratory)
|
||||
|
||||
### Acknowledgments
|
||||
## Acknowledgments
|
||||
|
||||
* We would like to thank **Yonghong Chen** (Midcontinent Independent System Operator), **Feng Pan** (Pacific Northwest National Laboratory) for valuable feedback on early versions of this package.
|
||||
|
||||
@@ -32,19 +31,19 @@
|
||||
|
||||
* Based upon work supported by the **U.S. Department of Energy Advanced Grid Modeling Program** under Grant DE-OE0000875.
|
||||
|
||||
### Citing
|
||||
## Citing
|
||||
|
||||
If you use UnitCommitment.jl in your research (instances, models or algorithms), we kindly request that you cite the package as follows:
|
||||
|
||||
* **Alinson S. Xavier, Aleksandr M. Kazachkov, 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).
|
||||
* **Alinson S. Xavier, Aleksandr M. Kazachkov, Ogün Yurdakul, Feng Qiu**, "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment (Version 0.3)". Zenodo (2022). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874).
|
||||
|
||||
If you use the instances, we additionally request that you cite the original sources, as described in the [instances page](instances.md).
|
||||
|
||||
### License
|
||||
## License
|
||||
|
||||
```text
|
||||
UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment
|
||||
Copyright © 2020, UChicago Argonne, LLC. All Rights Reserved.
|
||||
Copyright © 2020-2022, UChicago Argonne, LLC. All Rights Reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted
|
||||
provided that the following conditions are met:
|
||||
@@ -68,16 +67,3 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING N
|
||||
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
|
||||
## Site contents
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 2
|
||||
---
|
||||
usage.md
|
||||
format.md
|
||||
instances.md
|
||||
model.md
|
||||
```
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
```{sectnum}
|
||||
---
|
||||
start: 3
|
||||
depth: 2
|
||||
suffix: .
|
||||
---
|
||||
```
|
||||
|
||||
Instances
|
||||
=========
|
||||
|
||||
UnitCommitment.jl provides a large collection of benchmark instances collected from the literature and converted to a [common data format](format.md). In some cases, as indicated below, the original instances have been extended, with realistic parameters, using data-driven methods. If you use these instances in your research, we request that you cite UnitCommitment.jl, as well as the original sources, as listed below. Benchmark instances can be loaded with `UnitCommitment.read_benchmark(name)`, as explained in the [usage section](usage.md). Instance files can also be [directly downloaded from our website](https://axavier.org/UnitCommitment.jl/0.3/instances/).
|
||||
|
||||
```{warning}
|
||||
The instances included in UC.jl are still under development and may change in the future. If you use these instances in your research, for reproducibility, you should specify what version of UC.jl they came from.
|
||||
```
|
||||
!!! warning
|
||||
|
||||
The instances included in UC.jl are still under development and may change in the future. If you use these instances in your research, for reproducibility, you should specify what version of UC.jl they came from.
|
||||
|
||||
|
||||
MATPOWER
|
||||
@@ -33,7 +25,7 @@ Because most MATPOWER test cases were originally designed for power flow studies
|
||||
|
||||
* **Contingencies** were set to include all N-1 transmission line contingencies that do not generate islands or isolated buses. More specifically, there is one contingency for each transmission line, as long as that transmission line is not a bridge in the network graph.
|
||||
|
||||
For each MATPOWER test case, UC.jl provides 364 variations (`2017-01-01` to `2017-12-30`) corresponding different days of the year.
|
||||
For each MATPOWER test case, UC.jl provides 365 variations (`2017-01-01` to `2017-12-31`) corresponding different days of the year.
|
||||
|
||||
### MATPOWER/UW-PSTCA
|
||||
|
||||
@@ -288,7 +280,7 @@ Tejada19
|
||||
References
|
||||
----------
|
||||
|
||||
* [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)
|
||||
* [UCJL] **Alinson S. Xavier, Aleksandr M. Kazachkov, Ogün Yurdakul, Feng Qiu.** "UnitCommitment.jl: A Julia/JuMP Optimization Package for Security-Constrained Unit Commitment (Version 0.3)". Zenodo (2022). [DOI: 10.5281/zenodo.4269874](https://doi.org/10.5281/zenodo.4269874)
|
||||
|
||||
* [KnOsWa20] **Bernard Knueven, James Ostrowski and Jean-Paul Watson.** "On Mixed-Integer Programming Formulations for the Unit Commitment Problem". INFORMS Journal on Computing (2020). [DOI: 10.1287/ijoc.2019.0944](https://doi.org/10.1287/ijoc.2019.0944)
|
||||
|
||||
@@ -296,14 +288,9 @@ References
|
||||
|
||||
* [BaBlEh19] **Clayton Barrows, Aaron Bloom, Ali Ehlen, Jussi Ikaheimo, Jennie Jorgenson, Dheepak Krishnamurthy, Jessica Lau et al.** "The IEEE Reliability Test System: A Proposed 2019 Update." IEEE Transactions on Power Systems (2019). [DOI: 10.1109/TPWRS.2019.2925557](https://doi.org/10.1109/TPWRS.2019.2925557)
|
||||
|
||||
* [JoFlMa16] **C. Josz, S. Fliscounakis, J. Maeght, and P. Panciatici.** "AC Power Flow
|
||||
Data in MATPOWER and QCQP Format: iTesla, RTE Snapshots, and PEGASE". [ArXiv (2016)](https://arxiv.org/abs/1603.01533).
|
||||
* [JoFlMa16] **C. Josz, S. Fliscounakis, J. Maeght, and P. Panciatici.** "AC Power Flow Data in MATPOWER and QCQP Format: iTesla, RTE Snapshots, and PEGASE". [ArXiv (2016)](https://arxiv.org/abs/1603.01533).
|
||||
|
||||
* [FlPaCa13] **S. Fliscounakis, P. Panciatici, F. Capitanescu, and L. Wehenkel.**
|
||||
"Contingency ranking with respect to overloads in very large power
|
||||
systems taking into account uncertainty, preventive and corrective
|
||||
actions", Power Systems, IEEE Trans. on, (28)4:4909-4917, 2013.
|
||||
[DOI: 10.1109/TPWRS.2013.2251015](https://doi.org/10.1109/TPWRS.2013.2251015)
|
||||
* [FlPaCa13] **S. Fliscounakis, P. Panciatici, F. Capitanescu, and L. Wehenkel.** "Contingency ranking with respect to overloads in very large power systems taking into account uncertainty, preventive and corrective actions", Power Systems, IEEE Trans. on, (28)4:4909-4917, 2013. [DOI: 10.1109/TPWRS.2013.2251015](https://doi.org/10.1109/TPWRS.2013.2251015)
|
||||
|
||||
* [MTPWR] **D. Zimmerman, C. E. Murillo-Sandnchez and R. J. Thomas.** "Matpower: Steady-state operations, planning, and analysis tools forpower systems research and education", IEEE Transactions on PowerSystems, vol. 26, no. 1, pp. 12 –19, Feb. 2011. [DOI: 10.1109/TPWRS.2010.2051168](https://doi.org/10.1109/TPWRS.2010.2051168)
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
```{sectnum}
|
||||
---
|
||||
start: 4
|
||||
depth: 2
|
||||
suffix: .
|
||||
---
|
||||
```
|
||||
|
||||
JuMP Model
|
||||
==========
|
||||
|
||||
@@ -16,8 +8,10 @@ Decision variables
|
||||
|
||||
### Generators
|
||||
|
||||
#### Thermal Units
|
||||
|
||||
Name | Symbol | Description | Unit
|
||||
-----|:--------:|-------------|:------:
|
||||
:-----|:--------:|:-------------|:------:
|
||||
`is_on[g,t]` | $u_{g}(t)$ | True if generator `g` is on at time `t`. | Binary
|
||||
`switch_on[g,t]` | $v_{g}(t)$ | True is generator `g` switches on at time `t`. | Binary
|
||||
`switch_off[g,t]` | $w_{g}(t)$ | True if generator `g` switches off at time `t`. | Binary
|
||||
@@ -27,10 +21,17 @@ Name | Symbol | Description | Unit
|
||||
`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
|
||||
|
||||
|
||||
#### Profiled Units
|
||||
|
||||
Name | Symbol | Description | Unit
|
||||
:-----|:------:|:-------------|:------:
|
||||
`prod_profiled[s,t]` | $p^{\dagger}_{g}(t)$ | Amount of power produced by profiled unit `g` at time `t`. | MW
|
||||
|
||||
|
||||
### Buses
|
||||
|
||||
Name | Symbol | Description | Unit
|
||||
-----|:------:|-------------|:------:
|
||||
:-----|:------:|:-------------|:------:
|
||||
`net_injection[b,t]` | $n_b(t)$ | Net injection at bus `b` at time `t`. | MW
|
||||
`curtail[b,t]` | $s^+_b(t)$ | Amount of load curtailed at bus `b` at time `t` | MW
|
||||
|
||||
@@ -38,69 +39,24 @@ Name | Symbol | Description | Unit
|
||||
### Price-sensitive loads
|
||||
|
||||
Name | Symbol | Description | Unit
|
||||
-----|:------:|-------------|:------:
|
||||
:-----|:------:|:-------------|:------:
|
||||
`loads[s,t]` | $d_{s}(t)$ | Amount of power served to price-sensitive load `s` at time `t`. | MW
|
||||
|
||||
### Transmission lines
|
||||
|
||||
Name | Symbol | Description | Unit
|
||||
-----|:------:|-------------|:------:
|
||||
:-----|:------:|:-------------|:------:
|
||||
`flow[l,t]` | $f_l(t)$ | Power flow on line `l` at time `t`. | MW
|
||||
`overflow[l,t]` | $f^+_l(t)$ | Amount of flow above the limit for line `l` at time `t`. | MW
|
||||
|
||||
```{warning}
|
||||
!!! warning
|
||||
|
||||
Since transmission and N-1 security constraints are enforced in a lazy way, most of the `flow[l,t]` variables are never added to the model. Accessing `model[:flow][l,t]` without first checking that the variable exists will likely generate an error.
|
||||
```
|
||||
Since transmission and N-1 security constraints are enforced in a lazy way, most of the `flow[l,t]` variables are never added to the model. Accessing `model[:flow][l,t]` without first checking that the variable exists will likely generate an error.
|
||||
|
||||
Objective function
|
||||
------------------
|
||||
|
||||
$$
|
||||
\begin{align}
|
||||
\text{minimize} \;\; &
|
||||
\sum_{t \in \mathcal{T}}
|
||||
\sum_{g \in \mathcal{G}}
|
||||
C^\text{min}_g(t) u_g(t) \\
|
||||
&
|
||||
+ \sum_{t \in \mathcal{T}}
|
||||
\sum_{g \in \mathcal{G}}
|
||||
\sum_{g \in \mathcal{K}_g}
|
||||
C^k_g(t) p^k_g(t) \\
|
||||
&
|
||||
+ \sum_{t \in \mathcal{T}}
|
||||
\sum_{g \in \mathcal{G}}
|
||||
\sum_{s \in \mathcal{S}_g}
|
||||
C^s_{g}(t) \delta^s_g(t) \\
|
||||
&
|
||||
+ \sum_{t \in \mathcal{T}}
|
||||
\sum_{l \in \mathcal{L}}
|
||||
C^\text{overflow}_{l}(t) f^+_l(t) \\
|
||||
&
|
||||
+ \sum_{t \in \mathcal{T}}
|
||||
\sum_{b \in \mathcal{B}}
|
||||
C^\text{curtail}(t) s^+_b(t) \\
|
||||
&
|
||||
- \sum_{t \in \mathcal{T}}
|
||||
\sum_{s \in \mathcal{PS}}
|
||||
R_{s}(t) d_{s}(t) \\
|
||||
|
||||
\end{align}
|
||||
$$
|
||||
where
|
||||
- $\mathcal{B}$ is the set of buses
|
||||
- $\mathcal{G}$ is the set of generators
|
||||
- $\mathcal{L}$ is the set of transmission lines
|
||||
- $\mathcal{PS}$ is the set of price-sensitive loads
|
||||
- $\mathcal{S}_g$ is the set of start-up categories for generator $g$
|
||||
- $\mathcal{T}$ is the set of time steps
|
||||
- $C^\text{curtail}(t)$ is the curtailment penalty (in \$/MW)
|
||||
- $C^\text{min}_g(t)$ is the cost of keeping generator $g$ on and producing at minimum power during time $t$ (in \$)
|
||||
- $C^\text{overflow}_{l}(t)$ is the flow limit penalty for line $l$ at time $t$ (in \$/MW)
|
||||
- $C^k_g(t)$ is the cost for generator $g$ to produce 1 MW of power at time $t$ under piecewise linear segment $k$
|
||||
- $C^s_{g}(t)$ is the cost of starting up generator $g$ at time $t$ under start-up category $s$ (in \$)
|
||||
- $R_{s}(t)$ is the revenue obtained from serving price-sensitive load $s$ at time $t$ (in \$/MW)
|
||||
|
||||
TODO
|
||||
|
||||
Constraints
|
||||
-----------
|
||||
@@ -1,11 +1,3 @@
|
||||
```{sectnum}
|
||||
---
|
||||
start: 1
|
||||
depth: 2
|
||||
suffix: .
|
||||
---
|
||||
```
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
@@ -66,10 +58,7 @@ using UnitCommitment
|
||||
instance = UnitCommitment.read_benchmark("matpower/case3375wp/2017-02-01")
|
||||
```
|
||||
|
||||
Advanced usage
|
||||
--------------
|
||||
|
||||
### Customizing the formulation
|
||||
## Customizing the formulation
|
||||
|
||||
By default, `build_model` uses a formulation that combines modeling components from different publications, and that has been carefully tested, using our own benchmark scripts, to provide good performance across a wide variety of instances. This default formulation is expected to change over time, as new methods are proposed in the literature. You can, however, construct your own formulation, based on the modeling components that you choose, as shown in the next example.
|
||||
|
||||
@@ -102,7 +91,7 @@ model = UnitCommitment.build_model(
|
||||
)
|
||||
```
|
||||
|
||||
### Generating initial conditions
|
||||
## Generating initial conditions
|
||||
|
||||
When creating random unit commitment instances for benchmark purposes, it is often hard to compute, in advance, sensible initial conditions for all generators. Setting initial conditions naively (for example, making all generators initially off and producing no power) can easily cause the instance to become infeasible due to excessive ramping. Initial conditions can also make it hard to modify existing instances. For example, increasing the system load without carefully modifying the initial conditions may make the problem infeasible or unrealistically challenging to solve.
|
||||
|
||||
@@ -126,11 +115,11 @@ model = UnitCommitment.build_model(
|
||||
UnitCommitment.optimize!(model)
|
||||
```
|
||||
|
||||
```{warning}
|
||||
The function `generate_initial_conditions!` may return different initial conditions after each call, even if the same instance and the same optimizer is provided. The particular algorithm may also change in a future version of UC.jl. For these reasons, it is recommended that you generate initial conditions exactly once for each instance and store them for later use.
|
||||
```
|
||||
!!! warning
|
||||
|
||||
The function `generate_initial_conditions!` may return different initial conditions after each call, even if the same instance and the same optimizer is provided. The particular algorithm may also change in a future version of UC.jl. For these reasons, it is recommended that you generate initial conditions exactly once for each instance and store them for later use.
|
||||
|
||||
### Verifying solutions
|
||||
## Verifying solutions
|
||||
|
||||
When developing new formulations, it is very easy to introduce subtle errors in the model that result in incorrect solutions. To help with this, UC.jl includes a utility function that verifies if a given solution is feasible, and, if not, prints all the validation errors it found. The implementation of this function is completely independent from the implementation of the optimization model, and therefore can be used to validate it. The function can also be used to verify solutions produced by other optimization packages, as long as they follow the [UC.jl data format](format.md).
|
||||
|
||||
@@ -147,3 +136,91 @@ solution = JSON.parsefile("solution.json")
|
||||
# Validate solution and print validation errors
|
||||
UnitCommitment.validate(instance, solution)
|
||||
```
|
||||
|
||||
## Computing Locational Marginal Prices
|
||||
|
||||
Locational marginal prices (LMPs) refer to the cost of supplying electricity at a particular location of the network. Multiple methods for computing LMPs have been proposed in the literature. UnitCommitment.jl implements two commonly-used methods: conventional LMPs and Approximated Extended LMPs (AELMPs). To compute LMPs for a given unit commitment instance, the `compute_lmp` function can be used, as shown in the examples below. The function accepts three arguments -- a solved SCUC model, an LMP method, and a linear optimizer -- and it returns a dictionary mapping `(bus_name, time)` to the marginal price.
|
||||
|
||||
|
||||
!!! warning
|
||||
|
||||
Most mixed-integer linear optimizers, such as `HiGHS`, `Gurobi` and `CPLEX` can be used with `compute_lmp`, with the notable exception of `Cbc`, which does not support dual value evaluations. If using `Cbc`, please provide `Clp` as the linear optimizer.
|
||||
|
||||
### Conventional LMPs
|
||||
|
||||
LMPs are conventionally computed by: (1) solving the SCUC model, (2) fixing all binary variables to their optimal values, and (3) re-solving the resulting linear programming model. In this approach, the LMPs are defined as the dual variables' values associated with the net injection constraints. The example below shows how to compute conventional LMPs for a given unit commitment instance. First, we build and optimize the SCUC model. Then, we call the `compute_lmp` function, providing as the second argument `ConventionalLMP()`.
|
||||
|
||||
|
||||
```julia
|
||||
using UnitCommitment
|
||||
using HiGHS
|
||||
|
||||
import UnitCommitment: ConventionalLMP
|
||||
|
||||
# Read benchmark instance
|
||||
instance = UnitCommitment.read_benchmark("matpower/case118/2018-01-01")
|
||||
|
||||
# Build the model
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
|
||||
# Optimize the model
|
||||
UnitCommitment.optimize!(model)
|
||||
|
||||
# Compute the LMPs using the conventional method
|
||||
lmp = UnitCommitment.compute_lmp(
|
||||
model,
|
||||
ConventionalLMP(),
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
|
||||
# Access the LMPs
|
||||
# Example: "s1" is the scenario name, "b1" is the bus name, 1 is the first time slot
|
||||
@show lmp["s1","b1", 1]
|
||||
```
|
||||
|
||||
### Approximate Extended LMPs
|
||||
|
||||
Approximate Extended LMPs (AELMPs) are an alternative method to calculate locational marginal prices which attemps to minimize uplift payments. The method internally works by modifying the instance data in three ways: (1) it sets the minimum power output of each generator to zero, (2) it averages the start-up cost over the offer blocks for each generator, and (3) it relaxes all integrality constraints. To compute AELMPs, as shown in the example below, we call `compute_lmp` and provide `AELMP()` as the second argument.
|
||||
|
||||
This method has two configurable parameters: `allow_offline_participation` and `consider_startup_costs`. If `allow_offline_participation = true`, then offline generators are allowed to participate in the pricing. If instead `allow_offline_participation = false`, offline generators are not allowed and therefore are excluded from the system. A solved UC model is optional if offline participation is allowed, but is required if not allowed. The method forces offline participation to be allowed if the UC model supplied by the user is not solved. For the second field, If `consider_startup_costs = true`, then start-up costs are integrated and averaged over each unit production; otherwise the production costs stay the same. By default, both fields are set to `true`.
|
||||
|
||||
!!! warning
|
||||
|
||||
This approximation method is still under active research, and has several limitations. The implementation provided in the package is based on MISO Phase I only. It only supports fast start resources. More specifically, the minimum up/down time of all generators must be 1, the initial power of all generators must be 0, and the initial status of all generators must be negative. The method does not support time-varying start-up costs. The method does not support multiple scenarios. If offline participation is not allowed, AELMPs treats an asset to be offline if it is never on throughout all time periods.
|
||||
|
||||
```julia
|
||||
using UnitCommitment
|
||||
using HiGHS
|
||||
|
||||
import UnitCommitment: AELMP
|
||||
|
||||
# Read benchmark instance
|
||||
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
||||
|
||||
# Build the model
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
|
||||
# Optimize the model
|
||||
UnitCommitment.optimize!(model)
|
||||
|
||||
# Compute the AELMPs
|
||||
aelmp = UnitCommitment.compute_lmp(
|
||||
model,
|
||||
AELMP(
|
||||
allow_offline_participation = false,
|
||||
consider_startup_costs = true
|
||||
),
|
||||
optimizer = HiGHS.Optimizer
|
||||
)
|
||||
|
||||
# Access the AELMPs
|
||||
# Example: "s1" is the scenario name, "b1" is the bus name, 1 is the first time slot
|
||||
# Note: although scenario is supported, the query still keeps the scenario keys for consistency.
|
||||
@show aelmp["s1", "b1", 1]
|
||||
```
|
||||
75
juliaw
75
juliaw
@@ -1,75 +0,0 @@
|
||||
#!/bin/bash
|
||||
# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
if [ ! -e Project.toml ]; then
|
||||
echo "juliaw: Project.toml not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -e Manifest.toml ]; then
|
||||
julia --project=. -e 'using Pkg; Pkg.instantiate()' || exit 1
|
||||
fi
|
||||
|
||||
if [ ! -e build/sysimage.so -o Project.toml -nt build/sysimage.so ]; then
|
||||
echo "juliaw: rebuilding system image..."
|
||||
|
||||
# Generate temporary project folder
|
||||
rm -rf $HOME/.juliaw
|
||||
mkdir -p $HOME/.juliaw/src
|
||||
cp Project.toml Manifest.toml $HOME/.juliaw
|
||||
NAME=$(julia -e 'using TOML; toml = TOML.parsefile("Project.toml"); "name" in keys(toml) && print(toml["name"])')
|
||||
if [ ! -z $NAME ]; then
|
||||
cat > $HOME/.juliaw/src/$NAME.jl << EOF
|
||||
module $NAME
|
||||
end
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Add PackageCompiler dependencies to temporary project
|
||||
julia --project=$HOME/.juliaw -e 'using Pkg; Pkg.add(["PackageCompiler", "TOML", "Logging"])'
|
||||
|
||||
# Generate system image scripts
|
||||
cat > $HOME/.juliaw/sysimage.jl << EOF
|
||||
using PackageCompiler
|
||||
using TOML
|
||||
using Logging
|
||||
|
||||
Logging.disable_logging(Logging.Info)
|
||||
mkpath("$PWD/build")
|
||||
|
||||
println("juliaw: generating precompilation statements...")
|
||||
run(\`julia --project="$PWD" --trace-compile="$PWD"/build/precompile.jl \$(ARGS)\`)
|
||||
|
||||
println("juliaw: finding dependencies...")
|
||||
project = TOML.parsefile("Project.toml")
|
||||
manifest = TOML.parsefile("Manifest.toml")
|
||||
deps = Symbol[]
|
||||
for dep in keys(project["deps"])
|
||||
if dep in keys(manifest)
|
||||
# Up to Julia 1.6
|
||||
dep_entry = manifest[dep][1]
|
||||
else
|
||||
# Julia 1.7+
|
||||
dep_entry = manifest["deps"][dep][1]
|
||||
end
|
||||
if "path" in keys(dep_entry)
|
||||
println(" - \$(dep) [skip]")
|
||||
else
|
||||
println(" - \$(dep)")
|
||||
push!(deps, Symbol(dep))
|
||||
end
|
||||
end
|
||||
|
||||
println("juliaw: building system image...")
|
||||
create_sysimage(
|
||||
deps,
|
||||
precompile_statements_file = "$PWD/build/precompile.jl",
|
||||
sysimage_path = "$PWD/build/sysimage.so",
|
||||
)
|
||||
EOF
|
||||
julia --project=$HOME/.juliaw $HOME/.juliaw/sysimage.jl $*
|
||||
else
|
||||
julia --project=. --sysimage build/sysimage.so $*
|
||||
fi
|
||||
@@ -4,9 +4,12 @@
|
||||
|
||||
module UnitCommitment
|
||||
|
||||
using Base: String
|
||||
|
||||
include("instance/structs.jl")
|
||||
include("model/formulations/base/structs.jl")
|
||||
include("solution/structs.jl")
|
||||
include("lmp/structs.jl")
|
||||
|
||||
include("model/formulations/ArrCon2000/structs.jl")
|
||||
include("model/formulations/CarArr2006/structs.jl")
|
||||
@@ -29,6 +32,7 @@ include("model/formulations/base/psload.jl")
|
||||
include("model/formulations/base/sensitivity.jl")
|
||||
include("model/formulations/base/system.jl")
|
||||
include("model/formulations/base/unit.jl")
|
||||
include("model/formulations/base/punit.jl")
|
||||
include("model/formulations/CarArr2006/pwlcosts.jl")
|
||||
include("model/formulations/DamKucRajAta2016/ramp.jl")
|
||||
include("model/formulations/Gar1962/pwlcosts.jl")
|
||||
@@ -56,5 +60,7 @@ include("utils/log.jl")
|
||||
include("utils/benchmark.jl")
|
||||
include("validation/repair.jl")
|
||||
include("validation/validate.jl")
|
||||
include("lmp/conventional.jl")
|
||||
include("lmp/aelmp.jl")
|
||||
|
||||
end
|
||||
|
||||
@@ -18,9 +18,9 @@ function read_egret_solution(path::String)::OrderedDict
|
||||
|
||||
solution = OrderedDict()
|
||||
is_on = solution["Is on"] = OrderedDict()
|
||||
production = solution["Production (MW)"] = OrderedDict()
|
||||
production = solution["Thermal production (MW)"] = OrderedDict()
|
||||
reserve = solution["Reserve (MW)"] = OrderedDict()
|
||||
production_cost = solution["Production cost (\$)"] = OrderedDict()
|
||||
production_cost = solution["Thermal production cost (\$)"] = OrderedDict()
|
||||
startup_cost = solution["Startup cost (\$)"] = OrderedDict()
|
||||
|
||||
for (gen_name, gen_dict) in egret["elements"]["generator"]
|
||||
|
||||
@@ -17,6 +17,7 @@ function _migrate(json)
|
||||
end
|
||||
version = VersionNumber(version)
|
||||
version >= v"0.3" || _migrate_to_v03(json)
|
||||
version >= v"0.4" || _migrate_to_v04(json)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -36,3 +37,14 @@ function _migrate_to_v03(json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function _migrate_to_v04(json)
|
||||
# Migrate thermal units
|
||||
if json["Generators"] !== nothing
|
||||
for (gen_name, gen) in json["Generators"]
|
||||
if gen["Type"] === nothing
|
||||
gen["Type"] = "Thermal"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,15 +13,13 @@ const INSTANCES_URL = "https://axavier.org/UnitCommitment.jl/0.3/instances"
|
||||
"""
|
||||
read_benchmark(name::AbstractString)::UnitCommitmentInstance
|
||||
|
||||
Read one of the benchmark unit commitment instances included in the package.
|
||||
See "Instances" section of the documentation for the entire list of benchmark
|
||||
instances available.
|
||||
Read one of the benchmark instances included in the package. See
|
||||
[Instances](instances.md) for the entire list of benchmark instances available.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
import UnitCommitment
|
||||
instance = UnitCommitment.read_benchmark("matpower/case3375wp/2017-02-01")
|
||||
# Example
|
||||
```julia
|
||||
instance = UnitCommitment.read_benchmark("matpower/case3375wp/2017-02-01")
|
||||
```
|
||||
"""
|
||||
function read_benchmark(
|
||||
name::AbstractString;
|
||||
@@ -45,26 +43,76 @@ function read_benchmark(
|
||||
return UnitCommitment.read(filename)
|
||||
end
|
||||
|
||||
function _repair_scenario_names_and_probabilities!(
|
||||
scenarios::Vector{UnitCommitmentScenario},
|
||||
path::Vector{String},
|
||||
)::Nothing
|
||||
total_weight = sum([sc.probability for sc in scenarios])
|
||||
for (sc_path, sc) in zip(path, scenarios)
|
||||
sc.name !== "" ||
|
||||
(sc.name = first(split(last(split(sc_path, "/")), ".")))
|
||||
sc.probability = (sc.probability / total_weight)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
"""
|
||||
read(path::AbstractString)::UnitCommitmentInstance
|
||||
|
||||
Read a unit commitment instance from a file. The file may be gzipped.
|
||||
Read a deterministic test case from the given file. The file may be gzipped.
|
||||
|
||||
Example
|
||||
-------
|
||||
# Example
|
||||
|
||||
import UnitCommitment
|
||||
instance = UnitCommitment.read("/path/to/input.json.gz")
|
||||
```julia
|
||||
instance = UnitCommitment.read("s1.json.gz")
|
||||
```
|
||||
"""
|
||||
function read(path::AbstractString)::UnitCommitmentInstance
|
||||
if endswith(path, ".gz")
|
||||
return _read(gzopen(path))
|
||||
else
|
||||
return _read(open(path))
|
||||
end
|
||||
function read(path::String)::UnitCommitmentInstance
|
||||
scenarios = Vector{UnitCommitmentScenario}()
|
||||
scenario = _read_scenario(path)
|
||||
scenario.name = "s1"
|
||||
scenario.probability = 1.0
|
||||
scenarios = [scenario]
|
||||
instance =
|
||||
UnitCommitmentInstance(time = scenario.time, scenarios = scenarios)
|
||||
return instance
|
||||
end
|
||||
|
||||
function _read(file::IO)::UnitCommitmentInstance
|
||||
"""
|
||||
read(path::Vector{String})::UnitCommitmentInstance
|
||||
|
||||
Read a stochastic unit commitment instance from the given files. Each file
|
||||
describes a scenario. The files may be gzipped.
|
||||
|
||||
# Example
|
||||
|
||||
```julia
|
||||
instance = UnitCommitment.read(["s1.json.gz", "s2.json.gz"])
|
||||
```
|
||||
"""
|
||||
function read(paths::Vector{String})::UnitCommitmentInstance
|
||||
scenarios = UnitCommitmentScenario[]
|
||||
for p in paths
|
||||
push!(scenarios, _read_scenario(p))
|
||||
end
|
||||
_repair_scenario_names_and_probabilities!(scenarios, paths)
|
||||
instance =
|
||||
UnitCommitmentInstance(time = scenarios[1].time, scenarios = scenarios)
|
||||
return instance
|
||||
end
|
||||
|
||||
function _read_scenario(path::String)::UnitCommitmentScenario
|
||||
if endswith(path, ".gz")
|
||||
scenario = _read(gzopen(path))
|
||||
elseif endswith(path, ".json")
|
||||
scenario = _read(open(path))
|
||||
else
|
||||
error("Unsupported input format")
|
||||
end
|
||||
return scenario
|
||||
end
|
||||
|
||||
function _read(file::IO)::UnitCommitmentScenario
|
||||
return _from_json(
|
||||
JSON.parse(file, dicttype = () -> DefaultOrderedDict(nothing)),
|
||||
)
|
||||
@@ -79,14 +127,15 @@ function _read_json(path::String)::OrderedDict
|
||||
return JSON.parse(file, dicttype = () -> DefaultOrderedDict(nothing))
|
||||
end
|
||||
|
||||
function _from_json(json; repair = true)
|
||||
function _from_json(json; repair = true)::UnitCommitmentScenario
|
||||
_migrate(json)
|
||||
units = Unit[]
|
||||
thermal_units = ThermalUnit[]
|
||||
buses = Bus[]
|
||||
contingencies = Contingency[]
|
||||
lines = TransmissionLine[]
|
||||
loads = PriceSensitiveLoad[]
|
||||
reserves = Reserve[]
|
||||
profiled_units = ProfiledUnit[]
|
||||
|
||||
function scalar(x; default = nothing)
|
||||
x !== nothing || return default
|
||||
@@ -104,9 +153,14 @@ function _from_json(json; repair = true)
|
||||
time_multiplier = 60 ÷ time_step
|
||||
T = time_horizon * time_multiplier
|
||||
|
||||
probability = json["Parameters"]["Scenario weight"]
|
||||
probability !== nothing || (probability = 1)
|
||||
scenario_name = json["Parameters"]["Scenario name"]
|
||||
scenario_name !== nothing || (scenario_name = "")
|
||||
|
||||
name_to_bus = Dict{String,Bus}()
|
||||
name_to_line = Dict{String,TransmissionLine}()
|
||||
name_to_unit = Dict{String,Unit}()
|
||||
name_to_unit = Dict{String,ThermalUnit}()
|
||||
name_to_reserve = Dict{String,Reserve}()
|
||||
|
||||
function timeseries(x; default = nothing)
|
||||
@@ -120,15 +174,6 @@ function _from_json(json; repair = true)
|
||||
json["Parameters"]["Power balance penalty (\$/MW)"],
|
||||
default = [1000.0 for t in 1:T],
|
||||
)
|
||||
# Penalty price for shortage in meeting system-wide flexiramp requirements
|
||||
flexiramp_shortfall_penalty = timeseries(
|
||||
json["Parameters"]["Flexiramp penalty (\$/MW)"],
|
||||
default = [500.0 for t in 1:T],
|
||||
)
|
||||
shortfall_penalty = timeseries(
|
||||
json["Parameters"]["Reserve shortfall penalty (\$/MW)"],
|
||||
default = [-1.0 for t in 1:T],
|
||||
)
|
||||
|
||||
# Read buses
|
||||
for (bus_name, dict) in json["Buses"]
|
||||
@@ -136,8 +181,9 @@ function _from_json(json; repair = true)
|
||||
bus_name,
|
||||
length(buses),
|
||||
timeseries(dict["Load (MW)"]),
|
||||
Unit[],
|
||||
ThermalUnit[],
|
||||
PriceSensitiveLoad[],
|
||||
ProfiledUnit[],
|
||||
)
|
||||
name_to_bus[bus_name] = bus
|
||||
push!(buses, bus)
|
||||
@@ -150,7 +196,7 @@ function _from_json(json; repair = true)
|
||||
name = reserve_name,
|
||||
type = lowercase(dict["Type"]),
|
||||
amount = timeseries(dict["Amount (MW)"]),
|
||||
units = [],
|
||||
thermal_units = [],
|
||||
shortfall_penalty = scalar(
|
||||
dict["Shortfall penalty (\$/MW)"],
|
||||
default = -1,
|
||||
@@ -163,90 +209,127 @@ function _from_json(json; repair = true)
|
||||
|
||||
# Read units
|
||||
for (unit_name, dict) in json["Generators"]
|
||||
# Read and validate unit type
|
||||
unit_type = scalar(dict["Type"], default = nothing)
|
||||
unit_type !== nothing || error("unit $unit_name has no type specified")
|
||||
bus = name_to_bus[dict["Bus"]]
|
||||
|
||||
# Read production cost curve
|
||||
K = length(dict["Production cost curve (MW)"])
|
||||
curve_mw = hcat(
|
||||
[timeseries(dict["Production cost curve (MW)"][k]) for k in 1:K]...,
|
||||
)
|
||||
curve_cost = hcat(
|
||||
[timeseries(dict["Production cost curve (\$)"][k]) for k in 1:K]...,
|
||||
)
|
||||
min_power = curve_mw[:, 1]
|
||||
max_power = curve_mw[:, K]
|
||||
min_power_cost = curve_cost[:, 1]
|
||||
segments = CostSegment[]
|
||||
for k in 2:K
|
||||
amount = curve_mw[:, k] - curve_mw[:, k-1]
|
||||
cost = (curve_cost[:, k] - curve_cost[:, k-1]) ./ amount
|
||||
replace!(cost, NaN => 0.0)
|
||||
push!(segments, CostSegment(amount, cost))
|
||||
end
|
||||
|
||||
# Read startup costs
|
||||
startup_delays = scalar(dict["Startup delays (h)"], default = [1])
|
||||
startup_costs = scalar(dict["Startup costs (\$)"], default = [0.0])
|
||||
startup_categories = StartupCategory[]
|
||||
for k in 1:length(startup_delays)
|
||||
push!(
|
||||
startup_categories,
|
||||
StartupCategory(
|
||||
startup_delays[k] .* time_multiplier,
|
||||
startup_costs[k],
|
||||
),
|
||||
if lowercase(unit_type) === "thermal"
|
||||
# Read production cost curve
|
||||
K = length(dict["Production cost curve (MW)"])
|
||||
curve_mw = hcat(
|
||||
[
|
||||
timeseries(dict["Production cost curve (MW)"][k]) for
|
||||
k in 1:K
|
||||
]...,
|
||||
)
|
||||
end
|
||||
|
||||
# Read reserve eligibility
|
||||
unit_reserves = Reserve[]
|
||||
if "Reserve eligibility" in keys(dict)
|
||||
unit_reserves =
|
||||
[name_to_reserve[n] for n in dict["Reserve eligibility"]]
|
||||
end
|
||||
|
||||
# Read and validate initial conditions
|
||||
initial_power = scalar(dict["Initial power (MW)"], default = nothing)
|
||||
initial_status = scalar(dict["Initial status (h)"], default = nothing)
|
||||
if initial_power === nothing
|
||||
initial_status === nothing ||
|
||||
error("unit $unit_name has initial status but no initial power")
|
||||
else
|
||||
initial_status !== nothing ||
|
||||
error("unit $unit_name has initial power but no initial status")
|
||||
initial_status != 0 ||
|
||||
error("unit $unit_name has invalid initial status")
|
||||
if initial_status < 0 && initial_power > 1e-3
|
||||
error("unit $unit_name has invalid initial power")
|
||||
curve_cost = hcat(
|
||||
[
|
||||
timeseries(dict["Production cost curve (\$)"][k]) for
|
||||
k in 1:K
|
||||
]...,
|
||||
)
|
||||
min_power = curve_mw[:, 1]
|
||||
max_power = curve_mw[:, K]
|
||||
min_power_cost = curve_cost[:, 1]
|
||||
segments = CostSegment[]
|
||||
for k in 2:K
|
||||
amount = curve_mw[:, k] - curve_mw[:, k-1]
|
||||
cost = (curve_cost[:, k] - curve_cost[:, k-1]) ./ amount
|
||||
replace!(cost, NaN => 0.0)
|
||||
push!(segments, CostSegment(amount, cost))
|
||||
end
|
||||
initial_status *= time_multiplier
|
||||
end
|
||||
|
||||
unit = Unit(
|
||||
unit_name,
|
||||
bus,
|
||||
max_power,
|
||||
min_power,
|
||||
timeseries(dict["Must run?"], default = [false for t in 1:T]),
|
||||
min_power_cost,
|
||||
segments,
|
||||
scalar(dict["Minimum uptime (h)"], default = 1) * time_multiplier,
|
||||
scalar(dict["Minimum downtime (h)"], default = 1) * time_multiplier,
|
||||
scalar(dict["Ramp up limit (MW)"], default = 1e6),
|
||||
scalar(dict["Ramp down limit (MW)"], default = 1e6),
|
||||
scalar(dict["Startup limit (MW)"], default = 1e6),
|
||||
scalar(dict["Shutdown limit (MW)"], default = 1e6),
|
||||
initial_status,
|
||||
initial_power,
|
||||
startup_categories,
|
||||
unit_reserves,
|
||||
)
|
||||
push!(bus.units, unit)
|
||||
for r in unit_reserves
|
||||
push!(r.units, unit)
|
||||
# Read startup costs
|
||||
startup_delays = scalar(dict["Startup delays (h)"], default = [1])
|
||||
startup_costs = scalar(dict["Startup costs (\$)"], default = [0.0])
|
||||
startup_categories = StartupCategory[]
|
||||
for k in 1:length(startup_delays)
|
||||
push!(
|
||||
startup_categories,
|
||||
StartupCategory(
|
||||
startup_delays[k] .* time_multiplier,
|
||||
startup_costs[k],
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
# Read reserve eligibility
|
||||
unit_reserves = Reserve[]
|
||||
if "Reserve eligibility" in keys(dict)
|
||||
unit_reserves =
|
||||
[name_to_reserve[n] for n in dict["Reserve eligibility"]]
|
||||
end
|
||||
|
||||
# Read and validate initial conditions
|
||||
initial_power =
|
||||
scalar(dict["Initial power (MW)"], default = nothing)
|
||||
initial_status =
|
||||
scalar(dict["Initial status (h)"], default = nothing)
|
||||
if initial_power === nothing
|
||||
initial_status === nothing || error(
|
||||
"unit $unit_name has initial status but no initial power",
|
||||
)
|
||||
else
|
||||
initial_status !== nothing || error(
|
||||
"unit $unit_name has initial power but no initial status",
|
||||
)
|
||||
initial_status != 0 ||
|
||||
error("unit $unit_name has invalid initial status")
|
||||
if initial_status < 0 && initial_power > 1e-3
|
||||
error("unit $unit_name has invalid initial power")
|
||||
end
|
||||
initial_status *= time_multiplier
|
||||
end
|
||||
|
||||
# Read commitment status
|
||||
commitment_status = scalar(
|
||||
dict["Commitment status"],
|
||||
default = Vector{Union{Bool,Nothing}}(nothing, T),
|
||||
)
|
||||
|
||||
unit = ThermalUnit(
|
||||
unit_name,
|
||||
bus,
|
||||
max_power,
|
||||
min_power,
|
||||
timeseries(dict["Must run?"], default = [false for t in 1:T]),
|
||||
min_power_cost,
|
||||
segments,
|
||||
scalar(dict["Minimum uptime (h)"], default = 1) *
|
||||
time_multiplier,
|
||||
scalar(dict["Minimum downtime (h)"], default = 1) *
|
||||
time_multiplier,
|
||||
scalar(dict["Ramp up limit (MW)"], default = 1e6),
|
||||
scalar(dict["Ramp down limit (MW)"], default = 1e6),
|
||||
scalar(dict["Startup limit (MW)"], default = 1e6),
|
||||
scalar(dict["Shutdown limit (MW)"], default = 1e6),
|
||||
initial_status,
|
||||
initial_power,
|
||||
startup_categories,
|
||||
unit_reserves,
|
||||
commitment_status,
|
||||
)
|
||||
push!(bus.thermal_units, unit)
|
||||
for r in unit_reserves
|
||||
push!(r.thermal_units, unit)
|
||||
end
|
||||
name_to_unit[unit_name] = unit
|
||||
push!(thermal_units, unit)
|
||||
elseif lowercase(unit_type) === "profiled"
|
||||
bus = name_to_bus[dict["Bus"]]
|
||||
pu = ProfiledUnit(
|
||||
unit_name,
|
||||
bus,
|
||||
timeseries(scalar(dict["Minimum power (MW)"], default = 0.0)),
|
||||
timeseries(dict["Maximum power (MW)"]),
|
||||
timeseries(dict["Cost (\$/MW)"]),
|
||||
)
|
||||
push!(bus.profiled_units, pu)
|
||||
push!(profiled_units, pu)
|
||||
else
|
||||
error("unit $unit_name has an invalid type")
|
||||
end
|
||||
name_to_unit[unit_name] = unit
|
||||
push!(units, unit)
|
||||
end
|
||||
|
||||
# Read transmission lines
|
||||
@@ -280,7 +363,7 @@ function _from_json(json; repair = true)
|
||||
# Read contingencies
|
||||
if "Contingencies" in keys(json)
|
||||
for (cont_name, dict) in json["Contingencies"]
|
||||
affected_units = Unit[]
|
||||
affected_units = ThermalUnit[]
|
||||
affected_lines = TransmissionLine[]
|
||||
if "Affected lines" in keys(dict)
|
||||
affected_lines =
|
||||
@@ -310,7 +393,9 @@ function _from_json(json; repair = true)
|
||||
end
|
||||
end
|
||||
|
||||
instance = UnitCommitmentInstance(
|
||||
scenario = UnitCommitmentScenario(
|
||||
name = scenario_name,
|
||||
probability = probability,
|
||||
buses_by_name = Dict(b.name => b for b in buses),
|
||||
buses = buses,
|
||||
contingencies_by_name = Dict(c.name => c for c in contingencies),
|
||||
@@ -322,14 +407,16 @@ function _from_json(json; repair = true)
|
||||
price_sensitive_loads = loads,
|
||||
reserves = reserves,
|
||||
reserves_by_name = name_to_reserve,
|
||||
shortfall_penalty = shortfall_penalty,
|
||||
flexiramp_shortfall_penalty = flexiramp_shortfall_penalty,
|
||||
time = T,
|
||||
units_by_name = Dict(g.name => g for g in units),
|
||||
units = units,
|
||||
thermal_units_by_name = Dict(g.name => g for g in thermal_units),
|
||||
thermal_units = thermal_units,
|
||||
profiled_units_by_name = Dict(pu.name => pu for pu in profiled_units),
|
||||
profiled_units = profiled_units,
|
||||
isf = spzeros(Float64, length(lines), length(buses) - 1),
|
||||
lodf = spzeros(Float64, length(lines), length(lines)),
|
||||
)
|
||||
if repair
|
||||
UnitCommitment.repair!(instance)
|
||||
UnitCommitment.repair!(scenario)
|
||||
end
|
||||
return instance
|
||||
return scenario
|
||||
end
|
||||
|
||||
@@ -6,8 +6,9 @@ mutable struct Bus
|
||||
name::String
|
||||
offset::Int
|
||||
load::Vector{Float64}
|
||||
units::Vector
|
||||
thermal_units::Vector
|
||||
price_sensitive_loads::Vector
|
||||
profiled_units::Vector
|
||||
end
|
||||
|
||||
mutable struct CostSegment
|
||||
@@ -24,11 +25,11 @@ Base.@kwdef mutable struct Reserve
|
||||
name::String
|
||||
type::String
|
||||
amount::Vector{Float64}
|
||||
units::Vector
|
||||
thermal_units::Vector
|
||||
shortfall_penalty::Float64
|
||||
end
|
||||
|
||||
mutable struct Unit
|
||||
mutable struct ThermalUnit
|
||||
name::String
|
||||
bus::Bus
|
||||
max_power::Vector{Float64}
|
||||
@@ -46,6 +47,7 @@ mutable struct Unit
|
||||
initial_power::Union{Float64,Nothing}
|
||||
startup_categories::Vector{StartupCategory}
|
||||
reserves::Vector{Reserve}
|
||||
commitment_status::Vector{Union{Bool,Nothing}}
|
||||
end
|
||||
|
||||
mutable struct TransmissionLine
|
||||
@@ -63,7 +65,7 @@ end
|
||||
mutable struct Contingency
|
||||
name::String
|
||||
lines::Vector{TransmissionLine}
|
||||
units::Vector{Unit}
|
||||
thermal_units::Vector{ThermalUnit}
|
||||
end
|
||||
|
||||
mutable struct PriceSensitiveLoad
|
||||
@@ -73,35 +75,52 @@ mutable struct PriceSensitiveLoad
|
||||
revenue::Vector{Float64}
|
||||
end
|
||||
|
||||
Base.@kwdef mutable struct UnitCommitmentInstance
|
||||
mutable struct ProfiledUnit
|
||||
name::String
|
||||
bus::Bus
|
||||
min_power::Vector{Float64}
|
||||
max_power::Vector{Float64}
|
||||
cost::Vector{Float64}
|
||||
end
|
||||
|
||||
Base.@kwdef mutable struct UnitCommitmentScenario
|
||||
buses_by_name::Dict{AbstractString,Bus}
|
||||
buses::Vector{Bus}
|
||||
contingencies_by_name::Dict{AbstractString,Contingency}
|
||||
contingencies::Vector{Contingency}
|
||||
isf::Array{Float64,2}
|
||||
lines_by_name::Dict{AbstractString,TransmissionLine}
|
||||
lines::Vector{TransmissionLine}
|
||||
lodf::Array{Float64,2}
|
||||
name::String
|
||||
power_balance_penalty::Vector{Float64}
|
||||
price_sensitive_loads_by_name::Dict{AbstractString,PriceSensitiveLoad}
|
||||
price_sensitive_loads::Vector{PriceSensitiveLoad}
|
||||
reserves::Vector{Reserve}
|
||||
probability::Float64
|
||||
profiled_units_by_name::Dict{AbstractString,ProfiledUnit}
|
||||
profiled_units::Vector{ProfiledUnit}
|
||||
reserves_by_name::Dict{AbstractString,Reserve}
|
||||
shortfall_penalty::Vector{Float64}
|
||||
flexiramp_shortfall_penalty::Vector{Float64}
|
||||
reserves::Vector{Reserve}
|
||||
thermal_units_by_name::Dict{AbstractString,ThermalUnit}
|
||||
thermal_units::Vector{ThermalUnit}
|
||||
time::Int
|
||||
units_by_name::Dict{AbstractString,Unit}
|
||||
units::Vector{Unit}
|
||||
end
|
||||
|
||||
Base.@kwdef mutable struct UnitCommitmentInstance
|
||||
time::Int
|
||||
scenarios::Vector{UnitCommitmentScenario}
|
||||
end
|
||||
|
||||
function Base.show(io::IO, instance::UnitCommitmentInstance)
|
||||
sc = instance.scenarios[1]
|
||||
print(io, "UnitCommitmentInstance(")
|
||||
print(io, "$(length(instance.units)) units, ")
|
||||
print(io, "$(length(instance.buses)) buses, ")
|
||||
print(io, "$(length(instance.lines)) lines, ")
|
||||
print(io, "$(length(instance.contingencies)) contingencies, ")
|
||||
print(
|
||||
io,
|
||||
"$(length(instance.price_sensitive_loads)) price sensitive loads, ",
|
||||
)
|
||||
print(io, "$(length(instance.scenarios)) scenarios, ")
|
||||
print(io, "$(length(sc.thermal_units)) thermal units, ")
|
||||
print(io, "$(length(sc.profiled_units)) profiled units, ")
|
||||
print(io, "$(length(sc.buses)) buses, ")
|
||||
print(io, "$(length(sc.lines)) lines, ")
|
||||
print(io, "$(length(sc.contingencies)) contingencies, ")
|
||||
print(io, "$(length(sc.price_sensitive_loads)) price sensitive loads, ")
|
||||
print(io, "$(instance.time) time steps")
|
||||
print(io, ")")
|
||||
return
|
||||
|
||||
212
src/lmp/aelmp.jl
Normal file
212
src/lmp/aelmp.jl
Normal file
@@ -0,0 +1,212 @@
|
||||
# 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 JuMP
|
||||
|
||||
"""
|
||||
function compute_lmp(
|
||||
model::JuMP.Model,
|
||||
method::AELMP;
|
||||
optimizer,
|
||||
)::OrderedDict{Tuple{String,Int},Float64}
|
||||
|
||||
Calculates the approximate extended locational marginal prices of the given unit commitment instance.
|
||||
|
||||
The AELPM does the following three things:
|
||||
|
||||
1. It sets the minimum power output of each generator to zero
|
||||
2. It averages the start-up cost over the offer blocks for each generator
|
||||
3. It relaxes all integrality constraints
|
||||
|
||||
Returns a dictionary mapping `(bus_name, time)` to the marginal price.
|
||||
|
||||
WARNING: This approximation method is not fully developed. The implementation is based on MISO Phase I only.
|
||||
|
||||
1. It only supports Fast Start resources. More specifically, the minimum up/down time has to be zero.
|
||||
2. The method does NOT support time-varying start-up costs.
|
||||
3. An asset is considered offline if it is never on throughout all time periods.
|
||||
4. The method does NOT support multiple scenarios.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
|
||||
- `model`:
|
||||
the UnitCommitment model, must be solved before calling this function if offline participation is not allowed.
|
||||
|
||||
- `method`:
|
||||
the AELMP method.
|
||||
|
||||
- `optimizer`:
|
||||
the optimizer for solving the LP problem.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
```julia
|
||||
using UnitCommitment
|
||||
using HiGHS
|
||||
|
||||
import UnitCommitment: AELMP
|
||||
|
||||
# Read benchmark instance
|
||||
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
||||
|
||||
# Build the model
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
|
||||
# Optimize the model
|
||||
UnitCommitment.optimize!(model)
|
||||
|
||||
# Compute the AELMPs
|
||||
aelmp = UnitCommitment.compute_lmp(
|
||||
model,
|
||||
AELMP(
|
||||
allow_offline_participation = false,
|
||||
consider_startup_costs = true
|
||||
),
|
||||
optimizer = HiGHS.Optimizer
|
||||
)
|
||||
|
||||
# Access the AELMPs
|
||||
# Example: "s1" is the scenario name, "b1" is the bus name, 1 is the first time slot
|
||||
# Note: although scenario is supported, the query still keeps the scenario keys for consistency.
|
||||
@show aelmp["s1", "b1", 1]
|
||||
```
|
||||
"""
|
||||
function compute_lmp(
|
||||
model::JuMP.Model,
|
||||
method::AELMP;
|
||||
optimizer,
|
||||
)::OrderedDict{Tuple{String,String,Int},Float64}
|
||||
@info "Building the approximation model..."
|
||||
instance = deepcopy(model[:instance])
|
||||
_aelmp_check_parameters(instance, model, method)
|
||||
_modify_scenario!(instance.scenarios[1], model, method)
|
||||
|
||||
# prepare the result dictionary and solve the model
|
||||
elmp = OrderedDict()
|
||||
@info "Solving the approximation model."
|
||||
approx_model = build_model(instance = instance, variable_names = true)
|
||||
|
||||
# relax the binary constraint, and relax integrality
|
||||
for v in all_variables(approx_model)
|
||||
if is_binary(v)
|
||||
unset_binary(v)
|
||||
end
|
||||
end
|
||||
relax_integrality(approx_model)
|
||||
set_optimizer(approx_model, optimizer)
|
||||
|
||||
# solve the model
|
||||
set_silent(approx_model)
|
||||
optimize!(approx_model)
|
||||
|
||||
# access the dual values
|
||||
@info "Getting dual values (AELMPs)."
|
||||
for (key, val) in approx_model[:eq_net_injection]
|
||||
elmp[key] = dual(val)
|
||||
end
|
||||
return elmp
|
||||
end
|
||||
|
||||
function _aelmp_check_parameters(
|
||||
instance::UnitCommitmentInstance,
|
||||
model::JuMP.Model,
|
||||
method::AELMP,
|
||||
)
|
||||
# CHECK: model cannot have multiple scenarios
|
||||
if length(instance.scenarios) > 1
|
||||
error("The method does NOT support multiple scenarios.")
|
||||
end
|
||||
sc = instance.scenarios[1]
|
||||
# CHECK: model must be solved if allow_offline_participation=false
|
||||
if !method.allow_offline_participation
|
||||
if isnothing(model) || !has_values(model)
|
||||
error(
|
||||
"A solved UC model is required if allow_offline_participation=false.",
|
||||
)
|
||||
end
|
||||
end
|
||||
all_units = sc.thermal_units
|
||||
# CHECK: model cannot handle non-fast-starts (MISO Phase I: can ONLY solve fast-starts)
|
||||
if any(u -> u.min_uptime > 1 || u.min_downtime > 1, all_units)
|
||||
error(
|
||||
"The minimum up/down time of all generators must be 1. AELMP only supports fast-starts.",
|
||||
)
|
||||
end
|
||||
if any(u -> u.initial_power > 0, all_units)
|
||||
error("The initial power of all generators must be 0.")
|
||||
end
|
||||
if any(u -> u.initial_status >= 0, all_units)
|
||||
error("The initial status of all generators must be negative.")
|
||||
end
|
||||
# CHECK: model does not support startup costs (in time series)
|
||||
if any(u -> length(u.startup_categories) > 1, all_units)
|
||||
error("The method does NOT support time-varying start-up costs.")
|
||||
end
|
||||
end
|
||||
|
||||
function _modify_scenario!(
|
||||
sc::UnitCommitmentScenario,
|
||||
model::JuMP.Model,
|
||||
method::AELMP,
|
||||
)
|
||||
# this function modifies the sc units (generators)
|
||||
if !method.allow_offline_participation
|
||||
# 1. remove (if NOT allowing) the offline generators
|
||||
units_to_remove = []
|
||||
for unit in sc.thermal_units
|
||||
# remove based on the solved UC model result
|
||||
# remove the unit if it is never on
|
||||
if all(t -> value(model[:is_on][unit.name, t]) == 0, sc.time)
|
||||
# unregister from the bus
|
||||
filter!(x -> x.name != unit.name, unit.bus.thermal_units)
|
||||
# unregister from the reserve
|
||||
for r in unit.reserves
|
||||
filter!(x -> x.name != unit.name, r.thermal_units)
|
||||
end
|
||||
# append the name to the remove list
|
||||
push!(units_to_remove, unit.name)
|
||||
end
|
||||
end
|
||||
# unregister the units from the remove list
|
||||
filter!(x -> !(x.name in units_to_remove), sc.thermal_units)
|
||||
end
|
||||
|
||||
for unit in sc.thermal_units
|
||||
# 2. set min generation requirement to 0 by adding 0 to production curve and cost
|
||||
# min_power & min_costs are vectors with dimension T
|
||||
if unit.min_power[1] != 0
|
||||
first_cost_segment = unit.cost_segments[1]
|
||||
pushfirst!(
|
||||
unit.cost_segments,
|
||||
CostSegment(
|
||||
ones(size(first_cost_segment.mw)) * unit.min_power[1],
|
||||
ones(size(first_cost_segment.cost)) *
|
||||
unit.min_power_cost[1] / unit.min_power[1],
|
||||
),
|
||||
)
|
||||
unit.min_power = zeros(size(first_cost_segment.mw))
|
||||
unit.min_power_cost = zeros(size(first_cost_segment.cost))
|
||||
end
|
||||
|
||||
# 3. average the start-up costs (if considering)
|
||||
# if consider_startup_costs = false, then use the current first_startup_cost
|
||||
first_startup_cost = unit.startup_categories[1].cost
|
||||
if method.consider_startup_costs
|
||||
additional_unit_cost = first_startup_cost / unit.max_power[1]
|
||||
for i in eachindex(unit.cost_segments)
|
||||
unit.cost_segments[i].cost .+= additional_unit_cost
|
||||
end
|
||||
first_startup_cost = 0.0 # zero out the start up cost
|
||||
end
|
||||
unit.startup_categories =
|
||||
StartupCategory[StartupCategory(0, first_startup_cost)]
|
||||
end
|
||||
return sc.thermal_units_by_name =
|
||||
Dict(g.name => g for g in sc.thermal_units)
|
||||
end
|
||||
92
src/lmp/conventional.jl
Normal file
92
src/lmp/conventional.jl
Normal file
@@ -0,0 +1,92 @@
|
||||
# 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 JuMP
|
||||
|
||||
"""
|
||||
function compute_lmp(
|
||||
model::JuMP.Model,
|
||||
method::ConventionalLMP;
|
||||
optimizer,
|
||||
)::OrderedDict{Tuple{String,String,Int},Float64}
|
||||
|
||||
Calculates conventional locational marginal prices of the given unit commitment
|
||||
instance. Returns a dictionary mapping `(bus_name, time)` to the marginal price.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
|
||||
- `model`:
|
||||
the UnitCommitment model, must be solved before calling this function.
|
||||
|
||||
- `method`:
|
||||
the LMP method.
|
||||
|
||||
- `optimizer`:
|
||||
the optimizer for solving the LP problem.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
```julia
|
||||
using UnitCommitment
|
||||
using HiGHS
|
||||
|
||||
import UnitCommitment: ConventionalLMP
|
||||
|
||||
# Read benchmark instance
|
||||
instance = UnitCommitment.read_benchmark("matpower/case118/2018-01-01")
|
||||
|
||||
# Build the model
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
|
||||
# Optimize the model
|
||||
UnitCommitment.optimize!(model)
|
||||
|
||||
# Compute the LMPs using the conventional method
|
||||
lmp = UnitCommitment.compute_lmp(
|
||||
model,
|
||||
ConventionalLMP(),
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
|
||||
# Access the LMPs
|
||||
# Example: "s1" is the scenario name, "b1" is the bus name, 1 is the first time slot
|
||||
@show lmp["s1", "b1", 1]
|
||||
```
|
||||
"""
|
||||
function compute_lmp(
|
||||
model::JuMP.Model,
|
||||
::ConventionalLMP;
|
||||
optimizer,
|
||||
)::OrderedDict{Tuple{String,String,Int},Float64}
|
||||
if !has_values(model)
|
||||
error("The UC model must be solved before calculating the LMPs.")
|
||||
end
|
||||
lmp = OrderedDict()
|
||||
|
||||
@info "Fixing binary variables and relaxing integrality..."
|
||||
vals = Dict(v => value(v) for v in all_variables(model))
|
||||
for v in all_variables(model)
|
||||
if is_binary(v)
|
||||
unset_binary(v)
|
||||
fix(v, vals[v])
|
||||
end
|
||||
end
|
||||
relax_integrality(model)
|
||||
set_optimizer(model, optimizer)
|
||||
|
||||
@info "Solving the LP..."
|
||||
JuMP.optimize!(model)
|
||||
|
||||
@info "Getting dual values (LMPs)..."
|
||||
for (key, val) in model[:eq_net_injection]
|
||||
lmp[key] = dual(val)
|
||||
end
|
||||
|
||||
return lmp
|
||||
end
|
||||
28
src/lmp/structs.jl
Normal file
28
src/lmp/structs.jl
Normal file
@@ -0,0 +1,28 @@
|
||||
# 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.
|
||||
|
||||
abstract type PricingMethod end
|
||||
|
||||
struct ConventionalLMP <: PricingMethod end
|
||||
|
||||
"""
|
||||
struct AELMP <: PricingMethod
|
||||
allow_offline_participation::Bool = true
|
||||
consider_startup_costs::Bool = true
|
||||
end
|
||||
|
||||
Approximate Extended LMPs.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
|
||||
- `allow_offline_participation`:
|
||||
If true, offline assets are allowed to participate in pricing.
|
||||
- `consider_startup_costs`:
|
||||
If true, the start-up costs are averaged over each unit production; otherwise the production costs stay the same.
|
||||
"""
|
||||
Base.@kwdef struct AELMP <: PricingMethod
|
||||
allow_offline_participation::Bool = true
|
||||
consider_startup_costs::Bool = true
|
||||
end
|
||||
@@ -9,22 +9,59 @@ import JuMP: value, fix, set_name
|
||||
function build_model(;
|
||||
instance::UnitCommitmentInstance,
|
||||
optimizer = nothing,
|
||||
formulation = Formulation(),
|
||||
variable_names::Bool = false,
|
||||
)::JuMP.Model
|
||||
|
||||
Build the JuMP model corresponding to the given unit commitment instance.
|
||||
|
||||
Arguments
|
||||
=========
|
||||
---------
|
||||
|
||||
- `instance`:
|
||||
the instance.
|
||||
- `optimizer`:
|
||||
the optimizer factory that should be attached to this model (e.g. Cbc.Optimizer).
|
||||
If not provided, no optimizer will be attached.
|
||||
- `formulation`:
|
||||
the MIP formulation to use. By default, uses a formulation that combines
|
||||
modeling components from different publications that provides good
|
||||
performance across a wide variety of instances. An alternative formulation
|
||||
may also be provided.
|
||||
- `variable_names`:
|
||||
If true, set variable and constraint names. Important if the model is going
|
||||
if true, set variable and constraint names. Important if the model is going
|
||||
to be exported to an MPS file. For large models, this can take significant
|
||||
time, so it's disabled by default.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
```julia
|
||||
# Read benchmark instance
|
||||
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
||||
|
||||
# Construct model (using state-of-the-art defaults)
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = Cbc.Optimizer,
|
||||
)
|
||||
|
||||
# Construct model (using customized formulation)
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = Cbc.Optimizer,
|
||||
formulation = Formulation(
|
||||
pwl_costs = KnuOstWat2018.PwlCosts(),
|
||||
ramping = MorLatRam2013.Ramping(),
|
||||
startup_costs = MorLatRam2013.StartupCosts(),
|
||||
transmission = ShiftFactorsFormulation(
|
||||
isf_cutoff = 0.005,
|
||||
lodf_cutoff = 0.001,
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
"""
|
||||
function build_model(;
|
||||
instance::UnitCommitmentInstance,
|
||||
@@ -40,20 +77,30 @@ function build_model(;
|
||||
end
|
||||
model[:obj] = AffExpr()
|
||||
model[:instance] = instance
|
||||
_setup_transmission(model, formulation.transmission)
|
||||
for l in instance.lines
|
||||
_add_transmission_line!(model, l, formulation.transmission)
|
||||
for g in instance.scenarios[1].thermal_units
|
||||
_add_unit_commitment!(model, g, formulation)
|
||||
end
|
||||
for b in instance.buses
|
||||
_add_bus!(model, b)
|
||||
for sc in instance.scenarios
|
||||
@info "Building scenario $(sc.name) with " *
|
||||
"probability $(sc.probability)"
|
||||
_setup_transmission(formulation.transmission, sc)
|
||||
for l in sc.lines
|
||||
_add_transmission_line!(model, l, formulation.transmission, sc)
|
||||
end
|
||||
for b in sc.buses
|
||||
_add_bus!(model, b, sc)
|
||||
end
|
||||
for ps in sc.price_sensitive_loads
|
||||
_add_price_sensitive_load!(model, ps, sc)
|
||||
end
|
||||
for g in sc.thermal_units
|
||||
_add_unit_dispatch!(model, g, formulation, sc)
|
||||
end
|
||||
for pu in sc.profiled_units
|
||||
_add_profiled_unit!(model, pu, sc)
|
||||
end
|
||||
_add_system_wide_eqs!(model, sc)
|
||||
end
|
||||
for g in instance.units
|
||||
_add_unit!(model, g, formulation)
|
||||
end
|
||||
for ps in instance.price_sensitive_loads
|
||||
_add_price_sensitive_load!(model, ps)
|
||||
end
|
||||
_add_system_wide_eqs!(model)
|
||||
@objective(model, Min, model[:obj])
|
||||
end
|
||||
@info @sprintf("Built model in %.2f seconds", time_model)
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
function _add_ramp_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
formulation_ramping::ArrCon2000.Ramping,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
# TODO: Move upper case constants to model[:instance]
|
||||
RESERVES_WHEN_START_UP = true
|
||||
@@ -22,7 +23,7 @@ function _add_ramp_eqs!(
|
||||
eq_ramp_down = _init(model, :eq_ramp_down)
|
||||
eq_ramp_up = _init(model, :eq_ramp_up)
|
||||
is_initially_on = (g.initial_status > 0)
|
||||
reserve = _total_reserves(model, g)
|
||||
reserve = _total_reserves(model, g, sc)
|
||||
|
||||
# Gar1962.ProdVars
|
||||
prod_above = model[:prod_above]
|
||||
@@ -37,10 +38,10 @@ function _add_ramp_eqs!(
|
||||
if t == 1
|
||||
if is_initially_on
|
||||
# min power is _not_ multiplied by is_on because if !is_on, then ramp up is irrelevant
|
||||
eq_ramp_up[gn, t] = @constraint(
|
||||
eq_ramp_up[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
g.min_power[t] +
|
||||
prod_above[gn, t] +
|
||||
prod_above[sc.name, gn, t] +
|
||||
(RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) <=
|
||||
g.initial_power + RU
|
||||
)
|
||||
@@ -48,16 +49,16 @@ function _add_ramp_eqs!(
|
||||
else
|
||||
max_prod_this_period =
|
||||
g.min_power[t] * is_on[gn, t] +
|
||||
prod_above[gn, t] +
|
||||
prod_above[sc.name, gn, t] +
|
||||
(
|
||||
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ?
|
||||
reserve[t] : 0.0
|
||||
)
|
||||
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[sc.name, gn, t-1]
|
||||
|
||||
# Equation (24) in Kneuven et al. (2020)
|
||||
eq_ramp_up[gn, t] = @constraint(
|
||||
eq_ramp_up[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
max_prod_this_period - min_prod_last_period <=
|
||||
RU * is_on[gn, t-1] + SU * switch_on[gn, t]
|
||||
@@ -71,24 +72,25 @@ function _add_ramp_eqs!(
|
||||
# min_power + RD < initial_power < SD
|
||||
# then the generator should be able to shut down at time t = 1,
|
||||
# but the constraint below will force the unit to produce power
|
||||
eq_ramp_down[gn, t] = @constraint(
|
||||
eq_ramp_down[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
g.initial_power - (g.min_power[t] + prod_above[gn, t]) <= RD
|
||||
g.initial_power -
|
||||
(g.min_power[t] + prod_above[sc.name, gn, t]) <= RD
|
||||
)
|
||||
end
|
||||
else
|
||||
max_prod_last_period =
|
||||
g.min_power[t-1] * is_on[gn, t-1] +
|
||||
prod_above[gn, t-1] +
|
||||
prod_above[sc.name, gn, t-1] +
|
||||
(
|
||||
RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ?
|
||||
reserve[t-1] : 0.0
|
||||
)
|
||||
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[sc.name, gn, t]
|
||||
|
||||
# Equation (25) in Kneuven et al. (2020)
|
||||
eq_ramp_down[gn, t] = @constraint(
|
||||
eq_ramp_down[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
max_prod_last_period - min_prod_this_period <=
|
||||
RD * is_on[gn, t] + SD * switch_off[gn, t]
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
function _add_production_piecewise_linear_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
formulation_pwl_costs::CarArr2006.PwlCosts,
|
||||
formulation_status_vars::StatusVarsFormulation,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
eq_prod_above_def = _init(model, :eq_prod_above_def)
|
||||
eq_segprod_limit = _init(model, :eq_segprod_limit)
|
||||
@@ -26,28 +27,32 @@ function _add_production_piecewise_linear_eqs!(
|
||||
# difference between max power for segments k and k-1 so the
|
||||
# value of cost_segments[k].mw[t] is the max production *for
|
||||
# that segment*
|
||||
eq_segprod_limit[gn, t, k] = @constraint(
|
||||
eq_segprod_limit[sc.name, gn, t, k] = @constraint(
|
||||
model,
|
||||
segprod[gn, t, k] <= g.cost_segments[k].mw[t]
|
||||
segprod[sc.name, gn, t, k] <= g.cost_segments[k].mw[t]
|
||||
)
|
||||
|
||||
# Also add this as an explicit upper bound on segprod to make the
|
||||
# solver's work a bit easier
|
||||
set_upper_bound(segprod[gn, t, k], g.cost_segments[k].mw[t])
|
||||
set_upper_bound(
|
||||
segprod[sc.name, gn, t, k],
|
||||
g.cost_segments[k].mw[t],
|
||||
)
|
||||
|
||||
# Definition of production
|
||||
# Equation (43) in Kneuven et al. (2020)
|
||||
eq_prod_above_def[gn, t] = @constraint(
|
||||
eq_prod_above_def[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
prod_above[gn, t] == sum(segprod[gn, t, k] for k in 1:K)
|
||||
prod_above[sc.name, gn, t] ==
|
||||
sum(segprod[sc.name, gn, t, k] for k in 1:K)
|
||||
)
|
||||
|
||||
# Objective function
|
||||
# Equation (44) in Kneuven et al. (2020)
|
||||
add_to_expression!(
|
||||
model[:obj],
|
||||
segprod[gn, t, k],
|
||||
g.cost_segments[k].cost[t],
|
||||
segprod[sc.name, gn, t, k],
|
||||
sc.probability * g.cost_segments[k].cost[t],
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
function _add_ramp_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
formulation_ramping::DamKucRajAta2016.Ramping,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
# TODO: Move upper case constants to model[:instance]
|
||||
RESERVES_WHEN_START_UP = true
|
||||
@@ -23,7 +24,7 @@ function _add_ramp_eqs!(
|
||||
gn = g.name
|
||||
eq_str_ramp_down = _init(model, :eq_str_ramp_down)
|
||||
eq_str_ramp_up = _init(model, :eq_str_ramp_up)
|
||||
reserve = _total_reserves(model, g)
|
||||
reserve = _total_reserves(model, g, sc)
|
||||
|
||||
# Gar1962.ProdVars
|
||||
prod_above = model[:prod_above]
|
||||
@@ -48,15 +49,15 @@ function _add_ramp_eqs!(
|
||||
# end
|
||||
|
||||
max_prod_this_period =
|
||||
prod_above[gn, t] +
|
||||
prod_above[sc.name, gn, t] +
|
||||
(RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0)
|
||||
min_prod_last_period = 0.0
|
||||
if t > 1 && time_invariant
|
||||
min_prod_last_period = prod_above[gn, t-1]
|
||||
min_prod_last_period = prod_above[sc.name, gn, t-1]
|
||||
|
||||
# Equation (35) in Kneuven et al. (2020)
|
||||
# Sparser version of (24)
|
||||
eq_str_ramp_up[gn, t] = @constraint(
|
||||
eq_str_ramp_up[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
max_prod_this_period - min_prod_last_period <=
|
||||
(SU - g.min_power[t] - RU) * switch_on[gn, t] +
|
||||
@@ -65,7 +66,8 @@ function _add_ramp_eqs!(
|
||||
elseif (t == 1 && is_initially_on) || (t > 1 && !time_invariant)
|
||||
if t > 1
|
||||
min_prod_last_period =
|
||||
prod_above[gn, t-1] + g.min_power[t-1] * is_on[gn, t-1]
|
||||
prod_above[sc.name, gn, t-1] +
|
||||
g.min_power[t-1] * is_on[gn, t-1]
|
||||
else
|
||||
min_prod_last_period = max(g.initial_power, 0.0)
|
||||
end
|
||||
@@ -76,7 +78,7 @@ function _add_ramp_eqs!(
|
||||
|
||||
# Modified version of equation (35) in Kneuven et al. (2020)
|
||||
# Equivalent to (24)
|
||||
eq_str_ramp_up[gn, t] = @constraint(
|
||||
eq_str_ramp_up[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
max_prod_this_period - min_prod_last_period <=
|
||||
(SU - RU) * switch_on[gn, t] + RU * is_on[gn, t]
|
||||
@@ -88,7 +90,7 @@ function _add_ramp_eqs!(
|
||||
t > 1 && (RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN) ?
|
||||
reserve[t-1] : 0.0
|
||||
)
|
||||
min_prod_this_period = prod_above[gn, t]
|
||||
min_prod_this_period = prod_above[sc.name, gn, t]
|
||||
on_last_period = 0.0
|
||||
if t > 1
|
||||
on_last_period = is_on[gn, t-1]
|
||||
@@ -98,7 +100,7 @@ function _add_ramp_eqs!(
|
||||
|
||||
if t > 1 && time_invariant
|
||||
# Equation (36) in Kneuven et al. (2020)
|
||||
eq_str_ramp_down[gn, t] = @constraint(
|
||||
eq_str_ramp_down[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
max_prod_last_period - min_prod_this_period <=
|
||||
(SD - g.min_power[t] - RD) * switch_off[gn, t] +
|
||||
@@ -110,7 +112,7 @@ function _add_ramp_eqs!(
|
||||
|
||||
# Modified version of equation (36) in Kneuven et al. (2020)
|
||||
# Equivalent to (25)
|
||||
eq_str_ramp_down[gn, t] = @constraint(
|
||||
eq_str_ramp_down[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
max_prod_last_period - min_prod_this_period <=
|
||||
(SD - RD) * switch_off[gn, t] + RD * on_last_period
|
||||
|
||||
@@ -4,34 +4,35 @@
|
||||
|
||||
function _add_production_vars!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
prod_above = _init(model, :prod_above)
|
||||
segprod = _init(model, :segprod)
|
||||
for t in 1:model[:instance].time
|
||||
for k in 1:length(g.cost_segments)
|
||||
segprod[g.name, t, k] = @variable(model, lower_bound = 0)
|
||||
segprod[sc.name, g.name, t, k] = @variable(model, lower_bound = 0)
|
||||
end
|
||||
prod_above[g.name, t] = @variable(model, lower_bound = 0)
|
||||
prod_above[sc.name, g.name, t] = @variable(model, lower_bound = 0)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
function _add_production_limit_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
eq_prod_limit = _init(model, :eq_prod_limit)
|
||||
is_on = model[:is_on]
|
||||
prod_above = model[:prod_above]
|
||||
reserve = _total_reserves(model, g)
|
||||
reserve = _total_reserves(model, g, sc)
|
||||
gn = g.name
|
||||
for t in 1:model[:instance].time
|
||||
# Objective function terms for production costs
|
||||
# Part of (69) of Kneuven et al. (2020) as C^R_g * u_g(t) term
|
||||
add_to_expression!(model[:obj], is_on[gn, t], g.min_power_cost[t])
|
||||
|
||||
# Production limit
|
||||
# Equation (18) in Kneuven et al. (2020)
|
||||
@@ -42,9 +43,10 @@ function _add_production_limit_eqs!(
|
||||
if power_diff < 1e-7
|
||||
power_diff = 0.0
|
||||
end
|
||||
eq_prod_limit[gn, t] = @constraint(
|
||||
eq_prod_limit[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
prod_above[gn, t] + reserve[t] <= power_diff * is_on[gn, t]
|
||||
prod_above[sc.name, gn, t] + reserve[t] <=
|
||||
power_diff * is_on[gn, t]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
function _add_production_piecewise_linear_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
formulation_pwl_costs::Gar1962.PwlCosts,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
eq_prod_above_def = _init(model, :eq_prod_above_def)
|
||||
eq_segprod_limit = _init(model, :eq_segprod_limit)
|
||||
@@ -24,9 +25,10 @@ function _add_production_piecewise_linear_eqs!(
|
||||
for t in 1:model[:instance].time
|
||||
# Definition of production
|
||||
# Equation (43) in Kneuven et al. (2020)
|
||||
eq_prod_above_def[gn, t] = @constraint(
|
||||
eq_prod_above_def[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
prod_above[gn, t] == sum(segprod[gn, t, k] for k in 1:K)
|
||||
prod_above[sc.name, gn, t] ==
|
||||
sum(segprod[sc.name, gn, t, k] for k in 1:K)
|
||||
)
|
||||
|
||||
for k in 1:K
|
||||
@@ -37,21 +39,25 @@ function _add_production_piecewise_linear_eqs!(
|
||||
# difference between max power for segments k and k-1 so the
|
||||
# value of cost_segments[k].mw[t] is the max production *for
|
||||
# that segment*
|
||||
eq_segprod_limit[gn, t, k] = @constraint(
|
||||
eq_segprod_limit[sc.name, gn, t, k] = @constraint(
|
||||
model,
|
||||
segprod[gn, t, k] <= g.cost_segments[k].mw[t] * is_on[gn, t]
|
||||
segprod[sc.name, gn, t, k] <=
|
||||
g.cost_segments[k].mw[t] * is_on[gn, t]
|
||||
)
|
||||
|
||||
# Also add this as an explicit upper bound on segprod to make the
|
||||
# solver's work a bit easier
|
||||
set_upper_bound(segprod[gn, t, k], g.cost_segments[k].mw[t])
|
||||
set_upper_bound(
|
||||
segprod[sc.name, gn, t, k],
|
||||
g.cost_segments[k].mw[t],
|
||||
)
|
||||
|
||||
# Objective function
|
||||
# Equation (44) in Kneuven et al. (2020)
|
||||
add_to_expression!(
|
||||
model[:obj],
|
||||
segprod[gn, t, k],
|
||||
g.cost_segments[k].cost[t],
|
||||
segprod[sc.name, gn, t, k],
|
||||
sc.probability * g.cost_segments[k].cost[t],
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
function _add_status_vars!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
)::Nothing
|
||||
is_on = _init(model, :is_on)
|
||||
@@ -20,13 +20,14 @@ function _add_status_vars!(
|
||||
switch_on[g.name, t] = @variable(model, binary = true)
|
||||
switch_off[g.name, t] = @variable(model, binary = true)
|
||||
end
|
||||
add_to_expression!(model[:obj], is_on[g.name, t], g.min_power_cost[t])
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
function _add_status_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
)::Nothing
|
||||
eq_binary_link = _init(model, :eq_binary_link)
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
function _add_production_piecewise_linear_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
formulation_pwl_costs::KnuOstWat2018.PwlCosts,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
eq_prod_above_def = _init(model, :eq_prod_above_def)
|
||||
eq_segprod_limit_a = _init(model, :eq_segprod_limit_a)
|
||||
@@ -58,27 +59,27 @@ function _add_production_piecewise_linear_eqs!(
|
||||
|
||||
if g.min_uptime > 1
|
||||
# Equation (46) in Kneuven et al. (2020)
|
||||
eq_segprod_limit_a[gn, t, k] = @constraint(
|
||||
eq_segprod_limit_a[sc.name, gn, t, k] = @constraint(
|
||||
model,
|
||||
segprod[gn, t, k] <=
|
||||
segprod[sc.name, gn, t, k] <=
|
||||
g.cost_segments[k].mw[t] * is_on[gn, t] -
|
||||
Cv * switch_on[gn, t] -
|
||||
(t < T ? Cw * switch_off[gn, t+1] : 0.0)
|
||||
)
|
||||
else
|
||||
# Equation (47a)/(48a) in Kneuven et al. (2020)
|
||||
eq_segprod_limit_b[gn, t, k] = @constraint(
|
||||
eq_segprod_limit_b[sc.name, gn, t, k] = @constraint(
|
||||
model,
|
||||
segprod[gn, t, k] <=
|
||||
segprod[sc.name, gn, t, k] <=
|
||||
g.cost_segments[k].mw[t] * is_on[gn, t] -
|
||||
Cv * switch_on[gn, t] -
|
||||
(t < T ? max(0, Cv - Cw) * switch_off[gn, t+1] : 0.0)
|
||||
)
|
||||
|
||||
# Equation (47b)/(48b) in Kneuven et al. (2020)
|
||||
eq_segprod_limit_c[gn, t, k] = @constraint(
|
||||
eq_segprod_limit_c[sc.name, gn, t, k] = @constraint(
|
||||
model,
|
||||
segprod[gn, t, k] <=
|
||||
segprod[sc.name, gn, t, k] <=
|
||||
g.cost_segments[k].mw[t] * is_on[gn, t] -
|
||||
max(0, Cw - Cv) * switch_on[gn, t] -
|
||||
(t < T ? Cw * switch_off[gn, t+1] : 0.0)
|
||||
@@ -87,22 +88,26 @@ function _add_production_piecewise_linear_eqs!(
|
||||
|
||||
# Definition of production
|
||||
# Equation (43) in Kneuven et al. (2020)
|
||||
eq_prod_above_def[gn, t] = @constraint(
|
||||
eq_prod_above_def[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
prod_above[gn, t] == sum(segprod[gn, t, k] for k in 1:K)
|
||||
prod_above[sc.name, gn, t] ==
|
||||
sum(segprod[sc.name, gn, t, k] for k in 1:K)
|
||||
)
|
||||
|
||||
# Objective function
|
||||
# Equation (44) in Kneuven et al. (2020)
|
||||
add_to_expression!(
|
||||
model[:obj],
|
||||
segprod[gn, t, k],
|
||||
segprod[sc.name, gn, t, k],
|
||||
g.cost_segments[k].cost[t],
|
||||
)
|
||||
|
||||
# Also add an explicit upper bound on segprod to make the solver's
|
||||
# work a bit easier
|
||||
set_upper_bound(segprod[gn, t, k], g.cost_segments[k].mw[t])
|
||||
set_upper_bound(
|
||||
segprod[sc.name, gn, t, k],
|
||||
g.cost_segments[k].mw[t],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
function _add_ramp_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
formulation_ramping::MorLatRam2013.Ramping,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
# TODO: Move upper case constants to model[:instance]
|
||||
RESERVES_WHEN_START_UP = true
|
||||
@@ -22,7 +23,7 @@ function _add_ramp_eqs!(
|
||||
gn = g.name
|
||||
eq_ramp_down = _init(model, :eq_ramp_down)
|
||||
eq_ramp_up = _init(model, :eq_str_ramp_up)
|
||||
reserve = _total_reserves(model, g)
|
||||
reserve = _total_reserves(model, g, sc)
|
||||
|
||||
# Gar1962.ProdVars
|
||||
prod_above = model[:prod_above]
|
||||
@@ -39,10 +40,10 @@ function _add_ramp_eqs!(
|
||||
# Ramp up limit
|
||||
if t == 1
|
||||
if is_initially_on
|
||||
eq_ramp_up[gn, t] = @constraint(
|
||||
eq_ramp_up[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
g.min_power[t] +
|
||||
prod_above[gn, t] +
|
||||
prod_above[sc.name, gn, t] +
|
||||
(RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) <=
|
||||
g.initial_power + RU
|
||||
)
|
||||
@@ -58,13 +59,14 @@ function _add_ramp_eqs!(
|
||||
SU = g.startup_limit
|
||||
max_prod_this_period =
|
||||
g.min_power[t] * is_on[gn, t] +
|
||||
prod_above[gn, t] +
|
||||
prod_above[sc.name, gn, t] +
|
||||
(
|
||||
RESERVES_WHEN_START_UP || RESERVES_WHEN_RAMP_UP ?
|
||||
reserve[t] : 0.0
|
||||
)
|
||||
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[sc.name, gn, t-1]
|
||||
eq_ramp_up[gn, t] = @constraint(
|
||||
model,
|
||||
max_prod_this_period - min_prod_last_period <=
|
||||
@@ -74,11 +76,11 @@ function _add_ramp_eqs!(
|
||||
# Equation (26) in Kneuven et al. (2020)
|
||||
# TODO: what if RU < SU? places too stringent upper bound
|
||||
# prod_above[gn, t] when starting up, and creates diff with (24).
|
||||
eq_ramp_up[gn, t] = @constraint(
|
||||
eq_ramp_up[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
prod_above[gn, t] +
|
||||
prod_above[sc.name, gn, t] +
|
||||
(RESERVES_WHEN_RAMP_UP ? reserve[t] : 0.0) -
|
||||
prod_above[gn, t-1] <= RU
|
||||
prod_above[sc.name, gn, t-1] <= RU
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -90,9 +92,10 @@ function _add_ramp_eqs!(
|
||||
# min_power + RD < initial_power < SD
|
||||
# then the generator should be able to shut down at time t = 1,
|
||||
# but the constraint below will force the unit to produce power
|
||||
eq_ramp_down[gn, t] = @constraint(
|
||||
eq_ramp_down[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
g.initial_power - (g.min_power[t] + prod_above[gn, t]) <= RD
|
||||
g.initial_power -
|
||||
(g.min_power[t] + prod_above[sc.name, gn, t]) <= RD
|
||||
)
|
||||
end
|
||||
else
|
||||
@@ -102,13 +105,13 @@ function _add_ramp_eqs!(
|
||||
SD = g.shutdown_limit
|
||||
max_prod_last_period =
|
||||
g.min_power[t-1] * is_on[gn, t-1] +
|
||||
prod_above[gn, t-1] +
|
||||
prod_above[sc.name, gn, t-1] +
|
||||
(
|
||||
RESERVES_WHEN_SHUT_DOWN || RESERVES_WHEN_RAMP_DOWN ?
|
||||
reserve[t-1] : 0.0
|
||||
)
|
||||
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[sc.name, gn, t]
|
||||
eq_ramp_down[gn, t] = @constraint(
|
||||
model,
|
||||
max_prod_last_period - min_prod_this_period <=
|
||||
@@ -118,11 +121,11 @@ function _add_ramp_eqs!(
|
||||
# Equation (27) in Kneuven et al. (2020)
|
||||
# TODO: Similar to above, what to do if shutting down in time t
|
||||
# and RD < SD? There is a difference with (25).
|
||||
eq_ramp_down[gn, t] = @constraint(
|
||||
eq_ramp_down[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
prod_above[gn, t-1] +
|
||||
prod_above[sc.name, gn, t-1] +
|
||||
(RESERVES_WHEN_RAMP_DOWN ? reserve[t-1] : 0.0) -
|
||||
prod_above[gn, t] <= RD
|
||||
prod_above[sc.name, gn, t] <= RD
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
function _add_startup_cost_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation::MorLatRam2013.StartupCosts,
|
||||
)::Nothing
|
||||
eq_startup_choose = _init(model, :eq_startup_choose)
|
||||
|
||||
@@ -4,15 +4,16 @@
|
||||
|
||||
function _add_ramp_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation_prod_vars::Gar1962.ProdVars,
|
||||
formulation_ramping::PanGua2016.Ramping,
|
||||
formulation_status_vars::Gar1962.StatusVars,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
# TODO: Move upper case constants to model[:instance]
|
||||
RESERVES_WHEN_SHUT_DOWN = true
|
||||
gn = g.name
|
||||
reserve = _total_reserves(model, g)
|
||||
reserve = _total_reserves(model, g, sc)
|
||||
eq_str_prod_limit = _init(model, :eq_str_prod_limit)
|
||||
eq_prod_limit_ramp_up_extra_period =
|
||||
_init(model, :eq_prod_limit_ramp_up_extra_period)
|
||||
@@ -52,9 +53,9 @@ function _add_ramp_eqs!(
|
||||
# Generalization of (20)
|
||||
# Necessary that if any of the switch_on = 1 in the sum,
|
||||
# then switch_off[gn, t+1] = 0
|
||||
eq_str_prod_limit[gn, t] = @constraint(
|
||||
eq_str_prod_limit[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
prod_above[gn, t] +
|
||||
prod_above[sc.name, gn, t] +
|
||||
g.min_power[t] * is_on[gn, t] +
|
||||
reserve[t] <=
|
||||
Pbar * is_on[gn, t] -
|
||||
@@ -67,16 +68,17 @@ function _add_ramp_eqs!(
|
||||
if UT - 2 < TRU
|
||||
# Equation (40) in Kneuven et al. (2020)
|
||||
# Covers an additional time period of the ramp-up trajectory, compared to (38)
|
||||
eq_prod_limit_ramp_up_extra_period[gn, t] = @constraint(
|
||||
model,
|
||||
prod_above[gn, t] +
|
||||
g.min_power[t] * is_on[gn, t] +
|
||||
reserve[t] <=
|
||||
Pbar * is_on[gn, t] - sum(
|
||||
(Pbar - (SU + i * RU)) * switch_on[gn, t-i] for
|
||||
i in 0:min(UT - 1, TRU, t - 1)
|
||||
eq_prod_limit_ramp_up_extra_period[sc.name, gn, t] =
|
||||
@constraint(
|
||||
model,
|
||||
prod_above[sc.name, gn, t] +
|
||||
g.min_power[t] * is_on[gn, t] +
|
||||
reserve[t] <=
|
||||
Pbar * is_on[gn, t] - sum(
|
||||
(Pbar - (SU + i * RU)) * switch_on[gn, t-i] for
|
||||
i in 0:min(UT - 1, TRU, t - 1)
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
# Add in shutdown trajectory if KSD >= 0 (else this is dominated by (38))
|
||||
@@ -84,9 +86,9 @@ function _add_ramp_eqs!(
|
||||
if KSD > 0
|
||||
KSU = min(TRU, UT - 2 - KSD, t - 1)
|
||||
# Equation (41) in Kneuven et al. (2020)
|
||||
eq_prod_limit_shutdown_trajectory[gn, t] = @constraint(
|
||||
eq_prod_limit_shutdown_trajectory[sc.name, gn, t] = @constraint(
|
||||
model,
|
||||
prod_above[gn, t] +
|
||||
prod_above[sc.name, gn, t] +
|
||||
g.min_power[t] * is_on[gn, t] +
|
||||
(RESERVES_WHEN_SHUT_DOWN ? reserve[t] : 0.0) <=
|
||||
Pbar * is_on[gn, t] - sum(
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
function _add_ramp_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
::Gar1962.ProdVars,
|
||||
::WanHob2016.Ramping,
|
||||
::Gar1962.StatusVars,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
is_initially_on = (g.initial_status > 0)
|
||||
SU = g.startup_limit
|
||||
@@ -38,41 +39,43 @@ function _add_ramp_eqs!(
|
||||
for t in 1:model[:instance].time
|
||||
@constraint(
|
||||
model,
|
||||
prod_above[gn, t] + (is_on[gn, t] * minp[t]) <= mfg[rn, gn, t]
|
||||
prod_above[sc.name, gn, t] + (is_on[gn, t] * minp[t]) <=
|
||||
mfg[sc.name, gn, t]
|
||||
) # Eq. (19) in Wang & Hobbs (2016)
|
||||
@constraint(model, mfg[rn, gn, t] <= is_on[gn, t] * maxp[t]) # Eq. (22) in Wang & Hobbs (2016)
|
||||
@constraint(model, mfg[sc.name, gn, t] <= is_on[gn, t] * maxp[t]) # Eq. (22) in Wang & Hobbs (2016)
|
||||
if t != model[:instance].time
|
||||
@constraint(
|
||||
model,
|
||||
minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <=
|
||||
prod_above[gn, t] - dwflexiramp[rn, gn, t] +
|
||||
(is_on[gn, t] * minp[t])
|
||||
prod_above[sc.name, gn, t] -
|
||||
dwflexiramp[sc.name, rn, gn, t] + (is_on[gn, t] * minp[t])
|
||||
) # first inequality of Eq. (20) in Wang & Hobbs (2016)
|
||||
@constraint(
|
||||
model,
|
||||
prod_above[gn, t] - dwflexiramp[rn, gn, t] +
|
||||
prod_above[sc.name, gn, t] -
|
||||
dwflexiramp[sc.name, rn, gn, t] +
|
||||
(is_on[gn, t] * minp[t]) <=
|
||||
mfg[rn, gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
|
||||
mfg[sc.name, gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
|
||||
) # second inequality of Eq. (20) in Wang & Hobbs (2016)
|
||||
@constraint(
|
||||
model,
|
||||
minp[t] * (is_on[gn, t+1] + is_on[gn, t] - 1) <=
|
||||
prod_above[gn, t] +
|
||||
upflexiramp[rn, gn, t] +
|
||||
prod_above[sc.name, gn, t] +
|
||||
upflexiramp[sc.name, rn, gn, t] +
|
||||
(is_on[gn, t] * minp[t])
|
||||
) # first inequality of Eq. (21) in Wang & Hobbs (2016)
|
||||
@constraint(
|
||||
model,
|
||||
prod_above[gn, t] +
|
||||
upflexiramp[rn, gn, t] +
|
||||
prod_above[sc.name, gn, t] +
|
||||
upflexiramp[sc.name, rn, gn, t] +
|
||||
(is_on[gn, t] * minp[t]) <=
|
||||
mfg[rn, gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
|
||||
mfg[sc.name, gn, t+1] + (maxp[t] * (1 - is_on[gn, t+1]))
|
||||
) # second inequality of Eq. (21) in Wang & Hobbs (2016)
|
||||
if t != 1
|
||||
@constraint(
|
||||
model,
|
||||
mfg[rn, gn, t] <=
|
||||
prod_above[gn, t-1] +
|
||||
mfg[sc.name, gn, t] <=
|
||||
prod_above[sc.name, gn, t-1] +
|
||||
(is_on[gn, t-1] * minp[t]) +
|
||||
(RU * is_on[gn, t-1]) +
|
||||
(SU * (is_on[gn, t] - is_on[gn, t-1])) +
|
||||
@@ -80,8 +83,13 @@ function _add_ramp_eqs!(
|
||||
) # Eq. (23) in Wang & Hobbs (2016)
|
||||
@constraint(
|
||||
model,
|
||||
(prod_above[gn, t-1] + (is_on[gn, t-1] * minp[t])) -
|
||||
(prod_above[gn, t] + (is_on[gn, t] * minp[t])) <=
|
||||
(
|
||||
prod_above[sc.name, gn, t-1] +
|
||||
(is_on[gn, t-1] * minp[t])
|
||||
) - (
|
||||
prod_above[sc.name, gn, t] +
|
||||
(is_on[gn, t] * minp[t])
|
||||
) <=
|
||||
RD * is_on[gn, t] +
|
||||
SD * (is_on[gn, t-1] - is_on[gn, t]) +
|
||||
maxp[t] * (1 - is_on[gn, t-1])
|
||||
@@ -89,7 +97,7 @@ function _add_ramp_eqs!(
|
||||
else
|
||||
@constraint(
|
||||
model,
|
||||
mfg[rn, gn, t] <=
|
||||
mfg[sc.name, gn, t] <=
|
||||
initial_power +
|
||||
(RU * is_initially_on) +
|
||||
(SU * (is_on[gn, t] - is_initially_on)) +
|
||||
@@ -97,8 +105,10 @@ function _add_ramp_eqs!(
|
||||
) # Eq. (23) in Wang & Hobbs (2016) for the first time period
|
||||
@constraint(
|
||||
model,
|
||||
initial_power -
|
||||
(prod_above[gn, t] + (is_on[gn, t] * minp[t])) <=
|
||||
initial_power - (
|
||||
prod_above[sc.name, gn, t] +
|
||||
(is_on[gn, t] * minp[t])
|
||||
) <=
|
||||
RD * is_on[gn, t] +
|
||||
SD * (is_initially_on - is_on[gn, t]) +
|
||||
maxp[t] * (1 - is_initially_on)
|
||||
@@ -106,7 +116,7 @@ function _add_ramp_eqs!(
|
||||
end
|
||||
@constraint(
|
||||
model,
|
||||
mfg[rn, gn, t] <=
|
||||
mfg[sc.name, gn, t] <=
|
||||
(SD * (is_on[gn, t] - is_on[gn, t+1])) +
|
||||
(maxp[t] * is_on[gn, t+1])
|
||||
) # Eq. (24) in Wang & Hobbs (2016)
|
||||
@@ -114,11 +124,12 @@ function _add_ramp_eqs!(
|
||||
model,
|
||||
-RD * is_on[gn, t+1] -
|
||||
SD * (is_on[gn, t] - is_on[gn, t+1]) -
|
||||
maxp[t] * (1 - is_on[gn, t]) <= upflexiramp[rn, gn, t]
|
||||
maxp[t] * (1 - is_on[gn, t]) <=
|
||||
upflexiramp[sc.name, rn, gn, t]
|
||||
) # first inequality of Eq. (26) in Wang & Hobbs (2016)
|
||||
@constraint(
|
||||
model,
|
||||
upflexiramp[rn, gn, t] <=
|
||||
upflexiramp[sc.name, rn, gn, t] <=
|
||||
RU * is_on[gn, t] +
|
||||
SU * (is_on[gn, t+1] - is_on[gn, t]) +
|
||||
maxp[t] * (1 - is_on[gn, t+1])
|
||||
@@ -126,11 +137,12 @@ function _add_ramp_eqs!(
|
||||
@constraint(
|
||||
model,
|
||||
-RU * is_on[gn, t] - SU * (is_on[gn, t+1] - is_on[gn, t]) -
|
||||
maxp[t] * (1 - is_on[gn, t+1]) <= dwflexiramp[rn, gn, t]
|
||||
maxp[t] * (1 - is_on[gn, t+1]) <=
|
||||
dwflexiramp[sc.name, rn, gn, t]
|
||||
) # first inequality of Eq. (27) in Wang & Hobbs (2016)
|
||||
@constraint(
|
||||
model,
|
||||
dwflexiramp[rn, gn, t] <=
|
||||
dwflexiramp[sc.name, rn, gn, t] <=
|
||||
RD * is_on[gn, t+1] +
|
||||
SD * (is_on[gn, t] - is_on[gn, t+1]) +
|
||||
maxp[t] * (1 - is_on[gn, t])
|
||||
@@ -138,26 +150,27 @@ function _add_ramp_eqs!(
|
||||
@constraint(
|
||||
model,
|
||||
-maxp[t] * is_on[gn, t] + minp[t] * is_on[gn, t+1] <=
|
||||
upflexiramp[rn, gn, t]
|
||||
upflexiramp[sc.name, rn, gn, t]
|
||||
) # first inequality of Eq. (28) in Wang & Hobbs (2016)
|
||||
@constraint(
|
||||
model,
|
||||
upflexiramp[rn, gn, t] <= maxp[t] * is_on[gn, t+1]
|
||||
upflexiramp[sc.name, 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]
|
||||
-maxp[t] * is_on[gn, t+1] <=
|
||||
dwflexiramp[sc.name, rn, gn, t]
|
||||
) # first inequality of Eq. (29) in Wang & Hobbs (2016)
|
||||
@constraint(
|
||||
model,
|
||||
dwflexiramp[rn, gn, t] <=
|
||||
dwflexiramp[sc.name, rn, gn, t] <=
|
||||
(maxp[t] * is_on[gn, t]) - (minp[t] * is_on[gn, t+1])
|
||||
) # second inequality of Eq. (29) in Wang & Hobbs (2016)
|
||||
else
|
||||
@constraint(
|
||||
model,
|
||||
mfg[rn, gn, t] <=
|
||||
prod_above[gn, t-1] +
|
||||
mfg[sc.name, gn, t] <=
|
||||
prod_above[sc.name, gn, t-1] +
|
||||
(is_on[gn, t-1] * minp[t]) +
|
||||
(RU * is_on[gn, t-1]) +
|
||||
(SU * (is_on[gn, t] - is_on[gn, t-1])) +
|
||||
@@ -165,8 +178,11 @@ function _add_ramp_eqs!(
|
||||
) # Eq. (23) in Wang & Hobbs (2016) for the last time period
|
||||
@constraint(
|
||||
model,
|
||||
(prod_above[gn, t-1] + (is_on[gn, t-1] * minp[t])) -
|
||||
(prod_above[gn, t] + (is_on[gn, t] * minp[t])) <=
|
||||
(
|
||||
prod_above[sc.name, gn, t-1] +
|
||||
(is_on[gn, t-1] * minp[t])
|
||||
) -
|
||||
(prod_above[sc.name, gn, t] + (is_on[gn, t] * minp[t])) <=
|
||||
RD * is_on[gn, t] +
|
||||
SD * (is_on[gn, t-1] - is_on[gn, t]) +
|
||||
maxp[t] * (1 - is_on[gn, t-1])
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
"""
|
||||
Formulation described in:
|
||||
|
||||
B. Wang and B. F. Hobbs, "Real-Time Markets for Flexiramp: A Stochastic
|
||||
Unit Commitment-Based Analysis," in IEEE Transactions on Power Systems,
|
||||
vol. 31, no. 2, pp. 846-860, March 2016, doi: 10.1109/TPWRS.2015.2411268.
|
||||
|
||||
@@ -2,22 +2,30 @@
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
function _add_bus!(model::JuMP.Model, b::Bus)::Nothing
|
||||
function _add_bus!(
|
||||
model::JuMP.Model,
|
||||
b::Bus,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
net_injection = _init(model, :expr_net_injection)
|
||||
curtail = _init(model, :curtail)
|
||||
for t in 1:model[:instance].time
|
||||
# Fixed load
|
||||
net_injection[b.name, t] = AffExpr(-b.load[t])
|
||||
net_injection[sc.name, b.name, t] = AffExpr(-b.load[t])
|
||||
|
||||
# Load curtailment
|
||||
curtail[b.name, t] =
|
||||
curtail[sc.name, b.name, t] =
|
||||
@variable(model, lower_bound = 0, upper_bound = b.load[t])
|
||||
|
||||
add_to_expression!(net_injection[b.name, t], curtail[b.name, t], 1.0)
|
||||
add_to_expression!(
|
||||
net_injection[sc.name, b.name, t],
|
||||
curtail[sc.name, b.name, t],
|
||||
1.0,
|
||||
)
|
||||
add_to_expression!(
|
||||
model[:obj],
|
||||
curtail[b.name, t],
|
||||
model[:instance].power_balance_penalty[t],
|
||||
curtail[sc.name, b.name, t],
|
||||
sc.power_balance_penalty[t] * sc.probability,
|
||||
)
|
||||
end
|
||||
return
|
||||
|
||||
@@ -6,43 +6,43 @@ function _add_transmission_line!(
|
||||
model::JuMP.Model,
|
||||
lm::TransmissionLine,
|
||||
f::ShiftFactorsFormulation,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
overflow = _init(model, :overflow)
|
||||
for t in 1:model[:instance].time
|
||||
overflow[lm.name, t] = @variable(model, lower_bound = 0)
|
||||
overflow[sc.name, lm.name, t] = @variable(model, lower_bound = 0)
|
||||
add_to_expression!(
|
||||
model[:obj],
|
||||
overflow[lm.name, t],
|
||||
lm.flow_limit_penalty[t],
|
||||
overflow[sc.name, lm.name, t],
|
||||
lm.flow_limit_penalty[t] * sc.probability,
|
||||
)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
function _setup_transmission(
|
||||
model::JuMP.Model,
|
||||
formulation::ShiftFactorsFormulation,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
instance = model[:instance]
|
||||
isf = formulation.precomputed_isf
|
||||
lodf = formulation.precomputed_lodf
|
||||
if length(instance.buses) == 1
|
||||
if length(sc.buses) == 1
|
||||
isf = zeros(0, 0)
|
||||
lodf = zeros(0, 0)
|
||||
elseif isf === nothing
|
||||
@info "Computing injection shift factors..."
|
||||
time_isf = @elapsed begin
|
||||
isf = UnitCommitment._injection_shift_factors(
|
||||
lines = instance.lines,
|
||||
buses = instance.buses,
|
||||
buses = sc.buses,
|
||||
lines = sc.lines,
|
||||
)
|
||||
end
|
||||
@info @sprintf("Computed ISF in %.2f seconds", time_isf)
|
||||
@info "Computing line outage factors..."
|
||||
time_lodf = @elapsed begin
|
||||
lodf = UnitCommitment._line_outage_factors(
|
||||
lines = instance.lines,
|
||||
buses = instance.buses,
|
||||
buses = sc.buses,
|
||||
lines = sc.lines,
|
||||
isf = isf,
|
||||
)
|
||||
end
|
||||
@@ -55,7 +55,7 @@ function _setup_transmission(
|
||||
isf[abs.(isf).<formulation.isf_cutoff] .= 0
|
||||
lodf[abs.(lodf).<formulation.lodf_cutoff] .= 0
|
||||
end
|
||||
model[:isf] = isf
|
||||
model[:lodf] = lodf
|
||||
sc.isf = isf
|
||||
sc.lodf = lodf
|
||||
return
|
||||
end
|
||||
|
||||
@@ -5,21 +5,26 @@
|
||||
function _add_price_sensitive_load!(
|
||||
model::JuMP.Model,
|
||||
ps::PriceSensitiveLoad,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
loads = _init(model, :loads)
|
||||
net_injection = _init(model, :expr_net_injection)
|
||||
for t in 1:model[:instance].time
|
||||
# Decision variable
|
||||
loads[ps.name, t] =
|
||||
loads[sc.name, ps.name, t] =
|
||||
@variable(model, lower_bound = 0, upper_bound = ps.demand[t])
|
||||
|
||||
# Objective function terms
|
||||
add_to_expression!(model[:obj], loads[ps.name, t], -ps.revenue[t])
|
||||
add_to_expression!(
|
||||
model[:obj],
|
||||
loads[sc.name, ps.name, t],
|
||||
-ps.revenue[t] * sc.probability,
|
||||
)
|
||||
|
||||
# Net injection
|
||||
add_to_expression!(
|
||||
net_injection[ps.bus.name, t],
|
||||
loads[ps.name, t],
|
||||
net_injection[sc.name, ps.bus.name, t],
|
||||
loads[sc.name, ps.name, t],
|
||||
-1.0,
|
||||
)
|
||||
end
|
||||
|
||||
35
src/model/formulations/base/punit.jl
Normal file
35
src/model/formulations/base/punit.jl
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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.
|
||||
|
||||
function _add_profiled_unit!(
|
||||
model::JuMP.Model,
|
||||
pu::ProfiledUnit,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
punits = _init(model, :prod_profiled)
|
||||
net_injection = _init(model, :expr_net_injection)
|
||||
for t in 1:model[:instance].time
|
||||
# Decision variable
|
||||
punits[sc.name, pu.name, t] = @variable(
|
||||
model,
|
||||
lower_bound = pu.min_power[t],
|
||||
upper_bound = pu.max_power[t]
|
||||
)
|
||||
|
||||
# Objective function terms
|
||||
add_to_expression!(
|
||||
model[:obj],
|
||||
punits[sc.name, pu.name, t],
|
||||
pu.cost[t] * sc.probability,
|
||||
)
|
||||
|
||||
# Net injection
|
||||
add_to_expression!(
|
||||
net_injection[sc.name, pu.bus.name, t],
|
||||
punits[sc.name, pu.name, t],
|
||||
1.0,
|
||||
)
|
||||
end
|
||||
return
|
||||
end
|
||||
@@ -18,7 +18,7 @@ function _injection_shift_factors(;
|
||||
lines::Array{TransmissionLine},
|
||||
)
|
||||
susceptance = _susceptance_matrix(lines)
|
||||
incidence = _reduced_incidence_matrix(lines = lines, buses = buses)
|
||||
incidence = _reduced_incidence_matrix(buses = buses, lines = lines)
|
||||
laplacian = transpose(incidence) * susceptance * incidence
|
||||
isf = susceptance * incidence * inv(Array(laplacian))
|
||||
return isf
|
||||
|
||||
@@ -9,6 +9,27 @@ abstract type StartupCostsFormulation end
|
||||
abstract type StatusVarsFormulation end
|
||||
abstract type ProductionVarsFormulation end
|
||||
|
||||
"""
|
||||
struct Formulation
|
||||
prod_vars::ProductionVarsFormulation
|
||||
pwl_costs::PiecewiseLinearCostsFormulation
|
||||
ramping::RampingFormulation
|
||||
startup_costs::StartupCostsFormulation
|
||||
status_vars::StatusVarsFormulation
|
||||
transmission::TransmissionFormulation
|
||||
end
|
||||
|
||||
Struct provided to `build_model` that holds various formulation components.
|
||||
|
||||
# Fields
|
||||
|
||||
- `prod_vars`: Formulation for the production decision variables
|
||||
- `pwl_costs`: Formulation for the piecewise linear costs
|
||||
- `ramping`: Formulation for ramping constraints
|
||||
- `startup_costs`: Formulation for time-dependent start-up costs
|
||||
- `status_vars`: Formulation for the status variables (e.g. `is_on`, `is_off`)
|
||||
- `transmission`: Formulation for transmission and N-1 security constraints
|
||||
"""
|
||||
struct Formulation
|
||||
prod_vars::ProductionVarsFormulation
|
||||
pwl_costs::PiecewiseLinearCostsFormulation
|
||||
@@ -38,10 +59,10 @@ end
|
||||
|
||||
"""
|
||||
struct ShiftFactorsFormulation <: TransmissionFormulation
|
||||
isf_cutoff::Float64
|
||||
lodf_cutoff::Float64
|
||||
precomputed_isf::Union{Nothing,Matrix{Float64}}
|
||||
precomputed_lodf::Union{Nothing,Matrix{Float64}}
|
||||
isf_cutoff::Float64 = 0.005
|
||||
lodf_cutoff::Float64 = 0.001
|
||||
precomputed_isf=nothing
|
||||
precomputed_lodf=nothing
|
||||
end
|
||||
|
||||
Transmission formulation based on Injection Shift Factors (ISF) and Line
|
||||
@@ -49,15 +70,15 @@ Outage Distribution Factors (LODF). Constraints are enforced in a lazy way.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
- `precomputed_isf::Union{Matrix{Float64},Nothing} = nothing`:
|
||||
- `precomputed_isf`:
|
||||
the injection shift factors matrix. If not provided, it will be computed.
|
||||
- `precomputed_lodf::Union{Matrix{Float64},Nothing} = nothing`:
|
||||
- `precomputed_lodf`:
|
||||
the line outage distribution factors matrix. If not provided, it will be
|
||||
computed.
|
||||
- `isf_cutoff::Float64 = 0.005`:
|
||||
- `isf_cutoff`:
|
||||
the cutoff that should be applied to the ISF matrix. Entries with magnitude
|
||||
smaller than this value will be set to zero.
|
||||
- `lodf_cutoff::Float64 = 0.001`:
|
||||
- `lodf_cutoff`:
|
||||
the cutoff that should be applied to the LODF matrix. Entries with magnitude
|
||||
smaller than this value will be set to zero.
|
||||
"""
|
||||
|
||||
@@ -2,54 +2,68 @@
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
function _add_system_wide_eqs!(model::JuMP.Model)::Nothing
|
||||
_add_net_injection_eqs!(model)
|
||||
_add_spinning_reserve_eqs!(model)
|
||||
_add_flexiramp_reserve_eqs!(model)
|
||||
function _add_system_wide_eqs!(
|
||||
model::JuMP.Model,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
_add_net_injection_eqs!(model, sc)
|
||||
_add_spinning_reserve_eqs!(model, sc)
|
||||
_add_flexiramp_reserve_eqs!(model, sc)
|
||||
return
|
||||
end
|
||||
|
||||
function _add_net_injection_eqs!(model::JuMP.Model)::Nothing
|
||||
function _add_net_injection_eqs!(
|
||||
model::JuMP.Model,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
T = model[:instance].time
|
||||
net_injection = _init(model, :net_injection)
|
||||
eq_net_injection = _init(model, :eq_net_injection)
|
||||
eq_power_balance = _init(model, :eq_power_balance)
|
||||
for t in 1:T, b in model[:instance].buses
|
||||
n = net_injection[b.name, t] = @variable(model)
|
||||
eq_net_injection[b.name, t] =
|
||||
@constraint(model, -n + model[:expr_net_injection][b.name, t] == 0)
|
||||
for t in 1:T, b in sc.buses
|
||||
n = net_injection[sc.name, b.name, t] = @variable(model)
|
||||
eq_net_injection[sc.name, b.name, t] = @constraint(
|
||||
model,
|
||||
-n + model[:expr_net_injection][sc.name, b.name, t] == 0
|
||||
)
|
||||
end
|
||||
for t in 1:T
|
||||
eq_power_balance[t] = @constraint(
|
||||
eq_power_balance[sc.name, t] = @constraint(
|
||||
model,
|
||||
sum(net_injection[b.name, t] for b in model[:instance].buses) == 0
|
||||
sum(net_injection[sc.name, b.name, t] for b in sc.buses) == 0
|
||||
)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
function _add_spinning_reserve_eqs!(model::JuMP.Model)::Nothing
|
||||
instance = model[:instance]
|
||||
function _add_spinning_reserve_eqs!(
|
||||
model::JuMP.Model,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
T = model[:instance].time
|
||||
eq_min_spinning_reserve = _init(model, :eq_min_spinning_reserve)
|
||||
for r in instance.reserves
|
||||
for r in sc.reserves
|
||||
r.type == "spinning" || continue
|
||||
for t in 1:instance.time
|
||||
for t in 1:T
|
||||
# Equation (68) in Kneuven et al. (2020)
|
||||
# As in Morales-España et al. (2013a)
|
||||
# Akin to the alternative formulation with max_power_avail
|
||||
# from Carrión and Arroyo (2006) and Ostrowski et al. (2012)
|
||||
eq_min_spinning_reserve[r.name, t] = @constraint(
|
||||
eq_min_spinning_reserve[sc.name, r.name, t] = @constraint(
|
||||
model,
|
||||
sum(model[:reserve][r.name, g.name, t] for g in r.units) +
|
||||
model[:reserve_shortfall][r.name, t] >= r.amount[t]
|
||||
sum(
|
||||
model[:reserve][sc.name, r.name, g.name, t] for
|
||||
g in r.thermal_units
|
||||
) + model[:reserve_shortfall][sc.name, r.name, t] >=
|
||||
r.amount[t]
|
||||
)
|
||||
|
||||
# Account for shortfall contribution to objective
|
||||
if r.shortfall_penalty >= 0
|
||||
add_to_expression!(
|
||||
model[:obj],
|
||||
r.shortfall_penalty,
|
||||
model[:reserve_shortfall][r.name, t],
|
||||
r.shortfall_penalty * sc.probability,
|
||||
model[:reserve_shortfall][sc.name, r.name, t],
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -57,7 +71,10 @@ function _add_spinning_reserve_eqs!(model::JuMP.Model)::Nothing
|
||||
return
|
||||
end
|
||||
|
||||
function _add_flexiramp_reserve_eqs!(model::JuMP.Model)::Nothing
|
||||
function _add_flexiramp_reserve_eqs!(
|
||||
model::JuMP.Model,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
# Note: The flexpramp requirements in Wang & Hobbs (2016) are imposed as hard constraints
|
||||
# through Eq. (17) and Eq. (18). The constraints eq_min_upflexiramp and eq_min_dwflexiramp
|
||||
# provided below are modified versions of Eq. (17) and Eq. (18), respectively, in that
|
||||
@@ -65,29 +82,37 @@ function _add_flexiramp_reserve_eqs!(model::JuMP.Model)::Nothing
|
||||
# objective function.
|
||||
eq_min_upflexiramp = _init(model, :eq_min_upflexiramp)
|
||||
eq_min_dwflexiramp = _init(model, :eq_min_dwflexiramp)
|
||||
instance = model[:instance]
|
||||
for r in instance.reserves
|
||||
T = model[:instance].time
|
||||
for r in sc.reserves
|
||||
r.type == "flexiramp" || continue
|
||||
for t in 1:instance.time
|
||||
for t in 1:T
|
||||
# Eq. (17) in Wang & Hobbs (2016)
|
||||
eq_min_upflexiramp[r.name, t] = @constraint(
|
||||
eq_min_upflexiramp[sc.name, r.name, t] = @constraint(
|
||||
model,
|
||||
sum(model[:upflexiramp][r.name, g.name, t] for g in r.units) + model[:upflexiramp_shortfall][r.name, t] >= r.amount[t]
|
||||
sum(
|
||||
model[:upflexiramp][sc.name, r.name, g.name, t] for
|
||||
g in r.thermal_units
|
||||
) + model[:upflexiramp_shortfall][sc.name, r.name, t] >=
|
||||
r.amount[t]
|
||||
)
|
||||
# Eq. (18) in Wang & Hobbs (2016)
|
||||
eq_min_dwflexiramp[r.name, t] = @constraint(
|
||||
eq_min_dwflexiramp[sc.name, r.name, t] = @constraint(
|
||||
model,
|
||||
sum(model[:dwflexiramp][r.name, g.name, t] for g in r.units) + model[:dwflexiramp_shortfall][r.name, t] >= r.amount[t]
|
||||
sum(
|
||||
model[:dwflexiramp][sc.name, r.name, g.name, t] for
|
||||
g in r.thermal_units
|
||||
) + model[:dwflexiramp_shortfall][sc.name, r.name, t] >=
|
||||
r.amount[t]
|
||||
)
|
||||
|
||||
# Account for flexiramp shortfall contribution to objective
|
||||
if r.shortfall_penalty >= 0
|
||||
add_to_expression!(
|
||||
model[:obj],
|
||||
r.shortfall_penalty,
|
||||
r.shortfall_penalty * sc.probability,
|
||||
(
|
||||
model[:upflexiramp_shortfall][r.name, t] +
|
||||
model[:dwflexiramp_shortfall][r.name, t]
|
||||
model[:upflexiramp_shortfall][sc.name, r.name, t] +
|
||||
model[:dwflexiramp_shortfall][sc.name, r.name, t]
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
function _add_unit!(model::JuMP.Model, g::Unit, formulation::Formulation)
|
||||
# Function for adding variables, constraints, and objective function terms
|
||||
# related to the binary commitment, startup and shutdown decisions of units
|
||||
function _add_unit_commitment!(
|
||||
model::JuMP.Model,
|
||||
g::ThermalUnit,
|
||||
formulation::Formulation,
|
||||
)
|
||||
if !all(g.must_run) && any(g.must_run)
|
||||
error("Partially must-run units are not currently supported")
|
||||
end
|
||||
@@ -11,22 +17,41 @@ function _add_unit!(model::JuMP.Model, g::Unit, formulation::Formulation)
|
||||
end
|
||||
|
||||
# Variables
|
||||
_add_production_vars!(model, g, formulation.prod_vars)
|
||||
_add_spinning_reserve_vars!(model, g)
|
||||
_add_flexiramp_reserve_vars!(model, g)
|
||||
_add_startup_shutdown_vars!(model, g)
|
||||
_add_status_vars!(model, g, formulation.status_vars)
|
||||
|
||||
# Constraints and objective function
|
||||
_add_min_uptime_downtime_eqs!(model, g)
|
||||
_add_net_injection_eqs!(model, g)
|
||||
_add_production_limit_eqs!(model, g, formulation.prod_vars)
|
||||
_add_startup_cost_eqs!(model, g, formulation.startup_costs)
|
||||
_add_status_eqs!(model, g, formulation.status_vars)
|
||||
_add_commitment_status_eqs!(model, g)
|
||||
return
|
||||
end
|
||||
|
||||
# Function for adding variables, constraints, and objective function terms
|
||||
# related to the continuous dispatch decisions of units
|
||||
function _add_unit_dispatch!(
|
||||
model::JuMP.Model,
|
||||
g::ThermalUnit,
|
||||
formulation::Formulation,
|
||||
sc::UnitCommitmentScenario,
|
||||
)
|
||||
|
||||
# Variables
|
||||
_add_production_vars!(model, g, formulation.prod_vars, sc)
|
||||
_add_spinning_reserve_vars!(model, g, sc)
|
||||
_add_flexiramp_reserve_vars!(model, g, sc)
|
||||
|
||||
# Constraints and objective function
|
||||
_add_net_injection_eqs!(model, g, sc)
|
||||
_add_production_limit_eqs!(model, g, formulation.prod_vars, sc)
|
||||
_add_production_piecewise_linear_eqs!(
|
||||
model,
|
||||
g,
|
||||
formulation.prod_vars,
|
||||
formulation.pwl_costs,
|
||||
formulation.status_vars,
|
||||
sc,
|
||||
)
|
||||
_add_ramp_eqs!(
|
||||
model,
|
||||
@@ -34,26 +59,31 @@ function _add_unit!(model::JuMP.Model, g::Unit, formulation::Formulation)
|
||||
formulation.prod_vars,
|
||||
formulation.ramping,
|
||||
formulation.status_vars,
|
||||
sc,
|
||||
)
|
||||
_add_startup_cost_eqs!(model, g, formulation.startup_costs)
|
||||
_add_startup_shutdown_limit_eqs!(model, g)
|
||||
_add_status_eqs!(model, g, formulation.status_vars)
|
||||
_add_startup_shutdown_limit_eqs!(model, g, sc)
|
||||
return
|
||||
end
|
||||
|
||||
_is_initially_on(g::Unit)::Float64 = (g.initial_status > 0 ? 1.0 : 0.0)
|
||||
_is_initially_on(g::ThermalUnit)::Float64 = (g.initial_status > 0 ? 1.0 : 0.0)
|
||||
|
||||
function _add_spinning_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing
|
||||
function _add_spinning_reserve_vars!(
|
||||
model::JuMP.Model,
|
||||
g::ThermalUnit,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
reserve = _init(model, :reserve)
|
||||
reserve_shortfall = _init(model, :reserve_shortfall)
|
||||
for r in g.reserves
|
||||
r.type == "spinning" || continue
|
||||
for t in 1:model[:instance].time
|
||||
reserve[r.name, g.name, t] = @variable(model, lower_bound = 0)
|
||||
if (r.name, t) ∉ keys(reserve_shortfall)
|
||||
reserve_shortfall[r.name, t] = @variable(model, lower_bound = 0)
|
||||
reserve[sc.name, r.name, g.name, t] =
|
||||
@variable(model, lower_bound = 0)
|
||||
if (sc.name, r.name, t) ∉ keys(reserve_shortfall)
|
||||
reserve_shortfall[sc.name, r.name, t] =
|
||||
@variable(model, lower_bound = 0)
|
||||
if r.shortfall_penalty < 0
|
||||
set_upper_bound(reserve_shortfall[r.name, t], 0.0)
|
||||
set_upper_bound(reserve_shortfall[sc.name, r.name, t], 0.0)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -61,27 +91,37 @@ function _add_spinning_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing
|
||||
return
|
||||
end
|
||||
|
||||
function _add_flexiramp_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing
|
||||
function _add_flexiramp_reserve_vars!(
|
||||
model::JuMP.Model,
|
||||
g::ThermalUnit,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
upflexiramp = _init(model, :upflexiramp)
|
||||
upflexiramp_shortfall = _init(model, :upflexiramp_shortfall)
|
||||
mfg = _init(model, :mfg)
|
||||
dwflexiramp = _init(model, :dwflexiramp)
|
||||
dwflexiramp_shortfall = _init(model, :dwflexiramp_shortfall)
|
||||
for r in g.reserves
|
||||
r.type == "flexiramp" || continue
|
||||
for t in 1:model[:instance].time
|
||||
# maximum feasible generation, \bar{g_{its}} in Wang & Hobbs (2016)
|
||||
mfg[r.name, g.name, t] = @variable(model, lower_bound = 0)
|
||||
upflexiramp[r.name, g.name, t] = @variable(model) # up-flexiramp, ur_{it} in Wang & Hobbs (2016)
|
||||
dwflexiramp[r.name, g.name, t] = @variable(model) # down-flexiramp, dr_{it} in Wang & Hobbs (2016)
|
||||
if (r.name, t) ∉ keys(upflexiramp_shortfall)
|
||||
upflexiramp_shortfall[r.name, t] =
|
||||
for t in 1:model[:instance].time
|
||||
# maximum feasible generation, \bar{g_{its}} in Wang & Hobbs (2016)
|
||||
mfg[sc.name, g.name, t] = @variable(model, lower_bound = 0)
|
||||
for r in g.reserves
|
||||
r.type == "flexiramp" || continue
|
||||
upflexiramp[sc.name, r.name, g.name, t] = @variable(model) # up-flexiramp, ur_{it} in Wang & Hobbs (2016)
|
||||
dwflexiramp[sc.name, r.name, g.name, t] = @variable(model) # down-flexiramp, dr_{it} in Wang & Hobbs (2016)
|
||||
if (sc.name, r.name, t) ∉ keys(upflexiramp_shortfall)
|
||||
upflexiramp_shortfall[sc.name, r.name, t] =
|
||||
@variable(model, lower_bound = 0)
|
||||
dwflexiramp_shortfall[r.name, t] =
|
||||
dwflexiramp_shortfall[sc.name, 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)
|
||||
set_upper_bound(
|
||||
upflexiramp_shortfall[sc.name, r.name, t],
|
||||
0.0,
|
||||
)
|
||||
set_upper_bound(
|
||||
dwflexiramp_shortfall[sc.name, r.name, t],
|
||||
0.0,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -89,7 +129,7 @@ function _add_flexiramp_reserve_vars!(model::JuMP.Model, g::Unit)::Nothing
|
||||
return
|
||||
end
|
||||
|
||||
function _add_startup_shutdown_vars!(model::JuMP.Model, g::Unit)::Nothing
|
||||
function _add_startup_shutdown_vars!(model::JuMP.Model, g::ThermalUnit)::Nothing
|
||||
startup = _init(model, :startup)
|
||||
for t in 1:model[:instance].time
|
||||
for s in 1:length(g.startup_categories)
|
||||
@@ -99,32 +139,36 @@ function _add_startup_shutdown_vars!(model::JuMP.Model, g::Unit)::Nothing
|
||||
return
|
||||
end
|
||||
|
||||
function _add_startup_shutdown_limit_eqs!(model::JuMP.Model, g::Unit)::Nothing
|
||||
function _add_startup_shutdown_limit_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::ThermalUnit,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
eq_shutdown_limit = _init(model, :eq_shutdown_limit)
|
||||
eq_startup_limit = _init(model, :eq_startup_limit)
|
||||
is_on = model[:is_on]
|
||||
prod_above = model[:prod_above]
|
||||
reserve = _total_reserves(model, g)
|
||||
reserve = _total_reserves(model, g, sc)
|
||||
switch_off = model[:switch_off]
|
||||
switch_on = model[:switch_on]
|
||||
T = model[:instance].time
|
||||
for t in 1:T
|
||||
# Startup limit
|
||||
eq_startup_limit[g.name, t] = @constraint(
|
||||
eq_startup_limit[sc.name, g.name, t] = @constraint(
|
||||
model,
|
||||
prod_above[g.name, t] + reserve[t] <=
|
||||
prod_above[sc.name, g.name, t] + reserve[t] <=
|
||||
(g.max_power[t] - g.min_power[t]) * is_on[g.name, t] -
|
||||
max(0, g.max_power[t] - g.startup_limit) * switch_on[g.name, t]
|
||||
)
|
||||
# Shutdown limit
|
||||
if g.initial_power > g.shutdown_limit
|
||||
eq_shutdown_limit[g.name, 0] =
|
||||
eq_shutdown_limit[sc.name, g.name, 0] =
|
||||
@constraint(model, switch_off[g.name, 1] <= 0)
|
||||
end
|
||||
if t < T
|
||||
eq_shutdown_limit[g.name, t] = @constraint(
|
||||
eq_shutdown_limit[sc.name, g.name, t] = @constraint(
|
||||
model,
|
||||
prod_above[g.name, t] <=
|
||||
prod_above[sc.name, g.name, t] <=
|
||||
(g.max_power[t] - g.min_power[t]) * is_on[g.name, t] -
|
||||
max(0, g.max_power[t] - g.shutdown_limit) *
|
||||
switch_off[g.name, t+1]
|
||||
@@ -136,51 +180,55 @@ end
|
||||
|
||||
function _add_ramp_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::Unit,
|
||||
g::ThermalUnit,
|
||||
formulation::RampingFormulation,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
prod_above = model[:prod_above]
|
||||
reserve = _total_reserves(model, g)
|
||||
reserve = _total_reserves(model, g, sc)
|
||||
eq_ramp_up = _init(model, :eq_ramp_up)
|
||||
eq_ramp_down = _init(model, :eq_ramp_down)
|
||||
for t in 1:model[:instance].time
|
||||
# Ramp up limit
|
||||
if t == 1
|
||||
if _is_initially_on(g) == 1
|
||||
eq_ramp_up[g.name, t] = @constraint(
|
||||
eq_ramp_up[sc.name, g.name, t] = @constraint(
|
||||
model,
|
||||
prod_above[g.name, t] + reserve[t] <=
|
||||
prod_above[sc.name, g.name, t] + reserve[t] <=
|
||||
(g.initial_power - g.min_power[t]) + g.ramp_up_limit
|
||||
)
|
||||
end
|
||||
else
|
||||
eq_ramp_up[g.name, t] = @constraint(
|
||||
eq_ramp_up[sc.name, g.name, t] = @constraint(
|
||||
model,
|
||||
prod_above[g.name, t] + reserve[t] <=
|
||||
prod_above[g.name, t-1] + g.ramp_up_limit
|
||||
prod_above[sc.name, g.name, t] + reserve[t] <=
|
||||
prod_above[sc.name, g.name, t-1] + g.ramp_up_limit
|
||||
)
|
||||
end
|
||||
|
||||
# Ramp down limit
|
||||
if t == 1
|
||||
if _is_initially_on(g) == 1
|
||||
eq_ramp_down[g.name, t] = @constraint(
|
||||
eq_ramp_down[sc.name, g.name, t] = @constraint(
|
||||
model,
|
||||
prod_above[g.name, t] >=
|
||||
prod_above[sc.name, g.name, t] >=
|
||||
(g.initial_power - g.min_power[t]) - g.ramp_down_limit
|
||||
)
|
||||
end
|
||||
else
|
||||
eq_ramp_down[g.name, t] = @constraint(
|
||||
eq_ramp_down[sc.name, g.name, t] = @constraint(
|
||||
model,
|
||||
prod_above[g.name, t] >=
|
||||
prod_above[g.name, t-1] - g.ramp_down_limit
|
||||
prod_above[sc.name, g.name, t] >=
|
||||
prod_above[sc.name, g.name, t-1] - g.ramp_down_limit
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function _add_min_uptime_downtime_eqs!(model::JuMP.Model, g::Unit)::Nothing
|
||||
function _add_min_uptime_downtime_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::ThermalUnit,
|
||||
)::Nothing
|
||||
is_on = model[:is_on]
|
||||
switch_off = model[:switch_off]
|
||||
switch_on = model[:switch_on]
|
||||
@@ -223,30 +271,52 @@ function _add_min_uptime_downtime_eqs!(model::JuMP.Model, g::Unit)::Nothing
|
||||
end
|
||||
end
|
||||
|
||||
function _add_net_injection_eqs!(model::JuMP.Model, g::Unit)::Nothing
|
||||
function _add_commitment_status_eqs!(model::JuMP.Model, g::ThermalUnit)::Nothing
|
||||
is_on = model[:is_on]
|
||||
T = model[:instance].time
|
||||
eq_commitment_status = _init(model, :eq_commitment_status)
|
||||
for t in 1:T
|
||||
if g.commitment_status[t] !== nothing
|
||||
eq_commitment_status[g.name, t] = @constraint(
|
||||
model,
|
||||
is_on[g.name, t] == (g.commitment_status[t] ? 1.0 : 0.0)
|
||||
)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
function _add_net_injection_eqs!(
|
||||
model::JuMP.Model,
|
||||
g::ThermalUnit,
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
expr_net_injection = model[:expr_net_injection]
|
||||
for t in 1:model[:instance].time
|
||||
# Add to net injection expression
|
||||
add_to_expression!(
|
||||
expr_net_injection[g.bus.name, t],
|
||||
model[:prod_above][g.name, t],
|
||||
expr_net_injection[sc.name, g.bus.name, t],
|
||||
model[:prod_above][sc.name, g.name, t],
|
||||
1.0,
|
||||
)
|
||||
add_to_expression!(
|
||||
expr_net_injection[g.bus.name, t],
|
||||
expr_net_injection[sc.name, g.bus.name, t],
|
||||
model[:is_on][g.name, t],
|
||||
g.min_power[t],
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
function _total_reserves(model, g)::Vector
|
||||
function _total_reserves(model, g, sc)::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
|
||||
sum(
|
||||
model[:reserve][sc.name, r.name, g.name, t] for
|
||||
r in spinning_reserves
|
||||
) for t in 1:model[:instance].time
|
||||
]
|
||||
end
|
||||
return reserve
|
||||
|
||||
@@ -10,37 +10,43 @@ solution. Useful for computing LMPs.
|
||||
"""
|
||||
function fix!(model::JuMP.Model, solution::AbstractDict)::Nothing
|
||||
instance, T = model[:instance], model[:instance].time
|
||||
"Thermal production (MW)" ∈ keys(solution) ?
|
||||
solution = Dict("s1" => solution) : nothing
|
||||
is_on = model[:is_on]
|
||||
prod_above = model[:prod_above]
|
||||
reserve = model[:reserve]
|
||||
for g in instance.units
|
||||
for t in 1:T
|
||||
is_on_value = round(solution["Is on"][g.name][t])
|
||||
prod_value =
|
||||
round(solution["Production (MW)"][g.name][t], digits = 5)
|
||||
JuMP.fix(is_on[g.name, t], is_on_value, force = true)
|
||||
JuMP.fix(
|
||||
prod_above[g.name, t],
|
||||
prod_value - is_on_value * g.min_power[t],
|
||||
force = true,
|
||||
)
|
||||
end
|
||||
end
|
||||
for r in instance.reserves
|
||||
r.type == "spinning" || continue
|
||||
for g in r.units
|
||||
for sc in instance.scenarios
|
||||
for g in sc.thermal_units
|
||||
for t in 1:T
|
||||
reserve_value = round(
|
||||
solution["Spinning reserve (MW)"][r.name][g.name][t],
|
||||
is_on_value = round(solution[sc.name]["Is on"][g.name][t])
|
||||
prod_value = round(
|
||||
solution[sc.name]["Thermal production (MW)"][g.name][t],
|
||||
digits = 5,
|
||||
)
|
||||
JuMP.fix(is_on[g.name, t], is_on_value, force = true)
|
||||
JuMP.fix(
|
||||
reserve[r.name, g.name, t],
|
||||
reserve_value,
|
||||
prod_above[sc.name, g.name, t],
|
||||
prod_value - is_on_value * g.min_power[t],
|
||||
force = true,
|
||||
)
|
||||
end
|
||||
end
|
||||
for r in sc.reserves
|
||||
r.type == "spinning" || continue
|
||||
for g in r.thermal_units
|
||||
for t in 1:T
|
||||
reserve_value = round(
|
||||
solution[sc.name]["Spinning reserve (MW)"][r.name][g.name][t],
|
||||
digits = 5,
|
||||
)
|
||||
JuMP.fix(
|
||||
reserve[sc.name, r.name, g.name, t],
|
||||
reserve_value,
|
||||
force = true,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
function _enforce_transmission(
|
||||
model::JuMP.Model,
|
||||
violations::Vector{_Violation},
|
||||
sc::UnitCommitmentScenario,
|
||||
)::Nothing
|
||||
for v in violations
|
||||
_enforce_transmission(
|
||||
model = model,
|
||||
sc = sc,
|
||||
violation = v,
|
||||
isf = model[:isf],
|
||||
lodf = model[:lodf],
|
||||
isf = sc.isf,
|
||||
lodf = sc.lodf,
|
||||
)
|
||||
end
|
||||
return
|
||||
@@ -19,6 +21,7 @@ end
|
||||
|
||||
function _enforce_transmission(;
|
||||
model::JuMP.Model,
|
||||
sc::UnitCommitmentScenario,
|
||||
violation::_Violation,
|
||||
isf::Matrix{Float64},
|
||||
lodf::Matrix{Float64},
|
||||
@@ -31,19 +34,21 @@ function _enforce_transmission(;
|
||||
if violation.outage_line === nothing
|
||||
limit = violation.monitored_line.normal_flow_limit[violation.time]
|
||||
@info @sprintf(
|
||||
" %8.3f MW overflow in %-5s time %3d (pre-contingency)",
|
||||
" %8.3f MW overflow in %-5s time %3d (pre-contingency, scenario %s)",
|
||||
violation.amount,
|
||||
violation.monitored_line.name,
|
||||
violation.time,
|
||||
sc.name,
|
||||
)
|
||||
else
|
||||
limit = violation.monitored_line.emergency_flow_limit[violation.time]
|
||||
@info @sprintf(
|
||||
" %8.3f MW overflow in %-5s time %3d (outage: line %s)",
|
||||
" %8.3f MW overflow in %-5s time %3d (outage: line %s, scenario %s)",
|
||||
violation.amount,
|
||||
violation.monitored_line.name,
|
||||
violation.time,
|
||||
violation.outage_line.name,
|
||||
sc.name,
|
||||
)
|
||||
end
|
||||
|
||||
@@ -51,7 +56,7 @@ function _enforce_transmission(;
|
||||
t = violation.time
|
||||
flow = @variable(model, base_name = "flow[$fm,$t]")
|
||||
|
||||
v = overflow[violation.monitored_line.name, violation.time]
|
||||
v = overflow[sc.name, violation.monitored_line.name, violation.time]
|
||||
@constraint(model, flow <= limit + v)
|
||||
@constraint(model, -flow <= limit + v)
|
||||
|
||||
@@ -59,23 +64,23 @@ function _enforce_transmission(;
|
||||
@constraint(
|
||||
model,
|
||||
flow == sum(
|
||||
net_injection[b.name, violation.time] *
|
||||
net_injection[sc.name, b.name, violation.time] *
|
||||
isf[violation.monitored_line.offset, b.offset] for
|
||||
b in instance.buses if b.offset > 0
|
||||
b in sc.buses if b.offset > 0
|
||||
)
|
||||
)
|
||||
else
|
||||
@constraint(
|
||||
model,
|
||||
flow == sum(
|
||||
net_injection[b.name, violation.time] * (
|
||||
net_injection[sc.name, b.name, violation.time] * (
|
||||
isf[violation.monitored_line.offset, b.offset] + (
|
||||
lodf[
|
||||
violation.monitored_line.offset,
|
||||
violation.outage_line.offset,
|
||||
] * isf[violation.outage_line.offset, b.offset]
|
||||
)
|
||||
) for b in instance.buses if b.offset > 0
|
||||
) for b in sc.buses if b.offset > 0
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@@ -5,39 +5,35 @@
|
||||
import Base.Threads: @threads
|
||||
|
||||
function _find_violations(
|
||||
model::JuMP.Model;
|
||||
model::JuMP.Model,
|
||||
sc::UnitCommitmentScenario;
|
||||
max_per_line::Int,
|
||||
max_per_period::Int,
|
||||
)
|
||||
instance = model[:instance]
|
||||
net_injection = model[:net_injection]
|
||||
overflow = model[:overflow]
|
||||
length(instance.buses) > 1 || return []
|
||||
length(sc.buses) > 1 || return []
|
||||
violations = []
|
||||
@info "Verifying transmission limits..."
|
||||
time_screening = @elapsed begin
|
||||
non_slack_buses = [b for b in instance.buses if b.offset > 0]
|
||||
net_injection_values = [
|
||||
value(net_injection[b.name, t]) for b in non_slack_buses,
|
||||
t in 1:instance.time
|
||||
]
|
||||
overflow_values = [
|
||||
value(overflow[lm.name, t]) for lm in instance.lines,
|
||||
t in 1:instance.time
|
||||
]
|
||||
violations = UnitCommitment._find_violations(
|
||||
instance = instance,
|
||||
net_injections = net_injection_values,
|
||||
overflow = overflow_values,
|
||||
isf = model[:isf],
|
||||
lodf = model[:lodf],
|
||||
max_per_line = max_per_line,
|
||||
max_per_period = max_per_period,
|
||||
)
|
||||
end
|
||||
@info @sprintf(
|
||||
"Verified transmission limits in %.2f seconds",
|
||||
time_screening
|
||||
|
||||
non_slack_buses = [b for b in sc.buses if b.offset > 0]
|
||||
net_injection_values = [
|
||||
value(net_injection[sc.name, b.name, t]) for b in non_slack_buses,
|
||||
t in 1:instance.time
|
||||
]
|
||||
overflow_values = [
|
||||
value(overflow[sc.name, lm.name, t]) for lm in sc.lines,
|
||||
t in 1:instance.time
|
||||
]
|
||||
violations = UnitCommitment._find_violations(
|
||||
instance = instance,
|
||||
sc = sc,
|
||||
net_injections = net_injection_values,
|
||||
overflow = overflow_values,
|
||||
isf = sc.isf,
|
||||
lodf = sc.lodf,
|
||||
max_per_line = max_per_line,
|
||||
max_per_period = max_per_period,
|
||||
)
|
||||
return violations
|
||||
end
|
||||
@@ -64,6 +60,7 @@ matrix, where L is the number of transmission lines.
|
||||
"""
|
||||
function _find_violations(;
|
||||
instance::UnitCommitmentInstance,
|
||||
sc::UnitCommitmentScenario,
|
||||
net_injections::Array{Float64,2},
|
||||
overflow::Array{Float64,2},
|
||||
isf::Array{Float64,2},
|
||||
@@ -71,8 +68,8 @@ function _find_violations(;
|
||||
max_per_line::Int,
|
||||
max_per_period::Int,
|
||||
)::Array{_Violation,1}
|
||||
B = length(instance.buses) - 1
|
||||
L = length(instance.lines)
|
||||
B = length(sc.buses) - 1
|
||||
L = length(sc.lines)
|
||||
T = instance.time
|
||||
K = nthreads()
|
||||
|
||||
@@ -93,17 +90,17 @@ function _find_violations(;
|
||||
post_v::Array{Float64} = zeros(L, L, K) # post_v[lm, lc, thread]
|
||||
|
||||
normal_limits::Array{Float64,2} = [
|
||||
l.normal_flow_limit[t] + overflow[l.offset, t] for
|
||||
l in instance.lines, t in 1:T
|
||||
l.normal_flow_limit[t] + overflow[l.offset, t] for l in sc.lines,
|
||||
t in 1:T
|
||||
]
|
||||
|
||||
emergency_limits::Array{Float64,2} = [
|
||||
l.emergency_flow_limit[t] + overflow[l.offset, t] for
|
||||
l in instance.lines, t in 1:T
|
||||
l.emergency_flow_limit[t] + overflow[l.offset, t] for l in sc.lines,
|
||||
t in 1:T
|
||||
]
|
||||
|
||||
is_vulnerable::Array{Bool} = zeros(Bool, L)
|
||||
for c in instance.contingencies
|
||||
for c in sc.contingencies
|
||||
is_vulnerable[c.lines[1].offset] = true
|
||||
end
|
||||
|
||||
@@ -144,7 +141,7 @@ function _find_violations(;
|
||||
filters[t],
|
||||
_Violation(
|
||||
time = t,
|
||||
monitored_line = instance.lines[lm],
|
||||
monitored_line = sc.lines[lm],
|
||||
outage_line = nothing,
|
||||
amount = pre_v[lm, k],
|
||||
),
|
||||
@@ -159,8 +156,8 @@ function _find_violations(;
|
||||
filters[t],
|
||||
_Violation(
|
||||
time = t,
|
||||
monitored_line = instance.lines[lm],
|
||||
outage_line = instance.lines[lc],
|
||||
monitored_line = sc.lines[lm],
|
||||
outage_line = sc.lines[lc],
|
||||
amount = post_v[lm, lc, k],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -12,10 +12,15 @@ function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Nothing
|
||||
end
|
||||
initial_time = time()
|
||||
large_gap = false
|
||||
has_transmission = (length(model[:isf]) > 0)
|
||||
if has_transmission && method.two_phase_gap
|
||||
set_gap(1e-2)
|
||||
large_gap = true
|
||||
has_transmission = false
|
||||
for sc in model[:instance].scenarios
|
||||
if length(sc.isf) > 0
|
||||
has_transmission = true
|
||||
end
|
||||
if has_transmission && method.two_phase_gap
|
||||
set_gap(1e-2)
|
||||
large_gap = true
|
||||
end
|
||||
end
|
||||
while true
|
||||
time_elapsed = time() - initial_time
|
||||
@@ -31,13 +36,41 @@ function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Nothing
|
||||
JuMP.set_time_limit_sec(model, time_remaining)
|
||||
@info "Solving MILP..."
|
||||
JuMP.optimize!(model)
|
||||
|
||||
has_transmission || break
|
||||
violations = _find_violations(
|
||||
model,
|
||||
max_per_line = method.max_violations_per_line,
|
||||
max_per_period = method.max_violations_per_period,
|
||||
|
||||
@info "Verifying transmission limits..."
|
||||
time_screening = @elapsed begin
|
||||
violations = []
|
||||
for sc in model[:instance].scenarios
|
||||
push!(
|
||||
violations,
|
||||
_find_violations(
|
||||
model,
|
||||
sc,
|
||||
max_per_line = method.max_violations_per_line,
|
||||
max_per_period = method.max_violations_per_period,
|
||||
),
|
||||
)
|
||||
end
|
||||
end
|
||||
@info @sprintf(
|
||||
"Verified transmission limits in %.2f seconds",
|
||||
time_screening
|
||||
)
|
||||
if isempty(violations)
|
||||
|
||||
violations_found = false
|
||||
for v in violations
|
||||
if !isempty(v)
|
||||
violations_found = true
|
||||
end
|
||||
end
|
||||
|
||||
if violations_found
|
||||
for (i, v) in enumerate(violations)
|
||||
_enforce_transmission(model, v, model[:instance].scenarios[i])
|
||||
end
|
||||
else
|
||||
@info "No violations found"
|
||||
if large_gap
|
||||
large_gap = false
|
||||
@@ -45,8 +78,6 @@ function optimize!(model::JuMP.Model, method::XavQiuWanThi2019.Method)::Nothing
|
||||
else
|
||||
break
|
||||
end
|
||||
else
|
||||
_enforce_transmission(model, violations)
|
||||
end
|
||||
end
|
||||
return
|
||||
|
||||
@@ -2,14 +2,6 @@
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
Lazy constraint solution method described in:
|
||||
|
||||
Xavier, A. S., Qiu, F., Wang, F., & Thimmapuram, P. R. (2019). Transmission
|
||||
constraint filtering in large-scale security-constrained unit commitment.
|
||||
IEEE Transactions on Power Systems, 34(3), 2457-2460.
|
||||
DOI: https://doi.org/10.1109/TPWRS.2019.2892620
|
||||
"""
|
||||
module XavQiuWanThi2019
|
||||
import ..SolutionMethod
|
||||
"""
|
||||
@@ -21,6 +13,13 @@ import ..SolutionMethod
|
||||
max_violations_per_period::Int
|
||||
end
|
||||
|
||||
Lazy constraint solution method described in:
|
||||
|
||||
Xavier, A. S., Qiu, F., Wang, F., & Thimmapuram, P. R. (2019). Transmission
|
||||
constraint filtering in large-scale security-constrained unit commitment.
|
||||
IEEE Transactions on Power Systems, 34(3), 2457-2460.
|
||||
DOI: https://doi.org/10.1109/TPWRS.2019.2892620
|
||||
|
||||
Fields
|
||||
------
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
function optimize!(model::JuMP.Model)::Nothing
|
||||
optimize!(model::JuMP.Model)::Nothing
|
||||
|
||||
Solve the given unit commitment model. Unlike JuMP.optimize!, this uses more
|
||||
Solve the given unit commitment model. Unlike `JuMP.optimize!`, this uses more
|
||||
advanced methods to accelerate the solution process and to enforce transmission
|
||||
and N-1 security constraints.
|
||||
"""
|
||||
|
||||
@@ -2,36 +2,58 @@
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
solution(model::JuMP.Model)::OrderedDict
|
||||
|
||||
Extracts the optimal solution from the UC.jl model. The model must be solved beforehand.
|
||||
|
||||
# Example
|
||||
|
||||
```julia
|
||||
UnitCommitment.optimize!(model)
|
||||
solution = UnitCommitment.solution(model)
|
||||
```
|
||||
"""
|
||||
function solution(model::JuMP.Model)::OrderedDict
|
||||
instance, T = model[:instance], model[:instance].time
|
||||
function timeseries(vars, collection)
|
||||
return OrderedDict(
|
||||
b.name => [round(value(vars[b.name, t]), digits = 5) for t in 1:T]
|
||||
for b in collection
|
||||
)
|
||||
function timeseries(vars, collection; sc = nothing)
|
||||
if sc === nothing
|
||||
return OrderedDict(
|
||||
b.name =>
|
||||
[round(value(vars[b.name, t]), digits = 5) for t in 1:T] for
|
||||
b in collection
|
||||
)
|
||||
else
|
||||
return OrderedDict(
|
||||
b.name => [
|
||||
round(value(vars[sc.name, b.name, t]), digits = 5) for
|
||||
t in 1:T
|
||||
] for b in collection
|
||||
)
|
||||
end
|
||||
end
|
||||
function production_cost(g)
|
||||
function production_cost(g, sc)
|
||||
return [
|
||||
value(model[:is_on][g.name, t]) * g.min_power_cost[t] + sum(
|
||||
Float64[
|
||||
value(model[:segprod][g.name, t, k]) *
|
||||
value(model[:segprod][sc.name, g.name, t, k]) *
|
||||
g.cost_segments[k].cost[t] for
|
||||
k in 1:length(g.cost_segments)
|
||||
],
|
||||
) for t in 1:T
|
||||
]
|
||||
end
|
||||
function production(g)
|
||||
function production(g, sc)
|
||||
return [
|
||||
value(model[:is_on][g.name, t]) * g.min_power[t] + sum(
|
||||
Float64[
|
||||
value(model[:segprod][g.name, t, k]) for
|
||||
value(model[:segprod][sc.name, g.name, t, k]) for
|
||||
k in 1:length(g.cost_segments)
|
||||
],
|
||||
) for t in 1:T
|
||||
]
|
||||
end
|
||||
function startup_cost(g)
|
||||
function startup_cost(g, sc)
|
||||
S = length(g.startup_categories)
|
||||
return [
|
||||
sum(
|
||||
@@ -41,66 +63,87 @@ function solution(model::JuMP.Model)::OrderedDict
|
||||
]
|
||||
end
|
||||
sol = OrderedDict()
|
||||
sol["Production (MW)"] =
|
||||
OrderedDict(g.name => production(g) for g in instance.units)
|
||||
sol["Production cost (\$)"] =
|
||||
OrderedDict(g.name => production_cost(g) for g in instance.units)
|
||||
sol["Startup cost (\$)"] =
|
||||
OrderedDict(g.name => startup_cost(g) for g in instance.units)
|
||||
sol["Is on"] = timeseries(model[:is_on], instance.units)
|
||||
sol["Switch on"] = timeseries(model[:switch_on], instance.units)
|
||||
sol["Switch off"] = timeseries(model[:switch_off], instance.units)
|
||||
sol["Net injection (MW)"] =
|
||||
timeseries(model[:net_injection], instance.buses)
|
||||
sol["Load curtail (MW)"] = timeseries(model[:curtail], instance.buses)
|
||||
if !isempty(instance.lines)
|
||||
sol["Line overflow (MW)"] = timeseries(model[:overflow], instance.lines)
|
||||
for sc in instance.scenarios
|
||||
sol[sc.name] = OrderedDict()
|
||||
if !isempty(sc.thermal_units)
|
||||
sol[sc.name]["Thermal production (MW)"] = OrderedDict(
|
||||
g.name => production(g, sc) for g in sc.thermal_units
|
||||
)
|
||||
sol[sc.name]["Thermal production cost (\$)"] = OrderedDict(
|
||||
g.name => production_cost(g, sc) for g in sc.thermal_units
|
||||
)
|
||||
sol[sc.name]["Startup cost (\$)"] = OrderedDict(
|
||||
g.name => startup_cost(g, sc) for g in sc.thermal_units
|
||||
)
|
||||
sol[sc.name]["Is on"] = timeseries(model[:is_on], sc.thermal_units)
|
||||
sol[sc.name]["Switch on"] =
|
||||
timeseries(model[:switch_on], sc.thermal_units)
|
||||
sol[sc.name]["Switch off"] =
|
||||
timeseries(model[:switch_off], sc.thermal_units)
|
||||
sol[sc.name]["Net injection (MW)"] =
|
||||
timeseries(model[:net_injection], sc.buses, sc = sc)
|
||||
sol[sc.name]["Load curtail (MW)"] =
|
||||
timeseries(model[:curtail], sc.buses, sc = sc)
|
||||
end
|
||||
if !isempty(sc.lines)
|
||||
sol[sc.name]["Line overflow (MW)"] =
|
||||
timeseries(model[:overflow], sc.lines, sc = sc)
|
||||
end
|
||||
if !isempty(sc.price_sensitive_loads)
|
||||
sol[sc.name]["Price-sensitive loads (MW)"] =
|
||||
timeseries(model[:loads], sc.price_sensitive_loads, sc = sc)
|
||||
end
|
||||
if !isempty(sc.profiled_units)
|
||||
sol[sc.name]["Profiled production (MW)"] =
|
||||
timeseries(model[:prod_profiled], sc.profiled_units, sc = sc)
|
||||
sol[sc.name]["Profiled production cost (\$)"] = OrderedDict(
|
||||
pu.name => [
|
||||
value(model[:prod_profiled][sc.name, pu.name, t]) *
|
||||
pu.cost[t] for t in 1:instance.time
|
||||
] for pu in sc.profiled_units
|
||||
)
|
||||
end
|
||||
sol[sc.name]["Spinning reserve (MW)"] = OrderedDict(
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
value(model[:reserve][sc.name, r.name, g.name, t]) for t in 1:instance.time
|
||||
] for g in r.thermal_units
|
||||
) for r in sc.reserves if r.type == "spinning"
|
||||
)
|
||||
sol[sc.name]["Spinning reserve shortfall (MW)"] = OrderedDict(
|
||||
r.name => [
|
||||
value(model[:reserve_shortfall][sc.name, r.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for r in sc.reserves if r.type == "spinning"
|
||||
)
|
||||
sol[sc.name]["Up-flexiramp (MW)"] = OrderedDict(
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
value(model[:upflexiramp][sc.name, r.name, g.name, t]) for t in 1:instance.time
|
||||
] for g in r.thermal_units
|
||||
) for r in sc.reserves if r.type == "flexiramp"
|
||||
)
|
||||
sol[sc.name]["Up-flexiramp shortfall (MW)"] = OrderedDict(
|
||||
r.name => [
|
||||
value(model[:upflexiramp_shortfall][sc.name, r.name, t]) for t in 1:instance.time
|
||||
] for r in sc.reserves if r.type == "flexiramp"
|
||||
)
|
||||
sol[sc.name]["Down-flexiramp (MW)"] = OrderedDict(
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
value(model[:dwflexiramp][sc.name, r.name, g.name, t]) for t in 1:instance.time
|
||||
] for g in r.thermal_units
|
||||
) for r in sc.reserves if r.type == "flexiramp"
|
||||
)
|
||||
sol[sc.name]["Down-flexiramp shortfall (MW)"] = OrderedDict(
|
||||
r.name => [
|
||||
value(model[:dwflexiramp_shortfall][sc.name, r.name, t]) for t in 1:instance.time
|
||||
] for r in sc.reserves if r.type == "flexiramp"
|
||||
)
|
||||
end
|
||||
if !isempty(instance.price_sensitive_loads)
|
||||
sol["Price-sensitive loads (MW)"] =
|
||||
timeseries(model[:loads], instance.price_sensitive_loads)
|
||||
if length(instance.scenarios) == 1
|
||||
return first(values(sol))
|
||||
else
|
||||
return sol
|
||||
end
|
||||
sol["Spinning reserve (MW)"] = OrderedDict(
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
value(model[:reserve][r.name, g.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for g in r.units
|
||||
) for r in instance.reserves if r.type == "spinning"
|
||||
)
|
||||
sol["Spinning reserve shortfall (MW)"] = OrderedDict(
|
||||
r.name => [
|
||||
value(model[:reserve_shortfall][r.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for r in instance.reserves if r.type == "spinning"
|
||||
)
|
||||
sol["Up-flexiramp (MW)"] = OrderedDict(
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
value(model[:upflexiramp][r.name, g.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for g in r.units
|
||||
) for r in instance.reserves if r.type == "flexiramp"
|
||||
)
|
||||
sol["Up-flexiramp shortfall (MW)"] = OrderedDict(
|
||||
r.name => [
|
||||
value(model[:upflexiramp_shortfall][r.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for r in instance.reserves if r.type == "flexiramp"
|
||||
)
|
||||
sol["Down-flexiramp (MW)"] = OrderedDict(
|
||||
r.name => OrderedDict(
|
||||
g.name => [
|
||||
value(model[:dwflexiramp][r.name, g.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for g in r.units
|
||||
) for r in instance.reserves if r.type == "flexiramp"
|
||||
)
|
||||
sol["Down-flexiramp shortfall (MW)"] = OrderedDict(
|
||||
r.name => [
|
||||
value(model[:upflexiramp_shortfall][r.name, t]) for
|
||||
t in 1:instance.time
|
||||
] for r in instance.reserves if r.type == "flexiramp"
|
||||
)
|
||||
return sol
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
function set_warm_start!(model::JuMP.Model, solution::AbstractDict)::Nothing
|
||||
instance, T = model[:instance], model[:instance].time
|
||||
is_on = model[:is_on]
|
||||
for g in instance.units
|
||||
for g in instance.thermal_units
|
||||
for t in 1:T
|
||||
JuMP.set_start_value(is_on[g.name, t], solution["Is on"][g.name][t])
|
||||
JuMP.set_start_value(
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
write(filename::AbstractString, solution::AbstractDict)::Nothing
|
||||
|
||||
Write the given solution to a JSON file.
|
||||
|
||||
# Example
|
||||
|
||||
```julia
|
||||
solution = UnitCommitment.solution(model)
|
||||
UnitCommitment.write("/tmp/output.json", solution)
|
||||
```
|
||||
"""
|
||||
function write(filename::AbstractString, solution::AbstractDict)::Nothing
|
||||
open(filename, "w") do file
|
||||
return JSON.print(file, solution, 2)
|
||||
|
||||
@@ -5,36 +5,41 @@
|
||||
using JuMP
|
||||
|
||||
"""
|
||||
generate_initial_conditions!(instance, optimizer)
|
||||
generate_initial_conditions!(sc, optimizer)
|
||||
|
||||
Generates feasible initial conditions for the given instance, by constructing
|
||||
Generates feasible initial conditions for the given scenario, by constructing
|
||||
and solving a single-period mixed-integer optimization problem, using the given
|
||||
optimizer. The instance is modified in-place.
|
||||
optimizer. The scenario is modified in-place.
|
||||
"""
|
||||
function generate_initial_conditions!(
|
||||
instance::UnitCommitmentInstance,
|
||||
sc::UnitCommitmentScenario,
|
||||
optimizer,
|
||||
)::Nothing
|
||||
G = instance.units
|
||||
B = instance.buses
|
||||
G = sc.thermal_units
|
||||
B = sc.buses
|
||||
PU = sc.profiled_units
|
||||
t = 1
|
||||
mip = JuMP.Model(optimizer)
|
||||
|
||||
# Decision variables
|
||||
@variable(mip, x[G], Bin)
|
||||
@variable(mip, p[G] >= 0)
|
||||
@variable(mip, pu[PU])
|
||||
|
||||
# Constraint: Minimum power
|
||||
@constraint(mip, min_power[g in G], p[g] >= g.min_power[t] * x[g])
|
||||
@constraint(mip, pu_min_power[k in PU], pu[k] >= k.min_power[t])
|
||||
|
||||
# Constraint: Maximum power
|
||||
@constraint(mip, max_power[g in G], p[g] <= g.max_power[t] * x[g])
|
||||
@constraint(mip, pu_max_power[k in PU], pu[k] <= k.max_power[t])
|
||||
|
||||
# Constraint: Production equals demand
|
||||
@constraint(
|
||||
mip,
|
||||
power_balance,
|
||||
sum(b.load[t] for b in B) == sum(p[g] for g in G)
|
||||
sum(b.load[t] for b in B) ==
|
||||
sum(p[g] for g in G) + sum(pu[k] for k in PU)
|
||||
)
|
||||
|
||||
# Constraint: Must run
|
||||
@@ -58,7 +63,12 @@ function generate_initial_conditions!(
|
||||
return c / mw
|
||||
end
|
||||
end
|
||||
@objective(mip, Min, sum(p[g] * cost_slope(g) for g in G))
|
||||
@objective(
|
||||
mip,
|
||||
Min,
|
||||
sum(p[g] * cost_slope(g) for g in G) +
|
||||
sum(pu[k] * k.cost[t] for k in PU)
|
||||
)
|
||||
|
||||
JuMP.optimize!(mip)
|
||||
|
||||
|
||||
@@ -2,17 +2,11 @@
|
||||
# Copyright (C) 2020-2021, UChicago Argonne, LLC. All rights reserved.
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
Methods described in:
|
||||
|
||||
Xavier, Álinson S., Feng Qiu, and Shabbir Ahmed. "Learning to solve
|
||||
large-scale security-constrained unit commitment problems." INFORMS
|
||||
Journal on Computing 33.2 (2021): 739-756. DOI: 10.1287/ijoc.2020.0976
|
||||
"""
|
||||
module XavQiuAhm2021
|
||||
|
||||
using Distributions
|
||||
import ..UnitCommitmentInstance
|
||||
import ..UnitCommitmentScenario
|
||||
|
||||
"""
|
||||
struct Randomization
|
||||
@@ -55,6 +49,13 @@ load profile, as follows:
|
||||
The default parameters were obtained based on an analysis of publicly available
|
||||
bid and hourly data from PJM, corresponding to the month of January, 2017. For
|
||||
more details, see Section 4.2 of the paper.
|
||||
|
||||
# References
|
||||
|
||||
- **Xavier, Álinson S., Feng Qiu, and Shabbir Ahmed.** *"Learning to solve
|
||||
large-scale security-constrained unit commitment problems."* INFORMS Journal
|
||||
on Computing 33.2 (2021): 739-756. DOI: 10.1287/ijoc.2020.0976
|
||||
|
||||
"""
|
||||
Base.@kwdef struct Randomization
|
||||
cost = Uniform(0.95, 1.05)
|
||||
@@ -119,10 +120,10 @@ end
|
||||
|
||||
function _randomize_costs(
|
||||
rng,
|
||||
instance::UnitCommitmentInstance,
|
||||
sc::UnitCommitmentScenario,
|
||||
distribution,
|
||||
)::Nothing
|
||||
for unit in instance.units
|
||||
for unit in sc.thermal_units
|
||||
α = rand(rng, distribution)
|
||||
unit.min_power_cost *= α
|
||||
for k in unit.cost_segments
|
||||
@@ -132,22 +133,24 @@ function _randomize_costs(
|
||||
s.cost *= α
|
||||
end
|
||||
end
|
||||
for pu in sc.profiled_units
|
||||
α = rand(rng, distribution)
|
||||
pu.cost *= α
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
function _randomize_load_share(
|
||||
rng,
|
||||
instance::UnitCommitmentInstance,
|
||||
sc::UnitCommitmentScenario,
|
||||
distribution,
|
||||
)::Nothing
|
||||
α = rand(rng, distribution, length(instance.buses))
|
||||
for t in 1:instance.time
|
||||
total = sum(bus.load[t] for bus in instance.buses)
|
||||
den = sum(
|
||||
bus.load[t] / total * α[i] for
|
||||
(i, bus) in enumerate(instance.buses)
|
||||
)
|
||||
for (i, bus) in enumerate(instance.buses)
|
||||
α = rand(rng, distribution, length(sc.buses))
|
||||
for t in 1:sc.time
|
||||
total = sum(bus.load[t] for bus in sc.buses)
|
||||
den =
|
||||
sum(bus.load[t] / total * α[i] for (i, bus) in enumerate(sc.buses))
|
||||
for (i, bus) in enumerate(sc.buses)
|
||||
bus.load[t] *= α[i] / den
|
||||
end
|
||||
end
|
||||
@@ -156,12 +159,12 @@ end
|
||||
|
||||
function _randomize_load_profile(
|
||||
rng,
|
||||
instance::UnitCommitmentInstance,
|
||||
sc::UnitCommitmentScenario,
|
||||
params::Randomization,
|
||||
)::Nothing
|
||||
# Generate new system load
|
||||
system_load = [1.0]
|
||||
for t in 2:instance.time
|
||||
for t in 2:sc.time
|
||||
idx = (t - 1) % length(params.load_profile_mu) + 1
|
||||
gamma = rand(
|
||||
rng,
|
||||
@@ -169,14 +172,14 @@ function _randomize_load_profile(
|
||||
)
|
||||
push!(system_load, system_load[t-1] * gamma)
|
||||
end
|
||||
capacity = sum(maximum(u.max_power) for u in instance.units)
|
||||
capacity = sum(maximum(u.max_power) for u in sc.thermal_units)
|
||||
peak_load = rand(rng, params.peak_load) * capacity
|
||||
system_load = system_load ./ maximum(system_load) .* peak_load
|
||||
|
||||
# Scale bus loads to match the new system load
|
||||
prev_system_load = sum(b.load for b in instance.buses)
|
||||
for b in instance.buses
|
||||
for t in 1:instance.time
|
||||
prev_system_load = sum(b.load for b in sc.buses)
|
||||
for b in sc.buses
|
||||
for t in 1:sc.time
|
||||
b.load[t] *= system_load[t] / prev_system_load[t]
|
||||
end
|
||||
end
|
||||
@@ -200,16 +203,54 @@ function randomize!(
|
||||
method::XavQiuAhm2021.Randomization;
|
||||
rng = MersenneTwister(),
|
||||
)::Nothing
|
||||
if method.randomize_costs
|
||||
XavQiuAhm2021._randomize_costs(rng, instance, method.cost)
|
||||
end
|
||||
if method.randomize_load_share
|
||||
XavQiuAhm2021._randomize_load_share(rng, instance, method.load_share)
|
||||
end
|
||||
if method.randomize_load_profile
|
||||
XavQiuAhm2021._randomize_load_profile(rng, instance, method)
|
||||
for sc in instance.scenarios
|
||||
randomize!(sc, method; rng)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
function randomize!(
|
||||
sc::UnitCommitment.UnitCommitmentScenario,
|
||||
method::XavQiuAhm2021.Randomization;
|
||||
rng = MersenneTwister(),
|
||||
)::Nothing
|
||||
if method.randomize_costs
|
||||
XavQiuAhm2021._randomize_costs(rng, sc, method.cost)
|
||||
end
|
||||
if method.randomize_load_share
|
||||
XavQiuAhm2021._randomize_load_share(rng, sc, method.load_share)
|
||||
end
|
||||
if method.randomize_load_profile
|
||||
XavQiuAhm2021._randomize_load_profile(rng, sc, method)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
"""
|
||||
function randomize!(
|
||||
instance::UnitCommitmentInstance;
|
||||
method = UnitCommitment.XavQiuAhm2021.Randomization();
|
||||
rng = MersenneTwister(),
|
||||
)::Nothing
|
||||
|
||||
Randomizes instance parameters according to the provided randomization method.
|
||||
|
||||
# Example
|
||||
|
||||
```julia
|
||||
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
||||
UnitCommitment.randomize!(instance)
|
||||
model = UnitCommitment.build_model(; instance)
|
||||
```
|
||||
|
||||
"""
|
||||
function randomize!(
|
||||
instance::UnitCommitment.UnitCommitmentInstance;
|
||||
method = XavQiuAhm2021.Randomization(),
|
||||
rng = MersenneTwister(),
|
||||
)::Nothing
|
||||
randomize!(instance, method; rng)
|
||||
return
|
||||
end
|
||||
|
||||
export randomize!
|
||||
|
||||
@@ -12,10 +12,11 @@ conditions are also not modified.
|
||||
Example
|
||||
-------
|
||||
|
||||
# Build a 2-hour UC instance
|
||||
instance = UnitCommitment.read_benchmark("test/case14")
|
||||
modified = UnitCommitment.slice(instance, 1:2)
|
||||
|
||||
```julia
|
||||
# Build a 2-hour UC instance
|
||||
instance = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
||||
modified = UnitCommitment.slice(instance, 1:2)
|
||||
```
|
||||
"""
|
||||
function slice(
|
||||
instance::UnitCommitmentInstance,
|
||||
@@ -23,31 +24,38 @@ function slice(
|
||||
)::UnitCommitmentInstance
|
||||
modified = deepcopy(instance)
|
||||
modified.time = length(range)
|
||||
modified.power_balance_penalty = modified.power_balance_penalty[range]
|
||||
for r in modified.reserves
|
||||
r.amount = r.amount[range]
|
||||
end
|
||||
for u in modified.units
|
||||
u.max_power = u.max_power[range]
|
||||
u.min_power = u.min_power[range]
|
||||
u.must_run = u.must_run[range]
|
||||
u.min_power_cost = u.min_power_cost[range]
|
||||
for s in u.cost_segments
|
||||
s.mw = s.mw[range]
|
||||
s.cost = s.cost[range]
|
||||
for sc in modified.scenarios
|
||||
sc.power_balance_penalty = sc.power_balance_penalty[range]
|
||||
for r in sc.reserves
|
||||
r.amount = r.amount[range]
|
||||
end
|
||||
for u in sc.thermal_units
|
||||
u.max_power = u.max_power[range]
|
||||
u.min_power = u.min_power[range]
|
||||
u.must_run = u.must_run[range]
|
||||
u.min_power_cost = u.min_power_cost[range]
|
||||
for s in u.cost_segments
|
||||
s.mw = s.mw[range]
|
||||
s.cost = s.cost[range]
|
||||
end
|
||||
end
|
||||
for pu in sc.profiled_units
|
||||
pu.max_power = pu.max_power[range]
|
||||
pu.min_power = pu.min_power[range]
|
||||
pu.cost = pu.cost[range]
|
||||
end
|
||||
for b in sc.buses
|
||||
b.load = b.load[range]
|
||||
end
|
||||
for l in sc.lines
|
||||
l.normal_flow_limit = l.normal_flow_limit[range]
|
||||
l.emergency_flow_limit = l.emergency_flow_limit[range]
|
||||
l.flow_limit_penalty = l.flow_limit_penalty[range]
|
||||
end
|
||||
for ps in sc.price_sensitive_loads
|
||||
ps.demand = ps.demand[range]
|
||||
ps.revenue = ps.revenue[range]
|
||||
end
|
||||
end
|
||||
for b in modified.buses
|
||||
b.load = b.load[range]
|
||||
end
|
||||
for l in modified.lines
|
||||
l.normal_flow_limit = l.normal_flow_limit[range]
|
||||
l.emergency_flow_limit = l.emergency_flow_limit[range]
|
||||
l.flow_limit_penalty = l.flow_limit_penalty[range]
|
||||
end
|
||||
for ps in modified.price_sensitive_loads
|
||||
ps.demand = ps.demand[range]
|
||||
ps.revenue = ps.revenue[range]
|
||||
end
|
||||
return modified
|
||||
end
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
# Released under the modified BSD license. See COPYING.md for more details.
|
||||
|
||||
"""
|
||||
repair!(instance)
|
||||
repair!(sc)
|
||||
|
||||
Verifies that the given unit commitment instance is valid and automatically
|
||||
Verifies that the given unit commitment scenario is valid and automatically
|
||||
fixes some validation errors if possible, issuing a warning for each error
|
||||
found. If a validation error cannot be automatically fixed, issues an
|
||||
exception.
|
||||
|
||||
Returns the number of validation errors found.
|
||||
"""
|
||||
function repair!(instance::UnitCommitmentInstance)::Int
|
||||
function repair!(sc::UnitCommitmentScenario)::Int
|
||||
n_errors = 0
|
||||
|
||||
for g in instance.units
|
||||
for g in sc.thermal_units
|
||||
|
||||
# Startup costs and delays must be increasing
|
||||
for s in 2:length(g.startup_categories)
|
||||
@@ -38,7 +38,7 @@ function repair!(instance::UnitCommitmentInstance)::Int
|
||||
end
|
||||
end
|
||||
|
||||
for t in 1:instance.time
|
||||
for t in 1:sc.time
|
||||
# Production cost curve should be convex
|
||||
for k in 2:length(g.cost_segments)
|
||||
cost = g.cost_segments[k].cost[t]
|
||||
|
||||
@@ -28,6 +28,8 @@ function validate(
|
||||
instance::UnitCommitmentInstance,
|
||||
solution::Union{Dict,OrderedDict},
|
||||
)::Bool
|
||||
"Thermal production (MW)" ∈ keys(solution) ?
|
||||
solution = Dict("s1" => solution) : nothing
|
||||
err_count = 0
|
||||
err_count += _validate_units(instance, solution)
|
||||
err_count += _validate_reserve_and_demand(instance, solution)
|
||||
@@ -42,358 +44,407 @@ end
|
||||
|
||||
function _validate_units(instance::UnitCommitmentInstance, solution; tol = 0.01)
|
||||
err_count = 0
|
||||
|
||||
for unit in instance.units
|
||||
production = solution["Production (MW)"][unit.name]
|
||||
reserve = [0.0 for _ in 1:instance.time]
|
||||
spinning_reserves = [r for r in unit.reserves if r.type == "spinning"]
|
||||
if !isempty(spinning_reserves)
|
||||
reserve += sum(
|
||||
solution["Spinning reserve (MW)"][r.name][unit.name] for
|
||||
r in spinning_reserves
|
||||
)
|
||||
end
|
||||
actual_production_cost = solution["Production cost (\$)"][unit.name]
|
||||
actual_startup_cost = solution["Startup cost (\$)"][unit.name]
|
||||
is_on = bin(solution["Is on"][unit.name])
|
||||
|
||||
for t in 1:instance.time
|
||||
# Auxiliary variables
|
||||
if t == 1
|
||||
is_starting_up = (unit.initial_status < 0) && is_on[t]
|
||||
is_shutting_down = (unit.initial_status > 0) && !is_on[t]
|
||||
ramp_up =
|
||||
max(0, production[t] + reserve[t] - unit.initial_power)
|
||||
ramp_down = max(0, unit.initial_power - production[t])
|
||||
else
|
||||
is_starting_up = !is_on[t-1] && is_on[t]
|
||||
is_shutting_down = is_on[t-1] && !is_on[t]
|
||||
ramp_up = max(0, production[t] + reserve[t] - production[t-1])
|
||||
ramp_down = max(0, production[t-1] - production[t])
|
||||
for sc in instance.scenarios
|
||||
for unit in sc.thermal_units
|
||||
production = solution[sc.name]["Thermal production (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[sc.name]["Spinning reserve (MW)"][r.name][unit.name]
|
||||
for r in spinning_reserves
|
||||
)
|
||||
end
|
||||
actual_production_cost =
|
||||
solution[sc.name]["Thermal production cost (\$)"][unit.name]
|
||||
actual_startup_cost =
|
||||
solution[sc.name]["Startup cost (\$)"][unit.name]
|
||||
is_on = bin(solution[sc.name]["Is on"][unit.name])
|
||||
|
||||
# Compute production costs
|
||||
production_cost, startup_cost = 0, 0
|
||||
if is_on[t]
|
||||
production_cost += unit.min_power_cost[t]
|
||||
residual = max(0, production[t] - unit.min_power[t])
|
||||
for s in unit.cost_segments
|
||||
cleared = min(residual, s.mw[t])
|
||||
production_cost += cleared * s.cost[t]
|
||||
residual = max(0, residual - s.mw[t])
|
||||
for t in 1:instance.time
|
||||
# Auxiliary variables
|
||||
if t == 1
|
||||
is_starting_up = (unit.initial_status < 0) && is_on[t]
|
||||
is_shutting_down = (unit.initial_status > 0) && !is_on[t]
|
||||
ramp_up =
|
||||
max(0, production[t] + reserve[t] - unit.initial_power)
|
||||
ramp_down = max(0, unit.initial_power - production[t])
|
||||
else
|
||||
is_starting_up = !is_on[t-1] && is_on[t]
|
||||
is_shutting_down = is_on[t-1] && !is_on[t]
|
||||
ramp_up =
|
||||
max(0, production[t] + reserve[t] - production[t-1])
|
||||
ramp_down = max(0, production[t-1] - production[t])
|
||||
end
|
||||
end
|
||||
|
||||
# Production should be non-negative
|
||||
if production[t] < -tol
|
||||
@error @sprintf(
|
||||
"Unit %s produces negative amount of power at time %d (%.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
production[t]
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
# Compute production costs
|
||||
production_cost, startup_cost = 0, 0
|
||||
if is_on[t]
|
||||
production_cost += unit.min_power_cost[t]
|
||||
residual = max(0, production[t] - unit.min_power[t])
|
||||
for s in unit.cost_segments
|
||||
cleared = min(residual, s.mw[t])
|
||||
production_cost += cleared * s.cost[t]
|
||||
residual = max(0, residual - s.mw[t])
|
||||
end
|
||||
end
|
||||
|
||||
# Verify must-run
|
||||
if !is_on[t] && unit.must_run[t]
|
||||
@error @sprintf(
|
||||
"Must-run unit %s is offline at time %d",
|
||||
unit.name,
|
||||
t
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
# Production should be non-negative
|
||||
if production[t] < -tol
|
||||
@error @sprintf(
|
||||
"Unit %s produces negative amount of power at time %d (%.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
production[t]
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Verify reserve eligibility
|
||||
for r in instance.reserves
|
||||
if r.type == "spinning"
|
||||
if unit ∉ r.units &&
|
||||
(unit in keys(solution["Spinning reserve (MW)"][r.name]))
|
||||
# Verify must-run
|
||||
if !is_on[t] && unit.must_run[t]
|
||||
@error @sprintf(
|
||||
"Must-run unit %s is offline at time %d",
|
||||
unit.name,
|
||||
t
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Verify reserve eligibility
|
||||
for r in sc.reserves
|
||||
if r.type == "spinning"
|
||||
if unit ∉ r.thermal_units && (
|
||||
unit in keys(
|
||||
solution[sc.name]["Spinning reserve (MW)"][r.name],
|
||||
)
|
||||
)
|
||||
@error @sprintf(
|
||||
"Unit %s is not eligible to provide reserve %s",
|
||||
unit.name,
|
||||
r.name,
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# If unit is on, must produce at least its minimum power
|
||||
if is_on[t] && (production[t] < unit.min_power[t] - tol)
|
||||
@error @sprintf(
|
||||
"Unit %s produces below its minimum limit at time %d (%.2f < %.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
production[t],
|
||||
unit.min_power[t]
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# If unit is on, must produce at most its maximum power
|
||||
if is_on[t] &&
|
||||
(production[t] + reserve[t] > unit.max_power[t] + tol)
|
||||
@error @sprintf(
|
||||
"Unit %s produces above its maximum limit at time %d (%.2f + %.2f> %.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
production[t],
|
||||
reserve[t],
|
||||
unit.max_power[t]
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# If unit is off, must produce zero
|
||||
if !is_on[t] && production[t] + reserve[t] > tol
|
||||
@error @sprintf(
|
||||
"Unit %s produces power at time %d while off (%.2f + %.2f > 0)",
|
||||
unit.name,
|
||||
t,
|
||||
production[t],
|
||||
reserve[t],
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Startup limit
|
||||
if is_starting_up && (ramp_up > unit.startup_limit + tol)
|
||||
@error @sprintf(
|
||||
"Unit %s exceeds startup limit at time %d (%.2f > %.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
ramp_up,
|
||||
unit.startup_limit
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Shutdown limit
|
||||
if is_shutting_down && (ramp_down > unit.shutdown_limit + tol)
|
||||
@error @sprintf(
|
||||
"Unit %s exceeds shutdown limit at time %d (%.2f > %.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
ramp_down,
|
||||
unit.shutdown_limit
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Ramp-up limit
|
||||
if !is_starting_up &&
|
||||
!is_shutting_down &&
|
||||
(ramp_up > unit.ramp_up_limit + tol)
|
||||
@error @sprintf(
|
||||
"Unit %s exceeds ramp up limit at time %d (%.2f > %.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
ramp_up,
|
||||
unit.ramp_up_limit
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Ramp-down limit
|
||||
if !is_starting_up &&
|
||||
!is_shutting_down &&
|
||||
(ramp_down > unit.ramp_down_limit + tol)
|
||||
@error @sprintf(
|
||||
"Unit %s exceeds ramp down limit at time %d (%.2f > %.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
ramp_down,
|
||||
unit.ramp_down_limit
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Verify startup costs & minimum downtime
|
||||
if is_starting_up
|
||||
|
||||
# Calculate how much time the unit has been offline
|
||||
time_down = 0
|
||||
for k in 1:(t-1)
|
||||
if !is_on[t-k]
|
||||
time_down += 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
if (t == time_down + 1) && (unit.initial_status < 0)
|
||||
time_down -= unit.initial_status
|
||||
end
|
||||
|
||||
# Calculate startup costs
|
||||
for c in unit.startup_categories
|
||||
if time_down >= c.delay
|
||||
startup_cost = c.cost
|
||||
end
|
||||
end
|
||||
|
||||
# Check minimum downtime
|
||||
if time_down < unit.min_downtime
|
||||
@error @sprintf(
|
||||
"Unit %s is not eligible to provide reserve %s",
|
||||
"Unit %s violates minimum downtime at time %d",
|
||||
unit.name,
|
||||
r.name,
|
||||
t
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# If unit is on, must produce at least its minimum power
|
||||
if is_on[t] && (production[t] < unit.min_power[t] - tol)
|
||||
@error @sprintf(
|
||||
"Unit %s produces below its minimum limit at time %d (%.2f < %.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
production[t],
|
||||
unit.min_power[t]
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
# Verify minimum uptime
|
||||
if is_shutting_down
|
||||
|
||||
# If unit is on, must produce at most its maximum power
|
||||
if is_on[t] &&
|
||||
(production[t] + reserve[t] > unit.max_power[t] + tol)
|
||||
@error @sprintf(
|
||||
"Unit %s produces above its maximum limit at time %d (%.2f + %.2f> %.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
production[t],
|
||||
reserve[t],
|
||||
unit.max_power[t]
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# If unit is off, must produce zero
|
||||
if !is_on[t] && production[t] + reserve[t] > tol
|
||||
@error @sprintf(
|
||||
"Unit %s produces power at time %d while off (%.2f + %.2f > 0)",
|
||||
unit.name,
|
||||
t,
|
||||
production[t],
|
||||
reserve[t],
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Startup limit
|
||||
if is_starting_up && (ramp_up > unit.startup_limit + tol)
|
||||
@error @sprintf(
|
||||
"Unit %s exceeds startup limit at time %d (%.2f > %.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
ramp_up,
|
||||
unit.startup_limit
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Shutdown limit
|
||||
if is_shutting_down && (ramp_down > unit.shutdown_limit + tol)
|
||||
@error @sprintf(
|
||||
"Unit %s exceeds shutdown limit at time %d (%.2f > %.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
ramp_down,
|
||||
unit.shutdown_limit
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Ramp-up limit
|
||||
if !is_starting_up &&
|
||||
!is_shutting_down &&
|
||||
(ramp_up > unit.ramp_up_limit + tol)
|
||||
@error @sprintf(
|
||||
"Unit %s exceeds ramp up limit at time %d (%.2f > %.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
ramp_up,
|
||||
unit.ramp_up_limit
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Ramp-down limit
|
||||
if !is_starting_up &&
|
||||
!is_shutting_down &&
|
||||
(ramp_down > unit.ramp_down_limit + tol)
|
||||
@error @sprintf(
|
||||
"Unit %s exceeds ramp down limit at time %d (%.2f > %.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
ramp_down,
|
||||
unit.ramp_down_limit
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Verify startup costs & minimum downtime
|
||||
if is_starting_up
|
||||
|
||||
# Calculate how much time the unit has been offline
|
||||
time_down = 0
|
||||
for k in 1:(t-1)
|
||||
if !is_on[t-k]
|
||||
time_down += 1
|
||||
else
|
||||
break
|
||||
# Calculate how much time the unit has been online
|
||||
time_up = 0
|
||||
for k in 1:(t-1)
|
||||
if is_on[t-k]
|
||||
time_up += 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
if (t == time_up + 1) && (unit.initial_status > 0)
|
||||
time_up += unit.initial_status
|
||||
end
|
||||
end
|
||||
if (t == time_down + 1) && (unit.initial_status < 0)
|
||||
time_down -= unit.initial_status
|
||||
end
|
||||
|
||||
# Calculate startup costs
|
||||
for c in unit.startup_categories
|
||||
if time_down >= c.delay
|
||||
startup_cost = c.cost
|
||||
# Check minimum uptime
|
||||
if time_up < unit.min_uptime
|
||||
@error @sprintf(
|
||||
"Unit %s violates minimum uptime at time %d",
|
||||
unit.name,
|
||||
t
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
end
|
||||
|
||||
# Check minimum downtime
|
||||
if time_down < unit.min_downtime
|
||||
# Verify production costs
|
||||
if abs(actual_production_cost[t] - production_cost) > 1.00
|
||||
@error @sprintf(
|
||||
"Unit %s violates minimum downtime at time %d",
|
||||
"Unit %s has unexpected production cost at time %d (%.2f should be %.2f)",
|
||||
unit.name,
|
||||
t
|
||||
t,
|
||||
actual_production_cost[t],
|
||||
production_cost
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Verify startup costs
|
||||
if abs(actual_startup_cost[t] - startup_cost) > 1.00
|
||||
@error @sprintf(
|
||||
"Unit %s has unexpected startup cost at time %d (%.2f should be %.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
actual_startup_cost[t],
|
||||
startup_cost
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
for pu in sc.profiled_units
|
||||
production = solution[sc.name]["Profiled production (MW)"][pu.name]
|
||||
|
||||
# Verify minimum uptime
|
||||
if is_shutting_down
|
||||
|
||||
# Calculate how much time the unit has been online
|
||||
time_up = 0
|
||||
for k in 1:(t-1)
|
||||
if is_on[t-k]
|
||||
time_up += 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
if (t == time_up + 1) && (unit.initial_status > 0)
|
||||
time_up += unit.initial_status
|
||||
end
|
||||
|
||||
# Check minimum uptime
|
||||
if time_up < unit.min_uptime
|
||||
for t in 1:instance.time
|
||||
# Unit must produce at least its minimum power
|
||||
if production[t] < pu.min_power[t] - tol
|
||||
@error @sprintf(
|
||||
"Unit %s violates minimum uptime at time %d",
|
||||
unit.name,
|
||||
t
|
||||
"Profiled unit %s produces below its minimum limit at time %d (%.2f < %.2f)",
|
||||
pu.name,
|
||||
t,
|
||||
production[t],
|
||||
pu.min_power[t]
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
end
|
||||
|
||||
# Verify production costs
|
||||
if abs(actual_production_cost[t] - production_cost) > 1.00
|
||||
@error @sprintf(
|
||||
"Unit %s has unexpected production cost at time %d (%.2f should be %.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
actual_production_cost[t],
|
||||
production_cost
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Verify startup costs
|
||||
if abs(actual_startup_cost[t] - startup_cost) > 1.00
|
||||
@error @sprintf(
|
||||
"Unit %s has unexpected startup cost at time %d (%.2f should be %.2f)",
|
||||
unit.name,
|
||||
t,
|
||||
actual_startup_cost[t],
|
||||
startup_cost
|
||||
)
|
||||
err_count += 1
|
||||
# Unit must produce at most its maximum power
|
||||
if production[t] > pu.max_power[t] + tol
|
||||
@error @sprintf(
|
||||
"Profiled unit %s produces above its maximum limit at time %d (%.2f > %.2f)",
|
||||
pu.name,
|
||||
t,
|
||||
production[t],
|
||||
pu.max_power[t]
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return err_count
|
||||
end
|
||||
|
||||
function _validate_reserve_and_demand(instance, solution, tol = 0.01)
|
||||
err_count = 0
|
||||
for t in 1:instance.time
|
||||
load_curtail = 0
|
||||
fixed_load = sum(b.load[t] for b in instance.buses)
|
||||
ps_load = 0
|
||||
if length(instance.price_sensitive_loads) > 0
|
||||
ps_load = sum(
|
||||
solution["Price-sensitive loads (MW)"][ps.name][t] for
|
||||
ps in instance.price_sensitive_loads
|
||||
)
|
||||
end
|
||||
production =
|
||||
sum(solution["Production (MW)"][g.name][t] for g in instance.units)
|
||||
if "Load curtail (MW)" in keys(solution)
|
||||
load_curtail = sum(
|
||||
solution["Load curtail (MW)"][b.name][t] for
|
||||
b in instance.buses
|
||||
)
|
||||
end
|
||||
balance = fixed_load - load_curtail - production + ps_load
|
||||
|
||||
# Verify that production equals demand
|
||||
if abs(balance) > tol
|
||||
@error @sprintf(
|
||||
"Non-zero power balance at time %d (%.2f + %.2f - %.2f - %.2f != 0)",
|
||||
t,
|
||||
fixed_load,
|
||||
ps_load,
|
||||
load_curtail,
|
||||
production,
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Verify reserves
|
||||
for r in instance.reserves
|
||||
if r.type == "spinning"
|
||||
provided = sum(
|
||||
solution["Spinning reserve (MW)"][r.name][g.name][t] for
|
||||
g in r.units
|
||||
for sc in instance.scenarios
|
||||
for t in 1:instance.time
|
||||
load_curtail = 0
|
||||
fixed_load = sum(b.load[t] for b in sc.buses)
|
||||
ps_load = 0
|
||||
production = 0
|
||||
if length(sc.price_sensitive_loads) > 0
|
||||
ps_load = sum(
|
||||
solution[sc.name]["Price-sensitive loads (MW)"][ps.name][t]
|
||||
for ps in sc.price_sensitive_loads
|
||||
)
|
||||
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(
|
||||
solution["Up-flexiramp (MW)"][r.name][g.name][t] for
|
||||
g in r.units
|
||||
end
|
||||
if length(sc.thermal_units) > 0
|
||||
production = sum(
|
||||
solution[sc.name]["Thermal production (MW)"][g.name][t]
|
||||
for g in sc.thermal_units
|
||||
)
|
||||
upflexiramp_shortfall =
|
||||
solution["Up-flexiramp shortfall (MW)"][r.name][t]
|
||||
|
||||
if upflexiramp + upflexiramp_shortfall < r.amount[t] - tol
|
||||
@error @sprintf(
|
||||
"Insufficient up-flexiramp at time %d (%.2f + %.2f < %.2f)",
|
||||
t,
|
||||
upflexiramp,
|
||||
upflexiramp_shortfall,
|
||||
r.amount[t],
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
dwflexiramp = sum(
|
||||
solution["Down-flexiramp (MW)"][r.name][g.name][t] for
|
||||
g in r.units
|
||||
end
|
||||
if length(sc.profiled_units) > 0
|
||||
production += sum(
|
||||
solution[sc.name]["Profiled production (MW)"][pu.name][t]
|
||||
for pu in sc.profiled_units
|
||||
)
|
||||
dwflexiramp_shortfall =
|
||||
solution["Down-flexiramp shortfall (MW)"][r.name][t]
|
||||
end
|
||||
if "Load curtail (MW)" in keys(solution)
|
||||
load_curtail = sum(
|
||||
solution[sc.name]["Load curtail (MW)"][b.name][t] for
|
||||
b in sc.buses
|
||||
)
|
||||
end
|
||||
balance = fixed_load - load_curtail - production + ps_load
|
||||
|
||||
if dwflexiramp + dwflexiramp_shortfall < r.amount[t] - tol
|
||||
@error @sprintf(
|
||||
"Insufficient down-flexiramp at time %d (%.2f + %.2f < %.2f)",
|
||||
t,
|
||||
dwflexiramp,
|
||||
dwflexiramp_shortfall,
|
||||
r.amount[t],
|
||||
# Verify that production equals demand
|
||||
if abs(balance) > tol
|
||||
@error @sprintf(
|
||||
"Non-zero power balance at time %d (%.2f + %.2f - %.2f - %.2f != 0)",
|
||||
t,
|
||||
fixed_load,
|
||||
ps_load,
|
||||
load_curtail,
|
||||
production,
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
# Verify reserves
|
||||
for r in sc.reserves
|
||||
if r.type == "spinning"
|
||||
provided = sum(
|
||||
solution[sc.name]["Spinning reserve (MW)"][r.name][g.name][t]
|
||||
for g in r.thermal_units
|
||||
)
|
||||
err_count += 1
|
||||
shortfall =
|
||||
solution[sc.name]["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(
|
||||
solution[sc.name]["Up-flexiramp (MW)"][r.name][g.name][t]
|
||||
for g in r.thermal_units
|
||||
)
|
||||
upflexiramp_shortfall =
|
||||
solution[sc.name]["Up-flexiramp shortfall (MW)"][r.name][t]
|
||||
|
||||
if upflexiramp + upflexiramp_shortfall < r.amount[t] - tol
|
||||
@error @sprintf(
|
||||
"Insufficient up-flexiramp at time %d (%.2f + %.2f < %.2f)",
|
||||
t,
|
||||
upflexiramp,
|
||||
upflexiramp_shortfall,
|
||||
r.amount[t],
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
|
||||
dwflexiramp = sum(
|
||||
solution[sc.name]["Down-flexiramp (MW)"][r.name][g.name][t]
|
||||
for g in r.thermal_units
|
||||
)
|
||||
dwflexiramp_shortfall =
|
||||
solution[sc.name]["Down-flexiramp shortfall (MW)"][r.name][t]
|
||||
|
||||
if dwflexiramp + dwflexiramp_shortfall < r.amount[t] - tol
|
||||
@error @sprintf(
|
||||
"Insufficient down-flexiramp at time %d (%.2f + %.2f < %.2f)",
|
||||
t,
|
||||
dwflexiramp,
|
||||
dwflexiramp_shortfall,
|
||||
r.amount[t],
|
||||
)
|
||||
err_count += 1
|
||||
end
|
||||
else
|
||||
error("Unknown reserve type: $(r.type)")
|
||||
end
|
||||
else
|
||||
error("Unknown reserve type: $(r.type)")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
name = "UnitCommitmentT"
|
||||
uuid = "a3b7a17a-ab64-45e4-a924-cd5ae7dc644e"
|
||||
authors = ["Alinson S. Xavier <git@axavier.org>"]
|
||||
version = "0.1.0"
|
||||
|
||||
[deps]
|
||||
Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76"
|
||||
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
|
||||
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
|
||||
GZip = "92fee26a-97fe-5a0c-ad85-20a5f3185b63"
|
||||
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
|
||||
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
||||
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
|
||||
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
|
||||
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
|
||||
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
|
||||
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
|
||||
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
|
||||
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
|
||||
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
||||
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
|
||||
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
|
||||
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
||||
|
||||
[compat]
|
||||
DataStructures = "0.18"
|
||||
Distributions = "0.25"
|
||||
GZip = "0.5"
|
||||
JSON = "0.21"
|
||||
JuMP = "1"
|
||||
MathOptInterface = "1"
|
||||
PackageCompiler = "1"
|
||||
julia = "1"
|
||||
UnitCommitment = "64606440-39ea-11e9-0f29-3303a1d3d877"
|
||||
|
||||
BIN
test/fixtures/aelmp_simple.json.gz
vendored
Normal file
BIN
test/fixtures/aelmp_simple.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/case14-fixed-status.json.gz
vendored
Normal file
BIN
test/fixtures/case14-fixed-status.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/case14-profiled.json.gz
vendored
Normal file
BIN
test/fixtures/case14-profiled.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/lmp_simple_test_1.json.gz
vendored
Normal file
BIN
test/fixtures/lmp_simple_test_1.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/lmp_simple_test_2.json.gz
vendored
Normal file
BIN
test/fixtures/lmp_simple_test_2.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/lmp_simple_test_3.json.gz
vendored
Normal file
BIN
test/fixtures/lmp_simple_test_3.json.gz
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/lmp_simple_test_4.json.gz
vendored
Normal file
BIN
test/fixtures/lmp_simple_test_4.json.gz
vendored
Normal file
Binary file not shown.
@@ -1,20 +0,0 @@
|
||||
# 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
|
||||
|
||||
@testset "read_egret_solution" begin
|
||||
solution =
|
||||
UnitCommitment.read_egret_solution("$FIXTURES/egret_output.json.gz")
|
||||
for attr in ["Is on", "Production (MW)", "Production cost (\$)"]
|
||||
@test attr in keys(solution)
|
||||
@test "115_STEAM_1" in keys(solution[attr])
|
||||
@test length(solution[attr]["115_STEAM_1"]) == 48
|
||||
end
|
||||
@test solution["Production cost (\$)"]["315_CT_6"][15:20] ==
|
||||
[0.0, 0.0, 884.44, 1470.71, 1470.71, 884.44]
|
||||
@test solution["Startup cost (\$)"]["315_CT_6"][15:20] ==
|
||||
[0.0, 0.0, 5665.23, 0.0, 0.0, 0.0]
|
||||
@test length(keys(solution["Is on"])) == 154
|
||||
end
|
||||
@@ -1,18 +0,0 @@
|
||||
# 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
|
||||
@@ -1,126 +0,0 @@
|
||||
# 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_benchmark" begin
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
|
||||
@test length(instance.lines) == 20
|
||||
@test length(instance.buses) == 14
|
||||
@test length(instance.units) == 6
|
||||
@test length(instance.contingencies) == 19
|
||||
@test length(instance.price_sensitive_loads) == 1
|
||||
@test instance.time == 4
|
||||
|
||||
@test instance.lines[5].name == "l5"
|
||||
@test instance.lines[5].source.name == "b2"
|
||||
@test instance.lines[5].target.name == "b5"
|
||||
@test instance.lines[5].reactance ≈ 0.17388
|
||||
@test instance.lines[5].susceptance ≈ 10.037550333
|
||||
@test instance.lines[5].normal_flow_limit == [1e8 for t in 1:4]
|
||||
@test instance.lines[5].emergency_flow_limit == [1e8 for t in 1:4]
|
||||
@test instance.lines[5].flow_limit_penalty == [5e3 for t in 1:4]
|
||||
@test instance.lines_by_name["l5"].name == "l5"
|
||||
|
||||
@test instance.lines[1].name == "l1"
|
||||
@test instance.lines[1].source.name == "b1"
|
||||
@test instance.lines[1].target.name == "b2"
|
||||
@test instance.lines[1].reactance ≈ 0.059170
|
||||
@test instance.lines[1].susceptance ≈ 29.496860773945
|
||||
@test instance.lines[1].normal_flow_limit == [300.0 for t in 1:4]
|
||||
@test instance.lines[1].emergency_flow_limit == [400.0 for t in 1:4]
|
||||
@test instance.lines[1].flow_limit_penalty == [1e3 for t in 1:4]
|
||||
|
||||
@test instance.buses[9].name == "b9"
|
||||
@test instance.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353]
|
||||
@test instance.buses_by_name["b9"].name == "b9"
|
||||
|
||||
@test instance.reserves[1].name == "r1"
|
||||
@test instance.reserves[1].type == "spinning"
|
||||
@test instance.reserves[1].amount == [100.0, 100.0, 100.0, 100.0]
|
||||
@test instance.reserves_by_name["r1"].name == "r1"
|
||||
|
||||
unit = instance.units[1]
|
||||
@test unit.name == "g1"
|
||||
@test unit.bus.name == "b1"
|
||||
@test unit.ramp_up_limit == 1e6
|
||||
@test unit.ramp_down_limit == 1e6
|
||||
@test unit.startup_limit == 1e6
|
||||
@test unit.shutdown_limit == 1e6
|
||||
@test unit.must_run == [false 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_downtime == 1
|
||||
for t in 1:1
|
||||
@test unit.cost_segments[1].mw[t] == 10.0
|
||||
@test unit.cost_segments[2].mw[t] == 20.0
|
||||
@test unit.cost_segments[3].mw[t] == 5.0
|
||||
@test unit.cost_segments[1].cost[t] ≈ 20.0
|
||||
@test unit.cost_segments[2].cost[t] ≈ 30.0
|
||||
@test unit.cost_segments[3].cost[t] ≈ 40.0
|
||||
end
|
||||
@test length(unit.startup_categories) == 3
|
||||
@test unit.startup_categories[1].delay == 1
|
||||
@test unit.startup_categories[2].delay == 2
|
||||
@test unit.startup_categories[3].delay == 3
|
||||
@test unit.startup_categories[1].cost == 1000.0
|
||||
@test unit.startup_categories[2].cost == 1500.0
|
||||
@test unit.startup_categories[3].cost == 2000.0
|
||||
@test length(unit.reserves) == 0
|
||||
@test instance.units_by_name["g1"].name == "g1"
|
||||
|
||||
unit = instance.units[2]
|
||||
@test unit.name == "g2"
|
||||
@test unit.must_run == [false for t in 1:4]
|
||||
@test length(unit.reserves) == 1
|
||||
|
||||
unit = instance.units[3]
|
||||
@test unit.name == "g3"
|
||||
@test unit.bus.name == "b3"
|
||||
@test unit.ramp_up_limit == 70.0
|
||||
@test unit.ramp_down_limit == 70.0
|
||||
@test unit.startup_limit == 70.0
|
||||
@test unit.shutdown_limit == 70.0
|
||||
@test unit.must_run == [true 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_downtime == 1
|
||||
for t in 1:4
|
||||
@test unit.cost_segments[1].mw[t] ≈ 33
|
||||
@test unit.cost_segments[2].mw[t] ≈ 33
|
||||
@test unit.cost_segments[3].mw[t] ≈ 34
|
||||
@test unit.cost_segments[1].cost[t] ≈ 33.75
|
||||
@test unit.cost_segments[2].cost[t] ≈ 38.04
|
||||
@test unit.cost_segments[3].cost[t] ≈ 44.77853
|
||||
end
|
||||
@test length(unit.reserves) == 1
|
||||
@test unit.reserves[1].name == "r1"
|
||||
|
||||
@test instance.contingencies[1].lines == [instance.lines[1]]
|
||||
@test instance.contingencies[1].units == []
|
||||
@test instance.contingencies[1].name == "c1"
|
||||
@test instance.contingencies_by_name["c1"].name == "c1"
|
||||
|
||||
load = instance.price_sensitive_loads[1]
|
||||
@test load.name == "ps1"
|
||||
@test load.bus.name == "b3"
|
||||
@test load.revenue == [100.0 for t in 1:4]
|
||||
@test load.demand == [50.0 for t in 1:4]
|
||||
@test instance.price_sensitive_loads_by_name["ps1"].name == "ps1"
|
||||
end
|
||||
|
||||
@testset "read_benchmark sub-hourly" begin
|
||||
instance = UnitCommitment.read("$FIXTURES/case14-sub-hourly.json.gz")
|
||||
@test instance.time == 4
|
||||
unit = instance.units[1]
|
||||
@test unit.name == "g1"
|
||||
@test unit.min_uptime == 2
|
||||
@test unit.min_downtime == 2
|
||||
@test length(unit.startup_categories) == 3
|
||||
@test unit.startup_categories[1].delay == 2
|
||||
@test unit.startup_categories[2].delay == 4
|
||||
@test unit.startup_categories[3].delay == 6
|
||||
@test unit.initial_status == -200
|
||||
end
|
||||
@@ -1,84 +0,0 @@
|
||||
# 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
|
||||
using JuMP
|
||||
using Cbc
|
||||
using JSON
|
||||
import UnitCommitment:
|
||||
ArrCon2000,
|
||||
CarArr2006,
|
||||
DamKucRajAta2016,
|
||||
Formulation,
|
||||
Gar1962,
|
||||
KnuOstWat2018,
|
||||
MorLatRam2013,
|
||||
PanGua2016,
|
||||
XavQiuWanThi2019,
|
||||
WanHob2016
|
||||
|
||||
function _test(
|
||||
formulation::Formulation;
|
||||
instances = ["case14"],
|
||||
dump::Bool = false,
|
||||
)::Nothing
|
||||
for instance_name in instances
|
||||
instance = UnitCommitment.read("$(FIXTURES)/$(instance_name).json.gz")
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
formulation = formulation,
|
||||
optimizer = Cbc.Optimizer,
|
||||
variable_names = true,
|
||||
)
|
||||
set_silent(model)
|
||||
UnitCommitment.optimize!(model)
|
||||
solution = UnitCommitment.solution(model)
|
||||
if dump
|
||||
open("/tmp/ucjl.json", "w") do f
|
||||
return write(f, JSON.json(solution, 2))
|
||||
end
|
||||
write_to_file(model, "/tmp/ucjl.lp")
|
||||
end
|
||||
@test UnitCommitment.validate(instance, solution)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
@testset "formulations" begin
|
||||
@testset "default" begin
|
||||
_test(Formulation())
|
||||
end
|
||||
@testset "ArrCon2000" begin
|
||||
_test(Formulation(ramping = ArrCon2000.Ramping()))
|
||||
end
|
||||
@testset "DamKucRajAta2016" begin
|
||||
_test(Formulation(ramping = DamKucRajAta2016.Ramping()))
|
||||
end
|
||||
@testset "MorLatRam2013" begin
|
||||
_test(
|
||||
Formulation(
|
||||
ramping = MorLatRam2013.Ramping(),
|
||||
startup_costs = MorLatRam2013.StartupCosts(),
|
||||
),
|
||||
)
|
||||
end
|
||||
@testset "PanGua2016" begin
|
||||
_test(Formulation(ramping = PanGua2016.Ramping()))
|
||||
end
|
||||
@testset "Gar1962" begin
|
||||
_test(Formulation(pwl_costs = Gar1962.PwlCosts()))
|
||||
end
|
||||
@testset "CarArr2006" begin
|
||||
_test(Formulation(pwl_costs = CarArr2006.PwlCosts()))
|
||||
end
|
||||
@testset "KnuOstWat2018" begin
|
||||
_test(Formulation(pwl_costs = KnuOstWat2018.PwlCosts()))
|
||||
end
|
||||
@testset "WanHob2016" begin
|
||||
_test(
|
||||
Formulation(ramping = WanHob2016.Ramping()),
|
||||
instances = ["case14-flex"],
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1,42 +0,0 @@
|
||||
# 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 Test
|
||||
using UnitCommitment
|
||||
|
||||
push!(Base.LOAD_PATH, @__DIR__)
|
||||
UnitCommitment._setup_logger(level = Base.CoreLogging.Error)
|
||||
|
||||
FIXTURES = "$(@__DIR__)/fixtures"
|
||||
|
||||
@testset "UnitCommitment" begin
|
||||
include("usage.jl")
|
||||
@testset "import" begin
|
||||
include("import/egret_test.jl")
|
||||
end
|
||||
@testset "instance" begin
|
||||
include("instance/read_test.jl")
|
||||
include("instance/migrate_test.jl")
|
||||
end
|
||||
@testset "model" begin
|
||||
include("model/formulations_test.jl")
|
||||
end
|
||||
@testset "solution" begin
|
||||
@testset "XavQiuWanThi19" begin
|
||||
include("solution/methods/XavQiuWanThi19/filter_test.jl")
|
||||
include("solution/methods/XavQiuWanThi19/find_test.jl")
|
||||
include("solution/methods/XavQiuWanThi19/sensitivity_test.jl")
|
||||
end
|
||||
end
|
||||
@testset "transform" begin
|
||||
include("transform/initcond_test.jl")
|
||||
include("transform/slice_test.jl")
|
||||
@testset "randomize" begin
|
||||
include("transform/randomize/XavQiuAhm2021_test.jl")
|
||||
end
|
||||
end
|
||||
@testset "validation" begin
|
||||
include("validation/repair_test.jl")
|
||||
end
|
||||
end
|
||||
@@ -1,83 +0,0 @@
|
||||
# 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, Test, LinearAlgebra
|
||||
import UnitCommitment: _Violation, _offer, _query
|
||||
|
||||
@testset "_ViolationFilter" begin
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
filter = UnitCommitment._ViolationFilter(max_per_line = 1, max_total = 2)
|
||||
|
||||
_offer(
|
||||
filter,
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[1],
|
||||
outage_line = nothing,
|
||||
amount = 100.0,
|
||||
),
|
||||
)
|
||||
_offer(
|
||||
filter,
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[1],
|
||||
outage_line = instance.lines[1],
|
||||
amount = 300.0,
|
||||
),
|
||||
)
|
||||
_offer(
|
||||
filter,
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[1],
|
||||
outage_line = instance.lines[5],
|
||||
amount = 500.0,
|
||||
),
|
||||
)
|
||||
_offer(
|
||||
filter,
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[1],
|
||||
outage_line = instance.lines[4],
|
||||
amount = 400.0,
|
||||
),
|
||||
)
|
||||
_offer(
|
||||
filter,
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[2],
|
||||
outage_line = instance.lines[1],
|
||||
amount = 200.0,
|
||||
),
|
||||
)
|
||||
_offer(
|
||||
filter,
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[2],
|
||||
outage_line = instance.lines[8],
|
||||
amount = 100.0,
|
||||
),
|
||||
)
|
||||
|
||||
actual = _query(filter)
|
||||
expected = [
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[2],
|
||||
outage_line = instance.lines[1],
|
||||
amount = 200.0,
|
||||
),
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = instance.lines[1],
|
||||
outage_line = instance.lines[5],
|
||||
amount = 500.0,
|
||||
),
|
||||
]
|
||||
@test actual == expected
|
||||
end
|
||||
@@ -1,35 +0,0 @@
|
||||
# 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, Test, LinearAlgebra
|
||||
import UnitCommitment: _Violation, _offer, _query
|
||||
|
||||
@testset "find_violations" begin
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
for line in instance.lines, t in 1:instance.time
|
||||
line.normal_flow_limit[t] = 1.0
|
||||
line.emergency_flow_limit[t] = 1.0
|
||||
end
|
||||
isf = UnitCommitment._injection_shift_factors(
|
||||
lines = instance.lines,
|
||||
buses = instance.buses,
|
||||
)
|
||||
lodf = UnitCommitment._line_outage_factors(
|
||||
lines = instance.lines,
|
||||
buses = instance.buses,
|
||||
isf = isf,
|
||||
)
|
||||
inj = [1000.0 for b in 1:13, t in 1:instance.time]
|
||||
overflow = [0.0 for l in instance.lines, t in 1:instance.time]
|
||||
violations = UnitCommitment._find_violations(
|
||||
instance = instance,
|
||||
net_injections = inj,
|
||||
overflow = overflow,
|
||||
isf = isf,
|
||||
lodf = lodf,
|
||||
max_per_line = 1,
|
||||
max_per_period = 5,
|
||||
)
|
||||
@test length(violations) == 20
|
||||
end
|
||||
@@ -1,143 +0,0 @@
|
||||
# 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, Test, LinearAlgebra
|
||||
|
||||
@testset "_susceptance_matrix" begin
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
actual = UnitCommitment._susceptance_matrix(instance.lines)
|
||||
@test size(actual) == (20, 20)
|
||||
expected = Diagonal([
|
||||
29.5,
|
||||
7.83,
|
||||
8.82,
|
||||
9.9,
|
||||
10.04,
|
||||
10.2,
|
||||
41.45,
|
||||
8.35,
|
||||
3.14,
|
||||
6.93,
|
||||
8.77,
|
||||
6.82,
|
||||
13.4,
|
||||
9.91,
|
||||
15.87,
|
||||
20.65,
|
||||
6.46,
|
||||
9.09,
|
||||
8.73,
|
||||
5.02,
|
||||
])
|
||||
@test round.(actual, digits = 2) == expected
|
||||
end
|
||||
|
||||
@testset "_reduced_incidence_matrix" begin
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
actual = UnitCommitment._reduced_incidence_matrix(
|
||||
lines = instance.lines,
|
||||
buses = instance.buses,
|
||||
)
|
||||
@test size(actual) == (20, 13)
|
||||
@test actual[1, 1] == -1.0
|
||||
@test actual[3, 1] == 1.0
|
||||
@test actual[4, 1] == 1.0
|
||||
@test actual[5, 1] == 1.0
|
||||
@test actual[3, 2] == -1.0
|
||||
@test actual[6, 2] == 1.0
|
||||
@test actual[4, 3] == -1.0
|
||||
@test actual[6, 3] == -1.0
|
||||
@test actual[7, 3] == 1.0
|
||||
@test actual[8, 3] == 1.0
|
||||
@test actual[9, 3] == 1.0
|
||||
@test actual[2, 4] == -1.0
|
||||
@test actual[5, 4] == -1.0
|
||||
@test actual[7, 4] == -1.0
|
||||
@test actual[10, 4] == 1.0
|
||||
@test actual[10, 5] == -1.0
|
||||
@test actual[11, 5] == 1.0
|
||||
@test actual[12, 5] == 1.0
|
||||
@test actual[13, 5] == 1.0
|
||||
@test actual[8, 6] == -1.0
|
||||
@test actual[14, 6] == 1.0
|
||||
@test actual[15, 6] == 1.0
|
||||
@test actual[14, 7] == -1.0
|
||||
@test actual[9, 8] == -1.0
|
||||
@test actual[15, 8] == -1.0
|
||||
@test actual[16, 8] == 1.0
|
||||
@test actual[17, 8] == 1.0
|
||||
@test actual[16, 9] == -1.0
|
||||
@test actual[18, 9] == 1.0
|
||||
@test actual[11, 10] == -1.0
|
||||
@test actual[18, 10] == -1.0
|
||||
@test actual[12, 11] == -1.0
|
||||
@test actual[19, 11] == 1.0
|
||||
@test actual[13, 12] == -1.0
|
||||
@test actual[19, 12] == -1.0
|
||||
@test actual[20, 12] == 1.0
|
||||
@test actual[17, 13] == -1.0
|
||||
@test actual[20, 13] == -1.0
|
||||
end
|
||||
|
||||
@testset "_injection_shift_factors" begin
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
actual = UnitCommitment._injection_shift_factors(
|
||||
lines = instance.lines,
|
||||
buses = instance.buses,
|
||||
)
|
||||
@test size(actual) == (20, 13)
|
||||
@test round.(actual, digits = 2) == [
|
||||
-0.84 -0.75 -0.67 -0.61 -0.63 -0.66 -0.66 -0.65 -0.65 -0.64 -0.63 -0.63 -0.64
|
||||
-0.16 -0.25 -0.33 -0.39 -0.37 -0.34 -0.34 -0.35 -0.35 -0.36 -0.37 -0.37 -0.36
|
||||
0.03 -0.53 -0.15 -0.1 -0.12 -0.14 -0.14 -0.14 -0.13 -0.13 -0.12 -0.12 -0.13
|
||||
0.06 -0.14 -0.32 -0.22 -0.25 -0.3 -0.3 -0.29 -0.28 -0.27 -0.25 -0.26 -0.27
|
||||
0.08 -0.07 -0.2 -0.29 -0.26 -0.22 -0.22 -0.22 -0.23 -0.25 -0.26 -0.26 -0.24
|
||||
0.03 0.47 -0.15 -0.1 -0.12 -0.14 -0.14 -0.14 -0.13 -0.13 -0.12 -0.12 -0.13
|
||||
0.08 0.31 0.5 -0.3 -0.03 0.36 0.36 0.28 0.23 0.1 -0.0 0.02 0.17
|
||||
0.0 0.01 0.02 -0.01 -0.22 -0.63 -0.63 -0.45 -0.41 -0.32 -0.24 -0.25 -0.36
|
||||
0.0 0.01 0.01 -0.01 -0.12 -0.17 -0.17 -0.26 -0.24 -0.18 -0.14 -0.14 -0.21
|
||||
-0.0 -0.02 -0.03 0.02 -0.66 -0.2 -0.2 -0.29 -0.36 -0.5 -0.63 -0.61 -0.43
|
||||
-0.0 -0.01 -0.02 0.01 0.21 -0.12 -0.12 -0.17 -0.28 -0.53 0.18 0.15 -0.03
|
||||
-0.0 -0.0 -0.0 0.0 0.03 -0.02 -0.02 -0.03 -0.02 0.01 -0.52 -0.17 -0.09
|
||||
-0.0 -0.01 -0.01 0.01 0.11 -0.06 -0.06 -0.09 -0.05 0.02 -0.28 -0.59 -0.31
|
||||
-0.0 -0.0 -0.0 -0.0 -0.0 -0.0 -1.0 -0.0 -0.0 -0.0 -0.0 -0.0 0.0
|
||||
0.0 0.01 0.02 -0.01 -0.22 0.37 0.37 -0.45 -0.41 -0.32 -0.24 -0.25 -0.36
|
||||
0.0 0.01 0.02 -0.01 -0.21 0.12 0.12 0.17 -0.72 -0.47 -0.18 -0.15 0.03
|
||||
0.0 0.01 0.01 -0.01 -0.14 0.08 0.08 0.12 0.07 -0.03 -0.2 -0.24 -0.6
|
||||
0.0 0.01 0.02 -0.01 -0.21 0.12 0.12 0.17 0.28 -0.47 -0.18 -0.15 0.03
|
||||
-0.0 -0.0 -0.0 0.0 0.03 -0.02 -0.02 -0.03 -0.02 0.01 0.48 -0.17 -0.09
|
||||
-0.0 -0.01 -0.01 0.01 0.14 -0.08 -0.08 -0.12 -0.07 0.03 0.2 0.24 -0.4
|
||||
]
|
||||
end
|
||||
|
||||
@testset "_line_outage_factors" begin
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
isf_before = UnitCommitment._injection_shift_factors(
|
||||
lines = instance.lines,
|
||||
buses = instance.buses,
|
||||
)
|
||||
lodf = UnitCommitment._line_outage_factors(
|
||||
lines = instance.lines,
|
||||
buses = instance.buses,
|
||||
isf = isf_before,
|
||||
)
|
||||
for contingency in instance.contingencies
|
||||
for lc in contingency.lines
|
||||
prev_susceptance = lc.susceptance
|
||||
lc.susceptance = 0.0
|
||||
isf_after = UnitCommitment._injection_shift_factors(
|
||||
lines = instance.lines,
|
||||
buses = instance.buses,
|
||||
)
|
||||
lc.susceptance = prev_susceptance
|
||||
for lm in instance.lines
|
||||
expected = isf_after[lm.offset, :]
|
||||
actual =
|
||||
isf_before[lm.offset, :] +
|
||||
lodf[lm.offset, lc.offset] * isf_before[lc.offset, :]
|
||||
@test norm(expected - actual) < 1e-6
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
58
test/src/UnitCommitmentT.jl
Normal file
58
test/src/UnitCommitmentT.jl
Normal file
@@ -0,0 +1,58 @@
|
||||
module UnitCommitmentT
|
||||
|
||||
using JuliaFormatter
|
||||
using UnitCommitment
|
||||
using Test
|
||||
|
||||
include("usage.jl")
|
||||
include("import/egret_test.jl")
|
||||
include("instance/read_test.jl")
|
||||
include("instance/migrate_test.jl")
|
||||
include("model/formulations_test.jl")
|
||||
include("solution/methods/XavQiuWanThi19/filter_test.jl")
|
||||
include("solution/methods/XavQiuWanThi19/find_test.jl")
|
||||
include("solution/methods/XavQiuWanThi19/sensitivity_test.jl")
|
||||
include("transform/initcond_test.jl")
|
||||
include("transform/slice_test.jl")
|
||||
include("transform/randomize/XavQiuAhm2021_test.jl")
|
||||
include("validation/repair_test.jl")
|
||||
include("lmp/conventional_test.jl")
|
||||
include("lmp/aelmp_test.jl")
|
||||
|
||||
basedir = dirname(@__FILE__)
|
||||
|
||||
function fixture(path::String)::String
|
||||
return "$basedir/../fixtures/$path"
|
||||
end
|
||||
|
||||
function runtests()
|
||||
println("Running tests...")
|
||||
UnitCommitment._setup_logger(level = Base.CoreLogging.Error)
|
||||
@testset "UnitCommitment" begin
|
||||
usage_test()
|
||||
import_egret_test()
|
||||
instance_read_test()
|
||||
instance_migrate_test()
|
||||
model_formulations_test()
|
||||
solution_methods_XavQiuWanThi19_filter_test()
|
||||
solution_methods_XavQiuWanThi19_find_test()
|
||||
solution_methods_XavQiuWanThi19_sensitivity_test()
|
||||
transform_initcond_test()
|
||||
transform_slice_test()
|
||||
transform_randomize_XavQiuAhm2021_test()
|
||||
validation_repair_test()
|
||||
lmp_conventional_test()
|
||||
lmp_aelmp_test()
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
function format()
|
||||
JuliaFormatter.format(basedir, verbose = true)
|
||||
JuliaFormatter.format("$basedir/../../src", verbose = true)
|
||||
return
|
||||
end
|
||||
|
||||
export runtests, format
|
||||
|
||||
end # module UnitCommitmentT
|
||||
23
test/src/import/egret_test.jl
Normal file
23
test/src/import/egret_test.jl
Normal file
@@ -0,0 +1,23 @@
|
||||
# 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
|
||||
|
||||
function import_egret_test()
|
||||
@testset "read_egret_solution" begin
|
||||
solution =
|
||||
UnitCommitment.read_egret_solution(fixture("egret_output.json.gz"))
|
||||
for attr in
|
||||
["Is on", "Thermal production (MW)", "Thermal production cost (\$)"]
|
||||
@test attr in keys(solution)
|
||||
@test "115_STEAM_1" in keys(solution[attr])
|
||||
@test length(solution[attr]["115_STEAM_1"]) == 48
|
||||
end
|
||||
@test solution["Thermal production cost (\$)"]["315_CT_6"][15:20] ==
|
||||
[0.0, 0.0, 884.44, 1470.71, 1470.71, 884.44]
|
||||
@test solution["Startup cost (\$)"]["315_CT_6"][15:20] ==
|
||||
[0.0, 0.0, 5665.23, 0.0, 0.0, 0.0]
|
||||
@test length(keys(solution["Is on"])) == 154
|
||||
end
|
||||
end
|
||||
24
test/src/instance/migrate_test.jl
Normal file
24
test/src/instance/migrate_test.jl
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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
|
||||
|
||||
function instance_migrate_test()
|
||||
@testset "read v0.2" begin
|
||||
instance = UnitCommitment.read(fixture("/ucjl-0.2.json.gz"))
|
||||
@test length(instance.scenarios) == 1
|
||||
sc = instance.scenarios[1]
|
||||
@test length(sc.reserves_by_name["r1"].amount) == 4
|
||||
@test sc.thermal_units_by_name["g2"].reserves[1].name == "r1"
|
||||
end
|
||||
|
||||
@testset "read v0.3" begin
|
||||
instance = UnitCommitment.read(fixture("/ucjl-0.3.json.gz"))
|
||||
@test length(instance.scenarios) == 1
|
||||
sc = instance.scenarios[1]
|
||||
@test length(sc.thermal_units) == 6
|
||||
@test length(sc.buses) == 14
|
||||
@test length(sc.lines) == 20
|
||||
end
|
||||
end
|
||||
168
test/src/instance/read_test.jl
Normal file
168
test/src/instance/read_test.jl
Normal file
@@ -0,0 +1,168 @@
|
||||
# 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
|
||||
|
||||
function instance_read_test()
|
||||
@testset "read_benchmark" begin
|
||||
instance = UnitCommitment.read(fixture("case14.json.gz"))
|
||||
|
||||
@test repr(instance) == (
|
||||
"UnitCommitmentInstance(1 scenarios, 6 thermal units, 0 profiled units, 14 buses, " *
|
||||
"20 lines, 19 contingencies, 1 price sensitive loads, 4 time steps)"
|
||||
)
|
||||
|
||||
@test length(instance.scenarios) == 1
|
||||
sc = instance.scenarios[1]
|
||||
@test length(sc.lines) == 20
|
||||
@test length(sc.buses) == 14
|
||||
@test length(sc.thermal_units) == 6
|
||||
@test length(sc.contingencies) == 19
|
||||
@test length(sc.price_sensitive_loads) == 1
|
||||
@test instance.time == 4
|
||||
|
||||
@test sc.lines[5].name == "l5"
|
||||
@test sc.lines[5].source.name == "b2"
|
||||
@test sc.lines[5].target.name == "b5"
|
||||
@test sc.lines[5].reactance ≈ 0.17388
|
||||
@test sc.lines[5].susceptance ≈ 10.037550333
|
||||
@test sc.lines[5].normal_flow_limit == [1e8 for t in 1:4]
|
||||
@test sc.lines[5].emergency_flow_limit == [1e8 for t in 1:4]
|
||||
@test sc.lines[5].flow_limit_penalty == [5e3 for t in 1:4]
|
||||
@test sc.lines_by_name["l5"].name == "l5"
|
||||
|
||||
@test sc.lines[1].name == "l1"
|
||||
@test sc.lines[1].source.name == "b1"
|
||||
@test sc.lines[1].target.name == "b2"
|
||||
@test sc.lines[1].reactance ≈ 0.059170
|
||||
@test sc.lines[1].susceptance ≈ 29.496860773945
|
||||
@test sc.lines[1].normal_flow_limit == [300.0 for t in 1:4]
|
||||
@test sc.lines[1].emergency_flow_limit == [400.0 for t in 1:4]
|
||||
@test sc.lines[1].flow_limit_penalty == [1e3 for t in 1:4]
|
||||
|
||||
@test sc.buses[9].name == "b9"
|
||||
@test sc.buses[9].load == [35.36638, 33.25495, 31.67138, 31.14353]
|
||||
@test sc.buses_by_name["b9"].name == "b9"
|
||||
|
||||
@test sc.reserves[1].name == "r1"
|
||||
@test sc.reserves[1].type == "spinning"
|
||||
@test sc.reserves[1].amount == [100.0, 100.0, 100.0, 100.0]
|
||||
@test sc.reserves_by_name["r1"].name == "r1"
|
||||
|
||||
unit = sc.thermal_units[1]
|
||||
@test unit.name == "g1"
|
||||
@test unit.bus.name == "b1"
|
||||
@test unit.ramp_up_limit == 1e6
|
||||
@test unit.ramp_down_limit == 1e6
|
||||
@test unit.startup_limit == 1e6
|
||||
@test unit.shutdown_limit == 1e6
|
||||
@test unit.must_run == [false 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_downtime == 1
|
||||
for t in 1:1
|
||||
@test unit.cost_segments[1].mw[t] == 10.0
|
||||
@test unit.cost_segments[2].mw[t] == 20.0
|
||||
@test unit.cost_segments[3].mw[t] == 5.0
|
||||
@test unit.cost_segments[1].cost[t] ≈ 20.0
|
||||
@test unit.cost_segments[2].cost[t] ≈ 30.0
|
||||
@test unit.cost_segments[3].cost[t] ≈ 40.0
|
||||
end
|
||||
@test length(unit.startup_categories) == 3
|
||||
@test unit.startup_categories[1].delay == 1
|
||||
@test unit.startup_categories[2].delay == 2
|
||||
@test unit.startup_categories[3].delay == 3
|
||||
@test unit.startup_categories[1].cost == 1000.0
|
||||
@test unit.startup_categories[2].cost == 1500.0
|
||||
@test unit.startup_categories[3].cost == 2000.0
|
||||
@test length(unit.reserves) == 0
|
||||
@test sc.thermal_units_by_name["g1"].name == "g1"
|
||||
|
||||
unit = sc.thermal_units[2]
|
||||
@test unit.name == "g2"
|
||||
@test unit.must_run == [false for t in 1:4]
|
||||
@test length(unit.reserves) == 1
|
||||
|
||||
unit = sc.thermal_units[3]
|
||||
@test unit.name == "g3"
|
||||
@test unit.bus.name == "b3"
|
||||
@test unit.ramp_up_limit == 70.0
|
||||
@test unit.ramp_down_limit == 70.0
|
||||
@test unit.startup_limit == 70.0
|
||||
@test unit.shutdown_limit == 70.0
|
||||
@test unit.must_run == [true 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_downtime == 1
|
||||
for t in 1:4
|
||||
@test unit.cost_segments[1].mw[t] ≈ 33
|
||||
@test unit.cost_segments[2].mw[t] ≈ 33
|
||||
@test unit.cost_segments[3].mw[t] ≈ 34
|
||||
@test unit.cost_segments[1].cost[t] ≈ 33.75
|
||||
@test unit.cost_segments[2].cost[t] ≈ 38.04
|
||||
@test unit.cost_segments[3].cost[t] ≈ 44.77853
|
||||
end
|
||||
@test length(unit.reserves) == 1
|
||||
@test unit.reserves[1].name == "r1"
|
||||
|
||||
@test sc.contingencies[1].lines == [sc.lines[1]]
|
||||
@test sc.contingencies[1].thermal_units == []
|
||||
@test sc.contingencies[1].name == "c1"
|
||||
@test sc.contingencies_by_name["c1"].name == "c1"
|
||||
|
||||
load = sc.price_sensitive_loads[1]
|
||||
@test load.name == "ps1"
|
||||
@test load.bus.name == "b3"
|
||||
@test load.revenue == [100.0 for t in 1:4]
|
||||
@test load.demand == [50.0 for t in 1:4]
|
||||
@test sc.price_sensitive_loads_by_name["ps1"].name == "ps1"
|
||||
end
|
||||
|
||||
@testset "read_benchmark sub-hourly" begin
|
||||
instance = UnitCommitment.read(fixture("case14-sub-hourly.json.gz"))
|
||||
@test instance.time == 4
|
||||
unit = instance.scenarios[1].thermal_units[1]
|
||||
@test unit.name == "g1"
|
||||
@test unit.min_uptime == 2
|
||||
@test unit.min_downtime == 2
|
||||
@test length(unit.startup_categories) == 3
|
||||
@test unit.startup_categories[1].delay == 2
|
||||
@test unit.startup_categories[2].delay == 4
|
||||
@test unit.startup_categories[3].delay == 6
|
||||
@test unit.initial_status == -200
|
||||
end
|
||||
|
||||
@testset "read_benchmark profiled-units" begin
|
||||
instance = UnitCommitment.read(fixture("case14-profiled.json.gz"))
|
||||
sc = instance.scenarios[1]
|
||||
@test length(sc.profiled_units) == 2
|
||||
|
||||
first_pu = sc.profiled_units[1]
|
||||
@test first_pu.name == "g7"
|
||||
@test first_pu.bus.name == "b4"
|
||||
@test first_pu.cost == [100.0 for t in 1:4]
|
||||
@test first_pu.min_power == [60.0 for t in 1:4]
|
||||
@test first_pu.max_power == [100.0 for t in 1:4]
|
||||
@test sc.profiled_units_by_name["g7"].name == "g7"
|
||||
|
||||
second_pu = sc.profiled_units[2]
|
||||
@test second_pu.name == "g8"
|
||||
@test second_pu.bus.name == "b5"
|
||||
@test second_pu.cost == [50.0 for t in 1:4]
|
||||
@test second_pu.min_power == [0.0 for t in 1:4]
|
||||
@test second_pu.max_power == [120.0 for t in 1:4]
|
||||
@test sc.profiled_units_by_name["g8"].name == "g8"
|
||||
end
|
||||
|
||||
@testset "read_benchmark commitmemt-status" begin
|
||||
instance = UnitCommitment.read(fixture("case14-fixed-status.json.gz"))
|
||||
sc = instance.scenarios[1]
|
||||
|
||||
@test sc.thermal_units[1].commitment_status == [nothing for t in 1:4]
|
||||
@test sc.thermal_units[2].commitment_status == [true for t in 1:4]
|
||||
@test sc.thermal_units[4].commitment_status == [false for t in 1:4]
|
||||
@test sc.thermal_units[6].commitment_status ==
|
||||
[false, nothing, true, nothing]
|
||||
end
|
||||
end
|
||||
40
test/src/lmp/aelmp_test.jl
Normal file
40
test/src/lmp/aelmp_test.jl
Normal file
@@ -0,0 +1,40 @@
|
||||
# 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, Cbc, HiGHS, JuMP
|
||||
import UnitCommitment: AELMP
|
||||
|
||||
function lmp_aelmp_test()
|
||||
@testset "aelmp" begin
|
||||
path = fixture("aelmp_simple.json.gz")
|
||||
# model has to be solved first
|
||||
instance = UnitCommitment.read(path)
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = Cbc.Optimizer,
|
||||
variable_names = true,
|
||||
)
|
||||
JuMP.set_silent(model)
|
||||
UnitCommitment.optimize!(model)
|
||||
|
||||
# policy 1: allow offlines; consider startups
|
||||
aelmp_1 = UnitCommitment.compute_lmp(
|
||||
model,
|
||||
AELMP(),
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
@test aelmp_1["s1", "B1", 1] ≈ 231.7 atol = 0.1
|
||||
|
||||
# policy 2: do not allow offlines; but consider startups
|
||||
aelmp_2 = UnitCommitment.compute_lmp(
|
||||
model,
|
||||
AELMP(
|
||||
allow_offline_participation = false,
|
||||
consider_startup_costs = true,
|
||||
),
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
@test aelmp_2["s1", "B1", 1] ≈ 274.3 atol = 0.1
|
||||
end
|
||||
end
|
||||
53
test/src/lmp/conventional_test.jl
Normal file
53
test/src/lmp/conventional_test.jl
Normal file
@@ -0,0 +1,53 @@
|
||||
# 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, Cbc, HiGHS, JuMP
|
||||
import UnitCommitment: ConventionalLMP
|
||||
|
||||
function solve_conventional_testcase(path::String)
|
||||
instance = UnitCommitment.read(path)
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = Cbc.Optimizer,
|
||||
variable_names = true,
|
||||
)
|
||||
JuMP.set_silent(model)
|
||||
UnitCommitment.optimize!(model)
|
||||
lmp = UnitCommitment.compute_lmp(
|
||||
model,
|
||||
ConventionalLMP(),
|
||||
optimizer = HiGHS.Optimizer,
|
||||
)
|
||||
return lmp
|
||||
end
|
||||
|
||||
function lmp_conventional_test()
|
||||
@testset "conventional" begin
|
||||
# instance 1
|
||||
path = fixture("lmp_simple_test_1.json.gz")
|
||||
lmp = solve_conventional_testcase(path)
|
||||
@test lmp["s1", "A", 1] == 50.0
|
||||
@test lmp["s1", "B", 1] == 50.0
|
||||
|
||||
# instance 2
|
||||
path = fixture("lmp_simple_test_2.json.gz")
|
||||
lmp = solve_conventional_testcase(path)
|
||||
@test lmp["s1", "A", 1] == 50.0
|
||||
@test lmp["s1", "B", 1] == 60.0
|
||||
|
||||
# instance 3
|
||||
path = fixture("lmp_simple_test_3.json.gz")
|
||||
lmp = solve_conventional_testcase(path)
|
||||
@test lmp["s1", "A", 1] == 50.0
|
||||
@test lmp["s1", "B", 1] == 70.0
|
||||
@test lmp["s1", "C", 1] == 100.0
|
||||
|
||||
# instance 4
|
||||
path = fixture("lmp_simple_test_4.json.gz")
|
||||
lmp = solve_conventional_testcase(path)
|
||||
@test lmp["s1", "A", 1] == 50.0
|
||||
@test lmp["s1", "B", 1] == 70.0
|
||||
@test lmp["s1", "C", 1] == 90.0
|
||||
end
|
||||
end
|
||||
86
test/src/model/formulations_test.jl
Normal file
86
test/src/model/formulations_test.jl
Normal file
@@ -0,0 +1,86 @@
|
||||
# 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
|
||||
using JuMP
|
||||
using Cbc
|
||||
using JSON
|
||||
import UnitCommitment:
|
||||
ArrCon2000,
|
||||
CarArr2006,
|
||||
DamKucRajAta2016,
|
||||
Formulation,
|
||||
Gar1962,
|
||||
KnuOstWat2018,
|
||||
MorLatRam2013,
|
||||
PanGua2016,
|
||||
XavQiuWanThi2019,
|
||||
WanHob2016
|
||||
|
||||
function _test(
|
||||
formulation::Formulation;
|
||||
instances = ["case14"],
|
||||
dump::Bool = false,
|
||||
)::Nothing
|
||||
for instance_name in instances
|
||||
instance = UnitCommitment.read(fixture("$(instance_name).json.gz"))
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
formulation = formulation,
|
||||
optimizer = Cbc.Optimizer,
|
||||
variable_names = true,
|
||||
)
|
||||
set_silent(model)
|
||||
UnitCommitment.optimize!(model)
|
||||
solution = UnitCommitment.solution(model)
|
||||
if dump
|
||||
open("/tmp/ucjl.json", "w") do f
|
||||
return write(f, JSON.json(solution, 2))
|
||||
end
|
||||
write_to_file(model, "/tmp/ucjl.lp")
|
||||
end
|
||||
@test UnitCommitment.validate(instance, solution)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
function model_formulations_test()
|
||||
@testset "formulations" begin
|
||||
@testset "default" begin
|
||||
_test(Formulation())
|
||||
end
|
||||
@testset "ArrCon2000" begin
|
||||
_test(Formulation(ramping = ArrCon2000.Ramping()))
|
||||
end
|
||||
@testset "DamKucRajAta2016" begin
|
||||
_test(Formulation(ramping = DamKucRajAta2016.Ramping()))
|
||||
end
|
||||
@testset "MorLatRam2013" begin
|
||||
_test(
|
||||
Formulation(
|
||||
ramping = MorLatRam2013.Ramping(),
|
||||
startup_costs = MorLatRam2013.StartupCosts(),
|
||||
),
|
||||
)
|
||||
end
|
||||
@testset "PanGua2016" begin
|
||||
_test(Formulation(ramping = PanGua2016.Ramping()))
|
||||
end
|
||||
@testset "Gar1962" begin
|
||||
_test(Formulation(pwl_costs = Gar1962.PwlCosts()))
|
||||
end
|
||||
@testset "CarArr2006" begin
|
||||
_test(Formulation(pwl_costs = CarArr2006.PwlCosts()))
|
||||
end
|
||||
@testset "KnuOstWat2018" begin
|
||||
_test(Formulation(pwl_costs = KnuOstWat2018.PwlCosts()))
|
||||
end
|
||||
@testset "WanHob2016" begin
|
||||
_test(
|
||||
Formulation(ramping = WanHob2016.Ramping()),
|
||||
instances = ["case14-flex"],
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
86
test/src/solution/methods/XavQiuWanThi19/filter_test.jl
Normal file
86
test/src/solution/methods/XavQiuWanThi19/filter_test.jl
Normal file
@@ -0,0 +1,86 @@
|
||||
# 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, Test, LinearAlgebra
|
||||
import UnitCommitment: _Violation, _offer, _query
|
||||
|
||||
function solution_methods_XavQiuWanThi19_filter_test()
|
||||
@testset "_ViolationFilter" begin
|
||||
instance = UnitCommitment.read(fixture("case14.json.gz"))
|
||||
sc = instance.scenarios[1]
|
||||
filter =
|
||||
UnitCommitment._ViolationFilter(max_per_line = 1, max_total = 2)
|
||||
_offer(
|
||||
filter,
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = sc.lines[1],
|
||||
outage_line = nothing,
|
||||
amount = 100.0,
|
||||
),
|
||||
)
|
||||
_offer(
|
||||
filter,
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = sc.lines[1],
|
||||
outage_line = sc.lines[1],
|
||||
amount = 300.0,
|
||||
),
|
||||
)
|
||||
_offer(
|
||||
filter,
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = sc.lines[1],
|
||||
outage_line = sc.lines[5],
|
||||
amount = 500.0,
|
||||
),
|
||||
)
|
||||
_offer(
|
||||
filter,
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = sc.lines[1],
|
||||
outage_line = sc.lines[4],
|
||||
amount = 400.0,
|
||||
),
|
||||
)
|
||||
_offer(
|
||||
filter,
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = sc.lines[2],
|
||||
outage_line = sc.lines[1],
|
||||
amount = 200.0,
|
||||
),
|
||||
)
|
||||
_offer(
|
||||
filter,
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = sc.lines[2],
|
||||
outage_line = sc.lines[8],
|
||||
amount = 100.0,
|
||||
),
|
||||
)
|
||||
|
||||
actual = _query(filter)
|
||||
expected = [
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = sc.lines[2],
|
||||
outage_line = sc.lines[1],
|
||||
amount = 200.0,
|
||||
),
|
||||
_Violation(
|
||||
time = 1,
|
||||
monitored_line = sc.lines[1],
|
||||
outage_line = sc.lines[5],
|
||||
amount = 500.0,
|
||||
),
|
||||
]
|
||||
@test actual == expected
|
||||
end
|
||||
end
|
||||
39
test/src/solution/methods/XavQiuWanThi19/find_test.jl
Normal file
39
test/src/solution/methods/XavQiuWanThi19/find_test.jl
Normal file
@@ -0,0 +1,39 @@
|
||||
# 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, Test, LinearAlgebra
|
||||
import UnitCommitment: _Violation, _offer, _query
|
||||
|
||||
function solution_methods_XavQiuWanThi19_find_test()
|
||||
@testset "find_violations" begin
|
||||
instance = UnitCommitment.read(fixture("case14.json.gz"))
|
||||
sc = instance.scenarios[1]
|
||||
for line in sc.lines, t in 1:instance.time
|
||||
line.normal_flow_limit[t] = 1.0
|
||||
line.emergency_flow_limit[t] = 1.0
|
||||
end
|
||||
isf = UnitCommitment._injection_shift_factors(
|
||||
lines = sc.lines,
|
||||
buses = sc.buses,
|
||||
)
|
||||
lodf = UnitCommitment._line_outage_factors(
|
||||
lines = sc.lines,
|
||||
buses = sc.buses,
|
||||
isf = isf,
|
||||
)
|
||||
inj = [1000.0 for b in 1:13, t in 1:instance.time]
|
||||
overflow = [0.0 for l in sc.lines, t in 1:instance.time]
|
||||
violations = UnitCommitment._find_violations(
|
||||
instance = instance,
|
||||
sc = sc,
|
||||
net_injections = inj,
|
||||
overflow = overflow,
|
||||
isf = isf,
|
||||
lodf = lodf,
|
||||
max_per_line = 1,
|
||||
max_per_period = 5,
|
||||
)
|
||||
@test length(violations) == 20
|
||||
end
|
||||
end
|
||||
149
test/src/solution/methods/XavQiuWanThi19/sensitivity_test.jl
Normal file
149
test/src/solution/methods/XavQiuWanThi19/sensitivity_test.jl
Normal file
@@ -0,0 +1,149 @@
|
||||
# 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, Test, LinearAlgebra
|
||||
|
||||
function solution_methods_XavQiuWanThi19_sensitivity_test()
|
||||
@testset "_susceptance_matrix" begin
|
||||
instance = UnitCommitment.read(fixture("/case14.json.gz"))
|
||||
sc = instance.scenarios[1]
|
||||
actual = UnitCommitment._susceptance_matrix(sc.lines)
|
||||
@test size(actual) == (20, 20)
|
||||
expected = Diagonal([
|
||||
29.5,
|
||||
7.83,
|
||||
8.82,
|
||||
9.9,
|
||||
10.04,
|
||||
10.2,
|
||||
41.45,
|
||||
8.35,
|
||||
3.14,
|
||||
6.93,
|
||||
8.77,
|
||||
6.82,
|
||||
13.4,
|
||||
9.91,
|
||||
15.87,
|
||||
20.65,
|
||||
6.46,
|
||||
9.09,
|
||||
8.73,
|
||||
5.02,
|
||||
])
|
||||
@test round.(actual, digits = 2) == expected
|
||||
end
|
||||
|
||||
@testset "_reduced_incidence_matrix" begin
|
||||
instance = UnitCommitment.read(fixture("/case14.json.gz"))
|
||||
sc = instance.scenarios[1]
|
||||
actual = UnitCommitment._reduced_incidence_matrix(
|
||||
lines = sc.lines,
|
||||
buses = sc.buses,
|
||||
)
|
||||
@test size(actual) == (20, 13)
|
||||
@test actual[1, 1] == -1.0
|
||||
@test actual[3, 1] == 1.0
|
||||
@test actual[4, 1] == 1.0
|
||||
@test actual[5, 1] == 1.0
|
||||
@test actual[3, 2] == -1.0
|
||||
@test actual[6, 2] == 1.0
|
||||
@test actual[4, 3] == -1.0
|
||||
@test actual[6, 3] == -1.0
|
||||
@test actual[7, 3] == 1.0
|
||||
@test actual[8, 3] == 1.0
|
||||
@test actual[9, 3] == 1.0
|
||||
@test actual[2, 4] == -1.0
|
||||
@test actual[5, 4] == -1.0
|
||||
@test actual[7, 4] == -1.0
|
||||
@test actual[10, 4] == 1.0
|
||||
@test actual[10, 5] == -1.0
|
||||
@test actual[11, 5] == 1.0
|
||||
@test actual[12, 5] == 1.0
|
||||
@test actual[13, 5] == 1.0
|
||||
@test actual[8, 6] == -1.0
|
||||
@test actual[14, 6] == 1.0
|
||||
@test actual[15, 6] == 1.0
|
||||
@test actual[14, 7] == -1.0
|
||||
@test actual[9, 8] == -1.0
|
||||
@test actual[15, 8] == -1.0
|
||||
@test actual[16, 8] == 1.0
|
||||
@test actual[17, 8] == 1.0
|
||||
@test actual[16, 9] == -1.0
|
||||
@test actual[18, 9] == 1.0
|
||||
@test actual[11, 10] == -1.0
|
||||
@test actual[18, 10] == -1.0
|
||||
@test actual[12, 11] == -1.0
|
||||
@test actual[19, 11] == 1.0
|
||||
@test actual[13, 12] == -1.0
|
||||
@test actual[19, 12] == -1.0
|
||||
@test actual[20, 12] == 1.0
|
||||
@test actual[17, 13] == -1.0
|
||||
@test actual[20, 13] == -1.0
|
||||
end
|
||||
|
||||
@testset "_injection_shift_factors" begin
|
||||
instance = UnitCommitment.read(fixture("/case14.json.gz"))
|
||||
sc = instance.scenarios[1]
|
||||
actual = UnitCommitment._injection_shift_factors(
|
||||
lines = sc.lines,
|
||||
buses = sc.buses,
|
||||
)
|
||||
@test size(actual) == (20, 13)
|
||||
@test round.(actual, digits = 2) == [
|
||||
-0.84 -0.75 -0.67 -0.61 -0.63 -0.66 -0.66 -0.65 -0.65 -0.64 -0.63 -0.63 -0.64
|
||||
-0.16 -0.25 -0.33 -0.39 -0.37 -0.34 -0.34 -0.35 -0.35 -0.36 -0.37 -0.37 -0.36
|
||||
0.03 -0.53 -0.15 -0.1 -0.12 -0.14 -0.14 -0.14 -0.13 -0.13 -0.12 -0.12 -0.13
|
||||
0.06 -0.14 -0.32 -0.22 -0.25 -0.3 -0.3 -0.29 -0.28 -0.27 -0.25 -0.26 -0.27
|
||||
0.08 -0.07 -0.2 -0.29 -0.26 -0.22 -0.22 -0.22 -0.23 -0.25 -0.26 -0.26 -0.24
|
||||
0.03 0.47 -0.15 -0.1 -0.12 -0.14 -0.14 -0.14 -0.13 -0.13 -0.12 -0.12 -0.13
|
||||
0.08 0.31 0.5 -0.3 -0.03 0.36 0.36 0.28 0.23 0.1 -0.0 0.02 0.17
|
||||
0.0 0.01 0.02 -0.01 -0.22 -0.63 -0.63 -0.45 -0.41 -0.32 -0.24 -0.25 -0.36
|
||||
0.0 0.01 0.01 -0.01 -0.12 -0.17 -0.17 -0.26 -0.24 -0.18 -0.14 -0.14 -0.21
|
||||
-0.0 -0.02 -0.03 0.02 -0.66 -0.2 -0.2 -0.29 -0.36 -0.5 -0.63 -0.61 -0.43
|
||||
-0.0 -0.01 -0.02 0.01 0.21 -0.12 -0.12 -0.17 -0.28 -0.53 0.18 0.15 -0.03
|
||||
-0.0 -0.0 -0.0 0.0 0.03 -0.02 -0.02 -0.03 -0.02 0.01 -0.52 -0.17 -0.09
|
||||
-0.0 -0.01 -0.01 0.01 0.11 -0.06 -0.06 -0.09 -0.05 0.02 -0.28 -0.59 -0.31
|
||||
-0.0 -0.0 -0.0 -0.0 -0.0 -0.0 -1.0 -0.0 -0.0 -0.0 -0.0 -0.0 0.0
|
||||
0.0 0.01 0.02 -0.01 -0.22 0.37 0.37 -0.45 -0.41 -0.32 -0.24 -0.25 -0.36
|
||||
0.0 0.01 0.02 -0.01 -0.21 0.12 0.12 0.17 -0.72 -0.47 -0.18 -0.15 0.03
|
||||
0.0 0.01 0.01 -0.01 -0.14 0.08 0.08 0.12 0.07 -0.03 -0.2 -0.24 -0.6
|
||||
0.0 0.01 0.02 -0.01 -0.21 0.12 0.12 0.17 0.28 -0.47 -0.18 -0.15 0.03
|
||||
-0.0 -0.0 -0.0 0.0 0.03 -0.02 -0.02 -0.03 -0.02 0.01 0.48 -0.17 -0.09
|
||||
-0.0 -0.01 -0.01 0.01 0.14 -0.08 -0.08 -0.12 -0.07 0.03 0.2 0.24 -0.4
|
||||
]
|
||||
end
|
||||
|
||||
@testset "_line_outage_factors" begin
|
||||
instance = UnitCommitment.read(fixture("/case14.json.gz"))
|
||||
sc = instance.scenarios[1]
|
||||
isf_before = UnitCommitment._injection_shift_factors(
|
||||
lines = sc.lines,
|
||||
buses = sc.buses,
|
||||
)
|
||||
lodf = UnitCommitment._line_outage_factors(
|
||||
lines = sc.lines,
|
||||
buses = sc.buses,
|
||||
isf = isf_before,
|
||||
)
|
||||
for contingency in sc.contingencies
|
||||
for lc in contingency.lines
|
||||
prev_susceptance = lc.susceptance
|
||||
lc.susceptance = 0.0
|
||||
isf_after = UnitCommitment._injection_shift_factors(
|
||||
lines = sc.lines,
|
||||
buses = sc.buses,
|
||||
)
|
||||
lc.susceptance = prev_susceptance
|
||||
for lm in sc.lines
|
||||
expected = isf_after[lm.offset, :]
|
||||
actual =
|
||||
isf_before[lm.offset, :] +
|
||||
lodf[lm.offset, lc.offset] * isf_before[lc.offset, :]
|
||||
@test norm(expected - actual) < 1e-6
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
30
test/src/transform/initcond_test.jl
Normal file
30
test/src/transform/initcond_test.jl
Normal file
@@ -0,0 +1,30 @@
|
||||
# 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, Cbc, JuMP
|
||||
|
||||
function transform_initcond_test()
|
||||
@testset "generate_initial_conditions!" begin
|
||||
# Load instance
|
||||
instance = UnitCommitment.read(fixture("case118-initcond.json.gz"))
|
||||
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||
sc = instance.scenarios[1]
|
||||
# All units should have unknown initial conditions
|
||||
for g in sc.thermal_units
|
||||
@test g.initial_power === nothing
|
||||
@test g.initial_status === nothing
|
||||
end
|
||||
|
||||
# Generate initial conditions
|
||||
UnitCommitment.generate_initial_conditions!(sc, optimizer)
|
||||
|
||||
# All units should now have known initial conditions
|
||||
for g in sc.thermal_units
|
||||
@test g.initial_power !== nothing
|
||||
@test g.initial_status !== nothing
|
||||
end
|
||||
|
||||
# TODO: Check that initial conditions are feasible
|
||||
end
|
||||
end
|
||||
106
test/src/transform/randomize/XavQiuAhm2021_test.jl
Normal file
106
test/src/transform/randomize/XavQiuAhm2021_test.jl
Normal file
@@ -0,0 +1,106 @@
|
||||
# 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.
|
||||
|
||||
import Random
|
||||
import UnitCommitment: XavQiuAhm2021
|
||||
|
||||
using Distributions
|
||||
using Random
|
||||
using UnitCommitment, Cbc, JuMP
|
||||
|
||||
function get_scenario()
|
||||
return UnitCommitment.read_benchmark(
|
||||
"matpower/case118/2017-02-01",
|
||||
).scenarios[1]
|
||||
end
|
||||
system_load(sc) = sum(b.load for b in sc.buses)
|
||||
test_approx(x, y) = @test isapprox(x, y, atol = 1e-3)
|
||||
|
||||
function transform_randomize_XavQiuAhm2021_test()
|
||||
@testset "XavQiuAhm2021" begin
|
||||
@testset "cost and load share" begin
|
||||
sc = get_scenario()
|
||||
# Check original costs
|
||||
unit = sc.thermal_units[10]
|
||||
test_approx(unit.min_power_cost[1], 825.023)
|
||||
test_approx(unit.cost_segments[1].cost[1], 36.659)
|
||||
test_approx(unit.startup_categories[1].cost[1], 7570.42)
|
||||
|
||||
# Check original load share
|
||||
bus = sc.buses[1]
|
||||
prev_system_load = system_load(sc)
|
||||
test_approx(bus.load[1] / prev_system_load[1], 0.012)
|
||||
|
||||
randomize!(
|
||||
sc,
|
||||
XavQiuAhm2021.Randomization(randomize_load_profile = false),
|
||||
rng = MersenneTwister(42),
|
||||
)
|
||||
|
||||
# Check randomized costs
|
||||
test_approx(unit.min_power_cost[1], 831.977)
|
||||
test_approx(unit.cost_segments[1].cost[1], 36.968)
|
||||
test_approx(unit.startup_categories[1].cost[1], 7634.226)
|
||||
|
||||
# Check randomized load share
|
||||
curr_system_load = system_load(sc)
|
||||
test_approx(bus.load[1] / curr_system_load[1], 0.013)
|
||||
|
||||
# System load should not change
|
||||
@test prev_system_load ≈ curr_system_load
|
||||
end
|
||||
|
||||
@testset "load profile" begin
|
||||
sc = get_scenario()
|
||||
# Check original load profile
|
||||
@test round.(system_load(sc), digits = 1)[1:8] ≈ [
|
||||
3059.5,
|
||||
2983.2,
|
||||
2937.5,
|
||||
2953.9,
|
||||
3073.1,
|
||||
3356.4,
|
||||
4068.5,
|
||||
4018.8,
|
||||
]
|
||||
|
||||
randomize!(
|
||||
sc,
|
||||
XavQiuAhm2021.Randomization();
|
||||
rng = MersenneTwister(42),
|
||||
)
|
||||
|
||||
# Check randomized load profile
|
||||
@test round.(system_load(sc), digits = 1)[1:8] ≈ [
|
||||
4854.7,
|
||||
4849.2,
|
||||
4732.7,
|
||||
4848.2,
|
||||
4948.4,
|
||||
5231.1,
|
||||
5874.8,
|
||||
5934.8,
|
||||
]
|
||||
end
|
||||
|
||||
@testset "profiled unit cost" begin
|
||||
sc = UnitCommitment.read(
|
||||
fixture("case14-profiled.json.gz"),
|
||||
).scenarios[1]
|
||||
# Check original costs
|
||||
pu1 = sc.profiled_units[1]
|
||||
pu2 = sc.profiled_units[2]
|
||||
test_approx(pu1.cost[1], 100.0)
|
||||
test_approx(pu2.cost[1], 50.0)
|
||||
randomize!(
|
||||
sc,
|
||||
XavQiuAhm2021.Randomization(randomize_load_profile = false),
|
||||
rng = MersenneTwister(42),
|
||||
)
|
||||
# Check randomized costs
|
||||
test_approx(pu1.cost[1], 98.039)
|
||||
test_approx(pu2.cost[1], 48.385)
|
||||
end
|
||||
end
|
||||
end
|
||||
68
test/src/transform/slice_test.jl
Normal file
68
test/src/transform/slice_test.jl
Normal file
@@ -0,0 +1,68 @@
|
||||
# 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
|
||||
|
||||
function transform_slice_test()
|
||||
@testset "slice" begin
|
||||
instance = UnitCommitment.read(fixture("case14.json.gz"))
|
||||
modified = UnitCommitment.slice(instance, 1:2)
|
||||
sc = modified.scenarios[1]
|
||||
|
||||
# Should update all time-dependent fields
|
||||
@test modified.time == 2
|
||||
@test length(sc.power_balance_penalty) == 2
|
||||
@test length(sc.reserves_by_name["r1"].amount) == 2
|
||||
for u in sc.thermal_units
|
||||
@test length(u.max_power) == 2
|
||||
@test length(u.min_power) == 2
|
||||
@test length(u.must_run) == 2
|
||||
@test length(u.min_power_cost) == 2
|
||||
for s in u.cost_segments
|
||||
@test length(s.mw) == 2
|
||||
@test length(s.cost) == 2
|
||||
end
|
||||
end
|
||||
for b in sc.buses
|
||||
@test length(b.load) == 2
|
||||
end
|
||||
for l in sc.lines
|
||||
@test length(l.normal_flow_limit) == 2
|
||||
@test length(l.emergency_flow_limit) == 2
|
||||
@test length(l.flow_limit_penalty) == 2
|
||||
end
|
||||
for ps in sc.price_sensitive_loads
|
||||
@test length(ps.demand) == 2
|
||||
@test length(ps.revenue) == 2
|
||||
end
|
||||
|
||||
# Should be able to build model without errors
|
||||
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||
model = UnitCommitment.build_model(
|
||||
instance = modified,
|
||||
optimizer = optimizer,
|
||||
variable_names = true,
|
||||
)
|
||||
end
|
||||
|
||||
@testset "slice profiled units" begin
|
||||
instance = UnitCommitment.read(fixture("case14-profiled.json.gz"))
|
||||
modified = UnitCommitment.slice(instance, 1:2)
|
||||
sc = modified.scenarios[1]
|
||||
|
||||
# Should update all time-dependent fields
|
||||
for pu in sc.profiled_units
|
||||
@test length(pu.max_power) == 2
|
||||
@test length(pu.min_power) == 2
|
||||
end
|
||||
|
||||
# Should be able to build model without errors
|
||||
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||
model = UnitCommitment.build_model(
|
||||
instance = modified,
|
||||
optimizer = optimizer,
|
||||
variable_names = true,
|
||||
)
|
||||
end
|
||||
end
|
||||
66
test/src/usage.jl
Normal file
66
test/src/usage.jl
Normal file
@@ -0,0 +1,66 @@
|
||||
# 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
|
||||
|
||||
function _set_flow_limits!(instance)
|
||||
for sc in instance.scenarios
|
||||
sc.power_balance_penalty = [100_000 for _ in 1:instance.time]
|
||||
for line in sc.lines, t in 1:4
|
||||
line.normal_flow_limit[t] = 10.0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function usage_test()
|
||||
@testset "usage" begin
|
||||
@testset "deterministic" begin
|
||||
instance = UnitCommitment.read(fixture("case14.json.gz"))
|
||||
_set_flow_limits!(instance)
|
||||
optimizer =
|
||||
optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = optimizer,
|
||||
variable_names = true,
|
||||
)
|
||||
@test name(model[:is_on]["g1", 1]) == "is_on[g1,1]"
|
||||
|
||||
# Optimize and retrieve solution
|
||||
UnitCommitment.optimize!(model)
|
||||
solution = UnitCommitment.solution(model)
|
||||
|
||||
# Write solution to a file
|
||||
filename = tempname()
|
||||
UnitCommitment.write(filename, solution)
|
||||
loaded = JSON.parsefile(filename)
|
||||
@test length(loaded["Is on"]) == 6
|
||||
|
||||
# Verify solution
|
||||
@test UnitCommitment.validate(instance, solution)
|
||||
|
||||
# Reoptimize with fixed solution
|
||||
UnitCommitment.fix!(model, solution)
|
||||
UnitCommitment.optimize!(model)
|
||||
@test UnitCommitment.validate(instance, solution)
|
||||
end
|
||||
|
||||
@testset "stochastic" begin
|
||||
instance = UnitCommitment.read([
|
||||
fixture("case14.json.gz"),
|
||||
fixture("case14.json.gz"),
|
||||
])
|
||||
_set_flow_limits!(instance)
|
||||
@test length(instance.scenarios) == 2
|
||||
optimizer =
|
||||
optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||
model = UnitCommitment.build_model(
|
||||
instance = instance,
|
||||
optimizer = optimizer,
|
||||
)
|
||||
UnitCommitment.optimize!(model)
|
||||
solution = UnitCommitment.solution(model)
|
||||
end
|
||||
end
|
||||
end
|
||||
43
test/src/validation/repair_test.jl
Normal file
43
test/src/validation/repair_test.jl
Normal file
@@ -0,0 +1,43 @@
|
||||
# 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, JSON, GZip, DataStructures
|
||||
|
||||
function parse_case14()
|
||||
return JSON.parse(
|
||||
GZip.gzopen(fixture("case14.json.gz")),
|
||||
dicttype = () -> DefaultOrderedDict(nothing),
|
||||
)
|
||||
end
|
||||
|
||||
function validation_repair_test()
|
||||
@testset "repair!" begin
|
||||
@testset "Cost curve should be convex" begin
|
||||
json = parse_case14()
|
||||
json["Generators"]["g1"]["Production cost curve (MW)"] =
|
||||
[100, 150, 200]
|
||||
json["Generators"]["g1"]["Production cost curve (\$)"] =
|
||||
[10, 25, 30]
|
||||
sc = UnitCommitment._from_json(json, repair = false)
|
||||
@test UnitCommitment.repair!(sc) == 4
|
||||
end
|
||||
|
||||
@testset "Startup limit must be greater than Pmin" begin
|
||||
json = parse_case14()
|
||||
json["Generators"]["g1"]["Production cost curve (MW)"] = [100, 150]
|
||||
json["Generators"]["g1"]["Production cost curve (\$)"] = [100, 150]
|
||||
json["Generators"]["g1"]["Startup limit (MW)"] = 80
|
||||
sc = UnitCommitment._from_json(json, repair = false)
|
||||
@test UnitCommitment.repair!(sc) == 1
|
||||
end
|
||||
|
||||
@testset "Startup costs and delays must be increasing" begin
|
||||
json = parse_case14()
|
||||
json["Generators"]["g1"]["Startup costs (\$)"] = [300, 200, 100]
|
||||
json["Generators"]["g1"]["Startup delays (h)"] = [8, 4, 2]
|
||||
sc = UnitCommitment._from_json(json, repair = false)
|
||||
@test UnitCommitment.repair!(sc) == 4
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,28 +0,0 @@
|
||||
# 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, Cbc, JuMP
|
||||
|
||||
@testset "generate_initial_conditions!" begin
|
||||
# Load instance
|
||||
instance = UnitCommitment.read("$FIXTURES/case118-initcond.json.gz")
|
||||
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||
|
||||
# All units should have unknown initial conditions
|
||||
for g in instance.units
|
||||
@test g.initial_power === nothing
|
||||
@test g.initial_status === nothing
|
||||
end
|
||||
|
||||
# Generate initial conditions
|
||||
UnitCommitment.generate_initial_conditions!(instance, optimizer)
|
||||
|
||||
# All units should now have known initial conditions
|
||||
for g in instance.units
|
||||
@test g.initial_power !== nothing
|
||||
@test g.initial_status !== nothing
|
||||
end
|
||||
|
||||
# TODO: Check that initial conditions are feasible
|
||||
end
|
||||
@@ -1,67 +0,0 @@
|
||||
# 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.
|
||||
|
||||
import Random
|
||||
import UnitCommitment: XavQiuAhm2021
|
||||
|
||||
using Distributions
|
||||
using Random
|
||||
using UnitCommitment, Cbc, JuMP
|
||||
|
||||
get_instance() = UnitCommitment.read_benchmark("matpower/case118/2017-02-01")
|
||||
system_load(instance) = sum(b.load for b in instance.buses)
|
||||
test_approx(x, y) = @test isapprox(x, y, atol = 1e-3)
|
||||
|
||||
@testset "XavQiuAhm2021" begin
|
||||
@testset "cost and load share" begin
|
||||
instance = get_instance()
|
||||
|
||||
# Check original costs
|
||||
unit = instance.units[10]
|
||||
test_approx(unit.min_power_cost[1], 825.023)
|
||||
test_approx(unit.cost_segments[1].cost[1], 36.659)
|
||||
test_approx(unit.startup_categories[1].cost[1], 7570.42)
|
||||
|
||||
# Check original load share
|
||||
bus = instance.buses[1]
|
||||
prev_system_load = system_load(instance)
|
||||
test_approx(bus.load[1] / prev_system_load[1], 0.012)
|
||||
|
||||
randomize!(
|
||||
instance,
|
||||
XavQiuAhm2021.Randomization(randomize_load_profile = false),
|
||||
rng = MersenneTwister(42),
|
||||
)
|
||||
|
||||
# Check randomized costs
|
||||
test_approx(unit.min_power_cost[1], 831.977)
|
||||
test_approx(unit.cost_segments[1].cost[1], 36.968)
|
||||
test_approx(unit.startup_categories[1].cost[1], 7634.226)
|
||||
|
||||
# Check randomized load share
|
||||
curr_system_load = system_load(instance)
|
||||
test_approx(bus.load[1] / curr_system_load[1], 0.013)
|
||||
|
||||
# System load should not change
|
||||
@test prev_system_load ≈ curr_system_load
|
||||
end
|
||||
|
||||
@testset "load profile" begin
|
||||
instance = get_instance()
|
||||
|
||||
# Check original load profile
|
||||
@test round.(system_load(instance), digits = 1)[1:8] ≈
|
||||
[3059.5, 2983.2, 2937.5, 2953.9, 3073.1, 3356.4, 4068.5, 4018.8]
|
||||
|
||||
randomize!(
|
||||
instance,
|
||||
XavQiuAhm2021.Randomization(),
|
||||
rng = MersenneTwister(42),
|
||||
)
|
||||
|
||||
# Check randomized load profile
|
||||
@test round.(system_load(instance), digits = 1)[1:8] ≈
|
||||
[4854.7, 4849.2, 4732.7, 4848.2, 4948.4, 5231.1, 5874.8, 5934.8]
|
||||
end
|
||||
end
|
||||
@@ -1,44 +0,0 @@
|
||||
# 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 "slice" begin
|
||||
instance = UnitCommitment.read("$FIXTURES/case14.json.gz")
|
||||
modified = UnitCommitment.slice(instance, 1:2)
|
||||
|
||||
# Should update all time-dependent fields
|
||||
@test modified.time == 2
|
||||
@test length(modified.power_balance_penalty) == 2
|
||||
@test length(modified.reserves_by_name["r1"].amount) == 2
|
||||
for u in modified.units
|
||||
@test length(u.max_power) == 2
|
||||
@test length(u.min_power) == 2
|
||||
@test length(u.must_run) == 2
|
||||
@test length(u.min_power_cost) == 2
|
||||
for s in u.cost_segments
|
||||
@test length(s.mw) == 2
|
||||
@test length(s.cost) == 2
|
||||
end
|
||||
end
|
||||
for b in modified.buses
|
||||
@test length(b.load) == 2
|
||||
end
|
||||
for l in modified.lines
|
||||
@test length(l.normal_flow_limit) == 2
|
||||
@test length(l.emergency_flow_limit) == 2
|
||||
@test length(l.flow_limit_penalty) == 2
|
||||
end
|
||||
for ps in modified.price_sensitive_loads
|
||||
@test length(ps.demand) == 2
|
||||
@test length(ps.revenue) == 2
|
||||
end
|
||||
# Should be able to build model without errors
|
||||
optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
|
||||
model = UnitCommitment.build_model(
|
||||
instance = modified,
|
||||
optimizer = optimizer,
|
||||
variable_names = true,
|
||||
)
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user