commit b2480ef35676e99fc4e115f0f4deb437e4a92b84 Author: Alinson S Xavier Date: Tue Nov 3 15:49:12 2020 -0600 Initial public release diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..5c18f4a --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,33 @@ +name: Benchmark +on: + push: + paths-ignore: + - '**.csv' + - '**.md' + - '.git*' + - 'test/**' +jobs: + benchmark: + runs-on: [self-hosted, benchmark] + timeout-minutes: 10080 + steps: + - uses: actions/checkout@v1 + - name: Benchmark + run: | + julia --project=@. -e 'using Pkg; Pkg.instantiate()' + make build/sysimage.so + make -C benchmark clean + make -C benchmark -kj4 + make -C benchmark tables + make -C benchmark clean-mps clean-sol + - name: Upload logs + uses: actions/upload-artifact@v2 + with: + name: logs + path: benchmark/results/* + - name: Upload tables & charts + uses: actions/upload-artifact@v2 + with: + name: tables + path: benchmark/tables/* + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..059da54 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,9 @@ +name: Tests +on: push +jobs: + test: + runs-on: self-hosted + steps: + - uses: actions/checkout@v1 + - name: Run unit tests + run: julia --project=@. -e 'using Pkg; Pkg.test("UnitCommitment")' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c612bda --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.bak +*.gz +*.lastrun +*.so +*.mps +.ipy* +benchmark/results +benchmark/runs +benchmark/tables +build +instances/**/*.json +instances/_source +local +docs +notebooks +TODO.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0ffad49 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# UnitCommitment.jl + +### Version 0.1.0 (November 6, 2020) + +* Initial public release diff --git a/COPYING.md b/COPYING.md new file mode 100644 index 0000000..c8e8937 --- /dev/null +++ b/COPYING.md @@ -0,0 +1,25 @@ +Copyright © 2020, UChicago Argonne, LLC + +All Rights Reserved + +Software Name: UnitCommitment.jl + +By: Argonne National Laboratory + +OPEN SOURCE LICENSE +------------------- + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +******************************************************************************** + +DISCLAIMER +---------- + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +******************************************************************************** diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ac379c6 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +# 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. + +JULIA := julia --color=yes --project=@. +MKDOCS := ~/.local/bin/mkdocs +SRC_FILES := $(wildcard src/*.jl) $(wildcard test/*.jl) +VERSION := 0.1 + +build/sysimage.so: src/sysimage.jl Project.toml Manifest.toml + mkdir -p build + $(JULIA) src/sysimage.jl + +clean: + rm -rf build/* + +docs: + $(MKDOCS) build -d ../docs/$(VERSION)/ + rm ../docs/$(VERSION)/*.ipynb + +docs-push: + rsync -avP docs/ isoron@axavier.org:/www/axavier.org/projects/UnitCommitment.jl/ + +install-deps-docs: + pip install --user mkdocs mkdocs-cinder python-markdown-math + +test: build/sysimage.so + @echo Running tests... + cd test; $(JULIA) --sysimage ../build/sysimage.so runtests.jl | tee ../build/test.log + +.PHONY: docs docs-push build test diff --git a/Manifest.toml b/Manifest.toml new file mode 100644 index 0000000..c5b52a2 --- /dev/null +++ b/Manifest.toml @@ -0,0 +1,366 @@ +# This file is machine-generated - editing it directly is not advised + +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[BenchmarkTools]] +deps = ["JSON", "Logging", "Printf", "Statistics", "UUIDs"] +git-tree-sha1 = "9e62e66db34540a0c919d72172cc2f642ac71260" +uuid = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +version = "0.5.0" + +[[BinaryProvider]] +deps = ["Libdl", "Logging", "SHA"] +git-tree-sha1 = "ecdec412a9abc8db54c0efc5548c64dfce072058" +uuid = "b99e7846-7c00-51b0-8f62-c81ae34c0232" +version = "0.5.10" + +[[Bzip2_jll]] +deps = ["Libdl", "Pkg"] +git-tree-sha1 = "3663bfffede2ef41358b6fc2e1d8a6d50b3c3904" +uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0" +version = "1.0.6+2" + +[[CEnum]] +git-tree-sha1 = "1b77a77c3b28e0b3f413f7567c9bb8dd9bdccd14" +uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82" +version = "0.3.0" + +[[Calculus]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "f641eb0a4f00c343bbc32346e1217b86f3ce9dad" +uuid = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" +version = "0.5.1" + +[[Cbc]] +deps = ["BinaryProvider", "CEnum", "Cbc_jll", "Libdl", "MathOptInterface", "SparseArrays"] +git-tree-sha1 = "72e4299de0995a60a6230079adc7e47580870815" +uuid = "9961bab8-2fa3-5c5a-9d89-47fab24efd76" +version = "0.7.0" + +[[Cbc_jll]] +deps = ["Cgl_jll", "Clp_jll", "CoinUtils_jll", "CompilerSupportLibraries_jll", "Libdl", "OpenBLAS32_jll", "Osi_jll", "Pkg"] +git-tree-sha1 = "16b8ffa56b3ded6b201aa2f50623f260448aa205" +uuid = "38041ee0-ae04-5750-a4d2-bb4d0d83d27d" +version = "2.10.3+4" + +[[Cgl_jll]] +deps = ["Clp_jll", "CompilerSupportLibraries_jll", "Libdl", "Pkg"] +git-tree-sha1 = "32be20ec1e4c40e5c5d1bbf949ba9918a92a7569" +uuid = "3830e938-1dd0-5f3e-8b8e-b3ee43226782" +version = "0.60.2+5" + +[[Clp_jll]] +deps = ["CoinUtils_jll", "CompilerSupportLibraries_jll", "Libdl", "OpenBLAS32_jll", "Osi_jll", "Pkg"] +git-tree-sha1 = "70fe9e52fd95fa37f645e3d30f08f436cc5b1457" +uuid = "06985876-5285-5a41-9fcb-8948a742cc53" +version = "1.17.6+5" + +[[CodecBzip2]] +deps = ["Bzip2_jll", "Libdl", "TranscodingStreams"] +git-tree-sha1 = "2e62a725210ce3c3c2e1a3080190e7ca491f18d7" +uuid = "523fee87-0ab8-5b00-afb7-3ecf72e48cfd" +version = "0.7.2" + +[[CodecZlib]] +deps = ["TranscodingStreams", "Zlib_jll"] +git-tree-sha1 = "ded953804d019afa9a3f98981d99b33e3db7b6da" +uuid = "944b1d66-785c-5afd-91f1-9de20f533193" +version = "0.7.0" + +[[CoinUtils_jll]] +deps = ["CompilerSupportLibraries_jll", "Libdl", "OpenBLAS32_jll", "Pkg"] +git-tree-sha1 = "ee1f06ab89337b7f194c29377ab174e752cdf60d" +uuid = "be027038-0da8-5614-b30d-e42594cb92df" +version = "2.11.3+3" + +[[CommonSubexpressions]] +deps = ["MacroTools", "Test"] +git-tree-sha1 = "7b8a93dba8af7e3b42fecabf646260105ac373f7" +uuid = "bbf7d656-a473-5ed7-a52c-81e309532950" +version = "0.3.0" + +[[CompilerSupportLibraries_jll]] +deps = ["Libdl", "Pkg"] +git-tree-sha1 = "7c4f882c41faa72118841185afc58a2eb00ef612" +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "0.3.3+0" + +[[DataStructures]] +deps = ["InteractiveUtils", "OrderedCollections"] +git-tree-sha1 = "edad9434967fdc0a2631a65d902228400642120c" +uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +version = "0.17.19" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[DiffResults]] +deps = ["StaticArrays"] +git-tree-sha1 = "da24935df8e0c6cf28de340b958f6aac88eaa0cc" +uuid = "163ba53b-c6d8-5494-b064-1a9d43ac40c5" +version = "1.0.2" + +[[DiffRules]] +deps = ["NaNMath", "Random", "SpecialFunctions"] +git-tree-sha1 = "eb0c34204c8410888844ada5359ac8b96292cfd1" +uuid = "b552c78f-8df3-52c6-915a-8e097449b14b" +version = "1.0.1" + +[[Distributed]] +deps = ["Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" + +[[DocStringExtensions]] +deps = ["LibGit2", "Markdown", "Pkg", "Test"] +git-tree-sha1 = "c5714d9bcdba66389612dc4c47ed827c64112997" +uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +version = "0.8.2" + +[[Documenter]] +deps = ["Base64", "Dates", "DocStringExtensions", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"] +git-tree-sha1 = "1c593d1efa27437ed9dd365d1143c594b563e138" +uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +version = "0.25.1" + +[[ForwardDiff]] +deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "NaNMath", "Random", "SpecialFunctions", "StaticArrays"] +git-tree-sha1 = "1d090099fb82223abc48f7ce176d3f7696ede36d" +uuid = "f6369f11-7733-5829-9624-2563aa707210" +version = "0.10.12" + +[[GLPK]] +deps = ["BinaryProvider", "GLPK_jll", "Libdl", "MathOptInterface", "SparseArrays"] +git-tree-sha1 = "86573ecb852e303b209212046af44871f1c0e49c" +uuid = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" +version = "0.13.0" + +[[GLPK_jll]] +deps = ["GMP_jll", "Libdl", "Pkg"] +git-tree-sha1 = "ccc855de74292e478d4278e3a6fdd8212f75e81e" +uuid = "e8aa6df9-e6ca-548a-97ff-1f85fc5b8b98" +version = "4.64.0+0" + +[[GMP_jll]] +deps = ["Libdl", "Pkg"] +git-tree-sha1 = "4dd9301d3a027c05ec403e756ee7a60e3c367e5d" +uuid = "781609d7-10c4-51f6-84f2-b8444358ff6d" +version = "6.1.2+5" + +[[GZip]] +deps = ["Libdl"] +git-tree-sha1 = "039be665faf0b8ae36e089cd694233f5dee3f7d6" +uuid = "92fee26a-97fe-5a0c-ad85-20a5f3185b63" +version = "0.5.1" + +[[HTTP]] +deps = ["Base64", "Dates", "IniFile", "MbedTLS", "Sockets"] +git-tree-sha1 = "2ac03263ce44be4222342bca1c51c36ce7566161" +uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" +version = "0.8.17" + +[[IniFile]] +deps = ["Test"] +git-tree-sha1 = "098e4d2c533924c921f9f9847274f2ad89e018b8" +uuid = "83e8ac13-25f8-5344-8a64-a9f2b223428f" +version = "0.5.0" + +[[InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "b34d7cef7b337321e97d22242c3c2b91f476748e" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.0" + +[[JSONSchema]] +deps = ["HTTP", "JSON", "ZipFile"] +git-tree-sha1 = "832a4d327d9dafdae55a6ecae04f9997c83615cc" +uuid = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" +version = "0.3.0" + +[[JuMP]] +deps = ["Calculus", "DataStructures", "ForwardDiff", "LinearAlgebra", "MathOptInterface", "MutableArithmetics", "NaNMath", "Random", "SparseArrays", "Statistics"] +git-tree-sha1 = "cbab42e2e912109d27046aa88f02a283a9abac7c" +uuid = "4076af6c-e467-56ae-b986-b466b2749572" +version = "0.21.3" + +[[LibGit2]] +deps = ["Printf"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" + +[[Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[LinearAlgebra]] +deps = ["Libdl"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[MacroTools]] +deps = ["Markdown", "Random"] +git-tree-sha1 = "f7d2e3f654af75f01ec49be82c231c382214223a" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.5" + +[[Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[MathOptInterface]] +deps = ["BenchmarkTools", "CodecBzip2", "CodecZlib", "JSON", "JSONSchema", "LinearAlgebra", "MutableArithmetics", "OrderedCollections", "SparseArrays", "Test", "Unicode"] +git-tree-sha1 = "cd2049c055c7d192a235670d50faa375361624ba" +uuid = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +version = "0.9.14" + +[[MbedTLS]] +deps = ["Dates", "MbedTLS_jll", "Random", "Sockets"] +git-tree-sha1 = "426a6978b03a97ceb7ead77775a1da066343ec6e" +uuid = "739be429-bea8-5141-9913-cc70e7f3736d" +version = "1.0.2" + +[[MbedTLS_jll]] +deps = ["Libdl", "Pkg"] +git-tree-sha1 = "a0cb0d489819fa7ea5f9fa84c7e7eba19d8073af" +uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" +version = "2.16.6+1" + +[[Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[MutableArithmetics]] +deps = ["LinearAlgebra", "SparseArrays", "Test"] +git-tree-sha1 = "6cf09794783b9de2e662c4e8b60d743021e338d0" +uuid = "d8a4904e-b15c-11e9-3269-09a3773c0cb0" +version = "0.2.10" + +[[NaNMath]] +git-tree-sha1 = "c84c576296d0e2fbb3fc134d3e09086b3ea617cd" +uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +version = "0.3.4" + +[[OpenBLAS32_jll]] +deps = ["CompilerSupportLibraries_jll", "Libdl", "Pkg"] +git-tree-sha1 = "793b33911239d2651c356c823492b58d6490d36a" +uuid = "656ef2d0-ae68-5445-9ca0-591084a874a2" +version = "0.3.9+4" + +[[OpenSpecFun_jll]] +deps = ["CompilerSupportLibraries_jll", "Libdl", "Pkg"] +git-tree-sha1 = "d51c416559217d974a1113522d5919235ae67a87" +uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" +version = "0.5.3+3" + +[[OrderedCollections]] +git-tree-sha1 = "293b70ac1780f9584c89268a6e2a560d938a7065" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.3.0" + +[[Osi_jll]] +deps = ["CoinUtils_jll", "CompilerSupportLibraries_jll", "Libdl", "OpenBLAS32_jll", "Pkg"] +git-tree-sha1 = "bd436a97280df40938e66ae8d18e57aceb072856" +uuid = "7da25872-d9ce-5375-a4d3-7a845f58efdd" +version = "0.108.5+3" + +[[PackageCompiler]] +deps = ["Libdl", "Pkg", "UUIDs"] +git-tree-sha1 = "98aa9c653e1dc3473bb5050caf8501293db9eee1" +uuid = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" +version = "1.2.1" + +[[Parsers]] +deps = ["Dates", "Test"] +git-tree-sha1 = "10134f2ee0b1978ae7752c41306e131a684e1f06" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "1.0.7" + +[[Pkg]] +deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[Random]] +deps = ["Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[Requires]] +deps = ["UUIDs"] +git-tree-sha1 = "d37400976e98018ee840e0ca4f9d20baa231dc6b" +uuid = "ae029012-a4dd-5104-9daa-d747884805df" +version = "1.0.1" + +[[SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[SparseArrays]] +deps = ["LinearAlgebra", "Random"] +uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + +[[SpecialFunctions]] +deps = ["OpenSpecFun_jll"] +git-tree-sha1 = "d8d8b8a9f4119829410ecd706da4cc8594a1e020" +uuid = "276daf66-3868-5448-9aa4-cd146d93841b" +version = "0.10.3" + +[[StaticArrays]] +deps = ["LinearAlgebra", "Random", "Statistics"] +git-tree-sha1 = "016d1e1a00fabc556473b07161da3d39726ded35" +uuid = "90137ffa-7385-5640-81b9-e52037218182" +version = "0.12.4" + +[[Statistics]] +deps = ["LinearAlgebra", "SparseArrays"] +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[[Test]] +deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[[TimerOutputs]] +deps = ["Printf"] +git-tree-sha1 = "f458ca23ff80e46a630922c555d838303e4b9603" +uuid = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" +version = "0.5.6" + +[[TranscodingStreams]] +deps = ["Random", "Test"] +git-tree-sha1 = "7c53c35547de1c5b9d46a4797cf6d8253807108c" +uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" +version = "0.9.5" + +[[UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[[ZipFile]] +deps = ["Libdl", "Printf", "Zlib_jll"] +git-tree-sha1 = "254975fef2fc526583bb9b7c9420fe66ffe09f2f" +uuid = "a5390f91-8eb1-5f08-bee0-b1d1ffed6cea" +version = "0.9.2" + +[[Zlib_jll]] +deps = ["Libdl", "Pkg"] +git-tree-sha1 = "622d8b6dc0c7e8029f17127703de9819134d1b71" +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.2.11+14" diff --git a/Project.toml b/Project.toml new file mode 100644 index 0000000..272768d --- /dev/null +++ b/Project.toml @@ -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. + +name = "UnitCommitment" +uuid = "64606440-39ea-11e9-0f29-3303a1d3d877" +authors = ["Santos Xavier, Alinson "] +repo = "https://github.com/ANL-CEEESA/UnitCommitment.jl" +version = "0.1.0" + +[deps] +Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76" +DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" +GZip = "92fee26a-97fe-5a0c-ad85-20a5f3185b63" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +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" +Requires = "ae029012-a4dd-5104-9daa-d747884805df" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" + +[compat] +JuMP = "0.21" diff --git a/README.md b/README.md new file mode 100755 index 0000000..a660802 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ + + + +# UnitCommitment.jl + +**UnitCommitment.jl** is an optimization package for the Security-Constrained Unit Commitment Problem (SCUC), a fundamental optimization problem in power systems which is used, for example, to clear the day-ahead electricity markets. The problem asks for the most cost-effective power generation schedule under a number of physical, operational and economic constraints. + +### Package Components + +* **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. +* **Benchmark Instances:** The package provides a diverse collection of large-scale benchmark instances collected from the literature and extended to make them more challenging and realistic, based on publicly available data. +* **Model Implementation**: The package provides a Julia/JuMP implementation of state-of-the-art formulations and solution methods for SCUC. Our goal is to keep this implementation 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. + +### Documentation + +* [Installation Guide](https://axavier.org/projects/UnitCommitment.jl/install/) +* [Data Format Specification](https://axavier.org/projects/UnitCommitment.jl/format/) + +### Authors +* **Alinson Santos Xavier,** Argonne National Laboratory +* **Feng Qiu,** Argonne National Laboratory + +### Collaborators +* **Yonghong Chen,** Midcontinent Independent System Operator +* **Feng Pan,** Pacific Northwest National Laboratory diff --git a/benchmark/Makefile b/benchmark/Makefile new file mode 100644 index 0000000..be4c39b --- /dev/null +++ b/benchmark/Makefile @@ -0,0 +1,124 @@ +# 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. + +SHELL := /bin/bash +JULIA := julia --project=. --sysimage ../build/sysimage.so +TIMESTAMP := $(shell date "+%Y-%m-%d %H:%M") +SRC_FILES := $(wildcard ../src/*.jl) + +INSTANCES_PGLIB := \ + pglib-uc/ca/2014-09-01_reserves_0 \ + pglib-uc/ca/2014-09-01_reserves_1 \ + pglib-uc/ca/2014-09-01_reserves_3 \ + pglib-uc/ca/2014-09-01_reserves_5 \ + pglib-uc/ca/2014-12-01_reserves_0 \ + pglib-uc/ca/2014-12-01_reserves_1 \ + pglib-uc/ca/2014-12-01_reserves_3 \ + pglib-uc/ca/2014-12-01_reserves_5 \ + pglib-uc/ca/2015-03-01_reserves_0 \ + pglib-uc/ca/2015-03-01_reserves_1 \ + pglib-uc/ca/2015-03-01_reserves_3 \ + pglib-uc/ca/2015-03-01_reserves_5 \ + pglib-uc/ca/2015-06-01_reserves_0 \ + pglib-uc/ca/2015-06-01_reserves_1 \ + pglib-uc/ca/2015-06-01_reserves_3 \ + pglib-uc/ca/2015-06-01_reserves_5 \ + pglib-uc/ca/Scenario400_reserves_0 \ + pglib-uc/ca/Scenario400_reserves_1 \ + pglib-uc/ca/Scenario400_reserves_3 \ + pglib-uc/ca/Scenario400_reserves_5 \ + pglib-uc/ferc/2015-01-01_hw \ + pglib-uc/ferc/2015-01-01_lw \ + pglib-uc/ferc/2015-02-01_hw \ + pglib-uc/ferc/2015-02-01_lw \ + pglib-uc/ferc/2015-03-01_hw \ + pglib-uc/ferc/2015-03-01_lw \ + pglib-uc/ferc/2015-04-01_hw \ + pglib-uc/ferc/2015-04-01_lw \ + pglib-uc/ferc/2015-05-01_hw \ + pglib-uc/ferc/2015-05-01_lw \ + pglib-uc/ferc/2015-06-01_hw \ + pglib-uc/ferc/2015-06-01_lw \ + pglib-uc/ferc/2015-07-01_hw \ + pglib-uc/ferc/2015-07-01_lw \ + pglib-uc/ferc/2015-08-01_hw \ + pglib-uc/ferc/2015-08-01_lw \ + pglib-uc/ferc/2015-09-01_hw \ + pglib-uc/ferc/2015-09-01_lw \ + pglib-uc/ferc/2015-10-01_hw \ + pglib-uc/ferc/2015-10-01_lw \ + pglib-uc/ferc/2015-11-02_hw \ + pglib-uc/ferc/2015-11-02_lw \ + pglib-uc/ferc/2015-12-01_hw \ + pglib-uc/ferc/2015-12-01_lw \ + pglib-uc/rts_gmlc/2020-01-27 \ + pglib-uc/rts_gmlc/2020-02-09 \ + pglib-uc/rts_gmlc/2020-03-05 \ + pglib-uc/rts_gmlc/2020-04-03 \ + pglib-uc/rts_gmlc/2020-05-05 \ + pglib-uc/rts_gmlc/2020-06-09 \ + pglib-uc/rts_gmlc/2020-07-06 \ + pglib-uc/rts_gmlc/2020-08-12 \ + pglib-uc/rts_gmlc/2020-09-20 \ + pglib-uc/rts_gmlc/2020-10-27 \ + pglib-uc/rts_gmlc/2020-11-25 \ + pglib-uc/rts_gmlc/2020-12-23 + +INSTANCES_MATPOWER := \ + matpower/case118/2017-02-01 \ + matpower/case118/2017-08-01 \ + matpower/case300/2017-02-01 \ + matpower/case300/2017-08-01 \ + matpower/case1354pegase/2017-02-01 \ + matpower/case1354pegase/2017-08-01 \ + matpower/case1888rte/2017-02-01 \ + matpower/case1888rte/2017-08-01 \ + matpower/case1951rte/2017-02-01 \ + matpower/case1951rte/2017-08-01 \ + matpower/case2848rte/2017-02-01 \ + matpower/case2848rte/2017-08-01 \ + matpower/case2868rte/2017-02-01 \ + matpower/case2868rte/2017-08-01 \ + matpower/case3375wp/2017-02-01 \ + matpower/case3375wp/2017-08-01 \ + matpower/case6468rte/2017-02-01 \ + matpower/case6468rte/2017-08-01 \ + matpower/case6515rte/2017-02-01 \ + matpower/case6515rte/2017-08-01 + +SAMPLES := 1 2 3 +SOLUTIONS_MATPOWER := $(foreach s,$(SAMPLES),$(addprefix results/,$(addsuffix .$(s).sol.json,$(INSTANCES_MATPOWER)))) +SOLUTIONS_PGLIB := $(foreach s,$(SAMPLES),$(addprefix results/,$(addsuffix .$(s).sol.json,$(INSTANCES_PGLIB)))) + +.PHONY: tables save small large clean-mps matpower pglib + +all: matpower pglib + +matpower: $(SOLUTIONS_MATPOWER) + +pglib: $(SOLUTIONS_PGLIB) + +clean: + @rm -rf tables/benchmark* tables/compare* results + +clean-mps: + @rm -fv results/*/*/*.mps.gz + +clean-sol: + @rm -rf results/*/*/*.sol.* + +save: + mkdir -p "runs/$(TIMESTAMP)" + rsync -avP results tables "runs/$(TIMESTAMP)/" + +results/%.sol.json: run.jl + @echo "run $*" + @mkdir -p $(dir results/$*) + @$(JULIA) run.jl $* 2>&1 | cat > results/$*.log + @echo "run $* [done]" + +tables: + @mkdir -p tables + @python scripts/table.py + #@python scripts/compare.py tables/reference.csv tables/benchmark.csv diff --git a/benchmark/Manifest.toml b/benchmark/Manifest.toml new file mode 100644 index 0000000..c319e25 --- /dev/null +++ b/benchmark/Manifest.toml @@ -0,0 +1,417 @@ +# This file is machine-generated - editing it directly is not advised + +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[BenchmarkTools]] +deps = ["JSON", "Logging", "Printf", "Statistics", "UUIDs"] +git-tree-sha1 = "9e62e66db34540a0c919d72172cc2f642ac71260" +uuid = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +version = "0.5.0" + +[[BinaryProvider]] +deps = ["Libdl", "Logging", "SHA"] +git-tree-sha1 = "ecdec412a9abc8db54c0efc5548c64dfce072058" +uuid = "b99e7846-7c00-51b0-8f62-c81ae34c0232" +version = "0.5.10" + +[[Bzip2_jll]] +deps = ["Libdl", "Pkg"] +git-tree-sha1 = "3663bfffede2ef41358b6fc2e1d8a6d50b3c3904" +uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0" +version = "1.0.6+2" + +[[CEnum]] +git-tree-sha1 = "1b77a77c3b28e0b3f413f7567c9bb8dd9bdccd14" +uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82" +version = "0.3.0" + +[[Calculus]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "f641eb0a4f00c343bbc32346e1217b86f3ce9dad" +uuid = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" +version = "0.5.1" + +[[Cbc]] +deps = ["BinaryProvider", "CEnum", "Cbc_jll", "Libdl", "MathOptInterface", "SparseArrays"] +git-tree-sha1 = "72e4299de0995a60a6230079adc7e47580870815" +uuid = "9961bab8-2fa3-5c5a-9d89-47fab24efd76" +version = "0.7.0" + +[[Cbc_jll]] +deps = ["Cgl_jll", "Clp_jll", "CoinUtils_jll", "CompilerSupportLibraries_jll", "Libdl", "OpenBLAS32_jll", "Osi_jll", "Pkg"] +git-tree-sha1 = "16b8ffa56b3ded6b201aa2f50623f260448aa205" +uuid = "38041ee0-ae04-5750-a4d2-bb4d0d83d27d" +version = "2.10.3+4" + +[[Cgl_jll]] +deps = ["Clp_jll", "CompilerSupportLibraries_jll", "Libdl", "Pkg"] +git-tree-sha1 = "32be20ec1e4c40e5c5d1bbf949ba9918a92a7569" +uuid = "3830e938-1dd0-5f3e-8b8e-b3ee43226782" +version = "0.60.2+5" + +[[Clp_jll]] +deps = ["CoinUtils_jll", "CompilerSupportLibraries_jll", "Libdl", "OpenBLAS32_jll", "Osi_jll", "Pkg"] +git-tree-sha1 = "70fe9e52fd95fa37f645e3d30f08f436cc5b1457" +uuid = "06985876-5285-5a41-9fcb-8948a742cc53" +version = "1.17.6+5" + +[[CodeTracking]] +deps = ["InteractiveUtils", "UUIDs"] +git-tree-sha1 = "cab4da992adc0a64f63fa30d2db2fd8bec40cab4" +uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" +version = "0.5.11" + +[[CodecBzip2]] +deps = ["Bzip2_jll", "Libdl", "TranscodingStreams"] +git-tree-sha1 = "2e62a725210ce3c3c2e1a3080190e7ca491f18d7" +uuid = "523fee87-0ab8-5b00-afb7-3ecf72e48cfd" +version = "0.7.2" + +[[CodecZlib]] +deps = ["TranscodingStreams", "Zlib_jll"] +git-tree-sha1 = "ded953804d019afa9a3f98981d99b33e3db7b6da" +uuid = "944b1d66-785c-5afd-91f1-9de20f533193" +version = "0.7.0" + +[[CoinUtils_jll]] +deps = ["CompilerSupportLibraries_jll", "Libdl", "OpenBLAS32_jll", "Pkg"] +git-tree-sha1 = "ee1f06ab89337b7f194c29377ab174e752cdf60d" +uuid = "be027038-0da8-5614-b30d-e42594cb92df" +version = "2.11.3+3" + +[[CommonSubexpressions]] +deps = ["MacroTools", "Test"] +git-tree-sha1 = "7b8a93dba8af7e3b42fecabf646260105ac373f7" +uuid = "bbf7d656-a473-5ed7-a52c-81e309532950" +version = "0.3.0" + +[[CompilerSupportLibraries_jll]] +deps = ["Libdl", "Pkg"] +git-tree-sha1 = "7c4f882c41faa72118841185afc58a2eb00ef612" +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "0.3.3+0" + +[[DataStructures]] +deps = ["InteractiveUtils", "OrderedCollections"] +git-tree-sha1 = "edad9434967fdc0a2631a65d902228400642120c" +uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +version = "0.17.19" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[DiffResults]] +deps = ["StaticArrays"] +git-tree-sha1 = "da24935df8e0c6cf28de340b958f6aac88eaa0cc" +uuid = "163ba53b-c6d8-5494-b064-1a9d43ac40c5" +version = "1.0.2" + +[[DiffRules]] +deps = ["NaNMath", "Random", "SpecialFunctions"] +git-tree-sha1 = "eb0c34204c8410888844ada5359ac8b96292cfd1" +uuid = "b552c78f-8df3-52c6-915a-8e097449b14b" +version = "1.0.1" + +[[Distributed]] +deps = ["Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" + +[[DocStringExtensions]] +deps = ["LibGit2", "Markdown", "Pkg", "Test"] +git-tree-sha1 = "c5714d9bcdba66389612dc4c47ed827c64112997" +uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +version = "0.8.2" + +[[Documenter]] +deps = ["Base64", "Dates", "DocStringExtensions", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"] +git-tree-sha1 = "1c593d1efa27437ed9dd365d1143c594b563e138" +uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +version = "0.25.1" + +[[FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" + +[[ForwardDiff]] +deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "NaNMath", "Random", "SpecialFunctions", "StaticArrays"] +git-tree-sha1 = "1d090099fb82223abc48f7ce176d3f7696ede36d" +uuid = "f6369f11-7733-5829-9624-2563aa707210" +version = "0.10.12" + +[[GLPK]] +deps = ["BinaryProvider", "GLPK_jll", "Libdl", "MathOptInterface", "SparseArrays"] +git-tree-sha1 = "86573ecb852e303b209212046af44871f1c0e49c" +uuid = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" +version = "0.13.0" + +[[GLPK_jll]] +deps = ["GMP_jll", "Libdl", "Pkg"] +git-tree-sha1 = "ccc855de74292e478d4278e3a6fdd8212f75e81e" +uuid = "e8aa6df9-e6ca-548a-97ff-1f85fc5b8b98" +version = "4.64.0+0" + +[[GMP_jll]] +deps = ["Libdl", "Pkg"] +git-tree-sha1 = "4dd9301d3a027c05ec403e756ee7a60e3c367e5d" +uuid = "781609d7-10c4-51f6-84f2-b8444358ff6d" +version = "6.1.2+5" + +[[GZip]] +deps = ["Libdl"] +git-tree-sha1 = "039be665faf0b8ae36e089cd694233f5dee3f7d6" +uuid = "92fee26a-97fe-5a0c-ad85-20a5f3185b63" +version = "0.5.1" + +[[Gurobi]] +deps = ["Libdl", "LinearAlgebra", "MathOptInterface", "MathProgBase", "SparseArrays"] +git-tree-sha1 = "f36a2fa62909675681aec582ccfc4a4a629406e4" +uuid = "2e9cd046-0924-5485-92f1-d5272153d98b" +version = "0.8.1" + +[[HTTP]] +deps = ["Base64", "Dates", "IniFile", "MbedTLS", "Sockets"] +git-tree-sha1 = "2ac03263ce44be4222342bca1c51c36ce7566161" +uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" +version = "0.8.17" + +[[IniFile]] +deps = ["Test"] +git-tree-sha1 = "098e4d2c533924c921f9f9847274f2ad89e018b8" +uuid = "83e8ac13-25f8-5344-8a64-a9f2b223428f" +version = "0.5.0" + +[[InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "b34d7cef7b337321e97d22242c3c2b91f476748e" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.0" + +[[JSONSchema]] +deps = ["HTTP", "JSON", "ZipFile"] +git-tree-sha1 = "832a4d327d9dafdae55a6ecae04f9997c83615cc" +uuid = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" +version = "0.3.0" + +[[JuMP]] +deps = ["Calculus", "DataStructures", "ForwardDiff", "LinearAlgebra", "MathOptInterface", "MutableArithmetics", "NaNMath", "Random", "SparseArrays", "Statistics"] +git-tree-sha1 = "cbab42e2e912109d27046aa88f02a283a9abac7c" +uuid = "4076af6c-e467-56ae-b986-b466b2749572" +version = "0.21.3" + +[[JuliaInterpreter]] +deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"] +git-tree-sha1 = "79e4496b79e8af45198f8c291f26d4514d6b06d6" +uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" +version = "0.7.24" + +[[LibGit2]] +deps = ["Printf"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" + +[[Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[LinearAlgebra]] +deps = ["Libdl"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[LoweredCodeUtils]] +deps = ["JuliaInterpreter"] +git-tree-sha1 = "1b632dc108106101a9909db7be8f8b32ed8d02f7" +uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b" +version = "0.4.6" + +[[MacroTools]] +deps = ["Markdown", "Random"] +git-tree-sha1 = "f7d2e3f654af75f01ec49be82c231c382214223a" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.5" + +[[Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[MathOptFormat]] +deps = ["CodecZlib", "DataStructures", "HTTP", "JSON", "JSONSchema", "MathOptInterface"] +git-tree-sha1 = "0206edd9310b863c222af23f71fde5d09e95c20c" +uuid = "f4570300-c277-12e8-125c-4912f86ce65d" +version = "0.2.2" + +[[MathOptInterface]] +deps = ["BenchmarkTools", "CodecBzip2", "CodecZlib", "JSON", "JSONSchema", "LinearAlgebra", "MutableArithmetics", "OrderedCollections", "SparseArrays", "Test", "Unicode"] +git-tree-sha1 = "cd2049c055c7d192a235670d50faa375361624ba" +uuid = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +version = "0.9.14" + +[[MathProgBase]] +deps = ["LinearAlgebra", "SparseArrays"] +git-tree-sha1 = "9abbe463a1e9fc507f12a69e7f29346c2cdc472c" +uuid = "fdba3010-5040-5b88-9595-932c9decdf73" +version = "0.7.8" + +[[MbedTLS]] +deps = ["Dates", "MbedTLS_jll", "Random", "Sockets"] +git-tree-sha1 = "426a6978b03a97ceb7ead77775a1da066343ec6e" +uuid = "739be429-bea8-5141-9913-cc70e7f3736d" +version = "1.0.2" + +[[MbedTLS_jll]] +deps = ["Libdl", "Pkg"] +git-tree-sha1 = "a0cb0d489819fa7ea5f9fa84c7e7eba19d8073af" +uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" +version = "2.16.6+1" + +[[Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[MutableArithmetics]] +deps = ["LinearAlgebra", "SparseArrays", "Test"] +git-tree-sha1 = "6cf09794783b9de2e662c4e8b60d743021e338d0" +uuid = "d8a4904e-b15c-11e9-3269-09a3773c0cb0" +version = "0.2.10" + +[[NaNMath]] +git-tree-sha1 = "c84c576296d0e2fbb3fc134d3e09086b3ea617cd" +uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +version = "0.3.4" + +[[OpenBLAS32_jll]] +deps = ["CompilerSupportLibraries_jll", "Libdl", "Pkg"] +git-tree-sha1 = "793b33911239d2651c356c823492b58d6490d36a" +uuid = "656ef2d0-ae68-5445-9ca0-591084a874a2" +version = "0.3.9+4" + +[[OpenSpecFun_jll]] +deps = ["CompilerSupportLibraries_jll", "Libdl", "Pkg"] +git-tree-sha1 = "d51c416559217d974a1113522d5919235ae67a87" +uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e" +version = "0.5.3+3" + +[[OrderedCollections]] +git-tree-sha1 = "293b70ac1780f9584c89268a6e2a560d938a7065" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.3.0" + +[[Osi_jll]] +deps = ["CoinUtils_jll", "CompilerSupportLibraries_jll", "Libdl", "OpenBLAS32_jll", "Pkg"] +git-tree-sha1 = "bd436a97280df40938e66ae8d18e57aceb072856" +uuid = "7da25872-d9ce-5375-a4d3-7a845f58efdd" +version = "0.108.5+3" + +[[PackageCompiler]] +deps = ["Libdl", "Pkg", "UUIDs"] +git-tree-sha1 = "98aa9c653e1dc3473bb5050caf8501293db9eee1" +uuid = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" +version = "1.2.1" + +[[Parsers]] +deps = ["Dates", "Test"] +git-tree-sha1 = "10134f2ee0b1978ae7752c41306e131a684e1f06" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "1.0.7" + +[[Pkg]] +deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[Random]] +deps = ["Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[Requires]] +deps = ["UUIDs"] +git-tree-sha1 = "d37400976e98018ee840e0ca4f9d20baa231dc6b" +uuid = "ae029012-a4dd-5104-9daa-d747884805df" +version = "1.0.1" + +[[Revise]] +deps = ["CodeTracking", "Distributed", "FileWatching", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Pkg", "REPL", "UUIDs", "Unicode"] +git-tree-sha1 = "0992d4643e27b2deb9f2e4ec7a56b7033813a027" +uuid = "295af30f-e4ad-537b-8983-00126c2a3abe" +version = "2.7.3" + +[[SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[SparseArrays]] +deps = ["LinearAlgebra", "Random"] +uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + +[[SpecialFunctions]] +deps = ["OpenSpecFun_jll"] +git-tree-sha1 = "d8d8b8a9f4119829410ecd706da4cc8594a1e020" +uuid = "276daf66-3868-5448-9aa4-cd146d93841b" +version = "0.10.3" + +[[StaticArrays]] +deps = ["LinearAlgebra", "Random", "Statistics"] +git-tree-sha1 = "016d1e1a00fabc556473b07161da3d39726ded35" +uuid = "90137ffa-7385-5640-81b9-e52037218182" +version = "0.12.4" + +[[Statistics]] +deps = ["LinearAlgebra", "SparseArrays"] +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[[Test]] +deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[[TimerOutputs]] +deps = ["Printf"] +git-tree-sha1 = "f458ca23ff80e46a630922c555d838303e4b9603" +uuid = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" +version = "0.5.6" + +[[TranscodingStreams]] +deps = ["Random", "Test"] +git-tree-sha1 = "7c53c35547de1c5b9d46a4797cf6d8253807108c" +uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" +version = "0.9.5" + +[[UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[[UnitCommitment]] +deps = ["Cbc", "DataStructures", "Documenter", "GLPK", "GZip", "JSON", "JuMP", "LinearAlgebra", "Logging", "MathOptFormat", "MathOptInterface", "PackageCompiler", "Printf", "Requires", "Revise", "SparseArrays", "Test", "TimerOutputs"] +path = ".." +uuid = "64606440-39ea-11e9-0f29-3303a1d3d877" +version = "2.1.0" + +[[ZipFile]] +deps = ["Libdl", "Printf", "Zlib_jll"] +git-tree-sha1 = "254975fef2fc526583bb9b7c9420fe66ffe09f2f" +uuid = "a5390f91-8eb1-5f08-bee0-b1d1ffed6cea" +version = "0.9.2" + +[[Zlib_jll]] +deps = ["Libdl", "Pkg"] +git-tree-sha1 = "622d8b6dc0c7e8029f17127703de9819134d1b71" +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.2.11+14" diff --git a/benchmark/Project.toml b/benchmark/Project.toml new file mode 100644 index 0000000..454b1b1 --- /dev/null +++ b/benchmark/Project.toml @@ -0,0 +1,5 @@ +[deps] +Gurobi = "2e9cd046-0924-5485-92f1-d5272153d98b" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +UnitCommitment = "64606440-39ea-11e9-0f29-3303a1d3d877" diff --git a/benchmark/run.jl b/benchmark/run.jl new file mode 100644 index 0000000..b3c6fb9 --- /dev/null +++ b/benchmark/run.jl @@ -0,0 +1,61 @@ +# 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 Gurobi +using JSON +using Logging +using Printf +using LinearAlgebra + +function main() + basename, suffix = split(ARGS[1], ".") + solution_filename = "results/$basename.$suffix.sol.json" + model_filename = "results/$basename.$suffix.mps.gz" + + time_limit = 60 * 20 + + BLAS.set_num_threads(4) + global_logger(TimeLogger(initial_time = time())) + + total_time = @elapsed begin + @info "Reading: $basename" + time_read = @elapsed begin + instance = UnitCommitment.read_benchmark(basename) + end + @info @sprintf("Read problem in %.2f seconds", time_read) + + time_model = @elapsed begin + model = build_model(instance=instance, + optimizer=optimizer_with_attributes(Gurobi.Optimizer, + "Threads" => 4, + "Seed" => rand(1:1000), + )) + end + + @info "Optimizing..." + BLAS.set_num_threads(1) + UnitCommitment.optimize!(model, time_limit=time_limit, gap_limit=1e-3) + + end + @info @sprintf("Total time was %.2f seconds", total_time) + + @info "Writing: $solution_filename" + solution = UnitCommitment.get_solution(model) + open(solution_filename, "w") do file + JSON.print(file, solution, 2) + end + + @info "Verifying solution..." + UnitCommitment.validate(instance, solution) + + @info "Setting variable names..." + UnitCommitment.set_variable_names!(model) + + @info "Exporting model..." + JuMP.write_to_file(model.mip, model_filename) +end + +main() diff --git a/benchmark/scripts/compare.py b/benchmark/scripts/compare.py new file mode 100644 index 0000000..65df665 --- /dev/null +++ b/benchmark/scripts/compare.py @@ -0,0 +1,64 @@ +# 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 pandas as pd +import numpy as np +import seaborn as sns +import matplotlib.pyplot as plt +import sys + +easy_cutoff = 120 + +b1 = pd.read_csv(sys.argv[1], index_col=0) +b2 = pd.read_csv(sys.argv[2], index_col=0) + +c1 = b1.groupby(["Group", "Instance", "Sample"])[["Optimization time (s)", "Primal bound"]].mean() +c2 = b2.groupby(["Group", "Instance", "Sample"])[["Optimization time (s)", "Primal bound"]].mean() +c1.columns = ["A Time (s)", "A Value"] +c2.columns = ["B Time (s)", "B Value"] + +merged = pd.concat([c1, c2], axis=1) +merged["Speedup"] = merged["A Time (s)"] / merged["B Time (s)"] +merged["Time diff (s)"] = merged["B Time (s)"] - merged["A Time (s)"] +merged["Value diff (%)"] = np.round((merged["B Value"] - merged["A Value"]) / merged["A Value"] * 100.0, 5) +merged.loc[merged.loc[:, "B Time (s)"] <= 0, "Speedup"] = float("nan") +merged.loc[merged.loc[:, "B Time (s)"] <= 0, "Time diff (s)"] = float("nan") +merged = merged[(merged["A Time (s)"] >= easy_cutoff) | (merged["B Time (s)"] >= easy_cutoff)] +merged.reset_index(inplace=True) +merged["Name"] = merged["Group"] + "/" + merged["Instance"] +merged = merged.sort_values(by="Speedup", ascending=False) + + +k = len(merged.groupby("Name")) +plt.figure(figsize=(12, 0.50 * k)) +plt.rcParams['xtick.bottom'] = plt.rcParams['xtick.labelbottom'] = True +plt.rcParams['xtick.top'] = plt.rcParams['xtick.labeltop'] = True +sns.set_style("whitegrid") +sns.set_palette("Set1") +sns.barplot(data=merged, + x="Speedup", + y="Name", + color="tab:red", + capsize=0.15, + errcolor="k", + errwidth=1.25) +plt.axvline(1.0, linestyle="--", color="k") +plt.tight_layout() + +print("Writing tables/compare.png") +plt.savefig("tables/compare.png", dpi=150) + +print("Writing tables/compare.csv") +merged.loc[:, ["Group", + "Instance", + "Sample", + "A Time (s)", + "B Time (s)", + "Speedup", + "Time diff (s)", + "A Value", + "B Value", + "Value diff (%)", + ] + ].to_csv("tables/compare.csv", index_label="Index") diff --git a/benchmark/scripts/table.py b/benchmark/scripts/table.py new file mode 100644 index 0000000..7f5c6dc --- /dev/null +++ b/benchmark/scripts/table.py @@ -0,0 +1,184 @@ +# 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. + +from pathlib import Path +import pandas as pd +import re +from tabulate import tabulate + + +def process_all_log_files(): + pathlist = Path(".").glob('results/*/*/*.log') + rows = [] + for path in pathlist: + if ".ipy" in str(path): + continue + row = process(str(path)) + rows += [row] + df = pd.DataFrame(rows) + df = df.sort_values(["Group", "Buses"]) + df.index = range(len(df)) + print("Writing tables/benchmark.csv") + df.to_csv("tables/benchmark.csv", index_label="Index") + + +def process(filename): + parts = filename.replace(".log", "").split("/") + group_name = "/".join(parts[1:-1]) + instance_name = parts[-1] + instance_name, sample_name = instance_name.split(".") + nodes = 0.0 + optimize_time = 0.0 + simplex_iterations = 0.0 + primal_bound = None + dual_bound = None + gap = None + root_obj = None + root_iterations = 0.0 + root_time = 0.0 + n_rows_orig, n_rows_presolved = None, None + n_cols_orig, n_cols_presolved = None, None + n_nz_orig, n_nz_presolved = None, None + n_cont_vars_presolved, n_bin_vars_presolved = None, None + read_time, model_time, isf_time, total_time = None, None, None, None + cb_calls, cb_time = 0, 0.0 + transmission_count, transmission_time, transmission_calls = 0, 0.0, 0 + + # m = re.search("case([0-9]*)", instance_name) + # n_buses = int(m.group(1)) + n_buses = 0 + + with open(filename) as file: + for line in file.readlines(): + m = re.search(r"Explored ([0-9.e+]*) nodes \(([0-9.e+]*) simplex iterations\) in ([0-9.e+]*) seconds", line) + if m is not None: + nodes += int(m.group(1)) + simplex_iterations += int(m.group(2)) + optimize_time += float(m.group(3)) + + m = re.search(r"Best objective ([0-9.e+]*), best bound ([0-9.e+]*), gap ([0-9.e+]*)\%", line) + if m is not None: + primal_bound = float(m.group(1)) + dual_bound = float(m.group(2)) + gap = round(float(m.group(3)), 3) + + m = re.search(r"Root relaxation: objective ([0-9.e+]*), ([0-9.e+]*) iterations, ([0-9.e+]*) seconds", line) + if m is not None: + root_obj = float(m.group(1)) + root_iterations += int(m.group(2)) + root_time += float(m.group(3)) + + m = re.search(r"Presolved: ([0-9.e+]*) rows, ([0-9.e+]*) columns, ([0-9.e+]*) nonzeros", line) + if m is not None: + n_rows_presolved = int(m.group(1)) + n_cols_presolved = int(m.group(2)) + n_nz_presolved = int(m.group(3)) + + m = re.search(r"Optimize a model with ([0-9.e+]*) rows, ([0-9.e+]*) columns and ([0-9.e+]*) nonzeros", line) + if m is not None: + n_rows_orig = int(m.group(1)) + n_cols_orig = int(m.group(2)) + n_nz_orig = int(m.group(3)) + + m = re.search(r"Variable types: ([0-9.e+]*) continuous, ([0-9.e+]*) integer \(([0-9.e+]*) binary\)", line) + if m is not None: + n_cont_vars_presolved = int(m.group(1)) + n_bin_vars_presolved = int(m.group(3)) + + m = re.search(r"Read problem in ([0-9.e+]*) seconds", line) + if m is not None: + read_time = float(m.group(1)) + + m = re.search(r"Computed ISF in ([0-9.e+]*) seconds", line) + if m is not None: + isf_time = float(m.group(1)) + + m = re.search(r"Built model in ([0-9.e+]*) seconds", line) + if m is not None: + model_time = float(m.group(1)) + + m = re.search(r"Total time was ([0-9.e+]*) seconds", line) + if m is not None: + total_time = float(m.group(1)) + + m = re.search(r"User-callback calls ([0-9.e+]*), time in user-callback ([0-9.e+]*) sec", line) + if m is not None: + cb_calls = int(m.group(1)) + cb_time = float(m.group(2)) + + m = re.search(r"Verified transmission limits in ([0-9.e+]*) sec", line) + if m is not None: + transmission_time += float(m.group(1)) + transmission_calls += 1 + + m = re.search(r".*MW overflow", line) + if m is not None: + transmission_count += 1 + + return { + "Group": group_name, + "Instance": instance_name, + "Sample": sample_name, + "Optimization time (s)": optimize_time, + "Read instance time (s)": read_time, + "Model construction time (s)": model_time, + "ISF & LODF computation time (s)": isf_time, + "Total time (s)": total_time, + "User-callback time": cb_time, + "User-callback calls": cb_calls, + "Gap (%)": gap, + "B&B Nodes": nodes, + "Simplex iterations": simplex_iterations, + "Primal bound": primal_bound, + "Dual bound": dual_bound, + "Root relaxation iterations": root_iterations, + "Root relaxation time": root_time, + "Root relaxation value": root_obj, + "Rows": n_rows_orig, + "Cols": n_cols_orig, + "Nonzeros": n_nz_orig, + "Rows (presolved)": n_rows_presolved, + "Cols (presolved)": n_cols_presolved, + "Nonzeros (presolved)": n_nz_presolved, + "Bin vars (presolved)": n_bin_vars_presolved, + "Cont vars (presolved)": n_cont_vars_presolved, + "Buses": n_buses, + "Transmission screening constraints": transmission_count, + "Transmission screening time": transmission_time, + "Transmission screening calls": transmission_calls, + } + +def generate_chart(): + import pandas as pd + import matplotlib.pyplot as plt + import seaborn as sns + + tables = [] + files = ["tables/benchmark.csv"] + for f in files: + table = pd.read_csv(f, index_col=0) + table.loc[:, "Instance"] = table.loc[:,"Group"] + "/" + table.loc[:,"Instance"] + table.loc[:, "Filename"] = f + tables += [table] + benchmark = pd.concat(tables, sort=True) + benchmark = benchmark.sort_values(by="Instance") + k = len(benchmark.groupby("Instance")) + plt.figure(figsize=(12, 0.50 * k)) + sns.set_style("whitegrid") + sns.set_palette("Set1") + sns.barplot(y="Instance", + x="Total time (s)", + color="tab:red", + capsize=0.15, + errcolor="k", + errwidth=1.25, + data=benchmark); + plt.tight_layout() + print("Writing tables/benchmark.png") + plt.savefig("tables/benchmark.png", dpi=150); + + +if __name__ == "__main__": + process_all_log_files() + generate_chart() diff --git a/instances/README.md b/instances/README.md new file mode 100644 index 0000000..3d28541 --- /dev/null +++ b/instances/README.md @@ -0,0 +1,29 @@ +UnitCommitment.jl Instances +=========================== + +References +---------- + +### PGLIB-UC + +* Coffrin, Carleton and Knueven, Bernard. "Power Grid Lib - Unit Commitment". https://github.com/power-grid-lib/pglib-uc + +* Knueven, Bernard, James Ostrowski, and Jean-Paul Watson. "On mixed integer programming formulations for the unit commitment problem." Pre-print available at http://www.optimization-online.org/DB_HTML/2018/11/6930.pdf (2018). + +* Krall, Eric, Michael Higgins, and Richard P. O’Neill. "RTO unit commitment test system." Federal Energy Regulatory Commission. Available: http://ferc.gov/legal/staff-reports/rto-COMMITMENT-TEST.pdf (2012). + +### MATPOWER + +* https://github.com/MATPOWER/matpower + +* R. D. Zimmerman, C. E. Murillo-Sanchez, and R. J. Thomas, "MATPOWER: Steady-State Operations, Planning and Analysis Tools for Power Systems Research and Education," Power Systems, IEEE Transactions on, vol. 26, no. 1, pp. 12–19, Feb. 2011. + +* C. Josz, S. Fliscounakis, J. Maeght, and P. Panciatici, "AC Power Flow Data in MATPOWER and QCQP Format: iTesla, RTE Snapshots, and PEGASE" https://arxiv.org/abs/1603.01533 + +* 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. https://doi.org/10.1109/TPWRS.2013.2251015 + +### RTS-GMLC + +* https://github.com/GridMod/RTS-GMLC + +* Barrows, Clayton, 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). diff --git a/instances/matpower/case118/2017-02-01.json.gz b/instances/matpower/case118/2017-02-01.json.gz new file mode 100644 index 0000000..8543dac Binary files /dev/null and b/instances/matpower/case118/2017-02-01.json.gz differ diff --git a/instances/matpower/case118/2017-08-01.json.gz b/instances/matpower/case118/2017-08-01.json.gz new file mode 100644 index 0000000..aeff58f Binary files /dev/null and b/instances/matpower/case118/2017-08-01.json.gz differ diff --git a/instances/matpower/case1354pegase/2017-02-01.json.gz b/instances/matpower/case1354pegase/2017-02-01.json.gz new file mode 100644 index 0000000..c9b078e Binary files /dev/null and b/instances/matpower/case1354pegase/2017-02-01.json.gz differ diff --git a/instances/matpower/case1354pegase/2017-08-01.json.gz b/instances/matpower/case1354pegase/2017-08-01.json.gz new file mode 100644 index 0000000..9dcfaae Binary files /dev/null and b/instances/matpower/case1354pegase/2017-08-01.json.gz differ diff --git a/instances/matpower/case13659pegase/2017-02-01.json.gz b/instances/matpower/case13659pegase/2017-02-01.json.gz new file mode 100644 index 0000000..42f93f1 Binary files /dev/null and b/instances/matpower/case13659pegase/2017-02-01.json.gz differ diff --git a/instances/matpower/case13659pegase/2017-08-01.json.gz b/instances/matpower/case13659pegase/2017-08-01.json.gz new file mode 100644 index 0000000..5ae7417 Binary files /dev/null and b/instances/matpower/case13659pegase/2017-08-01.json.gz differ diff --git a/instances/matpower/case14/2017-02-01.json.gz b/instances/matpower/case14/2017-02-01.json.gz new file mode 100644 index 0000000..22b779c Binary files /dev/null and b/instances/matpower/case14/2017-02-01.json.gz differ diff --git a/instances/matpower/case14/2017-08-01.json.gz b/instances/matpower/case14/2017-08-01.json.gz new file mode 100644 index 0000000..381959a Binary files /dev/null and b/instances/matpower/case14/2017-08-01.json.gz differ diff --git a/instances/matpower/case145/2017-02-01.json.gz b/instances/matpower/case145/2017-02-01.json.gz new file mode 100644 index 0000000..d335af2 Binary files /dev/null and b/instances/matpower/case145/2017-02-01.json.gz differ diff --git a/instances/matpower/case145/2017-08-01.json.gz b/instances/matpower/case145/2017-08-01.json.gz new file mode 100644 index 0000000..0692fd7 Binary files /dev/null and b/instances/matpower/case145/2017-08-01.json.gz differ diff --git a/instances/matpower/case1888rte/2017-02-01.json.gz b/instances/matpower/case1888rte/2017-02-01.json.gz new file mode 100644 index 0000000..0150146 Binary files /dev/null and b/instances/matpower/case1888rte/2017-02-01.json.gz differ diff --git a/instances/matpower/case1888rte/2017-08-01.json.gz b/instances/matpower/case1888rte/2017-08-01.json.gz new file mode 100644 index 0000000..8f7563e Binary files /dev/null and b/instances/matpower/case1888rte/2017-08-01.json.gz differ diff --git a/instances/matpower/case1951rte/2017-02-01.json.gz b/instances/matpower/case1951rte/2017-02-01.json.gz new file mode 100644 index 0000000..c05fb59 Binary files /dev/null and b/instances/matpower/case1951rte/2017-02-01.json.gz differ diff --git a/instances/matpower/case1951rte/2017-08-01.json.gz b/instances/matpower/case1951rte/2017-08-01.json.gz new file mode 100644 index 0000000..9b05b54 Binary files /dev/null and b/instances/matpower/case1951rte/2017-08-01.json.gz differ diff --git a/instances/matpower/case2383wp/2017-02-01.json.gz b/instances/matpower/case2383wp/2017-02-01.json.gz new file mode 100644 index 0000000..925d1b6 Binary files /dev/null and b/instances/matpower/case2383wp/2017-02-01.json.gz differ diff --git a/instances/matpower/case2383wp/2017-08-01.json.gz b/instances/matpower/case2383wp/2017-08-01.json.gz new file mode 100644 index 0000000..f3ad108 Binary files /dev/null and b/instances/matpower/case2383wp/2017-08-01.json.gz differ diff --git a/instances/matpower/case24/2017-02-01.json.gz b/instances/matpower/case24/2017-02-01.json.gz new file mode 100644 index 0000000..48bec8f Binary files /dev/null and b/instances/matpower/case24/2017-02-01.json.gz differ diff --git a/instances/matpower/case24/2017-08-01.json.gz b/instances/matpower/case24/2017-08-01.json.gz new file mode 100644 index 0000000..5b40b21 Binary files /dev/null and b/instances/matpower/case24/2017-08-01.json.gz differ diff --git a/instances/matpower/case2736sp/2017-02-01.json.gz b/instances/matpower/case2736sp/2017-02-01.json.gz new file mode 100644 index 0000000..bbb7693 Binary files /dev/null and b/instances/matpower/case2736sp/2017-02-01.json.gz differ diff --git a/instances/matpower/case2736sp/2017-08-01.json.gz b/instances/matpower/case2736sp/2017-08-01.json.gz new file mode 100644 index 0000000..fa90b25 Binary files /dev/null and b/instances/matpower/case2736sp/2017-08-01.json.gz differ diff --git a/instances/matpower/case2737sop/2017-02-01.json.gz b/instances/matpower/case2737sop/2017-02-01.json.gz new file mode 100644 index 0000000..a8352d3 Binary files /dev/null and b/instances/matpower/case2737sop/2017-02-01.json.gz differ diff --git a/instances/matpower/case2737sop/2017-08-01.json.gz b/instances/matpower/case2737sop/2017-08-01.json.gz new file mode 100644 index 0000000..1cf6bb4 Binary files /dev/null and b/instances/matpower/case2737sop/2017-08-01.json.gz differ diff --git a/instances/matpower/case2746wop/2017-02-01.json.gz b/instances/matpower/case2746wop/2017-02-01.json.gz new file mode 100644 index 0000000..804d6ed Binary files /dev/null and b/instances/matpower/case2746wop/2017-02-01.json.gz differ diff --git a/instances/matpower/case2746wop/2017-08-01.json.gz b/instances/matpower/case2746wop/2017-08-01.json.gz new file mode 100644 index 0000000..2b2df26 Binary files /dev/null and b/instances/matpower/case2746wop/2017-08-01.json.gz differ diff --git a/instances/matpower/case2746wp/2017-02-01.json.gz b/instances/matpower/case2746wp/2017-02-01.json.gz new file mode 100644 index 0000000..58fd284 Binary files /dev/null and b/instances/matpower/case2746wp/2017-02-01.json.gz differ diff --git a/instances/matpower/case2746wp/2017-08-01.json.gz b/instances/matpower/case2746wp/2017-08-01.json.gz new file mode 100644 index 0000000..ecb1f78 Binary files /dev/null and b/instances/matpower/case2746wp/2017-08-01.json.gz differ diff --git a/instances/matpower/case2848rte/2017-02-01.json.gz b/instances/matpower/case2848rte/2017-02-01.json.gz new file mode 100644 index 0000000..413309f Binary files /dev/null and b/instances/matpower/case2848rte/2017-02-01.json.gz differ diff --git a/instances/matpower/case2848rte/2017-08-01.json.gz b/instances/matpower/case2848rte/2017-08-01.json.gz new file mode 100644 index 0000000..648e749 Binary files /dev/null and b/instances/matpower/case2848rte/2017-08-01.json.gz differ diff --git a/instances/matpower/case2868rte/2017-02-01.json.gz b/instances/matpower/case2868rte/2017-02-01.json.gz new file mode 100644 index 0000000..8f493af Binary files /dev/null and b/instances/matpower/case2868rte/2017-02-01.json.gz differ diff --git a/instances/matpower/case2868rte/2017-08-01.json.gz b/instances/matpower/case2868rte/2017-08-01.json.gz new file mode 100644 index 0000000..048dc75 Binary files /dev/null and b/instances/matpower/case2868rte/2017-08-01.json.gz differ diff --git a/instances/matpower/case2869pegase/2017-02-01.json.gz b/instances/matpower/case2869pegase/2017-02-01.json.gz new file mode 100644 index 0000000..e5267fb Binary files /dev/null and b/instances/matpower/case2869pegase/2017-02-01.json.gz differ diff --git a/instances/matpower/case2869pegase/2017-08-01.json.gz b/instances/matpower/case2869pegase/2017-08-01.json.gz new file mode 100644 index 0000000..32e3ac4 Binary files /dev/null and b/instances/matpower/case2869pegase/2017-08-01.json.gz differ diff --git a/instances/matpower/case30/2017-02-01.json.gz b/instances/matpower/case30/2017-02-01.json.gz new file mode 100644 index 0000000..f68abb2 Binary files /dev/null and b/instances/matpower/case30/2017-02-01.json.gz differ diff --git a/instances/matpower/case30/2017-08-01.json.gz b/instances/matpower/case30/2017-08-01.json.gz new file mode 100644 index 0000000..7fb5243 Binary files /dev/null and b/instances/matpower/case30/2017-08-01.json.gz differ diff --git a/instances/matpower/case300/2017-02-01.json.gz b/instances/matpower/case300/2017-02-01.json.gz new file mode 100644 index 0000000..443d3a0 Binary files /dev/null and b/instances/matpower/case300/2017-02-01.json.gz differ diff --git a/instances/matpower/case300/2017-08-01.json.gz b/instances/matpower/case300/2017-08-01.json.gz new file mode 100644 index 0000000..091d48e Binary files /dev/null and b/instances/matpower/case300/2017-08-01.json.gz differ diff --git a/instances/matpower/case3012wp/2017-02-01.json.gz b/instances/matpower/case3012wp/2017-02-01.json.gz new file mode 100644 index 0000000..9c640c3 Binary files /dev/null and b/instances/matpower/case3012wp/2017-02-01.json.gz differ diff --git a/instances/matpower/case3012wp/2017-08-01.json.gz b/instances/matpower/case3012wp/2017-08-01.json.gz new file mode 100644 index 0000000..2c1d0a8 Binary files /dev/null and b/instances/matpower/case3012wp/2017-08-01.json.gz differ diff --git a/instances/matpower/case3120sp/2017-02-01.json.gz b/instances/matpower/case3120sp/2017-02-01.json.gz new file mode 100644 index 0000000..8763a44 Binary files /dev/null and b/instances/matpower/case3120sp/2017-02-01.json.gz differ diff --git a/instances/matpower/case3120sp/2017-08-01.json.gz b/instances/matpower/case3120sp/2017-08-01.json.gz new file mode 100644 index 0000000..a5b206e Binary files /dev/null and b/instances/matpower/case3120sp/2017-08-01.json.gz differ diff --git a/instances/matpower/case3375wp/2017-02-01.json.gz b/instances/matpower/case3375wp/2017-02-01.json.gz new file mode 100644 index 0000000..0fa4aaf Binary files /dev/null and b/instances/matpower/case3375wp/2017-02-01.json.gz differ diff --git a/instances/matpower/case3375wp/2017-08-01.json.gz b/instances/matpower/case3375wp/2017-08-01.json.gz new file mode 100644 index 0000000..a4c93cd Binary files /dev/null and b/instances/matpower/case3375wp/2017-08-01.json.gz differ diff --git a/instances/matpower/case57/2017-02-01.json.gz b/instances/matpower/case57/2017-02-01.json.gz new file mode 100644 index 0000000..d16f655 Binary files /dev/null and b/instances/matpower/case57/2017-02-01.json.gz differ diff --git a/instances/matpower/case57/2017-08-01.json.gz b/instances/matpower/case57/2017-08-01.json.gz new file mode 100644 index 0000000..51c6fee Binary files /dev/null and b/instances/matpower/case57/2017-08-01.json.gz differ diff --git a/instances/matpower/case6468rte/2017-02-01.json.gz b/instances/matpower/case6468rte/2017-02-01.json.gz new file mode 100644 index 0000000..db1ee3e Binary files /dev/null and b/instances/matpower/case6468rte/2017-02-01.json.gz differ diff --git a/instances/matpower/case6468rte/2017-08-01.json.gz b/instances/matpower/case6468rte/2017-08-01.json.gz new file mode 100644 index 0000000..28c2ee5 Binary files /dev/null and b/instances/matpower/case6468rte/2017-08-01.json.gz differ diff --git a/instances/matpower/case6470rte/2017-02-01.json.gz b/instances/matpower/case6470rte/2017-02-01.json.gz new file mode 100644 index 0000000..2c5692f Binary files /dev/null and b/instances/matpower/case6470rte/2017-02-01.json.gz differ diff --git a/instances/matpower/case6470rte/2017-08-01.json.gz b/instances/matpower/case6470rte/2017-08-01.json.gz new file mode 100644 index 0000000..4d146cf Binary files /dev/null and b/instances/matpower/case6470rte/2017-08-01.json.gz differ diff --git a/instances/matpower/case6495rte/2017-02-01.json.gz b/instances/matpower/case6495rte/2017-02-01.json.gz new file mode 100644 index 0000000..b19a04e Binary files /dev/null and b/instances/matpower/case6495rte/2017-02-01.json.gz differ diff --git a/instances/matpower/case6495rte/2017-08-01.json.gz b/instances/matpower/case6495rte/2017-08-01.json.gz new file mode 100644 index 0000000..67947fd Binary files /dev/null and b/instances/matpower/case6495rte/2017-08-01.json.gz differ diff --git a/instances/matpower/case6515rte/2017-02-01.json.gz b/instances/matpower/case6515rte/2017-02-01.json.gz new file mode 100644 index 0000000..f1269af Binary files /dev/null and b/instances/matpower/case6515rte/2017-02-01.json.gz differ diff --git a/instances/matpower/case6515rte/2017-08-01.json.gz b/instances/matpower/case6515rte/2017-08-01.json.gz new file mode 100644 index 0000000..ddefb82 Binary files /dev/null and b/instances/matpower/case6515rte/2017-08-01.json.gz differ diff --git a/instances/matpower/case89pegase/2017-02-01.json.gz b/instances/matpower/case89pegase/2017-02-01.json.gz new file mode 100644 index 0000000..e6c463f Binary files /dev/null and b/instances/matpower/case89pegase/2017-02-01.json.gz differ diff --git a/instances/matpower/case89pegase/2017-08-01.json.gz b/instances/matpower/case89pegase/2017-08-01.json.gz new file mode 100644 index 0000000..7b4e59b Binary files /dev/null and b/instances/matpower/case89pegase/2017-08-01.json.gz differ diff --git a/instances/matpower/case9241pegase/2017-02-01.json.gz b/instances/matpower/case9241pegase/2017-02-01.json.gz new file mode 100644 index 0000000..67302f7 Binary files /dev/null and b/instances/matpower/case9241pegase/2017-02-01.json.gz differ diff --git a/instances/matpower/case9241pegase/2017-08-01.json.gz b/instances/matpower/case9241pegase/2017-08-01.json.gz new file mode 100644 index 0000000..3b2c913 Binary files /dev/null and b/instances/matpower/case9241pegase/2017-08-01.json.gz differ diff --git a/instances/pglib-uc/ca/2014-09-01_reserves_0.json.gz b/instances/pglib-uc/ca/2014-09-01_reserves_0.json.gz new file mode 100644 index 0000000..26cbf2f Binary files /dev/null and b/instances/pglib-uc/ca/2014-09-01_reserves_0.json.gz differ diff --git a/instances/pglib-uc/ca/2014-09-01_reserves_1.json.gz b/instances/pglib-uc/ca/2014-09-01_reserves_1.json.gz new file mode 100644 index 0000000..4f25b78 Binary files /dev/null and b/instances/pglib-uc/ca/2014-09-01_reserves_1.json.gz differ diff --git a/instances/pglib-uc/ca/2014-09-01_reserves_3.json.gz b/instances/pglib-uc/ca/2014-09-01_reserves_3.json.gz new file mode 100644 index 0000000..a201a48 Binary files /dev/null and b/instances/pglib-uc/ca/2014-09-01_reserves_3.json.gz differ diff --git a/instances/pglib-uc/ca/2014-09-01_reserves_5.json.gz b/instances/pglib-uc/ca/2014-09-01_reserves_5.json.gz new file mode 100644 index 0000000..f587b9a Binary files /dev/null and b/instances/pglib-uc/ca/2014-09-01_reserves_5.json.gz differ diff --git a/instances/pglib-uc/ca/2014-12-01_reserves_0.json.gz b/instances/pglib-uc/ca/2014-12-01_reserves_0.json.gz new file mode 100644 index 0000000..077afec Binary files /dev/null and b/instances/pglib-uc/ca/2014-12-01_reserves_0.json.gz differ diff --git a/instances/pglib-uc/ca/2014-12-01_reserves_1.json.gz b/instances/pglib-uc/ca/2014-12-01_reserves_1.json.gz new file mode 100644 index 0000000..a4701cd Binary files /dev/null and b/instances/pglib-uc/ca/2014-12-01_reserves_1.json.gz differ diff --git a/instances/pglib-uc/ca/2014-12-01_reserves_3.json.gz b/instances/pglib-uc/ca/2014-12-01_reserves_3.json.gz new file mode 100644 index 0000000..4779efd Binary files /dev/null and b/instances/pglib-uc/ca/2014-12-01_reserves_3.json.gz differ diff --git a/instances/pglib-uc/ca/2014-12-01_reserves_5.json.gz b/instances/pglib-uc/ca/2014-12-01_reserves_5.json.gz new file mode 100644 index 0000000..710e096 Binary files /dev/null and b/instances/pglib-uc/ca/2014-12-01_reserves_5.json.gz differ diff --git a/instances/pglib-uc/ca/2015-03-01_reserves_0.json.gz b/instances/pglib-uc/ca/2015-03-01_reserves_0.json.gz new file mode 100644 index 0000000..e890ec6 Binary files /dev/null and b/instances/pglib-uc/ca/2015-03-01_reserves_0.json.gz differ diff --git a/instances/pglib-uc/ca/2015-03-01_reserves_1.json.gz b/instances/pglib-uc/ca/2015-03-01_reserves_1.json.gz new file mode 100644 index 0000000..1172764 Binary files /dev/null and b/instances/pglib-uc/ca/2015-03-01_reserves_1.json.gz differ diff --git a/instances/pglib-uc/ca/2015-03-01_reserves_3.json.gz b/instances/pglib-uc/ca/2015-03-01_reserves_3.json.gz new file mode 100644 index 0000000..69c8a1a Binary files /dev/null and b/instances/pglib-uc/ca/2015-03-01_reserves_3.json.gz differ diff --git a/instances/pglib-uc/ca/2015-03-01_reserves_5.json.gz b/instances/pglib-uc/ca/2015-03-01_reserves_5.json.gz new file mode 100644 index 0000000..d00bc8e Binary files /dev/null and b/instances/pglib-uc/ca/2015-03-01_reserves_5.json.gz differ diff --git a/instances/pglib-uc/ca/2015-06-01_reserves_0.json.gz b/instances/pglib-uc/ca/2015-06-01_reserves_0.json.gz new file mode 100644 index 0000000..da6fd7d Binary files /dev/null and b/instances/pglib-uc/ca/2015-06-01_reserves_0.json.gz differ diff --git a/instances/pglib-uc/ca/2015-06-01_reserves_1.json.gz b/instances/pglib-uc/ca/2015-06-01_reserves_1.json.gz new file mode 100644 index 0000000..b387208 Binary files /dev/null and b/instances/pglib-uc/ca/2015-06-01_reserves_1.json.gz differ diff --git a/instances/pglib-uc/ca/2015-06-01_reserves_3.json.gz b/instances/pglib-uc/ca/2015-06-01_reserves_3.json.gz new file mode 100644 index 0000000..f9686f3 Binary files /dev/null and b/instances/pglib-uc/ca/2015-06-01_reserves_3.json.gz differ diff --git a/instances/pglib-uc/ca/2015-06-01_reserves_5.json.gz b/instances/pglib-uc/ca/2015-06-01_reserves_5.json.gz new file mode 100644 index 0000000..dab7eb3 Binary files /dev/null and b/instances/pglib-uc/ca/2015-06-01_reserves_5.json.gz differ diff --git a/instances/pglib-uc/ca/Scenario400_reserves_0.json.gz b/instances/pglib-uc/ca/Scenario400_reserves_0.json.gz new file mode 100644 index 0000000..032349f Binary files /dev/null and b/instances/pglib-uc/ca/Scenario400_reserves_0.json.gz differ diff --git a/instances/pglib-uc/ca/Scenario400_reserves_1.json.gz b/instances/pglib-uc/ca/Scenario400_reserves_1.json.gz new file mode 100644 index 0000000..8cbd8ae Binary files /dev/null and b/instances/pglib-uc/ca/Scenario400_reserves_1.json.gz differ diff --git a/instances/pglib-uc/ca/Scenario400_reserves_3.json.gz b/instances/pglib-uc/ca/Scenario400_reserves_3.json.gz new file mode 100644 index 0000000..0777f6a Binary files /dev/null and b/instances/pglib-uc/ca/Scenario400_reserves_3.json.gz differ diff --git a/instances/pglib-uc/ca/Scenario400_reserves_5.json.gz b/instances/pglib-uc/ca/Scenario400_reserves_5.json.gz new file mode 100644 index 0000000..7c49ef6 Binary files /dev/null and b/instances/pglib-uc/ca/Scenario400_reserves_5.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-01-01_hw.json.gz b/instances/pglib-uc/ferc/2015-01-01_hw.json.gz new file mode 100644 index 0000000..37b05d1 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-01-01_hw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-01-01_lw.json.gz b/instances/pglib-uc/ferc/2015-01-01_lw.json.gz new file mode 100644 index 0000000..e4131b5 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-01-01_lw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-02-01_hw.json.gz b/instances/pglib-uc/ferc/2015-02-01_hw.json.gz new file mode 100644 index 0000000..74b6f0d Binary files /dev/null and b/instances/pglib-uc/ferc/2015-02-01_hw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-02-01_lw.json.gz b/instances/pglib-uc/ferc/2015-02-01_lw.json.gz new file mode 100644 index 0000000..b8f9c7a Binary files /dev/null and b/instances/pglib-uc/ferc/2015-02-01_lw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-03-01_hw.json.gz b/instances/pglib-uc/ferc/2015-03-01_hw.json.gz new file mode 100644 index 0000000..5c1e6c1 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-03-01_hw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-03-01_lw.json.gz b/instances/pglib-uc/ferc/2015-03-01_lw.json.gz new file mode 100644 index 0000000..44476a6 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-03-01_lw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-04-01_hw.json.gz b/instances/pglib-uc/ferc/2015-04-01_hw.json.gz new file mode 100644 index 0000000..1d71ff7 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-04-01_hw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-04-01_lw.json.gz b/instances/pglib-uc/ferc/2015-04-01_lw.json.gz new file mode 100644 index 0000000..bf4fc5d Binary files /dev/null and b/instances/pglib-uc/ferc/2015-04-01_lw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-05-01_hw.json.gz b/instances/pglib-uc/ferc/2015-05-01_hw.json.gz new file mode 100644 index 0000000..f1929f2 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-05-01_hw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-05-01_lw.json.gz b/instances/pglib-uc/ferc/2015-05-01_lw.json.gz new file mode 100644 index 0000000..8d4aa84 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-05-01_lw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-06-01_hw.json.gz b/instances/pglib-uc/ferc/2015-06-01_hw.json.gz new file mode 100644 index 0000000..95a2b23 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-06-01_hw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-06-01_lw.json.gz b/instances/pglib-uc/ferc/2015-06-01_lw.json.gz new file mode 100644 index 0000000..0b39f38 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-06-01_lw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-07-01_hw.json.gz b/instances/pglib-uc/ferc/2015-07-01_hw.json.gz new file mode 100644 index 0000000..3019227 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-07-01_hw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-07-01_lw.json.gz b/instances/pglib-uc/ferc/2015-07-01_lw.json.gz new file mode 100644 index 0000000..9f0ce64 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-07-01_lw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-08-01_hw.json.gz b/instances/pglib-uc/ferc/2015-08-01_hw.json.gz new file mode 100644 index 0000000..25b22a0 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-08-01_hw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-08-01_lw.json.gz b/instances/pglib-uc/ferc/2015-08-01_lw.json.gz new file mode 100644 index 0000000..50a3867 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-08-01_lw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-09-01_hw.json.gz b/instances/pglib-uc/ferc/2015-09-01_hw.json.gz new file mode 100644 index 0000000..ad30323 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-09-01_hw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-09-01_lw.json.gz b/instances/pglib-uc/ferc/2015-09-01_lw.json.gz new file mode 100644 index 0000000..8eaf80b Binary files /dev/null and b/instances/pglib-uc/ferc/2015-09-01_lw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-10-01_hw.json.gz b/instances/pglib-uc/ferc/2015-10-01_hw.json.gz new file mode 100644 index 0000000..ae8ec6e Binary files /dev/null and b/instances/pglib-uc/ferc/2015-10-01_hw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-10-01_lw.json.gz b/instances/pglib-uc/ferc/2015-10-01_lw.json.gz new file mode 100644 index 0000000..a64be17 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-10-01_lw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-11-02_hw.json.gz b/instances/pglib-uc/ferc/2015-11-02_hw.json.gz new file mode 100644 index 0000000..7b77ff2 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-11-02_hw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-11-02_lw.json.gz b/instances/pglib-uc/ferc/2015-11-02_lw.json.gz new file mode 100644 index 0000000..cf00a2c Binary files /dev/null and b/instances/pglib-uc/ferc/2015-11-02_lw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-12-01_hw.json.gz b/instances/pglib-uc/ferc/2015-12-01_hw.json.gz new file mode 100644 index 0000000..f21388f Binary files /dev/null and b/instances/pglib-uc/ferc/2015-12-01_hw.json.gz differ diff --git a/instances/pglib-uc/ferc/2015-12-01_lw.json.gz b/instances/pglib-uc/ferc/2015-12-01_lw.json.gz new file mode 100644 index 0000000..99517d3 Binary files /dev/null and b/instances/pglib-uc/ferc/2015-12-01_lw.json.gz differ diff --git a/instances/pglib-uc/rts_gmlc/2020-01-27.json.gz b/instances/pglib-uc/rts_gmlc/2020-01-27.json.gz new file mode 100644 index 0000000..41417a1 Binary files /dev/null and b/instances/pglib-uc/rts_gmlc/2020-01-27.json.gz differ diff --git a/instances/pglib-uc/rts_gmlc/2020-02-09.json.gz b/instances/pglib-uc/rts_gmlc/2020-02-09.json.gz new file mode 100644 index 0000000..ab113b8 Binary files /dev/null and b/instances/pglib-uc/rts_gmlc/2020-02-09.json.gz differ diff --git a/instances/pglib-uc/rts_gmlc/2020-03-05.json.gz b/instances/pglib-uc/rts_gmlc/2020-03-05.json.gz new file mode 100644 index 0000000..e7f0869 Binary files /dev/null and b/instances/pglib-uc/rts_gmlc/2020-03-05.json.gz differ diff --git a/instances/pglib-uc/rts_gmlc/2020-04-03.json.gz b/instances/pglib-uc/rts_gmlc/2020-04-03.json.gz new file mode 100644 index 0000000..94da410 Binary files /dev/null and b/instances/pglib-uc/rts_gmlc/2020-04-03.json.gz differ diff --git a/instances/pglib-uc/rts_gmlc/2020-05-05.json.gz b/instances/pglib-uc/rts_gmlc/2020-05-05.json.gz new file mode 100644 index 0000000..c101016 Binary files /dev/null and b/instances/pglib-uc/rts_gmlc/2020-05-05.json.gz differ diff --git a/instances/pglib-uc/rts_gmlc/2020-06-09.json.gz b/instances/pglib-uc/rts_gmlc/2020-06-09.json.gz new file mode 100644 index 0000000..78ae2f1 Binary files /dev/null and b/instances/pglib-uc/rts_gmlc/2020-06-09.json.gz differ diff --git a/instances/pglib-uc/rts_gmlc/2020-07-06.json.gz b/instances/pglib-uc/rts_gmlc/2020-07-06.json.gz new file mode 100644 index 0000000..5718514 Binary files /dev/null and b/instances/pglib-uc/rts_gmlc/2020-07-06.json.gz differ diff --git a/instances/pglib-uc/rts_gmlc/2020-08-12.json.gz b/instances/pglib-uc/rts_gmlc/2020-08-12.json.gz new file mode 100644 index 0000000..9418f6d Binary files /dev/null and b/instances/pglib-uc/rts_gmlc/2020-08-12.json.gz differ diff --git a/instances/pglib-uc/rts_gmlc/2020-09-20.json.gz b/instances/pglib-uc/rts_gmlc/2020-09-20.json.gz new file mode 100644 index 0000000..0a90287 Binary files /dev/null and b/instances/pglib-uc/rts_gmlc/2020-09-20.json.gz differ diff --git a/instances/pglib-uc/rts_gmlc/2020-10-27.json.gz b/instances/pglib-uc/rts_gmlc/2020-10-27.json.gz new file mode 100644 index 0000000..50e7b9f Binary files /dev/null and b/instances/pglib-uc/rts_gmlc/2020-10-27.json.gz differ diff --git a/instances/pglib-uc/rts_gmlc/2020-11-25.json.gz b/instances/pglib-uc/rts_gmlc/2020-11-25.json.gz new file mode 100644 index 0000000..47729bb Binary files /dev/null and b/instances/pglib-uc/rts_gmlc/2020-11-25.json.gz differ diff --git a/instances/pglib-uc/rts_gmlc/2020-12-23.json.gz b/instances/pglib-uc/rts_gmlc/2020-12-23.json.gz new file mode 100644 index 0000000..e1a095e Binary files /dev/null and b/instances/pglib-uc/rts_gmlc/2020-12-23.json.gz differ diff --git a/instances/test/case14.json.gz b/instances/test/case14.json.gz new file mode 100644 index 0000000..99f1c25 Binary files /dev/null and b/instances/test/case14.json.gz differ diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..08fbcad --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,21 @@ +site_name: UnitCommitment.jl +theme: cinder +copyright: "Copyright © 2020, UChicago Argonne, LLC. All Rights Reserved." +repo_url: https://github.com/ +edit_uri: edit/master/src/docs/ +nav: + - Home: index.md + - Install: install.md + - Data: format.md +plugins: + - search +markdown_extensions: + - admonition + - mdx_math +extra_javascript: + - https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js?config=TeX-AMS-MML_HTMLorMML + - js/mathjax.js +docs_dir: src/docs +site_dir: docs +extra_css: + - "css/custom.css" diff --git a/src/UnitCommitment.jl b/src/UnitCommitment.jl new file mode 100644 index 0000000..7c335d3 --- /dev/null +++ b/src/UnitCommitment.jl @@ -0,0 +1,15 @@ +# 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. + +module UnitCommitment + include("log.jl") + include("dotdict.jl") + include("instance.jl") + include("screening.jl") + include("model.jl") + include("sensitivity.jl") + include("validate.jl") + include("convert.jl") + include("initcond.jl") +end diff --git a/src/convert.jl b/src/convert.jl new file mode 100644 index 0000000..6059e7e --- /dev/null +++ b/src/convert.jl @@ -0,0 +1,56 @@ +# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment +# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +using DataStructures, JSON, GZip + +function read_json(path::String)::OrderedDict + if endswith(path, ".gz") + file = GZip.gzopen(path) + else + file = open(path) + end + return JSON.parse(file, dicttype=()->DefaultOrderedDict(nothing)) +end + +function read_egret_solution(path::String)::OrderedDict + egret = read_json(path) + T = length(egret["system"]["time_keys"]) + + solution = OrderedDict() + is_on = solution["Is on"] = OrderedDict() + production = solution["Production (MW)"] = OrderedDict() + reserve = solution["Reserve (MW)"] = OrderedDict() + production_cost = solution["Production cost (\$)"] = OrderedDict() + startup_cost = solution["Startup cost (\$)"] = OrderedDict() + + for (gen_name, gen_dict) in egret["elements"]["generator"] + if endswith(gen_name, "_T") || endswith(gen_name, "_R") + gen_name = gen_name[1:end-2] + end + if "commitment" in keys(gen_dict) + is_on[gen_name] = gen_dict["commitment"]["values"] + else + is_on[gen_name] = ones(T) + end + production[gen_name] = gen_dict["pg"]["values"] + if "rg" in keys(gen_dict) + reserve[gen_name] = gen_dict["rg"]["values"] + else + reserve[gen_name] = zeros(T) + end + startup_cost[gen_name] = zeros(T) + production_cost[gen_name] = zeros(T) + if "commitment_cost" in keys(gen_dict) + for t in 1:T + x = gen_dict["commitment"]["values"][t] + commitment_cost = gen_dict["commitment_cost"]["values"][t] + prod_above_cost = gen_dict["production_cost"]["values"][t] + prod_base_cost = gen_dict["p_cost"]["values"][1][2] * x + startup_cost[gen_name][t] = commitment_cost - prod_base_cost + production_cost[gen_name][t] = prod_above_cost + prod_base_cost + end + end + end + return solution +end \ No newline at end of file diff --git a/src/docs/css/custom.css b/src/docs/css/custom.css new file mode 100644 index 0000000..366aa46 --- /dev/null +++ b/src/docs/css/custom.css @@ -0,0 +1,28 @@ +.navbar-default { + border-bottom: 0px; + background-color: #fff; + box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.2); +} + +a, .navbar-default a { + color: #06a !important; + font-weight: normal; +} + +.disabled > a { + color: #999 !important; +} + +.navbar-default a:hover, +.navbar-default .active, +.active > a { + background-color: #f0f0f0 !important; +} + +.icon-bar { + background-color: #666 !important; +} + +.navbar-collapse { + border-color: #fff !important; +} \ No newline at end of file diff --git a/src/docs/format.md b/src/docs/format.md new file mode 100644 index 0000000..4dcd6fa --- /dev/null +++ b/src/docs/format.md @@ -0,0 +1,257 @@ +Data Format +=========== + +Instances are specified by JSON files containing the following main sections: + +* [Parameters](#parameters) +* [Buses](#buses) +* [Generators](#generators) +* [Price-sensitive loads](#price-sensitive-loads) +* [Transmission lines](#transmission-lines) +* [Reserves](#reserves) +* [Contingencies](#contingencies) + +Each section is described in detail below. For a complete example, see [case14.json](https://github.com/ANL-CEEESA/UnitCommitment.jl/blob/dev/instances/matpower-24h/case14.json). + +## Parameters + +This section describes system-wide parameters, such as power balance penalties, and optimization parameters, such as the length of the planning horizon. + +| Key | Description | Default | Time series? +| :----------------------------- | :------------------------------------------------ | :------: | :------------: +| `Time (h)` | Length of the planning horizon (in hours) | Required | N +| `Power balance penalty ($/MW)` | Penalty for system-wide shortage or surplus in production (in $/MW). This is charged per time period. For example, if there is a shortage of 1 MW for three time periods, three times this amount will be charged. | `1000.0` | Y + + +### Example +```json +{ + "Parameters": { + "Time (h)": 4, + "Power balance penalty ($/MW)": 1000.0 + } +} +``` + +## Buses + +This section describes the characteristics of each bus in the system. + +| Key | Description | Default | Time series? +| :----------------- | :------------------------------------------------------------ | ------- | :-------------: +| `Load (MW)` | Fixed load connected to the bus (in MW). | Required | Y + + +### Example +```json +{ + "Buses": { + "b1": { + "Load (MW)": 0.0 + }, + "b2": { + "Load (MW)": [ + 26.01527, + 24.46212, + 23.29725, + 22.90897 + ] + } + } +} +``` + + +## Generators + +This section describes all generators in the system, including thermal units, renewable units and virtual units. + +| Key | Description | Default | Time series? +| :------------------------ | :------------------------------------------------| ------- | :-----------: +| `Bus` | Identifier of the bus where this generator is located (string) | 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 `t`, then it costs 300 to start up the generator at times `t+1`, `t+2` or `t+3`, and 400 to start the generator at time `t+4` 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. | `[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 1 and `Minimum uptime (h)` is set to 4, then the generator can only shut down at time 5. | `1` | N +| `Minimum downtime (h)` | Minimum amount of time the generator must stay offline after shutting down (in hours). For example, if the generator shuts down at time 1 and `Minimum downtime (h)` is set to 4, then the generator can only start producing power again at time 5. | `1` | N +| `Ramp up limit (MW)` | Maximum increase in production from one time period to the next (in MW). For example, if the generator is producing 100 MW at time 1 and if this parameter is set to 40 MW, then the generator will produce at most 140 MW at time 2. | `+inf` | N +| `Ramp down limit (MW)` | Maximum decrease in production from one time period to the next (in MW). For example, if the generator is producing 100 MW at time 1 and this parameter is set to 40 MW, then the generator will produce at least 60 MW at time 2. | `+inf` | N +| `Startup limit (MW)` | Maximum amount of power a generator can produce immediately after starting up (in MW). | `+inf` | N +| `Shutdown limit (MW)` | Maximum amount of power a generator can produce immediately before shutting down (in MW). Specifically, the generator can only shut down at time `t+1` if its production at time `t` is below this limit. | `+inf` | N +| `Initial status (h)` | If set to a positive number, indicates the amount of time the generator has been on at the beginning of the simulation, and if set to a negative number, the amount of time the generator has been off. For example, if `Initial status (h)` is `-2`, this means that the generator was off at simulation time `-2` and `-1`. The simulation starts at time `0`. | Required | N +| `Initial power (MW)` | Amount of power the generator at time period `-1`, immediately before the planning horizon starts. | Required | N +| `Must run?` | If `true`, the generator should be committed, even that is not economical (Boolean). | `false` | Y +| `Provides spinning reserves?` | If `true`, this generator may provide spinning reserves (Boolean). | `true` | 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 dollars 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. + +
+ +
Figure 1. Piecewise-linear production cost curve.
+
+
+ +**Additional remarks:** + +* For time-dependent production limits or time-dependent production costs, the usage of nested arrays is allowed. For example, if `Production cost curve (MW)` is set to `[5.0, [10.0, 12.0, 15.0, 20.0]]`, then the unit may generate at most 10, 12, 15 and 20 MW of power during time periods 1, 2, 3 and 4, respectively. The minimum output for all time periods is fixed to at 5 MW. +* There is no limit to the number of piecewise-linear segments, and different generators may have a different number of segments. +* If `Production cost curve (MW)` and `Production cost curve ($)` both contain a single element, then the generator must produce exactly that amount of power when operational. To specify that the generator may produce any amount of power up to a certain limit `P`, the parameter `Production cost curve (MW)` should be set to `[0, P]`. +* Production cost curves must be convex. + +### Example + +```json +{ + "Generators": { + "gen1": { + "Bus": "b1", + "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], + "Startup delays (h)": [1, 4], + "Ramp up limit (MW)": 232.68, + "Ramp down limit (MW)": 232.68, + "Startup limit (MW)": 232.68, + "Shutdown limit (MW)": 232.68, + "Minimum downtime (h)": 4, + "Minimum uptime (h)": 4, + "Initial status (h)": 12, + "Must run?": false, + "Provides spinning reserves?": true, + }, + "gen2": { + "Bus": "b5", + "Production cost curve (MW)": [0.0, [10.0, 8.0, 0.0, 3.0]], + "Production cost curve ($)": [0.0, 0.0], + "Provides spinning reserves?": true, + } + } +} +``` + +## Price-sensitive loads + +This section describes components in the system which may increase or reduce their energy consumption according to the energy prices. Fixed loads (as described in the `buses` section) are always served, regardless of the price, unless there is significant congestion in the system or insufficient production capacity. Price-sensitive loads, on the other hand, are only served if it is economical to do so. + +| Key | Description | Default | Time series? +| :---------------- | :------------------------------------------------ | :------: | :------------: +| `Bus` | Bus where the load is located. Multiple price-sensitive loads may be placed at the same bus. | Required | N +| `Revenue ($/MW)` | Revenue obtained for serving each MW of power to this load. | Required | Y +| `Demand (MW)` | Maximum amount of power required by this load. Any amount lower than this may be served. | Required | Y + + +### Example +```json +{ + "Price-sensitive loads": { + "p1": { + "Bus": "b3", + "Revenue ($/MW)": 23.0, + "Demand (MW)": 50.0 + } + } +} +``` + +## Transmission Lines + +This section describes the characteristics of transmission system, such as its topology and the susceptance of each transmission line. + +| Key | Description | Default | Time series? +| :--------------------- | :----------------------------------------------- | ------- | :------------: +| `Source bus` | Identifier of the bus where the transmission line originates. | Required | N +| `Target bus` | Identifier of the bus where the transmission line reaches. | Required | N +| `Reactance (ohms)` | Reactance of the transmission line (in ohms). | Required | N +| `Susceptance (S)` | Susceptance of the transmission line (in siemens). | Required | N +| `Normal flow limit (MW)` | Maximum amount of power (in MW) allowed to flow through the line when the system is in its regular, fully-operational state. May be `null` is there is no limit. | `+inf` | Y +| `Emergency flow limit (MW)` | Maximum amount of power (in MW) allowed to flow through the line when the system is in degraded state (for example, after the failure of another transmission line). | `+inf` | Y +| `Flow limit penalty ($/MW)` | Penalty for violating the flow limits of the transmission line (in $/MW). This is charged per time period. For example, if there is a thermal violation of 1 MW for three time periods, three times this amount will be charged. | `5000.0` | Y + +### Example + +```json +{ + "Transmission lines": { + "l1": { + "Source bus": "b1", + "Target bus": "b2", + "Reactance (ohms)": 0.05917, + "Susceptance (S)": 29.49686, + "Normal flow limit (MW)": 15000.0, + "Emergency flow limit (MW)": 20000.0, + "Flow limit penalty ($/MW)": 5000.0 + } + } +} +``` + + +## Reserves + +This section describes the hourly amount of operating reserves required. + + +| Key | Description | Default | Time series? +| :-------------------- | :------------------------------------------------- | --------- | :----: +| `Spinning (MW)` | Minimum amount of system-wide spinning reserves (in MW). Only generators which are online may provide this reserve. | `0.0` | Y + +### Example + +```json +{ + "Reserves": { + "Spinning (MW)": [ + 57.30552, + 53.88429, + 51.31838, + 50.46307 + ] + } +} +``` + +## Contingencies + +This section describes credible contingency scenarios in the optimization, such as the loss of a transmission line or generator. + +| Key | Description | Default +| :-------------------- | :----------------------------------------------- | ---------- +| `Affected generators` | List of generators affected by this contingency. May be omitted if no generators are affected. | `[]` +| `Affected lines` | List of transmission lines affected by this contingency. May be omitted if no lines are affected. | `[]` + +### Example + +```json +{ + "Contingencies": { + "c1": { + "Affected lines": ["l1", "l2", "l3"], + "Affected generators": ["g1"] + }, + "c2": { + "Affected lines": ["l4"] + }, + } +} +``` + +## Additional remarks + +### Time series parameters + +Many numerical properties in the JSON file can be specified either as a single floating point number if they are time-independent, or as an array containing exactly `T` elements, where `T` is the length of the planning horizon, if they are time-dependent. For example, both formats below are valid when `T=3`: + +```json +{ + "Load (MW)": 800.0, + "Load (MW)": [800.0, 850.0, 730.0] +} +``` + +### Current limitations + +* All reserves are system-wide (no zonal reserves) +* Network topology remains the same for all time periods +* Only N-1 transmission contingencies are supported. Generator contingencies are not supported. diff --git a/src/docs/illustrations.ipynb b/src/docs/illustrations.ipynb new file mode 100644 index 0000000..8674789 --- /dev/null +++ b/src/docs/illustrations.ipynb @@ -0,0 +1,60 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import seaborn as sns; sns.set()" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZMAAAEMCAYAAAABLFv3AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjMsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+AADFEAAAgAElEQVR4nO3deWBTdb7+8Xfa0kKBkra0pQWkosKAqGBbcPDiKKiIFwRxFGQbFBcuioojCKjUQb1a4SeOI4Ibi6gwOiAVUApeVESFAQQtooLs0o0m3aFLmvP7o5ihQPc2yWmf11/2fBPyEAMPOd+cTyyGYRiIiIjUgY+nA4iIiPmpTEREpM5UJiIiUmcqExERqTOViYiI1JnKRERE6kxlIiIidebn6QCelJVVgNNZ88tsQkNbYbPlN0Ci+memrGCuvGbKCubKa6asYK68dcnq42MhOLjledeadJk4nUatyuT3+5qFmbKCufKaKSuYK6+ZsoK58jZEVp3mEhGROlOZiIhInalMRESkztxSJllZWdx7770MHDiQIUOG8OCDD2K328vdZsaMGXTt2pWCggLXsU2bNnHTTTdxww038Mgjj3Dq1KlqrYmIiHu5pUwsFgv33HMPSUlJrFmzho4dOzJ37lzX+qZNm7BYLOXuU1BQwFNPPcXChQvZuHEjLVu25O23365yTUREzpW79RsOTvsrXw/7Mwen/ZXcrd/U66/vljKxWq306dPH9XPPnj1JSUkByt61vPrqq8yYMaPcfTZv3kyPHj2Ijo4GYOTIkXz66adVromISHm5W78h/Z0lOOw2MAwcdhvp7yyp10Jx+56J0+lk+fLl9O/fH4DZs2czefJkWrduXe52qampREVFuX6OiooiNTW1yjURESkvc9VKjOLicseM4mIyV62st8dw+3UmzzzzDIGBgYwZM4ZPP/2UZs2acd1117k7BlB28U5thYW1rvpGXsJMWcFcec2UFcyV10xZwbvz7suyn/e4I8teb7ndWiYJCQkcOXKEhQsX4uPjw7Zt29i6davrXQrA4MGDefPNN4mMjGTbtm2u4ykpKURGRgJUulYTNlt+rS7eCQtrzYkTeTW+nyeYKSuYK6+ZsoK58popK3h3XmdhIRY/P4ySknPW/IJDapTbx8dS4T/C3Xaaa968eezZs4f58+fj7+8PwNNPP83mzZvZtGkTmzZtAmDt2rVcfPHF9OvXj+TkZA4fPgzAihUrGDRoEEClayIiUsaRl8uxuQllReLrW27N4u9P2+G31dtjueWdyf79+1m4cCHR0dGMHDkSgA4dOjB//vwK79OqVStmz57N/fffj9PppFu3bjzxxBNVromICJRknuC3ef8Ph91G1IMP4yw8ReaqlTiy7PgFh9B2+G0EXdW33h7PYhiGeQbK1DOd5vI+Zsprpqxgrrxmygrel7fot2P89vL/wygupv3kKbS45BLXWl2yVnaaq0kPehQRaWxO7vuFlH+8jCUggI6PzySgfQe3PK7KRESkkcjfvYvU11/DLzSUDlMeo1loW7c9tspERKQRyPnqS9LfWULz6Atp/9AUfFu796PKKhMRERMzDIOsT9eRuepfBF7ag6j/eRCf5s3dnkNlIiJiUobTyYkPlpP92UZa97mKdnfdg8XPM3+tq0xEREzIcDhIW/QWef/eivX6Gwm7YyQWH899q4jKRETEZJyFhaQseJWTP+6h7W23E3zTzedMXnc3lYmIiIk48nI5/vd5FB09QsT4CbT5r36ejgSoTERETKPElslvL80tu6p90mRa9ezl6UguKhMRERM486r2Do9OK3dVuzdQmYiIeLlT+/dx/B8vY/H3d+tV7TWhMhER8WKevKq9JlQmIiJeKmfLZtKXLvbYVe01oTIREfEy3nJVe02oTEREvIg3XdVeE96fUESkiTAcDtIWv0XeNu+4qr0mVCYiIl6g3FXtw/9M8KD/9vhV7TWhMhER8TDXVe1HDhMx/m7a/Nc1no5UYyoTEREPKndV+wMPedVV7TWhMhER8ZCi47/x27y5p69qn0qLS7p4OlKtqUxERDzADFe114TKRETEzcxyVXtNqExERNwoZ8tXpL+zmIALOtHh4Ue9+qr2mlCZiIi4gRmvaq8JlYmISAMru6p9BdmfbaB176tod7c5rmqvicb1uxER8TLlr2q/gbA77jTNVe01oTIREWkgZr+qvSZUJiIiDaA0L4/jr8yj8PAh017VXhMqExGRelZiy+S3eXNx2Mx9VXtNqExEROpRY7qqvSZUJiIi9aTcVe3TZhDQoaOnI7mNW8okKyuLadOmcfToUfz9/enUqROzZ88mJyeHWbNmceLECfz8/LjsssuIj4+n+enPXm/atIkXX3yR0tJSLr30Up5/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rtHir732Gt27dyc/Px8oGw/y3HPPAWVlkJCQwLXXXkvfvn2ZNWuWa0TItm3buOaaa3jjjTe4+uqrmTFjxjmZvv/+e4KCgmjXrl2540OGDOGTTz5xzYFat24d119/Pc2aNXPdJjY21jU2pbS0lL179zJu3Lhyx3bt2uUaRw/Qu3dvvvzyyzo+k9KUqExEzpKamsrmzZtdM4r++te/0q5dO7766iteeeUVXnrpJb799lsCAgK47LLLXP/C37FjB1FRUezcudP1c+/evQGYM2cOhw4dYvXq1WzYsIGMjIxyk28zMzPJycnh888/55lnnjkn0759+7jwwgvPOR4REcHFF1/Mli1bAFi9ejXDhg0rd5u4uDhXxr1799K5c2f++Mc/ljvmcDi4/PLLXfe56KKLzjl9JlIZlYnIaQ888ACxsbGMGjWKuLg4Jk6cSGpqKjt37uSxxx4jICCAbt26cfvtt5OYmAj85y9qh8PBL7/8wtixY9m+fTtFRUUkJycTExODYRh8+OGHzJw5E6vVSqtWrbj//vtd88cAfHx8eOihh/D393fNpjpTbm5uhbOohg4dSmJiIgcPHiQvL49evXqVW4+Li2P//v3k5OSwc+dOYmNjiY6OJisry3XsiiuuKDdcsWXLluTm5tbH0ypNhL4cS+S0+fPn07dv33LHMjIyaNOmDa1atXIdi4qKYs+ePUDZ6aDnn3+evXv30qVLF66++mqeeOIJdu/eTadOnQgJCcFms3Hq1CmGDx/u+jUMw8DpdLp+Dg4Odm2sn09QUFCFHwi48cYbSUhIwGq1nrOnAmUDE9u1a8fOnTvZvn07I0aMAKBXr16uY7/vl/yuoKDAa77YS8xBZSJSifDwcHJycsjPz3cVyu/fpAdlfyEfOnSIjRs3EhcXx8UXX0xKSgpffPGF6y/o4OBgmjdvzrp161z3O1tVX6PatWtXli5det61Fi1acM0117B8+XI2btx43tvExMSwfft2du/eTUJCQrljO3fuPOdLsw4cOMAf/vCHSjOJnEmnuUQqERkZSa9evXjppZcoKiri559/5l//+hdDhgwByv4i79GjB++9955rf6RXr17885//dJWJj48Pt99+O//7v/+LzWYDyjb1v/rqq2rnuPzyy8nNzSU9Pf2861OmTGHZsmV06NDhvOtxcXEkJiYSHh7uKsWYmBgSExPJz8+nZ8+e5W6/fft2+vXrV+18IioTkSq89NJLHD9+nH79+vHggw8yefJkrr76atd6XFxcuQ3s3r17U1BQUO7U0dSpU+nUqRN33HEHV155JePHj+fQoUPVzuDv78+tt97q2qs5W0RERLlPY50tLi4Om81GTEyM61i3bt0oLCzk0ksvpUWLFq7jGRkZ/Prrr1x//fXVziei7zMRMQm73c6oUaNYvXr1eTfp68sLL7xAx44dGT16dIM9hjQ+KhMREakzneYSEZE6U5mIiEidqUxERKTOVCYiIlJnKhMREakzlYmIiNSZykREROpMZSIiInX2/wHLZq81VbHsdQAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.xlabel(\"Power (MW)\")\n", + "plt.ylabel(\"Cost ($)\")\n", + "plt.plot([ 100, 110, 130, 135],\n", + " [1400, 1600, 2200, 2400],\n", + " \"ro-\");\n", + "plt.savefig('images/cost_curve.png', dpi=150)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/src/docs/images/cost_curve.png b/src/docs/images/cost_curve.png new file mode 100644 index 0000000..04f3bd7 Binary files /dev/null and b/src/docs/images/cost_curve.png differ diff --git a/src/docs/index.md b/src/docs/index.md new file mode 100644 index 0000000..58ed244 --- /dev/null +++ b/src/docs/index.md @@ -0,0 +1,23 @@ +# UnitCommitment.jl + +**UnitCommitment.jl** is an optimization package for the Security-Constrained Unit Commitment Problem (SCUC), a fundamental optimization problem in power systems which is used, for example, to clear the day-ahead electricity markets. The problem asks for the most cost-effective power generation schedule under a number of physical, operational and economic constraints. + +### Package Components + +* **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. +* **Benchmark Instances:** The package provides a diverse collection of large-scale benchmark instances collected from the literature and extended to make them more challenging and realistic, based on publicly available data from ISOs. +* **Model Implementation**: The package provides a Julia/JuMP implementation of state-of-the-art formulations and solution methods for SCUC. Our goal is to keep this implementation 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. + +### Contents + +* [Installation Guide](install.md) +* [Data Format Specification](format.md) + +### Authors +* **Alinson Santos Xavier,** Argonne National Laboratory +* **Feng Qiu,** Argonne National Laboratory + +### Collaborators +* **Yonghong Chen,** Midcontinent Independent System Operator +* **Feng Pan,** Pacific Northwest National Laboratory \ No newline at end of file diff --git a/src/docs/install.md b/src/docs/install.md new file mode 100644 index 0000000..da07480 --- /dev/null +++ b/src/docs/install.md @@ -0,0 +1,21 @@ +# Installation Guide + +This package was tested and developed with [Julia 1.4](https://julialang.org/). To install Julia, please +follow the [installation guide on their website](https://julialang.org/downloads/platform.html). +To install `UnitCommitment.jl`, run the Julia interpreter, type `]` to open the +package manager, then type: + +```text +pkg> add https://github.com/ANL-CEEESA/UnitCommitment.jl.git +``` + +To test that the package has been correctly installed, run: + +```text +pkg> test UnitCommitment +``` + +If all tests pass, the package should now be ready to be used by any Julia script on the machine. To try it out in the julia interpreter hit `backspace` to return to the regular interpreter, and type the following command: +```julia +using UnitCommitment +``` diff --git a/src/docs/isf.md b/src/docs/isf.md new file mode 100644 index 0000000..4d46ff3 --- /dev/null +++ b/src/docs/isf.md @@ -0,0 +1,19 @@ +Linear Sensitivity Factors +========================== + +UnitCommitment.jl includes a number of functions to compute typical linear sensitivity +factors, such as [Injection Shift Factors](@ref) and [Line Outage Distribution Factors](@ref). These sensitivity factors can be used to quickly compute DC power flows in both base and N-1 contigency scenarios. + +Injection Shift Factors +----------------------- +Given a network with `B` buses and `L` transmission lines, the Injection Shift Factors (ISF) matrix is an `L`-by-`B` matrix which indicates much power flows through a certain transmission line when 1 MW of power is injected at bus `b` and withdrawn from the slack bus. For example, `isf[:l7, :b5]` indicates the amount of power (in MW) that flows through line `l7` when 1 MW of power is injected at bus `b5` and withdrawn from the slack bus. +This matrix is computed based on the DC linearization of power flow equations and does not include losses. +To compute the ISF matrix, the function `injection_shift_factors` can be used. It is necessary to specify the set of lines, buses and the slack bus: +```julia +using UnitCommitment +instance = UnitCommitment.load("ieee_rts/case14") +isf = UnitCommitment.injection_shift_factors(lines = instance.lines, + buses = instance.buses, + slack = :b14) +@show isf[:l7, :b5] +``` \ No newline at end of file diff --git a/src/docs/js/mathjax.js b/src/docs/js/mathjax.js new file mode 100644 index 0000000..bfc06b8 --- /dev/null +++ b/src/docs/js/mathjax.js @@ -0,0 +1,8 @@ +MathJax.Hub.Config({ + "tex2jax": { inlineMath: [ [ '$', '$' ] ] } +}); +MathJax.Hub.Config({ + config: ["MMLorHTML.js"], + jax: ["input/TeX", "output/HTML-CSS", "output/NativeMML"], + extensions: ["MathMenu.js", "MathZoom.js"] +}); \ No newline at end of file diff --git a/src/docs/model.md b/src/docs/model.md new file mode 100644 index 0000000..c874cc9 --- /dev/null +++ b/src/docs/model.md @@ -0,0 +1,90 @@ +Benchmark Model +=============== + +UnitCommitment.jl includes a reference Mixed-Integer Linear Programming +(MILP), built with [JuMP](https://github.com/JuliaOpt/JuMP.jl), which can +either be used as-is to solve instances of the problem, or be extended to +build more complex formulations. + +Building and Solving the Model +------------------------------- + +Given an instance and a JuMP optimizer, the function `build_model` can be used to +build the reference MILP model. For example: + +```julia +using UnitCommitment, JuMP, Cbc +instance = UnitCommitment.load("ieee_rts/case118") +model = build_model(instance, with_optimizer(Cbc.Optimizer)) +``` + +The model enforces all unit constraints described in [Unit Commitment +Instances](@ref), including ramping, minimum-up and minimum-down times. Some +system-wide constraints, such as spinning reserves, are also enforced. The +model, however, does not enforce transmission or N-1 security constraints, +since these are typically generated on-the-fly. + +A reference to the JuMP model is stored at `model.mip`. After constructed, the model can +be optimized as follows: + +```julia +optimize!(model.mip) +``` + +Decision Variables +------------------ + +References to all decision variables are stored at `model.vars`. +A complete list of available decision variables is as follows: + +| Variable | Description +| :---------------------------- | :--------------------------- +| `model.vars.production[gi,t]` | Amount of power (in MW) produced by unit with index `gi` at time `t`. +| `model.vars.reserve[gi,t]` | Amount of spinning reserves (in MW) provided by unit with index `gi` at time `t`. +| `model.vars.is_on[gi,t]` | Binary variable indicating if unit with index `gi` is operational at time `t`. +| `model.vars.switch_on[gi,t]` | Binary variable indicating if unit with index `gi` was switched on at time `t`. That is, the unit was not operational at time `t-1`, but it is operational at time `t`. +| `model.vars.switch_off[gi,t]` | Binary variable indicating if unit with index `gi` was switched off at time `t`. That is, the unit was operational at time `t-1`, but it is no longer operational at time `t`. +| `model.vars.unit_cost[gi,t]` | The total cost to operate unit with index `gi` at time `t`. Includes start-up costs, no-load costs and any other production costs. +| `model.vars.cost[t]` | Total cost at time `t`. +| `model.vars.net_injection[bi,t]` | Total net injection (in MW) at bus with index `bi` and time `t`. Net injection is defined as the total power being produced by units located at the bus minus the bus load. + + +Accessing the Solution +---------------------- +To access the value of a particular decision variable after the +optimization is completed, the function `JuMP.value(var)` can be used. The +following example prints the amount of power (in MW) produced by each unit at time 5: + +```julia +for g in instance.units + @show value(model.vars.production[g.index, 5]) +end +``` + +Modifying the Model +------------------- + +Prior to being solved, the reference model can be modified by using the variable references +above and conventional JuMP macros. For example, the +following code can be used to ensure that at most 10 units are operational at time 4: + +```julia +using UnitCommitment, JuMP, Cbc +instance = UnitCommitment.load("ieee_rts/case118") +model = build_model(instance, with_optimizer(Cbc.Optimizer)) + +@contraint(model.mip, + sum(model.vars.is_on[g.index, 4] + for g in instance.units) <= 10) + +optimize!(model.mip) +``` + +It is not currently possible to modify the constraints included in the +reference model. + +Reference +--------- +```@docs +UnitCommitment.build_model +``` \ No newline at end of file diff --git a/src/docs/solvers.md b/src/docs/solvers.md new file mode 100644 index 0000000..65e42aa --- /dev/null +++ b/src/docs/solvers.md @@ -0,0 +1,81 @@ +Benchmark Solver +================ + +Solving an instance of the Unit Commitment problem typically involves more +than simply building a Mixed-Integer Linear Programming and handing it over to the +solver. Since the number of transmission and N-1 security constraints can +easily exceed hundreds of millions for large instances of the problem, it is often +necessary to iterate between MILP optimization and contingency screening, so +that only necessary transmission constraints are added to the MILP. + +`UnitCommitment.jl` includes a fast implementation of the contingency +screening method described in +[[1]](https://doi.org/10.1109/TPWRS.2019.2892620), which is able to +efficiently handle even ISO-scale instances of the problem. The method makes +use of Injection Shift Factors (ISFs) and Line Outage Distribution Factors +(LODFs) to model DC power flows and N-1 contingencies. If Julia is configured +to use multiple threads (through the environment variable `JULIA_NUM_THREADS`) +then multiple contingency scenarios are evaluated in parallel. + +Usage +----- + +To solve one of the benchmark instances using the included benchmark solver, use the method `UnitCommitment.solve` +as shown in the example below. + + julia> UnitCommitment.solve("ieee_rts/case118") + [ Info: Loading instance: ieee_rts/case118 + [ Info: 54 units + [ Info: 118 buses + [ Info: 186 lines + [ Info: Scaling problem (0.6 demands, 1.0 limits)... + [ Info: Using Cbc as MILP solver (0.001 gap, 4 threads) + [ Info: Computing sensitivity factors (0.001 ISF cutoff, 0.0001 LODF cutoff)... + [ Info: Building MILP model (24 hours, 0.01 reserve)... + [ Info: Optimizing... + [ Info: Optimal value: 4.033106e+06 + [ Info: Solved in 8.73 seconds + +With default settings, the solver does not consider any transmission or +security constraints, and the peak load is automatically set to 60% of the +installed capacity of the system. These, and many other settings, can be +configured using keyword arguments. See the reference section below for more +details. Sample usage: + + julia> UnitCommitment.solve("ieee_rts/case118", demand_scale=0.7, security=true) + [ Info: Loading instance: ieee_rts/case118 + [ Info: 54 units + [ Info: 118 buses + [ Info: 186 lines + [ Info: Scaling problem (0.7 demands, 1.0 limits)... + [ Info: Using Cbc as MILP solver (0.001 gap, 4 threads) + [ Info: Computing sensitivity factors (0.001 ISF cutoff, 0.0001 LODF cutoff)... + [ Info: Building MILP model (24 hours, 0.01 reserve)... + [ Info: Optimizing... + [ Info: Verifying flow constraints... + [ Info: Optimal value: 4.888740e+06 + [ Info: Solved in 4.50 seconds + +When transmission or N-1 security constraints are activated, the solver uses an +iterative method to lazily enforce them. See [this +paper](https://doi.org/10.1109/TPWRS.2019.2892620) for a detailed description +of the method. Injection Shift Factors (ISF) and Line Outage Distribution +Factors (LODF) are used for the computation of DC power flows. + +!!! note + Many of the benchmark instances were not originally designed for N-1 + security-constrained studies, and may become infeasible if these constraints + are enforced. To avoid infeasibilities, the transmission limits can be + increased through the keyword argument `limit_scale`. + +By default, the MILP is solved using [Cbc, the COIN-OR Branch and Cut +solver](https://github.com/coin-or/Cbc). If `UnitCommitment` is loaded after +either [CPLEX](https://github.com/JuliaOpt/CPLEX.jl) or +[SCIP](https://github.com/SCIP-Interfaces/SCIP.jl), then these solvers will be +used instead. A detailed solver log can be displayed by setting `verbose=true`. + +## Reference + +```@docs +UnitCommitment.solve +``` diff --git a/src/dotdict.jl b/src/dotdict.jl new file mode 100644 index 0000000..061a63c --- /dev/null +++ b/src/dotdict.jl @@ -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. + +struct DotDict + inner::Dict +end + +DotDict() = DotDict(Dict()) + +function Base.setproperty!(d::DotDict, key::Symbol, value) + setindex!(getfield(d, :inner), value, key) +end + +function Base.getproperty(d::DotDict, key::Symbol) + (key == :inner ? getfield(d, :inner) : d.inner[key]) +end + +function Base.getindex(d::DotDict, key::Int64) + d.inner[Symbol(key)] +end + +function Base.getindex(d::DotDict, key::Symbol) + d.inner[key] +end + +function Base.keys(d::DotDict) + keys(d.inner) +end + +function Base.values(d::DotDict) + values(d.inner) +end + +function Base.iterate(d::DotDict) + iterate(values(d.inner)) +end + +function Base.iterate(d::DotDict, v::Int64) + iterate(values(d.inner), v) +end + +function Base.length(d::DotDict) + length(values(d.inner)) +end + +function Base.show(io::IO, d::DotDict) + print(io, "DotDict with $(length(keys(d.inner))) entries:\n") + count = 0 + for k in keys(d.inner) + count += 1 + if count > 10 + print(io, " ...\n") + break + end + print(io, " :$(k) => $(d.inner[k])\n") + end +end + +function recursive_to_dot_dict(el) + if typeof(el) == Dict{String, Any} + return DotDict(Dict(Symbol(k) => recursive_to_dot_dict(el[k]) for k in keys(el))) + else + return el + end +end + +export recursive_to_dot_dict \ No newline at end of file diff --git a/src/initcond.jl b/src/initcond.jl new file mode 100644 index 0000000..20deb45 --- /dev/null +++ b/src/initcond.jl @@ -0,0 +1,76 @@ +# 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 + +""" + generate_initial_conditions!(instance, optimizer) + +Generates feasible initial conditions for the given instance, by constructing +and solving a single-period mixed-integer optimization problem, using the given +optimizer. The instance is modified in-place. +""" +function generate_initial_conditions!(instance::UnitCommitmentInstance, + optimizer) + G = instance.units + B = instance.buses + t = 1 + mip = JuMP.Model(optimizer) + + # Decision variables + @variable(mip, x[G], Bin) + @variable(mip, p[G] >= 0) + + # Constraint: Minimum power + @constraint(mip, + min_power[g in G], + p[g] >= g.min_power[t] * x[g]) + + # Constraint: Maximum power + @constraint(mip, + max_power[g in G], + p[g] <= g.max_power[t] * x[g]) + + # Constraint: Production equals demand + @constraint(mip, + power_balance, + sum(b.load[t] for b in B) == sum(p[g] for g in G)) + + # Constraint: Must run + for g in G + if g.must_run[t] + @constraint(mip, x[g] == 1) + end + end + + # Objective function + function cost_slope(g) + mw = g.min_power[t] + c = g.min_power_cost[t] + for k in g.cost_segments + mw += k.mw[t] + c += k.mw[t] * k.cost[t] + end + if mw < 1e-3 + return 0.0 + else + return c / mw + end + end + @objective(mip, + Min, + sum(p[g] * cost_slope(g) for g in G)) + + JuMP.optimize!(mip) + + for g in G + if JuMP.value(x[g]) > 0 + g.initial_power = JuMP.value(p[g]) + g.initial_status = 24 + else + g.initial_power = 0 + g.initial_status = -24 + end + end +end diff --git a/src/instance.jl b/src/instance.jl new file mode 100644 index 0000000..a97e9b3 --- /dev/null +++ b/src/instance.jl @@ -0,0 +1,349 @@ +# 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 Printf +using JSON +using DataStructures +import Base: getindex, time +import GZip + + +mutable struct Bus + name::String + offset::Int + load::Array{Float64} + units::Array + price_sensitive_loads::Array +end + + +mutable struct CostSegment + mw::Array{Float64} + cost::Array{Float64} +end + + +mutable struct StartupCategory + delay::Int + cost::Float64 +end + + +mutable struct Unit + name::String + bus::Bus + max_power::Array{Float64} + min_power::Array{Float64} + must_run::Array{Bool} + min_power_cost::Array{Float64} + cost_segments::Array{CostSegment} + min_uptime::Int + min_downtime::Int + ramp_up_limit::Float64 + ramp_down_limit::Float64 + startup_limit::Float64 + shutdown_limit::Float64 + initial_status::Union{Int,Nothing} + initial_power::Union{Float64,Nothing} + provides_spinning_reserves::Array{Bool} + startup_categories::Array{StartupCategory} +end + + +mutable struct TransmissionLine + name::String + offset::Int + source::Bus + target::Bus + reactance::Float64 + susceptance::Float64 + normal_flow_limit::Array{Float64} + emergency_flow_limit::Array{Float64} + flow_limit_penalty::Array{Float64} +end + + +mutable struct Reserves + spinning::Array{Float64} +end + + +mutable struct Contingency + name::String + lines::Array{TransmissionLine} + units::Array{Unit} +end + + +mutable struct PriceSensitiveLoad + name::String + bus::Bus + demand::Array{Float64} + revenue::Array{Float64} +end + + +mutable struct UnitCommitmentInstance + time::Int + power_balance_penalty::Array{Float64} + units::Array{Unit} + buses::Array{Bus} + lines::Array{TransmissionLine} + reserves::Reserves + contingencies::Array{Contingency} + price_sensitive_loads::Array{PriceSensitiveLoad} +end + + +function Base.show(io::IO, instance::UnitCommitmentInstance) + print(io, "UnitCommitmentInstance with ") + 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") +end + + +function read_benchmark(name::AbstractString) :: UnitCommitmentInstance + basedir = dirname(@__FILE__) + return UnitCommitment.read("$basedir/../instances/$name.json.gz") +end + + +function read(path::AbstractString)::UnitCommitmentInstance + if endswith(path, ".gz") + return read(GZip.gzopen(path)) + else + return read(open(path)) + end +end + + +function read(file::IO)::UnitCommitmentInstance + return from_json(JSON.parse(file, dicttype=()->DefaultOrderedDict(nothing))) +end + +function from_json(json; fix=true) + units = Unit[] + buses = Bus[] + contingencies = Contingency[] + lines = TransmissionLine[] + loads = PriceSensitiveLoad[] + T = json["Parameters"]["Time (h)"] + + name_to_bus = Dict{String, Bus}() + name_to_line = Dict{String, TransmissionLine}() + name_to_unit = Dict{String, Unit}() + + function timeseries(x; default=nothing) + x != nothing || return default + x isa Array || return [x for t in 1:T] + return x + end + + function scalar(x; default=nothing) + x != nothing || return default + x + end + + # Read parameters + power_balance_penalty = timeseries(json["Parameters"]["Power balance penalty (\$/MW)"], + default=[1000.0 for t in 1:T]) + + # Read buses + for (bus_name, dict) in json["Buses"] + bus = Bus(bus_name, + length(buses), + timeseries(dict["Load (MW)"]), + Unit[], + PriceSensitiveLoad[]) + name_to_bus[bus_name] = bus + push!(buses, bus) + end + + # Read units + for (unit_name, dict) in json["Generators"] + 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.]) + startup_categories = StartupCategory[] + for k in 1:length(startup_delays) + push!(startup_categories, StartupCategory(startup_delays[k], + startup_costs[k])) + 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 + 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), + scalar(dict["Minimum downtime (h)"], default=1), + 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, + timeseries(dict["Provides spinning reserves?"], + default=[true for t in 1:T]), + startup_categories) + push!(bus.units, unit) + name_to_unit[unit_name] = unit + push!(units, unit) + end + + # Read reserves + reserves = Reserves(zeros(T)) + if "Reserves" in keys(json) + reserves.spinning = timeseries(json["Reserves"]["Spinning (MW)"], + default=zeros(T)) + end + + # Read transmission lines + if "Transmission lines" in keys(json) + for (line_name, dict) in json["Transmission lines"] + line = TransmissionLine(line_name, + length(lines) + 1, + name_to_bus[dict["Source bus"]], + name_to_bus[dict["Target bus"]], + scalar(dict["Reactance (ohms)"]), + scalar(dict["Susceptance (S)"]), + timeseries(dict["Normal flow limit (MW)"], + default=[1e8 for t in 1:T]), + timeseries(dict["Emergency flow limit (MW)"], + default=[1e8 for t in 1:T]), + timeseries(dict["Flow limit penalty (\$/MW)"], + default=[5000.0 for t in 1:T])) + name_to_line[line_name] = line + push!(lines, line) + end + end + + # Read contingencies + if "Contingencies" in keys(json) + for (cont_name, dict) in json["Contingencies"] + affected_units = Unit[] + affected_lines = TransmissionLine[] + if "Affected lines" in keys(dict) + affected_lines = [name_to_line[l] for l in dict["Affected lines"]] + end + if "Affected units" in keys(dict) + affected_units = [name_to_unit[u] for u in dict["Affected units"]] + end + cont = Contingency(cont_name, affected_lines, affected_units) + push!(contingencies, cont) + end + end + + # Read price-sensitive loads + if "Price-sensitive loads" in keys(json) + for (load_name, dict) in json["Price-sensitive loads"] + bus = name_to_bus[dict["Bus"]] + load = PriceSensitiveLoad(load_name, + bus, + timeseries(dict["Demand (MW)"]), + timeseries(dict["Revenue (\$/MW)"]), + ) + push!(bus.price_sensitive_loads, load) + push!(loads, load) + end + end + + instance = UnitCommitmentInstance(T, + power_balance_penalty, + units, + buses, + lines, + reserves, + contingencies, + loads) + if fix + UnitCommitment.fix!(instance) + end + return instance +end + + +""" + slice(instance, range) + +Creates a new instance, with only a subset of the time periods. +This function does not modify the provided instance. The initial +conditions are also not modified. + +Example +------- + + # Build a 2-hour UC instance + instance = UnitCommitment.read_benchmark("test/case14") + modified = UnitCommitment.slice(instance, 1:2) + +""" +function slice(instance::UnitCommitmentInstance, range::UnitRange{Int})::UnitCommitmentInstance + modified = deepcopy(instance) + modified.time = length(range) + modified.power_balance_penalty = modified.power_balance_penalty[range] + modified.reserves.spinning = modified.reserves.spinning[range] + for u in modified.units + u.max_power = u.max_power[range] + u.min_power = u.min_power[range] + u.must_run = u.must_run[range] + u.min_power_cost = u.min_power_cost[range] + u.provides_spinning_reserves = u.provides_spinning_reserves[range] + for s in u.cost_segments + s.mw = s.mw[range] + s.cost = s.cost[range] + 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 + + +export UnitCommitmentInstance diff --git a/src/log.jl b/src/log.jl new file mode 100644 index 0000000..8f9139d --- /dev/null +++ b/src/log.jl @@ -0,0 +1,50 @@ +# 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 Logging: min_enabled_level, shouldlog, handle_message +using Base.CoreLogging, Logging, Printf + +struct TimeLogger <: AbstractLogger + initial_time::Float64 + file::Union{Nothing, IOStream} + screen_log_level + io_log_level +end + +function TimeLogger(; + initial_time::Float64, + file::Union{Nothing, IOStream} = nothing, + screen_log_level = CoreLogging.Info, + io_log_level = CoreLogging.Info, + ) :: TimeLogger + return TimeLogger(initial_time, file, screen_log_level, io_log_level) +end + +min_enabled_level(logger::TimeLogger) = logger.io_log_level +shouldlog(logger::TimeLogger, level, _module, group, id) = true + +function handle_message(logger::TimeLogger, + level, + message, + _module, + group, + id, + filepath, + line; + kwargs...) + elapsed_time = time() - logger.initial_time + time_string = @sprintf("[%12.3f] ", elapsed_time) + if level >= logger.screen_log_level + print(time_string) + println(message) + end + if logger.file != nothing && level >= logger.io_log_level + write(logger.file, time_string) + write(logger.file, message) + write(logger.file, "\n") + flush(logger.file) + end +end + +export TimeLogger \ No newline at end of file diff --git a/src/model.jl b/src/model.jl new file mode 100644 index 0000000..ad4bf25 --- /dev/null +++ b/src/model.jl @@ -0,0 +1,646 @@ +# 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, MathOptInterface, DataStructures +import JuMP: value, fix, set_name + + +# Extend some JuMP functions so that decision variables can be safely replaced by +# (constant) floating point numbers. +function value(x::Float64) + x +end + +function fix(x::Float64, v::Float64; force) + abs(x - v) < 1e-6 || error("Value mismatch: $x != $v") +end + +function set_name(x::Float64, n::String) + # nop +end + + +mutable struct UnitCommitmentModel + mip::JuMP.Model + vars::DotDict + eqs::DotDict + exprs::DotDict + instance::UnitCommitmentInstance + isf::Array{Float64, 2} + lodf::Array{Float64, 2} + obj::AffExpr +end + + +function build_model(; + filename::Union{String, Nothing}=nothing, + instance::Union{UnitCommitmentInstance, Nothing}=nothing, + isf::Union{Array{Float64,2}, Nothing}=nothing, + lodf::Union{Array{Float64,2}, Nothing}=nothing, + isf_cutoff::Float64=0.005, + lodf_cutoff::Float64=0.001, + optimizer=nothing, + model=nothing, + variable_names::Bool=false, + ) :: UnitCommitmentModel + + if (filename == nothing) && (instance == nothing) + error("Either filename or instance must be specified") + end + + if filename != nothing + @info "Reading: $(filename)" + time_read = @elapsed begin + instance = UnitCommitment.read(filename) + end + @info @sprintf("Read problem in %.2f seconds", time_read) + end + + if length(instance.buses) == 1 + isf = zeros(0, 0) + lodf = zeros(0, 0) + else + if isf == nothing + @info "Computing injection shift factors..." + time_isf = @elapsed begin + isf = UnitCommitment.injection_shift_factors(lines=instance.lines, + buses=instance.buses) + 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, + isf=isf) + end + @info @sprintf("Computed LODF in %.2f seconds", time_lodf) + + @info @sprintf("Applying PTDF and LODF cutoffs (%.5f, %.5f)", isf_cutoff, lodf_cutoff) + isf[abs.(isf) .< isf_cutoff] .= 0 + lodf[abs.(lodf) .< lodf_cutoff] .= 0 + end + end + + @info "Building model..." + time_model = @elapsed begin + if model == nothing + if optimizer == nothing + mip = Model() + else + mip = Model(optimizer) + end + else + mip = model + end + model = UnitCommitmentModel(mip, + DotDict(), # vars + DotDict(), # eqs + DotDict(), # exprs + instance, + isf, + lodf, + AffExpr(), # obj + ) + for field in [:prod_above, :segprod, :reserve, :is_on, :switch_on, :switch_off, + :net_injection, :curtail, :overflow, :loads, :startup] + setproperty!(model.vars, field, OrderedDict()) + end + for field in [:startup_choose, :startup_restrict, :segprod_limit, :prod_above_def, + :prod_limit, :binary_link, :switch_on_off, :ramp_up, :ramp_down, + :startup_limit, :shutdown_limit, :min_uptime, :min_downtime, :power_balance, + :net_injection_def, :min_reserve] + setproperty!(model.eqs, field, OrderedDict()) + end + for field in [:inj, :reserve, :net_injection] + setproperty!(model.exprs, field, OrderedDict()) + end + for lm in instance.lines + add_transmission_line!(model, lm) + end + for b in instance.buses + add_bus!(model, b) + end + for g in instance.units + add_unit!(model, g) + end + for ps in instance.price_sensitive_loads + add_price_sensitive_load!(model, ps) + end + build_net_injection_eqs!(model) + build_reserve_eqs!(model) + build_obj_function!(model) + end + @info @sprintf("Built model in %.2f seconds", time_model) + + if variable_names + set_variable_names!(model) + end + + return model +end + + +function add_transmission_line!(model, lm) + vars, obj, T = model.vars, model.obj, model.instance.time + for t in 1:T + overflow = vars.overflow[lm.name, t] = @variable(model.mip, lower_bound=0) + add_to_expression!(obj, overflow, lm.flow_limit_penalty[t]) + end +end + + +function add_bus!(model::UnitCommitmentModel, b::Bus) + mip, vars, exprs = model.mip, model.vars, model.exprs + for t in 1:model.instance.time + # Fixed load + exprs.net_injection[b.name, t] = AffExpr(-b.load[t]) + + # Reserves + exprs.reserve[b.name, t] = AffExpr() + + # Load curtailment + vars.curtail[b.name, t] = @variable(mip, lower_bound=0, upper_bound=b.load[t]) + add_to_expression!(exprs.net_injection[b.name, t], vars.curtail[b.name, t], 1.0) + add_to_expression!(model.obj, + vars.curtail[b.name, t], + model.instance.power_balance_penalty[t]) + end +end + + +function add_price_sensitive_load!(model::UnitCommitmentModel, ps::PriceSensitiveLoad) + mip, vars = model.mip, model.vars + for t in 1:model.instance.time + # Decision variable + vars.loads[ps.name, t] = @variable(mip, lower_bound=0, upper_bound=ps.demand[t]) + + # Objective function terms + add_to_expression!(model.obj, vars.loads[ps.name, t], -ps.revenue[t]) + + # Net injection + add_to_expression!(model.exprs.net_injection[ps.bus.name, t], vars.loads[ps.name, t], -1.0) + end +end + + +function add_unit!(model::UnitCommitmentModel, g::Unit) + mip, vars, eqs, exprs, T = model.mip, model.vars, model.eqs, model.exprs, model.instance.time + gi, K, S = g.name, length(g.cost_segments), length(g.startup_categories) + + if !all(g.must_run) && any(g.must_run) + error("Partially must-run units are not currently supported") + end + + if g.initial_power == nothing || g.initial_status == nothing + error("Initial conditions for $(g.name) must be provided") + end + + is_initially_on = (g.initial_status > 0 ? 1.0 : 0.0) + + # Decision variables + for t in 1:T + for k in 1:K + model.vars.segprod[gi, t, k] = @variable(model.mip, lower_bound=0) + end + model.vars.prod_above[gi, t] = @variable(model.mip, lower_bound=0) + if g.provides_spinning_reserves[t] + model.vars.reserve[gi, t] = @variable(model.mip, lower_bound=0) + else + model.vars.reserve[gi, t] = 0.0 + end + for s in 1:S + model.vars.startup[gi, t, s] = @variable(model.mip, binary=true) + end + if g.must_run[t] + model.vars.is_on[gi, t] = 1.0 + model.vars.switch_on[gi, t] = (t == 1 ? 1.0 - is_initially_on : 0.0) + model.vars.switch_off[gi, t] = 0.0 + else + model.vars.is_on[gi, t] = @variable(model.mip, binary=true) + model.vars.switch_on[gi, t] = @variable(model.mip, binary=true) + model.vars.switch_off[gi, t] = @variable(model.mip, binary=true) + end + end + + for t in 1:T + # Time-dependent start-up costs + for s in 1:S + # If unit is switching on, we must choose a startup category + eqs.startup_choose[gi, t, s] = + @constraint(mip, vars.switch_on[gi, t] == sum(vars.startup[gi, t, s] for s in 1:S)) + + # If unit has not switched off in the last `delay` time periods, startup category is forbidden. + # The last startup category is always allowed. + if s < S + range = (t - g.startup_categories[s + 1].delay + 1):(t - g.startup_categories[s].delay) + initial_sum = (g.initial_status < 0 && (g.initial_status + 1 in range) ? 1.0 : 0.0) + eqs.startup_restrict[gi, t, s] = + @constraint(mip, vars.startup[gi, t, s] + <= initial_sum + sum(vars.switch_off[gi, i] for i in range if i >= 1)) + end + + # Objective function terms for start-up costs + add_to_expression!(model.obj, + vars.startup[gi, t, s], + g.startup_categories[s].cost) + end + + # Objective function terms for production costs + add_to_expression!(model.obj, vars.is_on[gi, t], g.min_power_cost[t]) + for k in 1:K + add_to_expression!(model.obj, vars.segprod[gi, t, k], g.cost_segments[k].cost[t]) + end + + # Production limits (piecewise-linear segments) + for k in 1:K + eqs.segprod_limit[gi, t, k] = + @constraint(mip, vars.segprod[gi, t, k] <= g.cost_segments[k].mw[t] * vars.is_on[gi, t]) + end + + # Definition of production + eqs.prod_above_def[gi, t] = + @constraint(mip, vars.prod_above[gi, t] == sum(vars.segprod[gi, t, k] for k in 1:K)) + + # Production limit + eqs.prod_limit[gi, t] = + @constraint(mip, + vars.prod_above[gi, t] + vars.reserve[gi, t] + <= (g.max_power[t] - g.min_power[t]) * vars.is_on[gi, t]) + + # Binary variable equations for economic units + if !g.must_run[t] + + # Link binary variables + if t == 1 + eqs.binary_link[gi, t] = + @constraint(mip, + vars.is_on[gi, t] - is_initially_on == + vars.switch_on[gi, t] - vars.switch_off[gi, t]) + else + eqs.binary_link[gi, t] = + @constraint(mip, + vars.is_on[gi, t] - vars.is_on[gi, t-1] == + vars.switch_on[gi, t] - vars.switch_off[gi, t]) + end + + # Cannot switch on and off at the same time + eqs.switch_on_off[gi, t] = + @constraint(mip, vars.switch_on[gi, t] + vars.switch_off[gi, t] <= 1) + end + + # Ramp up limit + if t == 1 + if is_initially_on == 1 + eqs.ramp_up[gi, t] = + @constraint(mip, + vars.prod_above[gi, t] + vars.reserve[gi, t] <= + (g.initial_power - g.min_power[t]) + g.ramp_up_limit) + end + else + eqs.ramp_up[gi, t] = + @constraint(mip, + vars.prod_above[gi, t] + vars.reserve[gi, t] <= + vars.prod_above[gi, t-1] + g.ramp_up_limit) + end + + # Ramp down limit + if t == 1 + if is_initially_on == 1 + eqs.ramp_down[gi, t] = + @constraint(mip, + vars.prod_above[gi, t] >= + (g.initial_power - g.min_power[t]) - g.ramp_down_limit) + end + else + eqs.ramp_down[gi, t] = + @constraint(mip, + vars.prod_above[gi, t] >= + vars.prod_above[gi, t-1] - g.ramp_down_limit) + end + + # Startup limit + eqs.startup_limit[gi, t] = + @constraint(mip, + vars.prod_above[gi, t] + vars.reserve[gi, t] <= + (g.max_power[t] - g.min_power[t]) * vars.is_on[gi, t] + - max(0, g.max_power[t] - g.startup_limit) * vars.switch_on[gi, t]) + + # Shutdown limit + if g.initial_power > g.shutdown_limit + eqs.shutdown_limit[gi, 0] = + @constraint(mip, vars.switch_off[gi, 1] <= 0) + end + if t < T + eqs.shutdown_limit[gi, t] = + @constraint(mip, + vars.prod_above[gi, t] <= + (g.max_power[t] - g.min_power[t]) * vars.is_on[gi, t] + - max(0, g.max_power[t] - g.shutdown_limit) * vars.switch_off[gi, t+1]) + end + + # Minimum up-time + eqs.min_uptime[gi, t] = + @constraint(mip, + sum(vars.switch_on[gi, i] + for i in (t - g.min_uptime + 1):t if i >= 1 + ) <= vars.is_on[gi, t]) + + # # Minimum down-time + eqs.min_downtime[gi, t] = + @constraint(mip, + sum(vars.switch_off[gi, i] + for i in (t - g.min_downtime + 1):t if i >= 1 + ) <= 1 - vars.is_on[gi, t]) + + # Minimum up/down-time for initial periods + if t == 1 + if g.initial_status > 0 + eqs.min_uptime[gi, 0] = + @constraint(mip, sum(vars.switch_off[gi, i] + for i in 1:(g.min_uptime - g.initial_status) if i <= T) == 0) + else + eqs.min_downtime[gi, 0] = + @constraint(mip, sum(vars.switch_on[gi, i] + for i in 1:(g.min_downtime + g.initial_status) if i <= T) == 0) + end + end + + # Add to net injection expression + add_to_expression!(exprs.net_injection[g.bus.name, t], vars.prod_above[g.name, t], 1.0) + add_to_expression!(exprs.net_injection[g.bus.name, t], vars.is_on[g.name, t], g.min_power[t]) + + # Add to reserves expression + add_to_expression!(exprs.reserve[g.bus.name, t], vars.reserve[gi, t], 1.0) + end +end + + +function build_obj_function!(model::UnitCommitmentModel) + @objective(model.mip, Min, model.obj) +end + + +function build_net_injection_eqs!(model::UnitCommitmentModel) + T = model.instance.time + for t in 1:T, b in model.instance.buses + net = model.vars.net_injection[b.name, t] = @variable(model.mip) + model.eqs.net_injection_def[t, b.name] = + @constraint(model.mip, net == model.exprs.net_injection[b.name, t]) + end + for t in 1:T + model.eqs.power_balance[t] = + @constraint(model.mip, sum(model.vars.net_injection[b.name, t] + for b in model.instance.buses) == 0) + end +end + + +function build_reserve_eqs!(model::UnitCommitmentModel) + reserves = model.instance.reserves + for t in 1:model.instance.time + model.eqs.min_reserve[t] = + @constraint(model.mip, sum(model.exprs.reserve[b.name, t] + for b in model.instance.buses) >= reserves.spinning[t]) + end +end + + +function enforce_transmission(; + model::UnitCommitmentModel, + violation::Violation, + isf::Array{Float64,2}, + lodf::Array{Float64,2})::Nothing + + instance, mip, vars = model.instance, model.mip, model.vars + limit::Float64 = 0.0 + + 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)", + violation.amount, + violation.monitored_line.name, + violation.time) + else + limit = violation.monitored_line.emergency_flow_limit[violation.time] + @info @sprintf(" %8.3f MW overflow in %-5s time %3d (outage: line %s)", + violation.amount, + violation.monitored_line.name, + violation.time, + violation.outage_line.name) + end + + fm = violation.monitored_line.name + t = violation.time + flow = @variable(mip, base_name="flow[$fm,$t]") + + overflow = vars.overflow[violation.monitored_line.name, violation.time] + @constraint(mip, flow <= limit + overflow) + @constraint(mip, -flow <= limit + overflow) + + if violation.outage_line == nothing + @constraint(mip, flow == sum(vars.net_injection[b.name, violation.time] * + isf[violation.monitored_line.offset, b.offset] + for b in instance.buses + if b.offset > 0)) + else + @constraint(mip, flow == sum(vars.net_injection[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)) + end + nothing +end + + +function set_variable_names!(model::UnitCommitmentModel) + @info "Setting variable and constraint names..." + time_varnames = @elapsed begin + set_jump_names!(model.vars) + set_jump_names!(model.eqs) + end + @info @sprintf("Set names in %.2f seconds", time_varnames) +end + + +function set_jump_names!(dict) + for name in keys(dict) + for idx in keys(dict[name]) + idx_str = join(map(string, idx), ",") + set_name(dict[name][idx], "$name[$idx_str]") + end + end +end + + +function get_solution(model::UnitCommitmentModel) + 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) + end + function production_cost(g) + return [value(model.vars.is_on[g.name, t]) * g.min_power_cost[t] + + sum(Float64[value(model.vars.segprod[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) + return [value(model.vars.is_on[g.name, t]) * g.min_power[t] + + sum(Float64[value(model.vars.segprod[g.name, t, k]) + for k in 1:length(g.cost_segments)]) + for t in 1:T] + end + function startup_cost(g) + S = length(g.startup_categories) + return [sum(g.startup_categories[s].cost * value(model.vars.startup[g.name, t, s]) + for s in 1:S) + for t in 1:T] + 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.vars.is_on, instance.units) + sol["Switch on"] = timeseries(model.vars.switch_on, instance.units) + sol["Switch off"] = timeseries(model.vars.switch_off, instance.units) + sol["Reserve (MW)"] = timeseries(model.vars.reserve, instance.units) + sol["Net injection (MW)"] = timeseries(model.vars.net_injection, instance.buses) + sol["Load curtail (MW)"] = timeseries(model.vars.curtail, instance.buses) + if !isempty(instance.lines) + sol["Line overflow (MW)"] = timeseries(model.vars.overflow, instance.lines) + end + if !isempty(instance.price_sensitive_loads) + sol["Price-sensitive loads (MW)"] = timeseries(model.vars.loads, instance.price_sensitive_loads) + end + return sol +end + + +function fix!(model::UnitCommitmentModel, solution)::Nothing + vars, instance, T = model.vars, model.instance, model.instance.time + for g in instance.units + for t in 1:T + is_on = round(solution["Is on"][g.name][t]) + production = round(solution["Production (MW)"][g.name][t], digits=5) + reserve = round(solution["Reserve (MW)"][g.name][t], digits=5) + JuMP.fix(vars.is_on[g.name, t], is_on, force=true) + JuMP.fix(vars.prod_above[g.name, t], production - is_on * g.min_power[t], force=true) + JuMP.fix(vars.reserve[g.name, t], reserve, force=true) + end + end +end + + +function set_warm_start!(model::UnitCommitmentModel, solution)::Nothing + vars, instance, T = model.vars, model.instance, model.instance.time + for g in instance.units + for t in 1:T + JuMP.set_start_value(vars.is_on[g.name, t], solution["Is on"][g.name][t]) + JuMP.set_start_value(vars.switch_on[g.name, t], solution["Switch on"][g.name][t]) + JuMP.set_start_value(vars.switch_off[g.name, t], solution["Switch off"][g.name][t]) + end + end +end + + +function optimize!(model::UnitCommitmentModel; + time_limit=3600, + gap_limit=1e-4, + two_phase_gap=true, + )::Nothing + + function set_gap(gap) + try + JuMP.set_optimizer_attribute(model.mip, "MIPGap", gap) + @info @sprintf("MIP gap tolerance set to %f", gap) + catch + @warn "Could not change MIP gap tolerance" + end + end + + instance = model.instance + initial_time = time() + + large_gap = false + has_transmission = (length(model.isf) > 0) + + if has_transmission && two_phase_gap + set_gap(1e-2) + large_gap = true + else + set_gap(gap_limit) + end + + while true + time_elapsed = time() - initial_time + time_remaining = time_limit - time_elapsed + if time_remaining < 0 + @info "Time limit exceeded" + break + end + + @info @sprintf("Setting MILP time limit to %.2f seconds", time_remaining) + JuMP.set_time_limit_sec(model.mip, time_remaining) + + @info "Solving MILP..." + JuMP.optimize!(model.mip) + + has_transmission || break + + violations = find_violations(model) + if isempty(violations) + @info "No violations found" + if large_gap + large_gap = false + set_gap(gap_limit) + else + break + end + else + enforce_transmission(model, violations) + end + end + + nothing +end + + +function find_violations(model::UnitCommitmentModel) + instance, vars = model.instance, model.vars + length(instance.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_injections = [value(vars.net_injection[b.name, t]) + for b in non_slack_buses, t in 1:instance.time] + overflow = [value(vars.overflow[lm.name, t]) + for lm in instance.lines, t in 1:instance.time] + violations = UnitCommitment.find_violations(instance=instance, + net_injections=net_injections, + overflow=overflow, + isf=model.isf, + lodf=model.lodf) + end + @info @sprintf("Verified transmission limits in %.2f seconds", time_screening) + return violations +end + + +function enforce_transmission(model::UnitCommitmentModel, violations::Array{Violation, 1}) + for v in violations + enforce_transmission(model=model, + violation=v, + isf=model.isf, + lodf=model.lodf) + end +end + + +export UnitCommitmentModel, build_model, get_solution, optimize! diff --git a/src/screening.jl b/src/screening.jl new file mode 100644 index 0000000..951962f --- /dev/null +++ b/src/screening.jl @@ -0,0 +1,198 @@ +# 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. +# Copyright (C) 2019 Argonne National Laboratory +# Written by Alinson Santos Xavier + + +using DataStructures +using Base.Threads + + +struct Violation + time::Int + monitored_line::TransmissionLine + outage_line::Union{TransmissionLine, Nothing} + amount::Float64 # Violation amount (in MW) +end + + +function Violation(; + time::Int, + monitored_line::TransmissionLine, + outage_line::Union{TransmissionLine, Nothing}, + amount::Float64, + ) :: Violation + return Violation(time, monitored_line, outage_line, amount) +end + + +mutable struct ViolationFilter + max_per_line::Int + max_total::Int + queues::Dict{Int, PriorityQueue{Violation, Float64}} +end + + +function ViolationFilter(; + max_per_line::Int=1, + max_total::Int=5, + )::ViolationFilter + return ViolationFilter(max_per_line, max_total, Dict()) +end + + +function offer(filter::ViolationFilter, v::Violation)::Nothing + if v.monitored_line.offset ∉ keys(filter.queues) + filter.queues[v.monitored_line.offset] = PriorityQueue{Violation, Float64}() + end + q::PriorityQueue{Violation, Float64} = filter.queues[v.monitored_line.offset] + if length(q) < filter.max_per_line + enqueue!(q, v => v.amount) + else + if v.amount > peek(q)[1].amount + dequeue!(q) + enqueue!(q, v => v.amount) + end + end + nothing +end + + +function query(filter::ViolationFilter)::Array{Violation, 1} + violations = Array{Violation,1}() + time_queue = PriorityQueue{Violation, Float64}() + for l in keys(filter.queues) + line_queue = filter.queues[l] + while length(line_queue) > 0 + v = dequeue!(line_queue) + if length(time_queue) < filter.max_total + enqueue!(time_queue, v => v.amount) + else + if v.amount > peek(time_queue)[1].amount + dequeue!(time_queue) + enqueue!(time_queue, v => v.amount) + end + end + end + end + while length(time_queue) > 0 + violations = [violations; dequeue!(time_queue)] + end + return violations +end + + +""" + + function find_violations(instance::UnitCommitmentInstance, + net_injections::Array{Float64, 2}; + isf::Array{Float64,2}, + lodf::Array{Float64,2}, + max_per_line::Int = 1, + max_per_period::Int = 5, + ) :: Array{Violation, 1} + +Find transmission constraint violations (both pre-contingency, as well as post-contingency). + +The argument `net_injection` should be a (B-1) x T matrix, where B is the number of buses +and T is the number of time periods. The arguments `isf` and `lodf` can be computed using +UnitCommitment.injection_shift_factors and UnitCommitment.line_outage_factors. +The argument `overflow` specifies how much flow above the transmission limits (in MW) is allowed. +It should be an L x T matrix, where L is the number of transmission lines. +""" +function find_violations(; + instance::UnitCommitmentInstance, + net_injections::Array{Float64, 2}, + overflow::Array{Float64, 2}, + isf::Array{Float64,2}, + lodf::Array{Float64,2}, + max_per_line::Int = 1, + max_per_period::Int = 5, + )::Array{Violation, 1} + + B = length(instance.buses) - 1 + L = length(instance.lines) + T = instance.time + K = nthreads() + + size(net_injections) == (B, T) || error("net_injections has incorrect size") + size(isf) == (L, B) || error("isf has incorrect size") + size(lodf) == (L, L) || error("lodf has incorrect size") + + filters = Dict(t => ViolationFilter(max_total=max_per_period, + max_per_line=max_per_line) + for t in 1:T) + + pre_flow::Array{Float64} = zeros(L, K) # pre_flow[lm, thread] + post_flow::Array{Float64} = zeros(L, L, K) # post_flow[lm, lc, thread] + pre_v::Array{Float64} = zeros(L, K) # pre_v[lm, thread] + 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] + + emergency_limits::Array{Float64,2} = [l.emergency_flow_limit[t] + overflow[l.offset, t] + for l in instance.lines, t in 1:T] + + is_vulnerable::Array{Bool} = zeros(Bool, L) + for c in instance.contingencies + is_vulnerable[c.lines[1].offset] = true + end + + @threads for t in 1:T + k = threadid() + + # Pre-contingency flows + pre_flow[:, k] = isf * net_injections[:, t] + + # Post-contingency flows + for lc in 1:L, lm in 1:L + post_flow[lm, lc, k] = pre_flow[lm, k] + pre_flow[lc, k] * lodf[lm, lc] + end + + # Pre-contingency violations + for lm in 1:L + pre_v[lm, k] = max(0.0, + pre_flow[lm, k] - normal_limits[lm, t], + - pre_flow[lm, k] - normal_limits[lm, t]) + end + + # Post-contingency violations + for lc in 1:L, lm in 1:L + post_v[lm, lc, k] = max(0.0, + post_flow[lm, lc, k] - emergency_limits[lm, t], + - post_flow[lm, lc, k] - emergency_limits[lm, t]) + end + + # Offer pre-contingency violations + for lm in 1:L + if pre_v[lm, k] > 1e-5 + offer(filters[t], Violation(time=t, + monitored_line=instance.lines[lm], + outage_line=nothing, + amount=pre_v[lm, k])) + end + end + + # Offer post-contingency violations + for lm in 1:L, lc in 1:L + if post_v[lm, lc, k] > 1e-5 && is_vulnerable[lc] + offer(filters[t], Violation(time=t, + monitored_line=instance.lines[lm], + outage_line=instance.lines[lc], + amount=post_v[lm, lc, k])) + end + end + end + + violations = Violation[] + for t in 1:instance.time + append!(violations, query(filters[t])) + end + + return violations +end + + +export Violation, ViolationFilter, offer, query, find_violations \ No newline at end of file diff --git a/src/sensitivity.jl b/src/sensitivity.jl new file mode 100644 index 0000000..2c51407 --- /dev/null +++ b/src/sensitivity.jl @@ -0,0 +1,80 @@ +# 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 SparseArrays, Base.Threads, LinearAlgebra, JuMP + +""" + injection_shift_factors(; buses, lines) + +Returns a (B-1)xL matrix M, where B is the number of buses and L is the number of transmission +lines. For a given bus b and transmission line l, the entry M[l.offset, b.offset] indicates +the amount of power (in MW) that flows through transmission line l when 1 MW of power is +injected at the slack bus (the bus that has offset zero) and withdrawn from b. +""" +function injection_shift_factors(; buses, lines) + susceptance = susceptance_matrix(lines) + incidence = reduced_incidence_matrix(lines = lines, buses = buses) + laplacian = transpose(incidence) * susceptance * incidence + isf = susceptance * incidence * inv(Array(laplacian)) + return isf +end + + +""" + reduced_incidence_matrix(; buses::Array{Bus}, lines::Array{TransmissionLine}) + +Returns the incidence matrix for the network, with the column corresponding to the slack +bus is removed. More precisely, returns a (B-1) x L matrix, where B is the number of buses +and L is the number of lines. For each row, there is a 1 element and a -1 element, indicating +the source and target buses, respectively, for that line. +""" +function reduced_incidence_matrix(; buses::Array{Bus}, lines::Array{TransmissionLine}) + matrix = spzeros(Float64, length(lines), length(buses) - 1) + for line in lines + if line.source.offset > 0 + matrix[line.offset, line.source.offset] = 1 + end + if line.target.offset > 0 + matrix[line.offset, line.target.offset] = -1 + end + end + matrix +end + +""" + susceptance_matrix(lines::Array{TransmissionLine}) + +Returns a LxL diagonal matrix, where each diagonal entry is the susceptance of the +corresponding transmission line. +""" +function susceptance_matrix(lines::Array{TransmissionLine}) + return Diagonal([l.susceptance for l in lines]) +end + + +""" + + line_outage_factors(; buses, lines, isf) + +Returns a LxL matrix containing the Line Outage Distribution Factors (LODFs) for the +given network. This matrix how does the pre-contingency flow change when each individual +transmission line is removed. +""" +function line_outage_factors(; + buses::Array{Bus, 1}, + lines::Array{TransmissionLine, 1}, + isf::Array{Float64,2}, + ) :: Array{Float64,2} + + n_lines, n_buses = size(isf) + incidence = Array(reduced_incidence_matrix(lines=lines, + buses=buses)) + lodf::Array{Float64,2} = isf * transpose(incidence) + m, n = size(lodf) + for i in 1:n + lodf[:, i] *= 1.0 / (1.0 - lodf[i, i]) + lodf[i, i] = -1 + end + return lodf +end diff --git a/src/sysimage.jl b/src/sysimage.jl new file mode 100644 index 0000000..e8d1263 --- /dev/null +++ b/src/sysimage.jl @@ -0,0 +1,26 @@ +# 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 PackageCompiler + +using DataStructures +using Documenter +using GLPK +using JSON +using JuMP +using MathOptInterface +using SparseArrays +using TimerOutputs + +pkg = [:DataStructures, + :Documenter, + :GLPK, + :JSON, + :JuMP, + :MathOptInterface, + :SparseArrays, + :TimerOutputs] + +@info "Building system image..." +create_sysimage(pkg, sysimage_path="build/sysimage.so") diff --git a/src/validate.jl b/src/validate.jl new file mode 100644 index 0000000..4f0b10c --- /dev/null +++ b/src/validate.jl @@ -0,0 +1,334 @@ +# 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 Printf + +bin(x) = [xi > 0.5 for xi in x] + +""" + fix!(instance) + +Verifies that the given unit commitment instance 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 fix!(instance::UnitCommitmentInstance)::Int + n_errors = 0 + + for g in instance.units + + # Startup costs and delays must be increasing + for s in 2:length(g.startup_categories) + if g.startup_categories[s].delay <= g.startup_categories[s-1].delay + prev_value = g.startup_categories[s].delay + new_value = g.startup_categories[s-1].delay + 1 + @warn "Generator $(g.name) has non-increasing startup delays (category $s). " * + "Changing delay: $prev_value → $new_value" + g.startup_categories[s].delay = new_value + n_errors += 1 + end + + if g.startup_categories[s].cost < g.startup_categories[s-1].cost + prev_value = g.startup_categories[s].cost + new_value = g.startup_categories[s-1].cost + @warn "Generator $(g.name) has decreasing startup cost (category $s). " * + "Changing cost: $prev_value → $new_value" + g.startup_categories[s].cost = new_value + n_errors += 1 + end + + end + + for t in 1:instance.time + # Production cost curve should be convex + for k in 2:length(g.cost_segments) + cost = g.cost_segments[k].cost[t] + min_cost = g.cost_segments[k-1].cost[t] + if cost < min_cost - 1e-5 + @warn "Generator $(g.name) has non-convex production cost curve " * + "(segment $k, time $t). Changing cost: $cost → $min_cost" + g.cost_segments[k].cost[t] = min_cost + n_errors += 1 + end + end + + # Startup limit must be greater than min_power + if g.startup_limit < g.min_power[t] + new_limit = g.min_power[t] + prev_limit = g.startup_limit + @warn "Generator $(g.name) has startup limit lower than minimum power. " * + "Changing startup limit: $prev_limit → $new_limit" + g.startup_limit = new_limit + n_errors += 1 + end + end + end + + + return n_errors +end + + +function validate(instance_filename::String, solution_filename::String) + instance = UnitCommitment.read(instance_filename) + solution = JSON.parse(open(solution_filename)) + return validate(instance, solution) +end + + +""" + validate(instance, solution)::Bool + +Verifies that the given solution is feasible for the problem. If feasible, +silently returns true. In infeasible, returns false and prints the validation +errors to the screen. + +This function is implemented independently from the optimization model in `model.jl`, and +therefore can be used to verify that the model is indeed producing valid solutions. It +can also be used to verify the solutions produced by other optimization packages. +""" +function validate(instance::UnitCommitmentInstance, + solution::Union{Dict,OrderedDict}; + )::Bool + err_count = 0 + err_count += validate_units(instance, solution) + err_count += validate_reserve_and_demand(instance, solution) + + if err_count > 0 + @error "Found $err_count validation errors" + return false + end + + return true +end + + +function validate_units(instance, solution; tol=0.01) + err_count = 0 + + for unit in instance.units + production = solution["Production (MW)"][unit.name] + reserve = solution["Reserve (MW)"][unit.name] + 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]) + 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 + + # 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 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 + if !unit.provides_spinning_reserves[t] && reserve[t] > tol + @error @sprintf("Unit %s is not eligible to provide spinning reserves at time %d", + unit.name, t) + err_count += 1 + 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", + unit.name, 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 + initial_down = unit.min_downtime + if unit.initial_status < 0 + initial_down = -unit.initial_status + end + time_down += initial_down + 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 violates minimum downtime at time %d", + unit.name, t) + err_count += 1 + end + end + + # 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 + initial_up = unit.min_uptime + if unit.initial_status > 0 + initial_up = unit.initial_status + end + time_up += initial_up + end + + if (t == time_up + 1) && (unit.initial_status > 0) + time_up += unit.initial_status + end + + # 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 + + # 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 + 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) + 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 + + # Verify that production equals demand + if abs(balance) > tol + @error @sprintf("Non-zero power balance at time %d (%.2f - %.2f - %.2f != 0)", + t, fixed_load, load_curtail, production) + err_count += 1 + end + + # Verify spinning reserves + reserve = sum(solution["Reserve (MW)"][g.name][t] for g in instance.units) + if reserve < instance.reserves.spinning[t] - tol + @error @sprintf("Insufficient spinning reserves at time %d (%.2f should be %.2f)", + t, reserve, instance.reserves.spinning[t]) + err_count += 1 + end + end + + return err_count +end + \ No newline at end of file diff --git a/test/convert_test.jl b/test/convert_test.jl new file mode 100644 index 0000000..bc1496e --- /dev/null +++ b/test/convert_test.jl @@ -0,0 +1,19 @@ +# 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 "convert" begin + @testset "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., 884.44, 1470.71, 1470.71, 884.44] + @test solution["Startup cost (\$)"]["315_CT_6"][15:20] == [0., 0., 5665.23, 0., 0., 0.] + @test length(keys(solution["Is on"])) == 154 + end +end \ No newline at end of file diff --git a/test/fixtures/case118-initcond.json.gz b/test/fixtures/case118-initcond.json.gz new file mode 100644 index 0000000..a81a787 Binary files /dev/null and b/test/fixtures/case118-initcond.json.gz differ diff --git a/test/fixtures/egret_output.json.gz b/test/fixtures/egret_output.json.gz new file mode 100644 index 0000000..f2a20c4 Binary files /dev/null and b/test/fixtures/egret_output.json.gz differ diff --git a/test/initcond_test.jl b/test/initcond_test.jl new file mode 100644 index 0000000..c1e5d83 --- /dev/null +++ b/test/initcond_test.jl @@ -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. + +using UnitCommitment, Cbc, JuMP + +@testset "Initial conditions" begin + # Load instance + instance = UnitCommitment.read("$(pwd())/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 diff --git a/test/instance_test.jl b/test/instance_test.jl new file mode 100644 index 0000000..17b1685 --- /dev/null +++ b/test/instance_test.jl @@ -0,0 +1,142 @@ +# 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 "Instance" begin + @testset "read" begin + instance = UnitCommitment.read_benchmark("test/case14") + + @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[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] + + 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. for t in 1:4] + @test unit.min_uptime == 1 + @test unit.min_downtime == 1 + @test unit.provides_spinning_reserves == [true for t in 1:4] + for t in 1:1 + @test unit.cost_segments[1].mw[t] == 10.0 + @test unit.cost_segments[2].mw[t] == 20.0 + @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 + + unit = instance.units[2] + @test unit.name == "g2" + @test unit.must_run == [false for t in 1:4] + + 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. for t in 1:4] + @test unit.min_uptime == 1 + @test unit.min_downtime == 1 + @test unit.provides_spinning_reserves == [true for t in 1:4] + for t in 1:4 + @test unit.cost_segments[1].mw[t] ≈ 33 + @test unit.cost_segments[2].mw[t] ≈ 33 + @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 instance.reserves.spinning == zeros(4) + + @test instance.contingencies[1].lines == [instance.lines[1]] + @test instance.contingencies[1].units == [] + + load = instance.price_sensitive_loads[1] + @test load.name == "ps1" + @test load.bus.name == "b3" + @test load.revenue == [100. for t in 1:4] + @test load.demand == [50. for t in 1:4] + end + + @testset "slice" begin + instance = UnitCommitment.read_benchmark("test/case14") + modified = UnitCommitment.slice(instance, 1:2) + + # Should update all time-dependent fields + @test modified.time == 2 + @test length(modified.power_balance_penalty) == 2 + @test length(modified.reserves.spinning) == 2 + for u in modified.units + @test length(u.max_power) == 2 + @test length(u.min_power) == 2 + @test length(u.must_run) == 2 + @test length(u.min_power_cost) == 2 + @test length(u.provides_spinning_reserves) == 2 + for s in u.cost_segments + @test length(s.mw) == 2 + @test length(s.cost) == 2 + 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 = build_model(instance=modified, + optimizer=optimizer, + variable_names=true) + end +end diff --git a/test/model_test.jl b/test/model_test.jl new file mode 100644 index 0000000..7d75d2e --- /dev/null +++ b/test/model_test.jl @@ -0,0 +1,32 @@ +# 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 + +@testset "Model" begin + @testset "Run" begin + instance = UnitCommitment.read_benchmark("test/case14") + for line in instance.lines, t in 1:4 + line.normal_flow_limit[t] = 10.0 + end + optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) + model = build_model(instance=instance, + optimizer=optimizer, + variable_names=true) + + JuMP.write_to_file(model.mip, "test.mps") + + # Optimize and retrieve solution + UnitCommitment.optimize!(model) + solution = get_solution(model) + + # Verify solution + @test UnitCommitment.validate(instance, solution) + + # Reoptimize with fixed solution + UnitCommitment.fix!(model, solution) + UnitCommitment.optimize!(model) + @test UnitCommitment.validate(instance, solution) + end +end diff --git a/test/runtests.jl b/test/runtests.jl new file mode 100644 index 0000000..6a64a10 --- /dev/null +++ b/test/runtests.jl @@ -0,0 +1,15 @@ +# 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 + +@testset "UnitCommitment" begin + include("instance_test.jl") + include("model_test.jl") + include("sensitivity_test.jl") + include("screening_test.jl") + include("convert_test.jl") + include("validate_test.jl") + include("initcond_test.jl") +end diff --git a/test/screening_test.jl b/test/screening_test.jl new file mode 100644 index 0000000..1670cf0 --- /dev/null +++ b/test/screening_test.jl @@ -0,0 +1,75 @@ +# 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 "Screening" begin + @testset "Violation filter" begin + instance = UnitCommitment.read_benchmark("test/case14") + filter = ViolationFilter(max_per_line=1, max_total=2) + + offer(filter, Violation(time=1, + monitored_line=instance.lines[1], + outage_line=nothing, + amount=100.)) + + offer(filter, Violation(time=1, + monitored_line=instance.lines[1], + outage_line=instance.lines[1], + amount=300.)) + + offer(filter, Violation(time=1, + monitored_line=instance.lines[1], + outage_line=instance.lines[5], + amount=500.)) + + offer(filter, Violation(time=1, + monitored_line=instance.lines[1], + outage_line=instance.lines[4], + amount=400.)) + + offer(filter, Violation(time=1, + monitored_line=instance.lines[2], + outage_line=instance.lines[1], + amount=200.)) + + offer(filter, Violation(time=1, + monitored_line=instance.lines[2], + outage_line=instance.lines[8], + amount=100.)) + + actual = query(filter) + expected = [Violation(time=1, + monitored_line=instance.lines[2], + outage_line=instance.lines[1], + amount=200.), + Violation(time=1, + monitored_line=instance.lines[1], + outage_line=instance.lines[5], + amount=500.)] + @test actual == expected + end + + @testset "find_violations" begin + instance = UnitCommitment.read_benchmark("test/case14") + 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) + + @test length(violations) == 20 + end +end \ No newline at end of file diff --git a/test/sensitivity_test.jl b/test/sensitivity_test.jl new file mode 100644 index 0000000..cfc97ee --- /dev/null +++ b/test/sensitivity_test.jl @@ -0,0 +1,115 @@ +# 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 "Sensitivity" begin + @testset "Susceptance matrix" begin + instance = UnitCommitment.read_benchmark("test/case14") + 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_benchmark("test/case14") + 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 (ISF)" begin + instance = UnitCommitment.read_benchmark("test/case14") + 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 Distribution Factors (LODF)" begin + instance = UnitCommitment.read_benchmark("test/case14") + 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 +end \ No newline at end of file diff --git a/test/validate_test.jl b/test/validate_test.jl new file mode 100644 index 0000000..b904358 --- /dev/null +++ b/test/validate_test.jl @@ -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, JSON, GZip, DataStructures + +parse_case14() = JSON.parse(GZip.gzopen("../instances/test/case14.json.gz"), + dicttype=()->DefaultOrderedDict(nothing)) + +@testset "Validation" begin + @testset "fix!" 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] + instance = UnitCommitment.from_json(json, fix=false) + @test UnitCommitment.fix!(instance) == 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 + instance = UnitCommitment.from_json(json, fix=false) + @test UnitCommitment.fix!(instance) == 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] + instance = UnitCommitment.from_json(json, fix=false) + @test UnitCommitment.fix!(instance) == 4 + end + + end +end