diff --git a/.appveyor.yml b/.appveyor.yml index e8ad0c539f..a6d4bfe1e0 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,6 +1,7 @@ environment: matrix: - - julia_version: 1.1 + - julia_version: 1.2 + - julia_version: 1.3 - julia_version: latest platform: @@ -12,6 +13,7 @@ platform: matrix: allow_failures: - julia_version: latest + - julia_version: 1.3 branches: only: @@ -33,4 +35,4 @@ build_script: test_script: - echo "%JL_TEST_SCRIPT%" - - C:\julia\bin\julia --check-bounds=yes --color=yes --project -e "%JL_TEST_SCRIPT%" \ No newline at end of file + - C:\julia\bin\julia --check-bounds=yes --color=yes --project -e "%JL_TEST_SCRIPT%" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..770d0c8a7f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Julia files +[*.jl] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore index f0d29e959f..3a920533f1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,14 +5,16 @@ docs/build/* .DS_Store *.R -*.PNG *.csv *.ipynb *.log *.m *.RAW *.zip -*.json data *.bak *~ +*.vscode +src/descriptors/validation_config.* +docs/src/man/data_requirements_table.md +/docs/Manifest.toml diff --git a/.travis.yml b/.travis.yml index 5a8f48db74..fa8e182034 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,13 @@ os: - linux - osx julia: - - 1.1 + - 1.2 + - 1.3 - nightly + +codecov: true + notifications: email: false @@ -25,21 +29,13 @@ matrix: #before_script: # homebrew for mac # - if [ $TRAVIS_OS_NAME = osx ]; then brew install gcc; fi -## uncomment the following lines to override the default test script -script: - - julia --project --color=yes -e "import Pkg; Pkg.instantiate(); Pkg.build();" - - julia --check-bounds=yes --depwarn=no --color=yes --project -e "import Pkg; Pkg.test(coverage=true);" - -after_success: - - julia -e 'using Pkg; cd(Pkg.dir("PowerSystems")); Pkg.add("Coverage"); using Coverage; Codecov.submit(Codecov.process_folder())' jobs: include: - stage: "Documentation" - julia: 1.0 + julia: 1.2 os: linux script: - - julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.build("PowerSystems"); Pkg.instantiate()' - - DOCUMENTER_DEBUG=true julia --project=docs/ docs/make.jl + - julia --project=docs -e 'using Pkg; Pkg.instantiate(); Pkg.add(PackageSpec(path=pwd()));' + - julia --project=docs --color=yes -e 'include("docs/make.jl")' after_success: skip - diff --git a/Manifest.toml b/Manifest.toml deleted file mode 100644 index 80e75a79bb..0000000000 --- a/Manifest.toml +++ /dev/null @@ -1,257 +0,0 @@ -# This file is machine-generated - editing it directly is not advised - -[[AxisArrays]] -deps = ["Compat", "Dates", "IntervalSets", "IterTools", "Random", "RangeArrays", "Test"] -git-tree-sha1 = "2e2536e9e6f27c4f8d09d8442b61a7ae0b910c28" -uuid = "39de3d68-74b9-583c-8d2d-e117c070f3a9" -version = "0.3.0" - -[[Base64]] -uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" - -[[CSV]] -deps = ["CategoricalArrays", "DataFrames", "DataStreams", "Dates", "Mmap", "Parsers", "Profile", "Random", "Tables", "Test", "Unicode", "WeakRefStrings"] -git-tree-sha1 = "b92c6f626a044cc9619156d54994b94084d40abe" -uuid = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" -version = "0.4.3" - -[[CategoricalArrays]] -deps = ["Compat", "Future", "Missings", "Printf", "Reexport", "Requires"] -git-tree-sha1 = "94d16e77dfacc59f6d6c1361866906dbb65b6f6b" -uuid = "324d7699-5711-5eae-9e2f-1d82baa6b597" -version = "0.5.2" - -[[Codecs]] -deps = ["Test"] -git-tree-sha1 = "70885e5e038cba1c4c17a84ad6c40756e10a4fb5" -uuid = "19ecbf4d-ef7c-5e4b-b54a-0a0ff23c5aed" -version = "0.5.0" - -[[Compat]] -deps = ["Base64", "Dates", "DelimitedFiles", "Distributed", "InteractiveUtils", "LibGit2", "Libdl", "LinearAlgebra", "Markdown", "Mmap", "Pkg", "Printf", "REPL", "Random", "Serialization", "SharedArrays", "Sockets", "SparseArrays", "Statistics", "Test", "UUIDs", "Unicode"] -git-tree-sha1 = "84aa74986c5b9b898b0d1acaf3258741ee64754f" -uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" -version = "2.1.0" - -[[DataFrames]] -deps = ["CategoricalArrays", "Compat", "IteratorInterfaceExtensions", "Missings", "PooledArrays", "Printf", "REPL", "Reexport", "SortingAlgorithms", "Statistics", "StatsBase", "TableTraits", "Tables", "Unicode"] -git-tree-sha1 = "4b83473baad52fbfe3b33b720e4fa722cdd62ce8" -uuid = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -version = "0.18.1" - -[[DataStreams]] -deps = ["Dates", "Missings", "Test", "WeakRefStrings"] -git-tree-sha1 = "69c72a1beb4fc79490c361635664e13c8e4a9548" -uuid = "9a8bc11e-79be-5b39-94d7-1ccc349a1a85" -version = "0.4.1" - -[[DataStructures]] -deps = ["InteractiveUtils", "OrderedCollections", "Random", "Serialization", "Test"] -git-tree-sha1 = "ca971f03e146cf144a9e2f2ce59674f5bf0e8038" -uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" -version = "0.15.0" - -[[Dates]] -deps = ["Printf"] -uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" - -[[DelimitedFiles]] -deps = ["Mmap"] -uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab" - -[[Distributed]] -deps = ["Random", "Serialization", "Sockets"] -uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" - -[[Future]] -deps = ["Random"] -uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820" - -[[InteractiveUtils]] -deps = ["Markdown"] -uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" - -[[IntervalSets]] -deps = ["Compat"] -git-tree-sha1 = "9dc556002f23740de13946e8c2e41798e09a9249" -uuid = "8197267c-284f-5f27-9208-e0e47529a953" -version = "0.3.1" - -[[IterTools]] -deps = ["SparseArrays", "Test"] -git-tree-sha1 = "79246285c43602384e6f1943b3554042a3712056" -uuid = "c8e1da08-722c-5040-9ed9-7db0dc04731e" -version = "1.1.1" - -[[IteratorInterfaceExtensions]] -git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856" -uuid = "82899510-4779-5014-852e-03e436cf321d" -version = "1.0.0" - -[[JSON]] -deps = ["Dates", "Distributed", "Mmap", "Sockets", "Test", "Unicode"] -git-tree-sha1 = "1f7a25b53ec67f5e9422f1f551ee216503f4a0fa" -uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -version = "0.20.0" - -[[LibGit2]] -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" - -[[Markdown]] -deps = ["Base64"] -uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" - -[[Missings]] -deps = ["Dates", "InteractiveUtils", "SparseArrays", "Test"] -git-tree-sha1 = "d1d2585677f2bd93a97cfeb8faa7a0de0f982042" -uuid = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" -version = "0.4.0" - -[[Mmap]] -uuid = "a63ad114-7e13-5084-954f-fe012c677804" - -[[OrderedCollections]] -deps = ["Random", "Serialization", "Test"] -git-tree-sha1 = "c4c13474d23c60d20a67b217f1d7f22a40edf8f1" -uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -version = "1.1.0" - -[[Parsers]] -deps = ["Dates", "Mmap", "Test", "WeakRefStrings"] -git-tree-sha1 = "d4e634c9cab597f0ec8f5a1d62c0787e98602c55" -uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "0.2.22" - -[[Pkg]] -deps = ["Dates", "LibGit2", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] -uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" - -[[PooledArrays]] -deps = ["Test"] -git-tree-sha1 = "6ea4cfb9136d3ff2b9d30d6696cd72166a3b837c" -uuid = "2dfb63ee-cc39-5dd5-95bd-886bf059d720" -version = "0.5.1" - -[[Printf]] -deps = ["Unicode"] -uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" - -[[Profile]] -deps = ["Printf"] -uuid = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79" - -[[REPL]] -deps = ["InteractiveUtils", "Markdown", "Sockets"] -uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" - -[[Random]] -deps = ["Serialization"] -uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" - -[[RangeArrays]] -deps = ["Compat"] -git-tree-sha1 = "d925adfd5b01cb46fde89dc9548d167b3b136f4a" -uuid = "b3c3ace0-ae52-54e7-9d0b-2c1406fd6b9d" -version = "0.3.1" - -[[RecipesBase]] -deps = ["Random", "Test"] -git-tree-sha1 = "0b3cb370ee4dc00f47f1193101600949f3dcf884" -uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" -version = "0.6.0" - -[[Reexport]] -deps = ["Pkg"] -git-tree-sha1 = "7b1d07f411bc8ddb7977ec7f377b97b158514fe0" -uuid = "189a3867-3050-52da-a836-e630ba90ab69" -version = "0.2.0" - -[[Requires]] -deps = ["Test"] -git-tree-sha1 = "f6fbf4ba64d295e146e49e021207993b6b48c7d1" -uuid = "ae029012-a4dd-5104-9daa-d747884805df" -version = "0.5.2" - -[[SHA]] -uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" - -[[Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" - -[[SharedArrays]] -deps = ["Distributed", "Mmap", "Random", "Serialization"] -uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383" - -[[Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" - -[[SortingAlgorithms]] -deps = ["DataStructures", "Random", "Test"] -git-tree-sha1 = "03f5898c9959f8115e30bc7226ada7d0df554ddd" -uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c" -version = "0.3.1" - -[[SparseArrays]] -deps = ["LinearAlgebra", "Random"] -uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" - -[[Statistics]] -deps = ["LinearAlgebra", "SparseArrays"] -uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" - -[[StatsBase]] -deps = ["DataStructures", "LinearAlgebra", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics"] -git-tree-sha1 = "8a0f4b09c7426478ab677245ab2b0b68552143c7" -uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" -version = "0.30.0" - -[[TableTraits]] -deps = ["IteratorInterfaceExtensions"] -git-tree-sha1 = "b1ad568ba658d8cbb3b892ed5380a6f3e781a81e" -uuid = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c" -version = "1.0.0" - -[[Tables]] -deps = ["IteratorInterfaceExtensions", "LinearAlgebra", "Requires", "TableTraits", "Test"] -git-tree-sha1 = "e2abd466ca2f00a1bb8cb80024d59745cc8f91a6" -uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" -version = "0.2.0" - -[[Test]] -deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] -uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[[TimeSeries]] -deps = ["Dates", "DelimitedFiles", "Random", "RecipesBase", "Reexport", "Statistics", "Test"] -git-tree-sha1 = "4bcd2713b71a46f1a0d708fd13eed367893c2403" -uuid = "9e3dc215-6440-5c97-bce1-76c03772f85e" -version = "0.14.1" - -[[UUIDs]] -deps = ["Random", "SHA"] -uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" - -[[Unicode]] -uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" - -[[WeakRefStrings]] -deps = ["Missings", "Random", "Test"] -git-tree-sha1 = "960639a12ffd223ee463e93392aeb260fa325566" -uuid = "ea10d353-3f73-51f8-a26c-33c1cb351aa5" -version = "0.5.8" - -[[YAML]] -deps = ["Codecs", "Compat"] -git-tree-sha1 = "3bde77cee95cce0c0b9b18813d85e18e8ed4f415" -uuid = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" -version = "0.3.2" diff --git a/Project.toml b/Project.toml index eb86d975da..90cb19384a 100644 --- a/Project.toml +++ b/Project.toml @@ -1,33 +1,37 @@ name = "PowerSystems" uuid = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd" -authors = ["Jose Daniel Lara", "Dheepak Krishnamurthy", "Clayton Barrows"] -version = "0.3.2" +authors = ["Jose Daniel Lara", "Dheepak Krishnamurthy", "Clayton Barrows", "Daniel Thom"] +version = "0.4.0" [deps] -AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +JSON2 = "2535ab7d-5cd8-5a07-80ac-9b1792aadce3" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" TimeSeries = "9e3dc215-6440-5c97-bce1-76c03772f85e" +UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" [compat] -AxisArrays = ">= 0.3.0" -CSV = ">= 0.4.3" -DataFrames = ">= 0.4.3" -JSON = ">= 0.4.3" -TimeSeries = ">= 0.14.1" -YAML = ">= 0.3.2" -julia = ">= 1.1.0" +CSV = "~0.5" +DataFrames = "~0.19" +JSON = "~0.21" +JSON2 = "~0.3" +TimeSeries = "~0.16" +YAML = "~0.3" +julia = "~1.2" +InfrastructureSystems = "~0.1.1" [extras] -SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +NLsolve = "2774e3e8-f4cf-5e23-947b-6d7e65073b56" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "SparseArrays"] +test = ["Test", "Random", "NLsolve"] diff --git a/README.md b/README.md index 6961db3611..0dab8cc09f 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,20 @@ [![codecov](https://codecov.io/gh/NREL/PowerSystems.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/NREL/PowerSystems.jl) [![Gitter](https://badges.gitter.im/NREL/PowerSystems.jl.svg)](https://gitter.im/NREL/PowerSystems.jl?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) -The `PowerSystems.jl` package provides a rigorous data model using Julia structures to enable power systems analysis. In addition to stand-alone system analysis tools and model building, the `PowerSystems.jl` package is used as the foundational data container for the [PowerSimulations.jl](https://github.com/NREL/PowerSimulations.jl) package. `PowerSystems.jl` enables data structures for different devices and relies on a limited number of data file formats for parsing. +The `PowerSystems.jl` package provides a rigorous data model using Julia structures to enable power systems analysis and modeling. In addition to stand-alone system analysis tools and data model building, the `PowerSystems.jl` package is used as the foundational data container for the [PowerSimulations.jl](https://github.com/NREL/PowerSimulations.jl) package. `PowerSystems.jl` supports a limited number of data file formats for parsing. ## Version Advisory -- The latest tagged version in PowerSystems (v0.3.2) will work with Julia v1.1+. +- The latest tagged version in PowerSystems (v0.4.0) will work with Julia v1.2+. ### Device data enabled in PowerSystems: - - Generators (Thermal, Renewable, Synchronous Condensers, and Hydro) + - Generators (Thermal, Renewable and Hydro) - Transmission (Lines, and Transformers) - Active Flow control devices (DC Lines and phase-shifters) - - Topological elements (Buses, Areas) + - Topological elements (Buses, Arcs, Areas) - Storage (Batteries) - Load (Static, and curtailable) - - Services (Reserves, inter-regional transfers) + - Services (Reserves, transfers) - Forecasts (Deterministic, scenario, stochastic) ### Parsing capabilities in PowerSystems: diff --git a/bin/generate_config_file.py b/bin/generate_config_file.py new file mode 100644 index 0000000000..a226552af0 --- /dev/null +++ b/bin/generate_config_file.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +"""Generates a user descriptor file for parsing power system raw data.""" + +# Note: This is written in Python instead of Julia because the Julia YAML +# package does not support writing (only reading). + +import json +import os +import sys + +import yaml + + +POWER_SYSTEM_DESCRIPTOR_FILE = os.path.join( + "descriptors", + "power_system_inputs.json" +) + + +def read_json_data(filename): + """Return the JSON data from a file.""" + with open(filename) as fp_in: + return json.load(fp_in) + + +def generate_config(input_file): + """Generate user descriptors from the PowerSystems descriptor file.""" + config = {} + data = read_json_data(input_file) + for key, value in data.items(): + items = [] + for item in value: + config_item = { + "name": item["name"], + "custom_name": item["name"], + } + items.append(config_item) + + config[key] = items + + return config + + +def generate_file(output_file, input_file=POWER_SYSTEM_DESCRIPTOR_FILE): + """Generate user file from the PowerSystems descriptor file.""" + config = generate_config(input_file) + with open(output_file, "w") as fp_out: + yaml.dump(config, fp_out) + + print("Generated {} from {}".format(output_file, input_file)) + + +def main(): + """Controls execution.""" + if len(sys.argv) != 2: + print("Usage: {} output_file".format(os.path.basename(__file__))) + sys.exit(1) + + generate_file(sys.argv[1]) + + +if __name__ == "__main__": + main() diff --git a/bin/generate_structs.jl b/bin/generate_structs.jl new file mode 100644 index 0000000000..6b642e1289 --- /dev/null +++ b/bin/generate_structs.jl @@ -0,0 +1,155 @@ +import Pkg +Pkg.add(Pkg.PackageSpec(name="Mustache", version="0.5.12")) + +import JSON2 +import Mustache + +template = """ +#= +This file is auto-generated. Do not edit. +=# + +{{#docstring}}\"\"\"{{docstring}}\"\"\"{{/docstring}} +mutable struct {{struct_name}}{{#parametric}}{T <: {{parametric}}}{{/parametric}} <: {{supertype}} + {{#parameters}} + {{name}}::{{data_type}}{{#comment}} # {{{comment}}}{{/comment}} + {{/parameters}} + {{#inner_constructor_check}} + + function {{struct_name}}({{#parameters}}{{name}}, {{/parameters}}) + ({{#parameters}}{{name}}, {{/parameters}}) = {{inner_constructor_check}}( + {{#parameters}} + {{name}}, + {{/parameters}} + ) + new({{#parameters}}{{name}}, {{/parameters}}) + end + {{/inner_constructor_check}} +end + +function {{struct_name}}({{#parameters}}{{^internal}}{{name}}, {{/internal}}{{/parameters}}) + {{#parameters}} + {{/parameters}} + {{struct_name}}({{#parameters}}{{^internal}}{{name}}, {{/internal}}{{/parameters}}PowerSystemInternal()) +end + +function {{struct_name}}(; {{#parameters}}{{^internal}}{{name}}, {{/internal}}{{/parameters}}) + {{struct_name}}({{#parameters}}{{^internal}}{{name}}, {{/internal}}{{/parameters}}) +end + +{{#has_null_values}} +# Constructor for demo purposes; non-functional. + +function {{struct_name}}(::Nothing) + {{struct_name}}(; + {{#parameters}} + {{^internal}} + {{name}}={{#quotes}}"{{null_value}}"{{/quotes}}{{^quotes}}{{null_value}}{{/quotes}}, + {{/internal}} + {{/parameters}} + ) +end +{{/has_null_values}} + +{{#accessors}} +\"\"\"Get {{struct_name}} {{name}}.\"\"\" +{{accessor}}(value::{{struct_name}}) = value.{{name}} +{{/accessors}} +""" + +function read_json_data(filename::String) + return open(filename) do io + data = JSON2.read(io, Vector{Dict}) + end +end + +function generate_structs(directory, data::Vector) + struct_names = Vector{String}() + unique_accessor_functions = Set{String}() + + for item in data + accessors = Vector{Dict}() + item["has_null_values"] = true + parameters = Vector{Dict}() + for field in item["fields"] + param = namedtuple_to_dict(field) + push!(parameters, param) + + accessor_name = "get_" * param["name"] + push!(accessors, Dict("name" => param["name"], "accessor" => accessor_name)) + if accessor_name != "internal" + push!(unique_accessor_functions, accessor_name) + end + + if param["name"] == "internal" + param["internal"] = true + continue + end + + # This controls whether a kwargs constructor will be generated. + if !haskey(param, "null_value") + item["has_null_values"] = false + else + if param["data_type"] == "String" + param["quotes"] = true + end + end + param["struct_name"] = item["struct_name"] + end + + item["parameters"] = parameters + item["accessors"] = accessors + + filename = joinpath(directory, item["struct_name"] * ".jl") + open(filename, "w") do io + write(io, Mustache.render(template, item)) + push!(struct_names, item["struct_name"]) + end + println("Wrote $filename") + end + + accessors = sort!(collect(unique_accessor_functions)) + + filename = joinpath(directory, "includes.jl") + open(filename, "w") do io + for name in struct_names + write(io, "include(\"$name.jl\")\n") + end + write(io, "\n") + + for accessor in accessors + write(io, "export $accessor\n") + end + println("Wrote $filename") + end +end + +function namedtuple_to_dict(tuple) + parameters = Dict() + for property in propertynames(tuple) + parameters[string(property)] = getproperty(tuple, property) + end + + return parameters +end + +function generate_structs(input_file::AbstractString, output_directory::AbstractString) + # Include each generated file. + if !isdir(output_directory) + mkdir(output_directory) + end + + data = read_json_data(input_file) + generate_structs(output_directory, data) +end + +function main(args) + if length(args) != 2 + println("Usage: julia generate_structs.jl INPUT_FILE OUTPUT_DIRECTORY") + exit(1) + end + + generate_structs(args[1], args[2]) +end + +main(ARGS) diff --git a/bin/generate_valid_config_file.py b/bin/generate_valid_config_file.py new file mode 100644 index 0000000000..b3421e72c2 --- /dev/null +++ b/bin/generate_valid_config_file.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +"""Generates a validation configuration file from JSON struct data""" + +# Note: This is written in Python instead of Julia because the Julia YAML +# package does not support writing (only reading). + +import json +import os +import sys +from collections import OrderedDict + +#import yaml + +POWER_SYSTEM_DESCRIPTOR_FILE = sys.argv[2] + +def read_json_data(filename): + """Return the JSON data from a file.""" + with open(filename) as fp_in: + return json.load(fp_in) + + +def generate_config(input_file): + """Generate validation descriptors from the PowerSystems struct data file.""" + config = {} + data = read_json_data(input_file) + items = [] + + for ps_struct in data: + new_struct = OrderedDict() + new_struct["struct_name"] = ps_struct["struct_name"] + new_struct["fields"] = [] + for field in ps_struct["fields"]: + new_field = OrderedDict() + new_field["name"] = field["name"] + if "data_type" in field: + new_field["data_type"] = field["data_type"] + if "valid_range" in field: + new_field["valid_range"] = field["valid_range"] + if "validation_action" in field: + new_field["validation_action"] = field["validation_action"] + new_struct["fields"].append(new_field) + items.append(new_struct) + return items + +def generate_file(output_file, input_file=POWER_SYSTEM_DESCRIPTOR_FILE): + """Generate validation descriptors from the PowerSystems struct data file.""" + config = generate_config(input_file) + with open(output_file, "w") as fp_out: + #yaml.dump(config, fp_out, vspacing=True) + json.dump(config, fp_out, indent=4) + + print("Generated {} from {}".format(output_file, input_file)) + + +def main(): + """Controls execution.""" + if len(sys.argv) != 3: + print("Usage: {} output_file".format(os.path.basename(__file__))) + sys.exit(1) + + generate_file(sys.argv[1]) + +if __name__ == "__main__": + main() diff --git a/deps/build.jl b/deps/build.jl index 993a26808f..04c28494c3 100644 --- a/deps/build.jl +++ b/deps/build.jl @@ -1,5 +1,5 @@ - include(joinpath(@__DIR__, "../src/utils/data.jl")) import .UtilsData: TestData -download(TestData) + +download(TestData; branch = "improve-timeseries") diff --git a/docs/Manifest.toml b/docs/Manifest.toml deleted file mode 100644 index 10f7d30eab..0000000000 --- a/docs/Manifest.toml +++ /dev/null @@ -1,80 +0,0 @@ -# This file is machine-generated - editing it directly is not advised - -[[Base64]] -uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" - -[[Dates]] -deps = ["Printf"] -uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" - -[[Distributed]] -deps = ["Random", "Serialization", "Sockets"] -uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" - -[[DocStringExtensions]] -deps = ["LibGit2", "Markdown", "Pkg", "Test"] -git-tree-sha1 = "1df01539a1c952cef21f2d2d1c092c2bcf0177d7" -uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -version = "0.6.0" - -[[Documenter]] -deps = ["Base64", "DocStringExtensions", "InteractiveUtils", "LibGit2", "Logging", "Markdown", "Pkg", "REPL", "Random", "Test", "Unicode"] -git-tree-sha1 = "a8c41ba3d0861240dbec942ee1d0f86c57c37c1c" -uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -version = "0.21.5" - -[[DocumenterTools]] -deps = ["Base64", "DocStringExtensions", "LibGit2", "Pkg", "Test"] -git-tree-sha1 = "f5803a9c2c23ff226e8eab2df7ac4c75e77a0d53" -uuid = "35a29f4d-8980-5a13-9543-d66fff28ecb8" -version = "0.1.0" - -[[InteractiveUtils]] -deps = ["Markdown"] -uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" - -[[LibGit2]] -uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" - -[[Logging]] -uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" - -[[Markdown]] -deps = ["Base64"] -uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" - -[[Pkg]] -deps = ["Dates", "LibGit2", "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" - -[[SHA]] -uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" - -[[Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" - -[[Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" - -[[Test]] -deps = ["Distributed", "InteractiveUtils", "Logging", "Random"] -uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[[UUIDs]] -deps = ["Random", "SHA"] -uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" - -[[Unicode]] -uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" diff --git a/docs/Project.toml b/docs/Project.toml index 2742e84b27..2549e0715e 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,3 +1,4 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8" +InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1" diff --git a/docs/make.jl b/docs/make.jl index 148423c6df..c7de89686d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,8 +1,12 @@ using Documenter, PowerSystems +using InfrastructureSystems +const PSYPATH = dirname(pathof(PowerSystems)) + +include(joinpath(@__DIR__, "src", "generate_validation_table.jl")) makedocs( modules = [PowerSystems], - format = :html, + format = Documenter.HTML(), sitename = "PowerSystems.jl", pages = Any[ # Compat: `Any` for 0.4 compat "Home" => "index.md", diff --git a/docs/src/assets/SIIP_power_icon.png b/docs/src/assets/SIIP_power_icon.png new file mode 100644 index 0000000000..c3a3349df3 Binary files /dev/null and b/docs/src/assets/SIIP_power_icon.png differ diff --git a/docs/src/generate_validation_table.jl b/docs/src/generate_validation_table.jl new file mode 100644 index 0000000000..54b838926c --- /dev/null +++ b/docs/src/generate_validation_table.jl @@ -0,0 +1,35 @@ +@info "Generating Validation Table" +function generate_validation_table(filepath::AbstractString) + descriptor = InfrastructureSystems.read_validation_descriptor(joinpath(PSYPATH,"descriptors","power_system_structs.json")) + open(filepath, "w") do io + write(io, "# Data Requirements\n\n") + write(io, "| Struct Name | Field Name | DataType | Min | Max | Action |\n") + write(io, "|---------------|--------------|------------|-------|-------|----------|\n") + for item in descriptor + for field in item["fields"] + write(io, "|$(item["struct_name"])|$(field["name"])|$(field["data_type"])|") + if haskey(field, "valid_range") + if field["valid_range"] isa Dict + valid_min = field["valid_range"]["min"] + valid_max = field["valid_range"]["max"] + for value in (valid_min, valid_max) + write(io, isnothing(value) ? "null" : string(value)) + write(io, "|") + end + else + write(io, "$(field["valid_range"])|$(field["valid_range"])|") + end + else + write(io, "-|-") + end + if haskey(field, "validation_action") + write(io, "$(field["validation_action"])|\n") + else + write(io, "|-\n") + end + end + end + end +end + +generate_validation_table(joinpath(PSYPATH, "../docs/src/man/data_requirements_table.md")) diff --git a/docs/src/index.md b/docs/src/index.md index 3e59b39e27..3552d8ff88 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -4,8 +4,6 @@ For more detailed documentation of each object in the library, see the API/[PowerSystems](@ref) page. -Some examples of usage can be found in the [examples directory](https://github.com/PowerSystems/PowerSystems.jl/tree/master/Notebooks) using IJulia. - ## Installation This package is not yet registered. **Until it is, things may change. It is perfectly @@ -22,4 +20,15 @@ Once installed, the `PowerSystems` package can by used by typing ```julia using PowerSystems -``` \ No newline at end of file +``` + +## Contents +```@contents +Pages = [ + "man/data_requirements_table.md", + "man/data.md", + "man/guide.md", + "man/logging.md", + "man/tests.md" +] +``` diff --git a/docs/src/man/data.md b/docs/src/man/data.md new file mode 100644 index 0000000000..5f47e40fc4 --- /dev/null +++ b/docs/src/man/data.md @@ -0,0 +1,56 @@ +# Saving and Viewing PowerSystems Data + +PowerSystems data can be serialized and deserialized in JSON. + +```julia +PowerSystems.to_json(system, "system.json") +system = System("system.json") +``` + +It can be useful to view and filter the PowerSystems data in this format. There +are many tools available to browse JSON data. + +Here is an example [GUI tool](http://jsonviewer.stack.hu) that is available +online in a browser. + +The command line utility [jq](https://stedolan.github.io/jq/) offers even more +features. The rest of this document provides example commands. + +## View the entire file pretty-printed. +``` +jq . system.json +``` + +## View the PowerSystems component hierarchy. +``` +jq '.components | keys' system.json +jq '.components.Devices | keys' system.json +jq '.components.Devices.Injection | keys' system.json +jq '.components.Devices.Injection.Generator | keys' system.json +``` + +## View specific components. +``` +jq '.components.Device.Injection.Generator.ThermalGen.ThermalStandard' system.json +jq '.components.Device.Injection.Generator.ThermalGen.ThermalStandard[0]' system.json +``` + +## Filter on a parameter. +``` +jq '.components.Device.Injection.Generator.ThermalGen.ThermalStandard | .[] | select(.name == "107_CC_1")' system.json +jq '.components.Device.Injection.Generator.ThermalGen.ThermalStandard | .[] | select(.op_cost.capacity > 3)' system.json +``` + +## Output a table with select fields. +``` +jq -r '["name", "econ.capacity"], (.components.Device.Injection.Generator.ThermalGen.ThermalStandard | .[] | [.name, .op_cost.capacity]) | @tsv' system.json +``` + +## View the forecast types and initial_time values. +jq '.forecasts.data | keys' system.json + +## View the fields of a forecast. +jq '.forecasts.data["PowerSystems.ForecastKey(2020-01-01T00:00:00, Deterministic{Bus})"][0] | keys' + +## View the value of every field in an array of forecasts. +jq '.forecasts.data["PowerSystems.ForecastKey(2020-01-01T00:00:00, Deterministic{Bus})"] | .[].initial_time' diff --git a/docs/src/style.md b/docs/src/style.md index a7dc819600..1076cd4fac 100644 --- a/docs/src/style.md +++ b/docs/src/style.md @@ -19,6 +19,7 @@ including its deviations from the Julia style guide. In particular, note its po * [return statements](http://www.juliaopt.org/JuMP.jl/dev/style/#Return-statements-1) * [variable names](http://www.juliaopt.org/JuMP.jl/dev/style/#Use-of-underscores-within-names-1). * Read [The Zen of Python](https://www.python.org/dev/peps/pep-0020). +* Consider using a plugin that configures your text editor to use [EditorConfig](https://editorconfig.org/) settings. ## Code Organization * Import standard modules, then 3rd-party modules, then yours. Include a blank diff --git a/src/PowerSystems.jl b/src/PowerSystems.jl index cd8cbaf3ba..731ad71ee0 100644 --- a/src/PowerSystems.jl +++ b/src/PowerSystems.jl @@ -9,8 +9,8 @@ module PowerSystems # Exports export System -export ConcreteSystem export Bus +export Arc export LoadZones export PowerSystemType @@ -18,42 +18,34 @@ export Component export Device export Branch export Injection +export ACBranch export Line export MonitoredLine -export DCLine +export DCBranch export HVDCLine export VSCDCLine export Transformer2W export TapTransformer export PhaseShiftingTransformer -export Forecast -export Deterministic -export Scenarios -export Probabilistic +export ThreePartCost +export TwoPartCost export Generator -export GenClasses - export HydroGen export HydroFix -export HydroCurtailment +export HydroDispatch export HydroStorage export TechHydro -export EconHydro export RenewableGen export TechRenewable -export EconRenewable export RenewableFix -export RenewableCurtailment -export RenewableFullDispatch +export RenewableDispatch export ThermalGen export TechThermal -export EconThermal -export ThermalDispatch -export ThermalGenSeason +export ThermalStandard export ElectricLoad export StaticLoad @@ -72,22 +64,55 @@ export ProportionalReserve export StaticReserve export Transfer -export parsestandardfiles +export PTDF +export Ybus +export LODF +export GeneratorCostModel +export BusType + +export Forecast +export Deterministic +export Probabilistic +export ScenarioBased + +export make_pf +export solve_powerflow! + +export parse_standard_files export parse_file -export ps_dict2ps_struct -export assign_ts_data -export read_data_files -export validate +export add_forecasts! +export add_forecast! +export remove_forecast! +export clear_forecasts! +export add_component! +export remove_component! +export remove_components! +export get_component export get_components -export get_mixed_components -export get_component_counts -export show_component_counts +export get_components_by_name +export get_component_forecasts +export get_forecast_initial_times +export get_forecasts +export get_forecasts_horizon +export get_forecasts_initial_time +export get_forecasts_interval +export get_forecasts_resolution +export get_forecast_component_name +export get_forecast_value +export get_horizon +export get_timeseries +export get_data +export iterate_components +export iterate_forecasts +export make_forecasts +export split_forecasts! +export get_name +export to_json ################################################################################# # Imports import SparseArrays -import AxisArrays import LinearAlgebra: LAPACK.getri! import LinearAlgebra: LAPACK.getrf! import LinearAlgebra: BLAS.gemm @@ -96,32 +121,47 @@ import Dates import TimeSeries import DataFrames import JSON +import JSON2 import CSV import YAML +import UUIDs +import Base.to_index + +import InfrastructureSystems +import InfrastructureSystems: Components, Deterministic, Probabilistic, Forecast, + ScenarioBased, InfrastructureSystemsType, InfrastructureSystemsInternal, + FlattenIteratorWrapper, LazyDictFromIterator, DataFormatError, InvalidRange, + InvalidValue + +const IS = InfrastructureSystems ################################################################################# # Includes -# supertype for all PowerSystems types -abstract type PowerSystemType end +""" +Supertype for all PowerSystems types. +All subtypes must include a InfrastructureSystemsInternal member. +Subtypes should call InfrastructureSystemsInternal() by default, but also must +provide a constructor that allows existing values to be deserialized. +""" +abstract type PowerSystemType <: IS.InfrastructureSystemsType end + abstract type Component <: PowerSystemType end # supertype for "devices" (bus, line, etc.) abstract type Device <: Component end abstract type Injection <: Device end # supertype for generation technologies (thermal, renewable, etc.) -abstract type TechnicalParams <: Component end +abstract type TechnicalParams <: PowerSystemType end include("common.jl") # Include utilities -include("utils/utils.jl") -include("utils/logging.jl") include("utils/IO/base_checks.jl") # PowerSystems models include("models/topological_elements.jl") -include("models/forecasts.jl") include("models/branches.jl") +include("models/operational_cost.jl") #include("models/network.jl") # Static types @@ -130,26 +170,44 @@ include("models/storage.jl") include("models/loads.jl") include("models/services.jl") +# Include all auto-generated structs. +include("models/generated/includes.jl") +include("models/supplemental_constructors.jl") + +# Definitions of PowerSystem +include("base.jl") + +#Interfacing with Forecasts +include("forecasts.jl") + +#Data Checks +include("utils/IO/system_checks.jl") +include("utils/IO/branchdata_checks.jl") + +# network calculations +include("utils/network_calculations/common.jl") +include("utils/network_calculations/ybus_calculations.jl") +include("utils/network_calculations/ptdf_calculations.jl") +include("utils/network_calculations/lodf_calculations.jl") + +#PowerFlow +include("utils/power_flow/make_pf.jl") +include("utils/power_flow/power_flow.jl") + # Include Parsing files +include("parsers/common.jl") +include("parsers/enums.jl") include("parsers/pm_io.jl") include("parsers/im_io.jl") -include("parsers/dict_to_struct.jl") include("parsers/standardfiles_parser.jl") -include("parsers/cdm_parser.jl") include("parsers/forecast_parser.jl") +include("parsers/power_system_table_data.jl") include("parsers/pm2ps_parser.jl") -#Data Checks -include("utils/IO/system_checks.jl") -include("utils/IO/branchdata_checks.jl") - -# Definitions of System -include("base.jl") -include("validation/powersystem.jl") - # Better printing include("utils/print.jl") -include("utils/lodf_calculations.jl") + +include("models/serialization.jl") # Download test data include("utils/data.jl") diff --git a/src/base.jl b/src/base.jl index 84418af1a0..a7cb363092 100644 --- a/src/base.jl +++ b/src/base.jl @@ -1,324 +1,891 @@ -### Struct and different Power System constructors depending on the data provided #### + +const SKIP_PM_VALIDATION = false """ System -A power system defined by fields for buses, generators, loads, branches, and -overall system parameters. - -# Constructor -```julia -System(buses, generators, loads, branches, storage, basepower; kwargs...) -System(buses, generators, loads, branches, basepower; kwargs...) -System(buses, generators, loads, basepower; kwargs...) -System(ps_dict; kwargs...) -System(file, ts_folder; kwargs...) -System(; kwargs...) -``` - -# Arguments - -* `buses`::Vector{Bus} : an array of buses -* `generators`::Vector{Generator} : an array of generators of (possibly) different types -* `loads`::Vector{ElectricLoad} : an array of load specifications that includes timing of the loads -* `branches`::Union{Nothing, Vector{Branch}} : an array of branches; may be `nothing` -* `storage`::Union{Nothing, Vector{Storage}} : an array of storage devices; may be `nothing` -* `basepower`::Float64 : the base power of the system (DOCTODO: is this true? what are the units of base power?) -* `ps_dict`::Dict{String,Any} : the dictionary object containing System data -* `file`::String, `ts_folder`::String : the filename and foldername that contain the System data - -# Keyword arguments - -* `runchecks`::Bool : run available checks on input fields -DOCTODO: any other keyword arguments? genmap_file, REGEX_FILE - + A power system defined by fields for basepower, components, and forecasts. + + # Constructor + ```julia + System(basepower) + System(components, forecasts, basepower) + System(buses, generators, loads, branches, storage, basepower, forecasts, services, annex; kwargs...) + System(buses, generators, loads, basepower; kwargs...) + System(file; kwargs...) + System(; buses, generators, loads, branches, storage, basepower, forecasts, services, annex, kwargs...) + System(; kwargs...) + ``` + + # Arguments + + * `buses`::Vector{Bus} : an array of buses + * `generators`::Vector{Generator} : an array of generators of (possibly) different types + * `loads`::Vector{ElectricLoad} : an array of load specifications that includes timing of the loads + * `branches`::Union{Nothing, Vector{Branch}} : an array of branches; may be `nothing` + * `storage`::Union{Nothing, Vector{Storage}} : an array of storage devices; may be `nothing` + * `basepower`::Float64 : the base power value for the system + * `forecasts`::Union{Nothing, IS.Forecasts} : dictionary of forecasts + * `services`::Union{Nothing, Vector{ <: Service}} : an array of services; may be `nothing` + + # Keyword arguments + + * `runchecks`::Bool : run available checks on input fields. If an error is found in a field, that component will not be added to the system and InvalidRange is thrown. + * `configpath`::String : specify path to validation config file + DOCTODO: any other keyword arguments? genmap_file, REGEX_FILE """ struct System <: PowerSystemType - # DOCTODO docs for System fields are currently not working, JJS 1/15/19 - buses::Vector{Bus} - generators::GenClasses - loads::Vector{<:ElectricLoad} - branches::Union{Nothing, Vector{<:Branch}} - storage::Union{Nothing, Vector{<:Storage}} - basepower::Float64 # [MVA] - time_periods::Int64 - - function System(buses, generators, loads, branches, storage_devices, basepower, - time_periods; kwargs...) - - sys = new(buses, generators, loads, branches, storage_devices, basepower, - time_periods) - - # TODO Default validate to true once validation code is written. - if get(kwargs, :validate, false) && !validate(sys) - error("System is not valid") - end - - return sys + data::IS.SystemData + basepower::Float64 # [MVA] + runchecks::Bool + internal::InfrastructureSystemsInternal + + function System(data, basepower, internal; kwargs...) + runchecks = get(kwargs, :runchecks, true) + sys = new(data, basepower, runchecks, internal) end end -"""Primary System constructor. Funnel point for all other outer constructors.""" +"""Construct an empty System. Useful for building a System while parsing raw data.""" +function System(basepower; kwargs...) + return System(_create_system_data_from_kwargs(; kwargs...), basepower) +end + +function System(data, basepower; kwargs...) + return System(data, basepower, InfrastructureSystemsInternal(); kwargs...) +end + +"""System constructor when components are constructed externally.""" function System(buses::Vector{Bus}, generators::Vector{<:Generator}, loads::Vector{<:ElectricLoad}, branches::Union{Nothing, Vector{<:Branch}}, storage::Union{Nothing, Vector{<:Storage}}, - basepower::Float64; kwargs...) - runchecks = in(:runchecks, keys(kwargs)) ? kwargs[:runchecks] : true - if runchecks - slackbuscheck(buses) - buscheck(buses) - if !isnothing(branches) - calculatethermallimits!(branches, basepower) - check_branches!(branches) - end + basepower::Float64, + forecasts::Union{Nothing, IS.Forecasts}, + services::Union{Nothing, Vector{ <: Service}}, + annex::Union{Nothing,Dict}; kwargs...) + + data = _create_system_data_from_kwargs(; kwargs...) - pvbuscheck(buses, generators) - generators = checkramp(generators, minimumtimestep(loads)) + if isnothing(forecasts) + forecasts = IS.Forecasts() end - time_periods = timeseriescheckload(loads) + sys = System(data, basepower; kwargs...) - # This constructor receives an array of Generator structs. It separates them by category - # in GenClasses. - gen_classes = genclassifier(generators) - if !isnothing(gen_classes.renewable) - timeserieschecksources(gen_classes.renewable, time_periods) + arrays = [buses, generators, loads] + if !isnothing(branches) + push!(arrays, branches) + end + if !isnothing(storage) + push!(arrays, storage) end + if !isnothing(services) + push!(arrays, services) + end + + error_detected = false - if !isnothing(gen_classes.hydro) - timeserieschecksources(gen_classes.hydro, time_periods) + for component in Iterators.flatten(arrays) + try + add_component!(sys, component) + catch e + if isa(e, InvalidRange) + error_detected = true + else + rethrow() + end + end end - return System(buses, gen_classes, loads, branches, storage, basepower, time_periods; - kwargs...) -end + load_zones = isnothing(annex) ? nothing : get(annex, :LoadZones, nothing) + if !isnothing(load_zones) + for lz in load_zones + try + add_component!(sys, lz) + catch e + if isa(e, InvalidRange) + error_detected = true + else + rethrow() + end + end + end + end -"""Constructs System with Generators but no branches or storage.""" -function System(buses::Vector{Bus}, - generators::Vector{<:Generator}, - loads::Vector{<:ElectricLoad}, - basepower::Float64; kwargs...) - return System(buses, generators, loads, nothing, nothing, basepower; kwargs...) + runchecks = get(kwargs, :runchecks, true) + + if error_detected + throw(InvalidRange("Invalid value(s) detected")) + end + + if runchecks + check!(sys) + end + + return sys end -"""Constructs System with Generators but no storage.""" +"""System constructor without nothing-able arguments.""" function System(buses::Vector{Bus}, generators::Vector{<:Generator}, loads::Vector{<:ElectricLoad}, - branches::Vector{<:Branch}, basepower::Float64; kwargs...) - return System(buses, generators, loads, branches, nothing, basepower; kwargs...) + return System(buses, generators, loads, nothing, nothing, basepower, nothing, nothing, nothing; kwargs...) end -"""Constructs System with Generators but no branches.""" -function System(buses::Vector{Bus}, - generators::Vector{<:Generator}, - loads::Vector{<:ElectricLoad}, - storage::Vector{<:Storage}, - basepower::Float64; kwargs...) - return System(buses, generators, loads, nothing, storage, basepower; kwargs...) +"""System constructor with keyword arguments.""" +function System(; basepower=100.0, + buses, + generators, + loads, + branches, + storage, + forecasts, + services, + annex, + kwargs...) + return System(buses, generators, loads, branches, storage, basepower, forecasts, services, annex; kwargs...) end -"""Constructs System with default values.""" -function System(; buses=[Bus()], - generators=[ThermalDispatch(), RenewableFix()], - loads=[PowerLoad()], +"""Constructs a non-functional System for demo purposes.""" +function System(::Nothing; buses=[Bus(nothing)], + generators=[ThermalStandard(nothing), RenewableFix(nothing)], + loads=[PowerLoad(nothing)], branches=nothing, storage=nothing, basepower=100.0, + forecasts = nothing, + services = nothing, + annex = nothing, kwargs...) - return System(buses, generators, loads, branches, storage, basepower; kwargs...) + return System(buses, generators, loads, branches, storage, basepower, forecasts, services, annex; kwargs...) end -"""Constructs System from a ps_dict.""" -function System(ps_dict::Dict{String,Any}; kwargs...) - buses, generators, storage, branches, loads, loadZones, shunts, services = - ps_dict2ps_struct(ps_dict) - return System(buses, generators, loads, branches, storage, ps_dict["baseMVA"]; - kwargs...); +""" + to_json(sys::System, filename::AbstractString) + +Serializes a system to a JSON string. +""" +function to_json(sys::System, filename::AbstractString) + return IS.to_json(sys, filename) end -"""Constructs System from a file containing Matpower, PTI, or JSON data.""" -function System(file::String, ts_folder::String; kwargs...) - ps_dict = parsestandardfiles(file,ts_folder; kwargs...) - buses, generators, storage, branches, loads, loadZones, shunts, services = - ps_dict2ps_struct(ps_dict) +""" + to_json(io::IO, sys::System) - return System(buses, generators, loads, branches, storage, ps_dict["baseMVA"]; - kwargs...); +Serializes a system an IO stream in JSON. +""" +function to_json(io::IO, sys::System) + return IS.to_json(io, sys) end -"""A System struct that stores all devices in arrays with concrete types. -This is a temporary implementation that will allow consumers of PowerSystems to test the -functionality before it is finalized. -""" -struct ConcreteSystem <: PowerSystemType - data::Dict{DataType, Vector{<:Component}} # Contains arrays of concrete types. - components::Dict{DataType, Any} # Nested dict based on type hierarchy - # containing references to component arrays. - basepower::Float64 # [MVA] +"""Constructs a System from a JSON file.""" +function System(filename::String) + sys = IS.from_json(System, filename) + check!(sys) + return sys end -function ConcreteSystem(sys::System) - data = Dict{DataType, Vector{<:Component}}() - for subtype in get_all_concrete_subtypes(Component) - data[subtype] = Vector{subtype}() - end +""" + add_component!(sys::System, component::T; kwargs...) where T <: Component - @debug "Created data keys" keys(data) +Add a component to the system. - for field in (:buses, :loads) - objs = getfield(sys, field) - for obj in objs - push!(data[typeof(obj)], obj) - end +Throws ArgumentError if the component's name is already stored for its concrete type. + +Throws InvalidRange if any of the component's field values are outside of defined valid +range. +""" +function add_component!(sys::System, component::T; kwargs...) where T <: Component + if T <: Branch + arc = get_arc(component) + check_bus(sys, get_from(arc), arc) + check_bus(sys, get_to(arc), arc) + elseif Bus in fieldtypes(T) + check_bus(sys, get_bus(component), component) end - for field in (:thermal, :renewable, :hydro) - generators = getfield(sys.generators, field) - if !isnothing(generators) - for gen in generators - push!(data[typeof(gen)], gen) - end - end + if sys.runchecks && !validate_struct(sys, component) + throw(InvalidValue("Invalid value for $(component)")) end - for field in (:branches, :storage) - objs = getfield(sys, field) - if !isnothing(objs) - for obj in objs - push!(data[typeof(obj)], obj) + IS.add_component!(sys.data, component; kwargs...) +end + +""" + add_forecasts!( + sys::System, + metadata::Union{AbstractString, Vector{IS.TimeseriesFileMetadata}}; + resolution=nothing, + ) + +Adds forecasts from a metadata file or metadata descriptors. + +# Arguments +- `sys::System`: system +- `metadata::Union{AbstractString, Vector{IS.TimeseriesFileMetadata}}`: metdata filename + that includes an array of IS.TimeseriesFileMetadata instances or a vector. +- `resolution::DateTime.Period=nothing`: skip forecast that don't match this resolution. +""" +function add_forecasts!( + sys::System, + metadata::Union{AbstractString, Vector{IS.TimeseriesFileMetadata}}; + resolution=nothing) + forecasts = Vector{Forecast}() + for forecast in make_forecasts(sys, metadata; resolution=resolution) + component = IS.get_component(forecast) + if component isa LoadZones + uuids = Set([IS.get_uuid(x) for x in get_buses(component)]) + for component_ in (load for load in get_components(ElectricLoad, sys) + if get_bus(load) |> IS.get_uuid in uuids) + if forecast isa Deterministic + forecast_ = Deterministic(component_, + IS.get_label(forecast), + IS.get_timeseries(forecast)) + # TODO: others + else + @assert false + end + push!(forecasts, forecast_) end + else + push!(forecasts, forecast) end end - for (key, value) in data - @debug "data: $(string(key)): count=$(string(length(value)))" - end - - components = _get_components_by_type(Component, data) - return ConcreteSystem(data, components, sys.basepower) + add_forecasts!(sys, forecasts) end -"""Returns an array of components from the System. T must be a concrete type. +""" + iterate_components(sys::System) + +Iterates over all components. -# Example +# Examples ```julia -devices = PowerSystems.get_components(ThermalDispatch, system) +for component in iterate_components(sys) + @show component +end ``` + +See also: [`get_components`](@ref) """ -function get_components(::Type{T}, sys::ConcreteSystem)::Vector{T} where {T <: Component} - if !isconcretetype(T) - error("$T must be a concrete type") - end +function iterate_components(sys::System) + return IS.iterate_components(sys.data) +end + +""" + remove_components!(::Type{T}, sys::System) where T <: Component + +Remove all components of type T from the system. + +Throws ArgumentError if the type is not stored. +""" +function remove_components!(::Type{T}, sys::System) where T <: Component + return IS.remove_components!(T, sys.data) +end + +""" + remove_component!(sys::System, component::T) where T <: Component - return sys.data[T] +Remove a component from the system by its value. + +Throws ArgumentError if the component is not stored. +""" +function remove_component!(sys::System, component) + return IS.remove_component!(sys.data, component) end -"""Returns an iterable over component arrays that are subtypes of T. To create a new array -with all component arrays concatenated, call collect on the returned iterable. Note that -this will involve copying the data and the resulting array will be less performant than -arrays of concrete types. +""" + remove_component!( + ::Type{T}, + sys::System, + name::AbstractString, + ) where T <: Component + +Remove a component from the system by its name. -# Example +Throws ArgumentError if the component is not stored. +""" +function remove_component!( + ::Type{T}, + sys::System, + name::AbstractString, + ) where T <: Component + return IS.remove_component!(T, sys.data, name) +end + +""" + get_component( + ::Type{T}, + sys::System, + name::AbstractString + )::Union{T, Nothing} where {T <: Component} + +Get the component of concrete type T with name. Returns nothing if no component matches. + +See [`get_components_by_name`](@ref) if the concrete type is unknown. + +Throws ArgumentError if T is not a concrete type. +""" +function get_component(::Type{T}, sys::System, name::AbstractString) where T <: Component + return IS.get_component(T, sys.data, name) +end + +""" + get_components( + ::Type{T}, + sys::System, + )::FlattenIteratorWrapper{T} where {T <: Component} + +Returns an iterator of components. T can be concrete or abstract. +Call collect on the result if an array is desired. + +# Examples ```julia -iter = PowerSystems.get_mixed_components(Device, system) -for device in iter - @show device +iter = PowerSystems.get_components(ThermalStandard, sys) +iter = PowerSystems.get_components(Generator, sys) +generators = PowerSystems.get_components(Generator, sys) |> collect +generators = collect(PowerSystems.get_components(Generator, sys)) +``` + +See also: [`iterate_components`](@ref) +""" +function get_components( + ::Type{T}, + sys::System, + )::FlattenIteratorWrapper{T} where {T <: Component} + return IS.get_components(T, sys.data) end + +""" + get_components_by_name( + ::Type{T}, + sys::System, + name::AbstractString + )::Vector{T} where {T <: Component} + +Get the components of abstract type T with name. Note that PowerSystems enforces unique +names on each concrete type but not across concrete types. + +See [`get_component`](@ref) if the concrete type is known. + +Throws ArgumentError if T is not an abstract type. +""" +function get_components_by_name( + ::Type{T}, + sys::System, + name::AbstractString + )::Vector{T} where {T <: Component} + return IS.get_components_by_name(T, sys.data, name) +end + +""" + get_component_forecasts( + ::Type{T}, + sys::System, + initial_time::Dates.DateTime, + ) where T <: Component + +Get the forecasts of a component of type T with initial_time. + The resulting container can contain Forecasts of dissimilar types. + +Throws ArgumentError if T is not a concrete type. + +See also: [`get_component`](@ref) +""" +function get_component_forecasts( + ::Type{T}, + sys::System, + initial_time::Dates.DateTime, + ) where T <: Component + return IS.get_component_forecasts(T, sys.data, initial_time) +end + +""" + add_forecast!(sys::System, forecast) + +Adds forecast to the system. + +# Arguments +- `sys::System`: system +- `forecast`: Any object of subtype forecast + +Throws ArgumentError if the forecast's component is not stored in the system. + +""" +function add_forecast!(sys::System, forecast) + return IS.add_forecast!(sys.data, forecast) +end + +""" + add_forecast!( + sys::System, + filename::AbstractString, + component::Component, + label::AbstractString, + scaling_factor::Union{String, Float64}=1.0, + ) + +Add a forecast to a system from a CSV file. + +See [`InfrastructureSystems.TimeseriesFileMetadata`](@ref) for description of +scaling_factor. +""" +function add_forecast!( + sys::System, + filename::AbstractString, + component::Component, + label::AbstractString, + scaling_factor::Union{String, Float64}=1.0, + ) + return IS.add_forecast!(sys.data, filename, component, label, scaling_factor) +end + +""" + add_forecast!( + sys::System, + ta::TimeSeries.TimeArray, + component, + label, + scaling_factor::Union{String, Float64}=1.0, + ) + +Add a forecast to a system from a TimeSeries.TimeArray. + +See [`InfrastructureSystems.TimeseriesFileMetadata`](@ref) for description of +scaling_factor. +""" +function add_forecast!( + sys::System, + ta::TimeSeries.TimeArray, + component, + label, + scaling_factor::Union{String, Float64}=1.0, + ) + return IS.add_forecast!(sys.data, ta, component, label, scaling_factor) +end + +""" + add_forecast!( + sys::System, + df::DataFrames.DataFrame, + component, + label, + scaling_factor::Union{String, Float64}=1.0, + ) + +Add a forecast to a system from a DataFrames.DataFrame. + +See [`InfrastructureSystems.TimeseriesFileMetadata`](@ref) for description of +scaling_factor. +""" +function add_forecast!( + sys::System, + df::DataFrames.DataFrame, + component, + label, + scaling_factor::Union{String, Float64}=1.0, + ) + return IS.add_forecast!(sys.data, df, component, label, scaling_factor) +end + +""" + add_forecasts!(sys::System, forecasts) + +Add forecasts to the system. + +# Arguments +- `sys::System`: system +- `forecasts`: iterable (array, iterator, etc.) of Forecast values + +Throws DataFormatError if +- A component-label pair is not unique within a forecast array. +- A forecast has a different resolution than others. +- A forecast has a different horizon than others. + +Throws ArgumentError if the forecast's component is not stored in the system. + +""" +function add_forecasts!(sys::System, forecasts) + return IS.add_forecasts!(sys.data, forecasts) +end + +""" + make_forecasts(sys::System, metadata_file::AbstractString; resolution=nothing) + +Return a vector of forecasts from a metadata file. + +# Arguments +- `data::SystemData`: system +- `metadata_file::AbstractString`: path to metadata file +- `resolution::{Nothing, Dates.Period}`: skip any forecasts that don't match this resolution + +See [`InfrastructureSystems.TimeseriesFileMetadata`](@ref) for description of what the file +should contain. +""" +function make_forecasts(sys::System, metadata_file::AbstractString; resolution=nothing) + return IS.make_forecasts(sys.data, metadata_file, PowerSystems; resolution=resolution) +end + +""" + make_forecasts(data::SystemData, timeseries_metadata::Vector{TimeseriesFileMetadata}; + resolution=nothing) + +Return a vector of forecasts from a vector of TimeseriesFileMetadata values. + +# Arguments +- `data::SystemData`: system +- `timeseries_metadata::Vector{TimeseriesFileMetadata}`: metadata values +- `resolution::{Nothing, Dates.Period}`: skip any forecasts that don't match this resolution +""" +function make_forecasts(sys::System, metadata::Vector{IS.TimeseriesFileMetadata}; + resolution=nothing) + return IS.make_forecasts(sys.data, metadata, PowerSystems; resolution=resolution) +end + +""" + get_forecasts(::Type{T}, sys::System, initial_time::Dates.DateTime) + +Return an iterator of forecasts. T can be concrete or abstract. + +Call collect on the result if an array is desired. + +This method is fast and efficient because it returns an iterator to existing vectors. + +# Examples +```julia +iter = PowerSystems.get_forecasts(Deterministic{RenewableFix}, sys, initial_time) +iter = PowerSystems.get_forecasts(Forecast, sys, initial_time) +forecasts = PowerSystems.get_forecasts(Forecast, sys, initial_time) |> collect +forecasts = collect(PowerSystems.get_forecasts(Forecast, sys)) ``` + +See also: [`iterate_forecasts`](@ref) """ -function get_mixed_components(::Type{T}, sys::ConcreteSystem) where {T <: Component} - return _get_mixed_components(T, sys.data) +function get_forecasts( + ::Type{T}, + sys::System, + initial_time::Dates.DateTime, + )::FlattenIteratorWrapper{T} where T <: Forecast + return IS.get_forecasts(T, sys.data, initial_time) end -function _get_mixed_components( - ::Type{T}, - data::Dict{DataType, Vector{<:Component}}, - ) where {T <: Component} - if !isabstracttype(T) - error("$T must be an abstract type") - end +""" + get_forecasts( + ::Type{T}, + sys::System, + initial_time::Dates.DateTime, + components_iterator, + label::Union{String, Nothing}=nothing, + )::Vector{Forecast} - return Iterators.flatten(data[x] for x in get_all_concrete_subtypes(T)) +# Arguments +- `forecasts::Forecasts`: system +- `initial_time::Dates.DateTime`: time designator for the forecast +- `components_iter`: iterable (array, iterator, etc.) of Component values +- `label::Union{String, Nothing}`: forecast label or nothing + +Return forecasts that match the components and label. + +This method is slower than the first version because it has to compare components and label +as well as build a new vector. + +Throws ArgumentError if eltype(components_iterator) is a concrete type and no forecast is +found for a component. +""" +function get_forecasts( + ::Type{T}, + sys::System, + initial_time::Dates.DateTime, + components_iterator, + label::Union{String, Nothing}=nothing, + )::Vector{T} where T <: Forecast + return IS.get_forecasts(T, sys.data, initial_time, components_iterator, label) end -"""Builds a nested dictionary by traversing through the PowerSystems type hierarchy. The -bottom of each dictionary is an array of concrete types. +""" + iterate_forecasts(sys::System) -# Example +Iterates over all forecasts in order of initial time. + +# Examples ```julia -data[Device][Injection][Generator][ThermalGen][ThermalDispatch][1] -ThermalDispatch: - name: 322_CT_6 - available: true - bus: Bus(name="Cole") - tech: TechThermal - econ: EconThermal +for forecast in iterate_forecasts(sys) + @show forecast +end ``` + +See also: [`get_forecasts`](@ref) """ -function _get_components_by_type( - ::Type{T}, - data::Dict{DataType, Vector{<:Component}}, - components::Dict{DataType, Any}=Dict{DataType, Any}(), - ) where {T <: Component} - abstract_types = get_abstract_subtypes(T) - if length(abstract_types) > 0 - for abstract_type in abstract_types - components[abstract_type] = Dict{DataType, Any}() - _get_components_by_type(abstract_type, data, components[abstract_type]) +function iterate_forecasts(sys::System) + return IS.iterate_forecasts(sys.data) +end + +""" + remove_forecast(sys::System, forecast::Forecast) + +Remove the forecast from the system. + +Throws ArgumentError if the forecast is not stored. +""" +function remove_forecast!(sys::System, forecast::Forecast) + return IS.remove_forecast!(sys.data, forecast) +end + +""" + clear_forecasts!(sys::System) + +Remove all forecasts from the system. +""" +function clear_forecasts!(sys::System) + return IS.clear_forecasts!(sys.data) +end + +""" + split_forecasts!( + sys::System, + forecasts::FlattenIteratorWrapper{T}, # must be an iterable + interval::Dates.Period, + horizon::Int, + ) where T <: Forecast + +Replaces system forecasts with a set of forecasts by incrementing through an iterable +set of forecasts by interval and horizon. + +""" +function split_forecasts!( + sys::System, + forecasts::FlattenIteratorWrapper{T}, # must be an iterable + interval::Dates.Period, + horizon::Int, + ) where T <: Forecast + return IS.split_forecasts!(sys.data, forecasts, interval, horizon) +end + +""" + get_forecast_initial_times(sys::System)::Vector{Dates.DateTime} + +Return sorted forecast initial times. + +""" +function get_forecast_initial_times(sys::System)::Vector{Dates.DateTime} + return IS.get_forecast_initial_times(sys.data) +end + +""" + get_forecasts_horizon(sys::System) + +Return the horizon for all forecasts. +""" +function get_forecasts_horizon(sys::System) + return IS.get_forecasts_horizon(sys.data) +end + +""" + get_forecasts_initial_time(sys::System) + +Return the earliest initial_time for a forecast. +""" +function get_forecasts_initial_time(sys::System) + return IS.get_forecasts_initial_time(sys.data) +end + +""" + get_forecasts_interval(sys::System) + +Return the interval for all forecasts. +""" +function get_forecasts_interval(sys::System) + return IS.get_forecasts_interval(sys.data) +end + +""" + get_forecasts_resolution(sys::System) + +Return the resolution for all forecasts. +""" +function get_forecasts_resolution(sys::System) + return IS.get_forecasts_resolution(sys.data) +end + +""" + validate_struct(sys::System, value::PowerSystemType) + +Validates an instance of a PowerSystemType against System data. +Returns true if the instance is valid. + +Users implementing this function for custom types should consider implementing +InfrastructureSystems.validate_struct instead if the validation logic only requires data +contained within the instance. +""" +function validate_struct(sys::System, value::PowerSystemType)::Bool + return true +end + +function check!(sys::System) + buses = get_components(Bus, sys) + slack_bus_check(buses) + buscheck(buses) +end + +function JSON2.read(io::IO, ::Type{System}) + raw = JSON2.read(io, NamedTuple) + sys = System(float(raw.basepower); runchecks=raw.runchecks) + component_cache = Dict{Base.UUID, Component}() + + # Buses and Arcs are encoded as UUIDs. + composite_components = [Bus] + for composite_component in composite_components + for component in IS.get_components_raw(IS.SystemData, composite_component, raw.data) + comp = IS.convert_type(composite_component, component) + add_component!(sys, comp) + component_cache[IS.get_uuid(comp)] = comp + end + end + + # Skip Services this round because they have Devices. + for c_type_sym in IS.get_component_types_raw(IS.SystemData, raw.data) + c_type = getfield(PowerSystems, Symbol(IS.strip_module_names(string(c_type_sym)))) + (c_type in composite_components || c_type <: Service) && continue + for component in IS.get_components_raw(IS.SystemData, c_type, raw.data) + comp = IS.convert_type(c_type, component, component_cache) + add_component!(sys, comp) + component_cache[IS.get_uuid(comp)] = comp end end - for concrete_type in get_concrete_subtypes(T) - components[concrete_type] = data[concrete_type] + # Now get the Services. + for c_type_sym in IS.get_component_types_raw(IS.SystemData, raw.data) + c_type = getfield(PowerSystems, Symbol(IS.strip_module_names(string(c_type_sym)))) + if c_type <: Service + for component in IS.get_components_raw(IS.SystemData, c_type, raw.data) + comp = IS.convert_type(c_type, component, component_cache) + add_component!(sys, comp) + component_cache[IS.get_uuid(comp)] = comp + end + end end - return components + # Now get the Forecasts, which have reference to components. + IS.convert_forecasts!(sys.data, raw.data, component_cache) + return sys +end + +function JSON2.write(io::IO, component::T) where T <: Component + return JSON2.write(io, encode_for_json(component)) end -"""Returns a Tuple of Arrays of component types and counts.""" -function get_component_counts(components::Dict{DataType, Any}) - return _get_component_counts(components) +function JSON2.write(component::T) where T <: Component + return JSON2.write(encode_for_json(component)) end -function _get_component_counts(components::Dict{DataType, Any}, types=[], counts=[]) - for (ps_type, val) in components - if isabstracttype(ps_type) - _get_component_counts(val, types, counts) +""" +Encode composite buses as UUIDs. +""" +function encode_for_json(component::T) where T <: Component + fields = fieldnames(T) + vals = [] + + for name in fields + val = getfield(component, name) + if val isa Bus + push!(vals, IS.get_uuid(val)) else - push!(types, ps_type) - push!(counts, length(val)) + push!(vals, val) end end - return types, counts + return NamedTuple{fields}(vals) end -"""Shows the component types and counts in a table. If show_hierarchy is true then include -a column showing the type hierachy. The display is in order of depth-first type -hierarchy. -""" -function show_component_counts(sys::ConcreteSystem, io::IO=stderr; - show_hierarchy::Bool=false) - # Build a table of strings showing the counts. - types, counts = get_component_counts(sys.components) - if show_hierarchy - hierarchies = [] - for ps_type in types - text = join([string(type_to_symbol(x)) for x in supertypes(ps_type)], " <: ") - push!(hierarchies, text) +function IS.convert_type( + ::Type{T}, + data::NamedTuple, + component_cache::Dict, + ) where T <: Component + @debug T data + values = [] + for (fieldname, fieldtype) in zip(fieldnames(T), fieldtypes(T)) + val = getfield(data, fieldname) + if fieldtype <: Bus + uuid = Base.UUID(val.value) + bus = component_cache[uuid] + push!(values, bus) + elseif fieldtype <: Component + # Recurse. + push!(values, IS.convert_type(fieldtype, val, component_cache)) + else + obj = IS.convert_type(fieldtype, val) + push!(values, obj) end + end + + return T(values...) +end - df = DataFrames.DataFrame(PowerSystemType=types, Count=counts, - Hierarchy=hierarchies) - else - df = DataFrames.DataFrame(PowerSystemType=types, Count=counts) +function get_bus(sys::System, bus_number::Int) + for bus in get_components(Bus, sys) + if bus.number == bus_number + return bus + end end - print(io, df) return nothing end + +""" + get_buses(sys::System, bus_numbers::Set{Int}) + +Return all buses values with bus_numbers. +""" +function get_buses(sys::System, bus_numbers::Set{Int}) + buses = Vector{Bus}() + for bus in get_components(Bus, sys) + if bus.number in bus_numbers + push!(buses, bus) + end + end + + return buses +end + +""" +Throws ArgumentError if the bus is not stored in the system. +""" +function check_bus(sys::System, bus::Bus, component::Component) + name = get_name(bus) + if isnothing(get_component(Bus, sys, name)) + throw(ArgumentError("$component has bus $name that is not stored in the system")) + end +end + +function IS.compare_values(x::System, y::System)::Bool + match = true + + if !IS.compare_values(x.data, y.data) + @debug "SystemData values do not match" + match = false + end + + if x.basepower != y.basepower + @debug "basepower does not match" x.basepower y.basepower + match = false + end + + return match +end + +function _create_system_data_from_kwargs(; kwargs...) + validation_descriptor_file = nothing + runchecks = get(kwargs, :runchecks, true) + if runchecks + validation_descriptor_file = get(kwargs, :configpath, + POWER_SYSTEM_STRUCT_DESCRIPTOR_FILE) + end + + return IS.SystemData(; validation_descriptor_file=validation_descriptor_file) +end diff --git a/src/common.jl b/src/common.jl index a493582dbc..3fa3748c2d 100644 --- a/src/common.jl +++ b/src/common.jl @@ -1,3 +1,6 @@ +const Min_Max = NamedTuple{(:min, :max),Tuple{Float64,Float64}} +const From_To_Float = NamedTuple{(:from, :to),Tuple{Float64,Float64}} +const FromTo_ToFrom_Float = NamedTuple{(:from_to, :to_from),Tuple{Float64,Float64}} "From http://www.pserc.cornell.edu/matpower/MATPOWER-manual.pdf Table B-4" @enum GeneratorCostModel begin @@ -5,15 +8,73 @@ POLYNOMIAL = 2 end +@enum AngleUnit begin + DEGREES + RADIANS +end + +@enum BusType begin + ISOLATED + PQ + PV + REF + SLACK +end -"Thrown upon detection of user data that is not supported." -struct DataFormatError <: Exception - msg::String +@enum LoadModel begin + ConstantImpedance #Z + ConstantCurrent #I + ConstantPower #P end -struct InvalidParameter <: Exception - msg::String +"From https://www.eia.gov/survey/form/eia_923/instructions.pdf" +@enum PrimeMovers begin + BA #Energy Storage, Battery + BT #Turbines Used in a Binary Cycle (including those used for geothermal applications) + CA #Combined-Cycle – Steam Part + CC #Combined-Cycle - Aggregated Plant *augmentation of EIA + CE #Energy Storage, Compressed Air + CP #Energy Storage, Concentrated Solar Power + CS #Combined-Cycle Single-Shaft Combustion turbine and steam turbine share a single generator + CT #Combined-Cycle Combustion Turbine Part + ES #Energy Storage, Other (Specify on Schedule 9, Comments) + FC #Fuel Cell + FW #Energy Storage, Flywheel + GT #Combustion (Gas) Turbine (including jet engine design) + HA #Hydrokinetic, Axial Flow Turbine + HB #Hydrokinetic, Wave Buoy + HK #Hydrokinetic, Other + HY #Hydraulic Turbine (including turbines associated with delivery of water by pipeline) + IC #Internal Combustion (diesel, piston, reciprocating) Engine + PS #Energy Storage, Reversible Hydraulic Turbine (Pumped Storage) + OT #Other – Specify on SCHEDULE 9. + ST #Steam Turbine (including nuclear, geothermal and solar steam; does not include combined-cycle turbine) + PVe #Photovoltaic *renaming from EIA PV to PVe to avoid conflict with BusType::PV + WT #Wind Turbine, Onshore + WS #Wind Turbine, Offshore +end + +"AER Aggregated Fuel Code From https://www.eia.gov/survey/form/eia_923/instructions.pdf" +@enum ThermalFuels begin + COAL #COL #Anthracite Coal and Bituminous Coal + WASTE_COAL #WOC #Waste/Other Coal (includes anthracite culm, gob, fine coal, lignite waste, waste coal) + DISTILLATE_FUEL_OIL #DFO #Distillate Fuel Oil (Diesel, No. 1, No. 2, and No. 4 + WASTE_OIL #WOO #Waste Oil Kerosene and JetFuel Butane, Propane, + PETROLEUM_COKE #PC #Petroleum Coke + RESIDUAL_FUEL_OIL #RFO #Residual Fuel Oil (No. 5, No. 6 Fuel Oils, and Bunker Oil) + NATURAL_GAS #NG #Natural Gas + OTHER_GAS #OOG #Other Gas and blast furnace gas + NUCLEAR #NUC #Nuclear Fission (Uranium, Plutonium, Thorium) + AG_BIPRODUCT #ORW #Agricultural Crop Byproducts/Straw/Energy Crops + MUNICIPAL_WASTE #MLG #Municipal Solid Waste – Biogenic component + WOOD_WASTE #WWW #Wood Waste Liquids excluding Black Liquor (BLQ) (Includes red liquor, sludge wood, spent sulfite liquor, and other wood-based liquids) + GEOTHERMAL #GEO #Geothermal + OTHER #OTH #Other end PS_MAX_LOG = parse(Int, get(ENV, "PS_MAX_LOG", "50")) +DEFAULT_BASE_MVA = 100.0 + +const POWER_SYSTEM_STRUCT_DESCRIPTOR_FILE = + joinpath(dirname(pathof(PowerSystems)), "descriptors", "power_system_structs.json") diff --git a/src/descriptors/power_system_inputs.json b/src/descriptors/power_system_inputs.json new file mode 100644 index 0000000000..4df0bc82c5 --- /dev/null +++ b/src/descriptors/power_system_inputs.json @@ -0,0 +1,579 @@ +{ + "dc_branch": [ + { + "name": "name", + "description": "Unique ID" + }, + { + "name": "connection_points_from", + "description": "From Bus ID" + }, + { + "name": "connection_points_to", + "description": "To Bus ID" + }, + { + "name": "activepower_flow", + "unit": "MW", + "description": "Active power flow" + }, + { + "name": "mw_load", + "description": "Power demand (MW)", + "system_per_unit": true + }, + { + "name": "rectifier_firing_angle_max", + "unit": "degree", + "value_range": [], + "description": "Nominal maximum firing angle" + }, + { + "name": "rectifier_firing_angle_min", + "unit": "degree", + "value_range": [], + "description": "Minimum steady state firing angle" + }, + { + "name": "rectifier_xrc", + "unit": "per unit", + "description": "Commutating transformer reactance/bridge (ohm)" + }, + { + "name": "rectifier_tap_limits_max", + "unit": "per unit", + "value_range": [], + "description": "Max tap setting" + }, + { + "name": "rectifier_tap_limits_min", + "unit": "per unit", + "value_range": [], + "description": "Min tap setting" + }, + { + "name": "inverter_firing_angle_max", + "unit": "degree", + "value_range": [], + "description": "Nominal maximum firing angle" + }, + { + "name": "inverter_firing_angle_min", + "unit": "degree", + "value_range": [], + "description": "Minimum steady state firing angle" + }, + { + "name": "inverter_xrc", + "unit": "per unit", + "description": "Commutating transformer reactance/bridge (ohm)" + }, + { + "name": "inverter_tap_limits_max", + "unit": "per unit", + "value_range": [], + "description": "Max tap setting" + }, + { + "name": "inverter_tap_limits_min", + "unit": "per unit", + "value_range": [], + "description": "Min tap setting" + }, + { + "name": "inverter_tap_limits", + "unit": "per unit", + "value_range": [], + "description": "Min tap setting" + }, + { + "name": "loss", + "unit": "%", + "description": "Power Losses on the Line" + }, + { + "name": "min_active_power_limit_from", + "unit": "per unit", + "value_range": [], + "description": "Minimum Active Power Limit" + }, + { + "name": "max_active_power_limit_from", + "unit": "per unit", + "value_range": [], + "description": "Maximum Active Power Limit" + }, + { + "name": "min_active_power_limit_to", + "unit": "per unit", + "value_range": [], + "description": "Minimum Active Power Limit" + }, + { + "name": "max_active_power_limit_to", + "unit": "per unit", + "value_range": [], + "description": "Maximum Active Power Limit" + }, + { + "name": "control_mode", + "description": "Control Mode" + }, + { + "name": "dc_line_category", + "value_options": [ + "VSCDCLine", + "HVDCLine" + ], + "description": "Type of Struct" + } + ], + "branch": [ + { + "name": "name", + "description": "Unique branch ID" + }, + { + "name": "connection_points_from", + "description": "From Bus ID" + }, + { + "name": "connection_points_to", + "description": "To Bus ID" + }, + { + "unit": "per unit", + "name": "r", + "description": "Branch resistance p.u." + }, + { + "unit": "per unit", + "name": "x", + "description": "Branch reactance p.u." + }, + { + "unit": "per unit", + "name": "primary_shunt", + "description": "Branch line charging susceptance p.u." + }, + { + "unit": "MW", + "name": "rate", + "description": "Continuous MW flow limit", + "system_per_unit": true + }, + { + "unit": "radian", + "name": "min_angle_limits", + "description": "Minimum Angle Limits" + }, + { + "unit": "radian", + "name": "max_angle_limits", + "description": "Continuous MW flow limit" + }, + { + "name": "activepower_flow", + "unit": "MW", + "description": "Active power flow", + "system_per_unit": true + }, + { + "name": "reactivepower_flow", + "unit": "MVAr", + "description": "Reactive power flow", + "system_per_unit": true + }, + { + "name": "tap", + "unit": "%", + "description": "Transformer winding ratio" + }, + { + "name": "branch_category", + "value_options": [ + "Transformers", + "Lines" + ], + "description": "Type of Struct" + } + ], + "generator": [ + { + "name": "name", + "description": "Unique generator ID: Concatenated from Bus ID_Unit Type_Gen ID" + }, + { + "name": "bus_id", + "description": "Connection Bus ID" + }, + { + "name": "fuel", + "description": "Unit Fuel" + }, + { + "name": "fuel_price", + "unit": "$/MMBTU", + "description": "Fuel Price" + }, + { + "unit": "MW", + "name": "active_power", + "description": "Real power injection setpoint", + "system_per_unit": true + }, + { + "unit": "MW", + "name": "reactive_power", + "description": "Reactive power injection setpoint", + "system_per_unit": true + }, + { + "unit": "MW", + "name": "active_power_limits_max", + "description": "Maximum real power injection (Unit Capacity)", + "system_per_unit": true + }, + { + "unit": "MW", + "name": "active_power_limits_min", + "description": "Minimum real power injection (Unit minimum stable level)", + "system_per_unit": true + }, + { + "unit": "MVAR", + "name": "reactive_power_limits_max", + "description": "Maximum reactive power injection", + "system_per_unit": true + }, + { + "unit": "MVAR", + "name": "reactive_power_limits_min", + "description": "Minimum reactive power injection", + "system_per_unit": true + }, + { + "unit": "hours", + "name": "min_down_time", + "description": "Minimum off time required before unit restart" + }, + { + "unit": "hours", + "name": "min_up_time", + "description": "Minimum on time required before unit shutdown" + }, + { + "unit": "MW/Min", + "name": "ramp_limits", + "description": "Maximum ramp up and ramp down rate", + "system_per_unit": true + }, + { + "unit": "MMBTU", + "name": "startup_heat_cold_cost", + "description": "Heat required to startup from cold" + }, + { + "name": "heat_rate_avg_0", + "description": "Heat rate Average 0 TODO" + }, + { + "name": "heat_rate_avg_1", + "description": "Heat rate Average 1 TODO" + }, + { + "name": "heat_rate_avg_2", + "description": "Heat rate Average 2 TODO" + }, + { + "name": "heat_rate_avg_3", + "description": "Heat rate Average 3 TODO" + }, + { + "name": "heat_rate_avg_4", + "description": "Heat rate Average 4 TODO" + }, + { + "unit": "%", + "name": "output_percent_0", + "description": "Output point 0 on heat rate curve as a percentage of PMax" + }, + { + "unit": "%", + "name": "output_percent_1", + "description": "Output point 1 on heat rate curve as a percentage of PMax" + }, + { + "unit": "%", + "name": "output_percent_2", + "description": "Output point 2 on heat rate curve as a percentage of PMax" + }, + { + "unit": "%", + "name": "output_percent_3", + "description": "Output point 3 on heat rate curve as a percentage of PMax" + }, + { + "unit": "%", + "name": "output_percent_4", + "description": "Output point 4 on heat rate curve as a percentage of PMax" + }, + { + "unit": "MVA", + "name": "base_mva", + "description": "Unit equivalent circuit base_mva" + }, + { + "unit": "$/MW", + "name": "variable_cost", + "description": "Variable Cost of Generation" + }, + { + "unit": "$/MW", + "name": "fixed_cost", + "description": "Fixed Cost of Generation" + }, + { + "unit": "$/start", + "name": "startup_cost", + "description": "Cost associated with Start-up" + }, + { + "unit": "$/start", + "name": "shutdown_cost", + "description": "Cost associated with Shutdown" + }, + { + "unit": "%", + "name": "annual_capacity_factor", + "description": "Annual Capacity factor for a Unit" + }, + { + "unit": "$/MW", + "name": "curtailment_cost", + "description": "Cost of curtailing production" + }, + { + "unit": "$/MW", + "name": "interruption_cost", + "description": "Cost of Non-served Energy" + }, + { + "unit": "%", + "name": "power_factor", + "description": "Power Factor" + }, + { + "name": "unit_type", + "description": "Unit Prime Mover Type" + }, + { + "name": "category", + "description": "Category" + }, + { + "name": "generator_category", + "value_options": [ + "HydroFix", + "HydroDispatch", + "HydroStorage", + "RenewableFix", + "RenewableDispatch", + "ThermalStandard" + ], + "description": "Type of Struct" + } + ], + "simulation_objects": [], + "reserves": [ + { + "name": "name", + "description": "Reserve product name" + }, + { + "name": "contributing_devices", + "description": "Contributing Devices for reserve requirement" + }, + { + "units": "MW", + "name": "requirement", + "description": "Contributing Devices for reserve requirement" + }, + { + "unit": "seconds", + "name": "timeframe", + "description": "Response time to satisfy reserve requirement" + }, + { + "name": "eligible_device_categories", + "description": "Eligible Device Categories" + }, + { + "name": "eligible_device_subcategories", + "description": "Eligible Device SubCategories" + }, + { + "name": "eligible_generator_categories", + "description": "Eligible Generator Categories" + }, + { + "name": "eligible_regions", + "description": "Eligible Regions" + }, + { + "name": "reserve_category", + "value_options": [ + "ProportionalReserve", + "StaticReserve", + "Transfer" + ], + "description": "Type of Struct" + } + ], + "storage": [ + { + "name": "name", + "description": "Gen ID associated with storage" + }, + { + "name": "name", + "description": "Storage object name" + }, + { + "name": "bus_id", + "description": "Connection Bus ID" + }, + { + "name": "energy_level", + "unit": "kW/h", + "description": "Energy Level setpoint" + }, + { + "unit": "MW", + "name": "active_power", + "description": "Real power injection setpoint", + "system_per_unit": true + }, + { + "unit": "MW", + "name": "reactive_power", + "description": "Reactive power injection setpoint", + "system_per_unit": true + }, + { + "unit": "MW", + "name": "input_power_limits_max", + "description": "Maximum real power limit on charging", + "system_per_unit": true + }, + { + "unit": "MW", + "name": "input_power_limits_min", + "description": "Minimum real power limit on charging", + "system_per_unit": true + }, + { + "unit": "MW", + "name": "output_active_power_limits_max", + "description": "Maximum real power injection", + "system_per_unit": true + }, + { + "unit": "MW", + "name": "output_active_power_limits_min", + "description": "Minimum real power injection", + "system_per_unit": true + }, + { + "unit": "MVAR", + "name": "reactive_power_limits_max", + "description": "Maximum reactive power injection", + "system_per_unit": true + }, + { + "unit": "MVAR", + "name": "reactive_power_limits_min", + "description": "Minimum reactive power injection", + "system_per_unit": true + }, + { + "unit": "MW", + "name": "rating", + "description": "Continuous MW flow limit", + "system_per_unit": true + }, + { + "unit": "%", + "name": "efficiency", + "description": "Battery Efficiency" + } + ], + "bus": [ + { + "name": "bus_id", + "description": "Numeric Bus ID" + }, + { + "name": "name", + "description": "Bus name from RTS-96" + }, + { + "name": "area", + "description": "area membership" + }, + { + "unit": "kV", + "name": "base_voltage", + "description": "Bus voltage rating" + }, + { + "name": "bus_type", + "value_options": [ + "PQ", + "PV", + "REF" + ], + "description": "Bus control type" + }, + { + "unit": "kV", + "name": "voltage", + "description": "voltage magnitude setpoint" + }, + { + "unit": "radian", + "name": "angle", + "description": "voltage angle setpoint" + }, + { + "unit": "kV", + "name": "voltage_limits_min", + "description": "Minimum voltage setpoint" + }, + { + "unit": "kV", + "name": "voltage_limits_max", + "description": "Maximum voltage setpoint" + }, + { + "unit": "per unit", + "name": "mw_shunt_g", + "description": "Shunt conductance" + }, + { + "unit": "per unit", + "name": "mvar_shut_b", + "description": "Shunt susceptance" + }, + { + "name": "max_active_power", + "description": "Maximum Active Power", + "system_per_unit": true + }, + { + "name": "max_reactive_power", + "description": "Maximum Rective Power", + "system_per_unit": true + } + ] +} diff --git a/src/descriptors/power_system_structs.json b/src/descriptors/power_system_structs.json new file mode 100644 index 0000000000..dab1eaf837 --- /dev/null +++ b/src/descriptors/power_system_structs.json @@ -0,0 +1,1410 @@ +[ + { + "struct_name": "TwoPartCost", + "docstring": "Data Structure Operational Cost Data in two parts fixed and variable cost.", + "fields": [ + { + "name": "variable", + "null_value": "VariableCost((0.0, 0.0))", + "data_type": "VariableCost" + }, + { + "name": "fixed", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "warn" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "OperationalCost" + }, + { + "struct_name": "ThreePartCost", + "docstring": "Data Structure Operational Cost Data in Three parts fixed, variable cost and start - stop costs.", + "fields": [ + { + "name": "variable", + "null_value": "VariableCost((0.0, 0.0))", + "data_type": "VariableCost" + }, + { + "name": "fixed", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "startup", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "warn" + }, + { + "name": "shutdn", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "warn" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "OperationalCost" + }, + { + "struct_name": "TechHydro", + "fields": [ + { + "name": "rating", + "comment": "Thermal limited MVA Power Output of the unit. <= Capacity", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "primemover", + "comment": "PrimeMover Technology according to EIA 923", + "null_value": "HY::PrimeMovers", + "data_type": "PrimeMovers" + }, + { + "name": "activepowerlimits", + "null_value": "(min=0.0, max=0.0)", + "data_type": "Min_Max" + }, + { + "name": "reactivepowerlimits", + "validation_action": "warn", + "null_value": "nothing", + "data_type": "Union{Nothing, Min_Max}" + }, + { + "name": "ramplimits", + "null_value": "nothing", + "data_type": "Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}", + "valid_range": {"min":0.0, "max": null}, + "validation_action": "error" + }, + { + "name": "timelimits", + "null_value": "nothing", + "data_type": "Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "TechnicalParams" + }, + { + "struct_name": "TechRenewable", + "fields": [ + { + "name": "rating", + "comment": "Thermal limited MVA Power Output of the unit. <= Capacity", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "primemover", + "comment": "PrimeMover Technology according to EIA 923", + "null_value": "OT::PrimeMovers", + "data_type": "PrimeMovers" + }, + { + "name": "reactivepowerlimits", + "null_value": "nothing", + "data_type": "Union{Nothing, Min_Max}" + }, + { + "name": "powerfactor", + "null_value": "1.0", + "data_type": "Float64", + "valid_range": {"min":0.0, "max":1.0}, + "validation_action": "error" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "TechnicalParams" + }, + { + "struct_name": "TechThermal", + "docstring": "Data Structure for the technical parameters of thermal generation technologies.", + "fields": [ + { + "name": "rating", + "comment": "Thermal limited MVA Power Output of the unit. <= Capacity", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "primemover", + "comment": "PrimeMover Technology according to EIA 923", + "null_value": "OT::PrimeMovers", + "data_type": "PrimeMovers" + }, + { + "name": "fuel", + "comment": "PrimeMover Fuel according to EIA 923", + "null_value": "OTHER::ThermalFuels", + "data_type": "ThermalFuels" + }, + { + "name": "activepowerlimits", + "null_value": "(min=0.0, max=0.0)", + "data_type": "Min_Max" + }, + { + "name": "reactivepowerlimits", + "null_value": "nothing", + "data_type": "Union{Nothing, Min_Max}" + }, + { + "name": "ramplimits", + "null_value": "nothing", + "data_type": "Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "timelimits", + "null_value": "nothing", + "data_type": "Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}}", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "TechnicalParams" + }, + { + "struct_name": "Bus", + "docstring": "A power-system bus.", + "inner_constructor_check": "CheckBusParams", + "fields": [ + { + "name": "number", + "comment": "number associated with the bus", + "null_value": "0", + "data_type": "Int64" + }, + { + "null_value": "init", + "name": "name", + "comment": "the name of the bus", + "data_type": "String" + }, + { + "name": "bustype", + "comment": "bus type", + "null_value": "nothing", + "data_type": "Union{Nothing, BusType}" + }, + { + "name": "angle", + "comment": "angle of the bus in radians", + "null_value": "0.0", + "data_type": "Union{Nothing, Float64}", + "valid_range": {"min":-1.571, "max":1.571}, + "validation_action": "error" + }, + { + "name": "voltage", + "comment": "voltage as a multiple of basevoltage", + "null_value": "0.0", + "data_type": "Union{Nothing, Float64}", + "valid_range": "voltagelimits", + "validation_action": "warn" + }, + { + "name": "voltagelimits", + "comment": "limits on the voltage variation as multiples of basevoltage", + "null_value": "(min=0.0, max=0.0)", + "data_type": "Union{Nothing, Min_Max}" + }, + { + "name": "basevoltage", + "comment": "the base voltage in kV", + "null_value": "nothing", + "data_type": "Union{Nothing, Float64}", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "Topology" + }, + { + "struct_name": "Arc", + "docstring": "A topological Arc.", + "fields": [ + { + "name": "from", + "null_vale": "Bus(nothing)", + "data_type": "Bus" + }, + { + "name": "to", + "null_vale": "Bus(nothing)", + "data_type": "Bus" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "Topology" + }, + { + "struct_name": "Line", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "activepower_flow", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "reactivepower_flow", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "null_value": "Arc(Bus(nothing), Bus(nothing))", + "name": "arc", + "data_type": "Arc" + }, + { + "null_value": "0.0", + "name": "r", + "data_type": "Float64", + "comment": "System per-unit value", + "valid_range": {"min":0.0, "max":2.0}, + "validation_action": "error" + }, + { + "null_value": "0.0", + "name": "x", + "data_type": "Float64", + "comment": "System per-unit value", + "valid_range": {"min":0.0, "max":2.0}, + "validation_action": "error" + }, + { + "name": "b", + "null_value": "(from=0.0, to=0.0)", + "data_type": "NamedTuple{(:from, :to), Tuple{Float64, Float64}}", + "comment": "System per-unit value", + "valid_range": {"min":0.0, "max":100.0}, + "validation_action": "error" + }, + { + "null_value": "0.0", + "name": "rate", + "data_type": "Float64" + }, + { + "name": "anglelimits", + "null_value": "(min=-1.571, max=1.571)", + "data_type": "NamedTuple{(:min, :max), Tuple{Float64, Float64}}", + "valid_range": {"min":-1.571, "max":1.571}, + "validation_action": "error" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "ACBranch" + }, + { + "struct_name": "MonitoredLine", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "activepower_flow", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "reactivepower_flow", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "null_value": "Arc(Bus(nothing), Bus(nothing))", + "name": "arc", + "data_type": "Arc" + }, + { + "null_value": "0.0", + "name": "r", + "data_type": "Float64", + "comment": "System per-unit value", + "valid_range": {"min":0.0, "max":2.0}, + "validation_action": "error" + }, + { + "null_value": "0.0", + "name": "x", + "data_type": "Float64", + "comment": "System per-unit value", + "valid_range": {"min":0.0, "max":2.0}, + "validation_action": "error" + }, + { + "null_value": "(from=0.0, to=0.0)", + "name": "b", + "data_type": "NamedTuple{(:from, :to), Tuple{Float64, Float64}}", + "comment": "System per-unit value", + "valid_range": {"min":0.0, "max":2.0}, + "validation_action": "error" + }, + { + "name": "flowlimits", + "null_value": "(from_to=0.0, to_from=0.0)", + "comment": "TODO: throw warning above max SIL", + "data_type": "NamedTuple{(:from_to, :to_from), Tuple{Float64, Float64}}" + }, + { + "null_value": "0.0", + "name": "rate", + "data_type": "Float64", + "comment": "TODO: compare to SIL (warn) (theoretical limit)" + }, + { + "name": "anglelimits", + "null_value": "(min=-1.571, max=1.571)", + "data_type": "Min_Max", + "valid_range": {"min":-1.571, "max":1.571}, + "validation_action": "error" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "ACBranch" + }, + { + "struct_name": "PhaseShiftingTransformer", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "activepower_flow", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "reactivepower_flow", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "null_value": "Arc(Bus(nothing), Bus(nothing))", + "name": "arc", + "data_type": "Arc" + }, + { + "null_value": "0.0", + "name": "r", + "data_type": "Float64", + "comment": "System per-unit value", + "valid_range": {"min":0.0, "max":2.0}, + "validation_action": "error" + }, + { + "null_value": "0.0", + "name": "x", + "data_type": "Float64", + "comment": "System per-unit value", + "valid_range": {"min":0.0, "max":2.0}, + "validation_action": "error" + }, + { + "name": "primaryshunt", + "null_value": "0.0", + "data_type": "Float64", + "Comment": "System per-unit value", + "valid_range": {"min":0.0, "max":2.0}, + "validation_action": "error" + }, + { + "name": "tap", + "null_value": "1.0", + "data_type": "Float64", + "valid_range": {"min":0.0, "max":2.0}, + "validation_action": "error" + }, + { + "name": "\u03B1", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": {"min":-1.571, "max":1.571}, + "validation_action": "warn" + }, + { + "null_value": "0.0", + "name": "rate", + "data_type": "Union{Nothing, Float64}", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "ACBranch" + }, + { + "struct_name": "TapTransformer", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "activepower_flow", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "reactivepower_flow", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "null_value": "Arc(Bus(nothing), Bus(nothing))", + "name": "arc", + "data_type": "Arc" + }, + { + "null_value": "0.0", + "name": "r", + "data_type": "Float64", + "comment": "System per-unit value", + "valid_range": {"min":-2.0, "max":2.0}, + "validation_action": "error" + }, + { + "null_value": "0.0", + "name": "x", + "data_type": "Float64", + "comment": "System per-unit value", + "valid_range": {"min":-2.0, "max":2.0}, + "validation_action": "error" + }, + { + "name": "primaryshunt", + "null_value": "0.0", + "data_type": "Float64", + "comment": "System per-unit value", + "valid_range": {"min":0.0, "max":2.0}, + "validation_action": "error" + }, + { + "name": "tap", + "null_value": "1.0", + "data_type": "Float64", + "valid_range": {"min":0.0, "max":2.0}, + "validation_action": "error" + }, + { + "name": "rate", + "null_value": "0.0", + "data_type": "Union{Nothing, Float64}", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "ACBranch" + }, + { + "struct_name": "Transformer2W", + "docstring": "The 2-W transformer model uses an equivalent circuit assuming the impedance is on the High Voltage Side of the transformer. The model allocates the iron losses and magnetizing susceptance to the primary side.", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "activepower_flow", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "reactivepower_flow", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "null_value": "Arc(Bus(nothing), Bus(nothing))", + "name": "arc", + "data_type": "Arc" + }, + { + "null_value": "0.0", + "name": "r", + "data_type": "Float64", + "comment": "System per-unit value", + "valid_range": {"min":-2.0, "max":2.5}, + "validation_action": "error" + }, + { + "null_value": "0.0", + "name": "x", + "data_type": "Float64", + "comment": "System per-unit value", + "valid_range": {"min":-2.0, "max":2.5}, + "validation_action": "error" + }, + { + "name": "primaryshunt", + "null_value": "0.0", + "data_type": "Float64", + "comment": "System per-unit value", + "valid_range": {"min":0.0, "max":2.0}, + "validation_action": "error" + }, + { + "name": "rate", + "null_value": "nothing", + "data_type": "Union{Nothing, Float64}", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "ACBranch" + }, + { + "struct_name": "HVDCLine", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "activepower_flow", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "null_value": "Arc(Bus(nothing), Bus(nothing))", + "name": "arc", + "data_type": "Arc" + }, + { + "name": "activepowerlimits_from", + "null_value": "(min=0.0, max=0.0)", + "data_type": "Min_Max" + }, + { + "name": "activepowerlimits_to", + "null_value": "(min=0.0, max=0.0)", + "data_type": "Min_Max" + }, + { + "name": "reactivepowerlimits_from", + "null_value": "(min=0.0, max=0.0)", + "data_type": "Min_Max" + }, + { + "name": "reactivepowerlimits_to", + "null_value": "(min=0.0, max=0.0)", + "data_type": "Min_Max" + }, + { + "name": "loss", + "null_value": "(l0=0.0, l1=0.0)", + "data_type": "NamedTuple{(:l0, :l1), Tuple{Float64, Float64}}" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "DCBranch" + }, + { + "struct_name": "VSCDCLine", + "docstring": "As implemented in Milano's Book, Page 397", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "activepower_flow", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "null_value": "Arc(Bus(nothing), Bus(nothing))", + "name": "arc", + "data_type": "Arc" + }, + { + "name": "rectifier_taplimits", + "null_value": "(min=0.0, max=0.0)", + "data_type": "Min_Max" + }, + { + "name": "rectifier_xrc", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "rectifier_firing_angle", + "null_value": "(min=0.0, max=0.0)", + "data_type": "Min_Max" + }, + { + "name": "inverter_taplimits", + "null_value": "(min=0.0, max=0.0)", + "data_type": "Min_Max" + }, + { + "name": "inverter_xrc", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "inverter_firing_angle", + "null_value": "(min=0.0, max=0.0)", + "data_type": "Min_Max" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "DCBranch" + }, + { + "struct_name": "InterruptibleLoad", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "bus", + "null_value": "Bus(nothing)", + "data_type": "Bus" + }, + { + "name": "model", + "null_value": "ConstantPower::LoadModel", + "data_type": "LoadModel" + }, + { + "name": "activepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "reactivepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "maxactivepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "maxreactivepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "op_cost", + "null_value": "TwoPartCost(nothing)", + "data_type": "TwoPartCost" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "ControllableLoad" + }, + { + "struct_name": "FixedAdmittance", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "bus", + "null_value": "Bus(nothing)", + "data_type": "Bus" + }, + { + "name": "Y", + "null_value": "0.0", + "data_type": "Complex{Float64}" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "ElectricLoad" + }, + { + "struct_name": "PowerLoad", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "bus", + "null_value": "Bus(nothing)", + "data_type": "Bus" + }, + { + "name": "model", + "null_value": "nothing", + "data_type": "Union{Nothing, LoadModel}" + }, + { + "name": "activepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "reactivepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "maxactivepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "maxreactivepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "StaticLoad" + }, + { + "struct_name": "HydroDispatch", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "bus", + "null_value": "Bus(nothing)", + "data_type": "Bus" + }, + { + "name": "activepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "reactivepower", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": "tech.reactivepowerlimits", + "validation_action": "warn" + }, + { + "name": "tech", + "null_value": "TechHydro(nothing)", + "data_type": "TechHydro" + }, + { + "name": "op_cost", + "null_value": "TwoPartCost(nothing)", + "data_type": "TwoPartCost" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "HydroGen" + }, + { + "struct_name": "HydroFix", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "bus", + "null_value": "Bus(nothing)", + "data_type": "Bus" + }, + { + "name": "activepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "reactivepower", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": "tech.reactivepowerlimits", + "validation_action": "warn" + }, + { + "name": "tech", + "null_value": "TechHydro(nothing)", + "data_type": "TechHydro" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "HydroGen" + }, + { + "struct_name": "HydroStorage", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "bus", + "null_value": "Bus(nothing)", + "data_type": "Bus" + }, + { + "name": "activepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "reactivepower", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": "tech.reactivepowerlimits", + "validation_action": "warn" + }, + { + "name": "tech", + "null_value": "TechHydro(nothing)", + "data_type": "TechHydro" + }, + { + "name": "op_cost", + "null_value": "TwoPartCost(nothing)", + "data_type": "TwoPartCost" + }, + { + "name": "storagecapacity", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "initial_storage", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "HydroGen" + }, + { + "struct_name": "RenewableDispatch", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "bus", + "null_value": "Bus(nothing)", + "data_type": "Bus" + }, + { + "name": "activepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "reactivepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "tech", + "null_value": "TechRenewable(nothing)", + "data_type": "TechRenewable" + }, + { + "name": "op_cost", + "null_value": "TwoPartCost(nothing)", + "data_type": "TwoPartCost" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "RenewableGen" + }, + { + "struct_name": "RenewableFix", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "bus", + "null_value": "Bus(nothing)", + "data_type": "Bus" + }, + { + "name": "activepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "reactivepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "tech", + "null_value": "TechRenewable(nothing)", + "data_type": "TechRenewable" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "RenewableGen" + }, + { + "struct_name": "ThermalStandard", + "docstring": "Data Structure for thermal generation technologies.", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "bus", + "null_value": "Bus(nothing)", + "data_type": "Bus" + }, + { + "name": "activepower", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": "tech.activepowerlimits", + "validation_action": "warn" + }, + { + "name": "reactivepower", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": "tech.reactivepowerlimits", + "validation_action": "warn" + }, + { + "name": "tech", + "null_value": "TechThermal(nothing)", + "data_type": "Union{Nothing, TechThermal}" + }, + { + "name": "op_cost", + "null_value": "ThreePartCost(nothing)", + "data_type": "ThreePartCost" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "ThermalGen" + }, + { + "struct_name": "LoadZones", + "fields": [ + { + "name": "number", + "null_value": "0", + "data_type": "Int64" + }, + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "name": "buses", + "null_value": "[Bus(nothing)]", + "data_type": "Vector{Bus}" + }, + { + "name": "maxactivepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "maxreactivepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "Topology" + }, + { + "struct_name": "GenericBattery", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "null_value": "false", + "name": "available", + "data_type": "Bool" + }, + { + "name": "bus", + "null_value": "Bus(nothing)", + "data_type": "Bus" + }, + { + "name": "primemover", + "comment": "PrimeMover Technology according to EIA 923", + "null_value": "BA::PrimeMovers", + "data_type": "PrimeMovers" + }, + { + "name": "energy", + "null_value": "0.0", + "comment": "State of Charge of the Battery p.u.-hr", + "data_type": "Float64", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "capacity", + "null_value": "(min=0.0, max=0.0)", + "comment": "Maximum and Minimum storage capacity in p.u.-hr", + "data_type": "Min_Max", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "rating", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "activepower", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "inputactivepowerlimits", + "null_value": "(min=0.0, max=0.0)", + "data_type": "Min_Max", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "outputactivepowerlimits", + "null_value": "(min=0.0, max=0.0)", + "data_type": "Min_Max", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "efficiency", + "null_value": "(in=0.0, out=0.0)", + "data_type": "NamedTuple{(:in, :out), Tuple{Float64, Float64}}", + "valid_range": {"min":0.0, "max":1.0}, + "validation_action": "warn" + }, + { + "name": "reactivepower", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": "reactivepowerlimits", + "validation_action": "warn" + }, + { + "name": "reactivepowerlimits", + "null_value": "(min=0.0, max=0.0)", + "data_type": "Union{Nothing, Min_Max}" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "Storage" + }, + { + "struct_name": "ProportionalReserve", + "docstring": "Data Structure for a proportional reserve product for system simulations.", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "name": "contributingdevices", + "comment": "devices from which the product can be procured", + "null_value": "[ThermalStandard(nothing)]", + "data_type": "Vector{Device}" + }, + { + "name": "timeframe", + "comment": "the relative saturation timeframe", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "Reserve" + }, + { + "struct_name": "StaticReserve", + "docstring": "Data Structure for the procurement products for system simulations.", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "name": "contributingdevices", + "comment": "devices from which the product can be procured", + "null_value": "[ThermalStandard(nothing)]", + "data_type": "Vector{Device}" + }, + { + "name": "timeframe", + "comment": "the relative saturation timeframe", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "requirement", + "comment": "the required quantity of the product should be scaled by a Forecast", + "null_value": "0.0", + "data_type": "Float64" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "Reserve" + }, + { + "struct_name": "Transfer", + "fields": [ + { + "null_value": "init", + "name": "name", + "data_type": "String" + }, + { + "name": "contributingdevices", + "null_value": "[ThermalStandard(nothing)]", + "data_type": "Vector{Device}" + }, + { + "name": "timeframe", + "null_value": "0.0", + "data_type": "Float64", + "valid_range": {"min":0.0, "max":null}, + "validation_action": "error" + }, + { + "name": "requirement", + "null_value": "[]", + "data_type": "TimeSeries.TimeArray" + }, + { + "name": "internal", + "data_type": "InfrastructureSystemsInternal" + } + ], + "supertype": "Service" + } +] diff --git a/src/forecasts.jl b/src/forecasts.jl new file mode 100644 index 0000000000..09b50829a4 --- /dev/null +++ b/src/forecasts.jl @@ -0,0 +1,20 @@ +function get_component(forecast::Forecast) + return IS.get_component(forecast) +end + +function get_forecast_component_name(forecast::Forecast) + return IS.get_forecast_component_name(forecast) +end +function get_forecast_value(forecast::Forecast, ix) + return IS.get_forecast_value(forecast, ix) +end +function get_horizon(forecast::Forecast) + return IS.get_horizon(forecast) +end +function get_timeseries(forecast::Forecast) + return IS.get_timeseries(forecast) +end + +function get_data(forecast::Forecast) + return IS.get_data(forecast) +end diff --git a/src/models/Readme.md b/src/models/Readme.md deleted file mode 100644 index 8be53b2825..0000000000 --- a/src/models/Readme.md +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file diff --git a/src/models/branches.jl b/src/models/branches.jl index 9af39ecae4..e795b37e03 100644 --- a/src/models/branches.jl +++ b/src/models/branches.jl @@ -1,7 +1,6 @@ abstract type Branch <: Device end +abstract type ACBranch <: Branch end +abstract type DCBranch <: Branch end -include("./branches/lines.jl") -include("./branches/transformers.jl") -include("./branches/dc_lines.jl") -include("../utils/ybus_calculations.jl") -include("../utils/ptdf_calculations.jl") +get_from_bus(b::T) where {T<: Branch} = b.arc.from +get_to_bus(b::T) where {T<: Branch} = b.arc.to diff --git a/src/models/branches/dc_lines.jl b/src/models/branches/dc_lines.jl deleted file mode 100644 index 13e1bafaaa..0000000000 --- a/src/models/branches/dc_lines.jl +++ /dev/null @@ -1,49 +0,0 @@ -abstract type DCLine <: Branch end - -struct HVDCLine <: Branch - name::String - available::Bool - connectionpoints::From_To_Bus - activepowerlimits_from::NamedTuple{(:min, :max),Tuple{Float64,Float64}} #MW - activepowerlimits_to::NamedTuple{(:min, :max),Tuple{Float64,Float64}} #MW - reactivepowerlimits_from::NamedTuple{(:min, :max),Tuple{Float64,Float64}} #MVar - reactivepowerlimits_to::NamedTuple{(:min, :max),Tuple{Float64,Float64}} #MVar - loss::NamedTuple{(:l0, :l1),Tuple{Float64,Float64}} -end - -HVDCLine(; name ="init", - available = true, - connectionpoints = (from = Bus(), to = Bus()), - activepowerlimits_from = (min=0.0, max=0.0), - activepowerlimits_to = (min=0.0, max=0.0), - reactivepowerlimits_from = (min=0.0, max=0.0), - reactivepowerlimits_to = (min=0.0, max=0.0), - loss = (l0=0.0, l1=0.0) - ) = HVDCLine(name, available, connectionpoints, activepowerlimits_from, activepowerlimits_to, reactivepowerlimits_from, reactivepowerlimits_to,loss ) - - -""" -As implemented in Milano's Book Page 397 -""" -struct VSCDCLine <: DCLine - name::String - available::Bool - connectionpoints::From_To_Bus - rectifier_taplimits::NamedTuple{(:min, :max),Tuple{Float64,Float64}} #pu - rectifier_xrc::Float64 - rectifier_firingangle::NamedTuple{(:min, :max),Tuple{Float64,Float64}} #radians - inverter_taplimits::NamedTuple{(:min, :max),Tuple{Float64,Float64}} #pu - inverter_xrc::Float64 - inverter_firingangle::NamedTuple{(:min, :max),Tuple{Float64,Float64}} #radians -end - -VSCDCLine(; name ="init", - available = true, - connectionpoints = (from = Bus(), to = Bus()), - rectifier_taplimits = (min=0.0, max=0.0), - rectifier_xrc = 0.0, - rectifier_firingangle = (min=0.0, max=0.0), - inverter_taplimits = (min=0.0, max=0.0), - inverter_xrc = 0.0, - inverter_firingangle = (min=0.0, max=0.0), - ) = VSCDCLine(name, available, connectionpoints,rectifier_taplimits, rectifier_xrc, rectifier_firingangle, inverter_taplimits, inverter_xrc, inverter_firingangle) \ No newline at end of file diff --git a/src/models/branches/lines.jl b/src/models/branches/lines.jl deleted file mode 100644 index c94eb768f5..0000000000 --- a/src/models/branches/lines.jl +++ /dev/null @@ -1,61 +0,0 @@ -const Min_Max = NamedTuple{(:min, :max),Tuple{Float64,Float64}} -const From_To_Float = NamedTuple{(:from, :to),Tuple{Float64,Float64}} -const From_To_Bus = NamedTuple{(:from, :to),Tuple{Bus,Bus}} -const FromTo_ToFrom_Float = NamedTuple{(:from_to, :to_from),Tuple{Float64,Float64}} - -struct Line <: Branch - name::String - available::Bool - connectionpoints::From_To_Bus - r::Float64 #[pu] - x::Float64 #[pu] - b::From_To_Float # [pu] - rate::Float64 - anglelimits::Min_Max #Degrees -end - -function Line(name::String, - available::Bool, - connectionpoints::From_To_Bus, - r::Float64, - x::Float64, - b::From_To_Float, - rate::Float64, - anglelimits::Float64) - return Line(name, available, connectionpoints, r, x, b, rate, - (min=-anglelimits, max=anglelimits)) -end - -function Line(; name="init", - available=false, - connectionpoints=From_To_Bus((from=Bus(), to=Bus())), - r=0.0, - x=0.0, - b=(from=0.0, to=0.0), - rate=0.0, - anglelimits=(min=-1.57, max=1.57)) - return Line(name, available, connectionpoints, r, x, b, rate, anglelimits) -end - -struct MonitoredLine <: Branch - name::String - available::Bool - connectionpoints::From_To_Bus - r::Float64 #[pu] - x::Float64 #[pu] - b::From_To_Float # [pu] - flowlimits::FromTo_ToFrom_Float #MW - rate::Float64 - anglelimits::Min_Max #Degrees -end - -function MonitoredLine(; name="init", - available=false, - connectionpoints=From_To_Bus((from=Bus(), to=Bus())), - r=0.0, - x=0.0, - b=(from=0.0, to=0.0), - rate=0.0, - anglelimits=(min=-90.0, max=90.0)) - return Line(name, available, connectionpoints, r, x, b, rate, anglelimits) -end diff --git a/src/models/branches/transformers.jl b/src/models/branches/transformers.jl deleted file mode 100644 index d4c34fe71a..0000000000 --- a/src/models/branches/transformers.jl +++ /dev/null @@ -1,81 +0,0 @@ -""" -The 2-W transformer model uses an equivalent circuit assuming the impedance is on the High Voltage Side of the transformer. -The model allocates the iron losses and magnetezing suceptance to the primary side -""" -struct Transformer2W <: Branch - name::String - available::Bool - connectionpoints::From_To_Bus - r::Float64 #[pu] - x::Float64 #[pu] - primaryshunt::Float64 #[pu] - rate::Union{Nothing,Float64} #[MVA] -end - -Transformer2W(; name = "init", - available = false, - connectionpoints = (from = Bus(), to =Bus()), - r = 0.0, - x = 0.0, - primaryshunt = 0.0, - rate = nothing - ) = Transformer2W(name, available, connectionpoints, r, x, primaryshunt, rate) - -struct TapTransformer <: Branch - name::String - available::Bool - connectionpoints::From_To_Bus - r::Float64 #[pu] - x::Float64 #[pu] - primaryshunt::Float64 #[pu] - tap::Float64 # [0 - 2] - rate::Union{Float64,Nothing} #[MVA] -end - -TapTransformer(; name = "init", - available = false, - connectionpoints = (from=Bus(), to=Bus()), - r = 0.0, - x = 0.0, - primaryshunt = 0.0, - tap = 1.0, - rate = nothing - ) = TapTransformer(name, available, connectionpoints, r, x, primaryshunt, tap, rate) - -#= -struct Transformer3W <: Branch - name::String - available::Bool - transformer::Transformer2W - line::Line -end - -Transformer3W(; name = "init", - available = false, - transformer = Transformer2W(), - line = Line() - ) = Transformer3W(name, available, transformer, line) -=# - -struct PhaseShiftingTransformer <: Branch - name::String - available::Bool - connectionpoints::From_To_Bus - r::Float64 #[pu] - x::Float64 #[pu] - primaryshunt::Float64 #[pu] - tap::Float64 #[0 - 2] - α::Float64 # [radians] - rate::Float64 #[MVA] -end - -PhaseShiftingTransformer(; name = "init", - available = false, - connectionpoints = (from=Bus(), to=Bus()), - r = 0.0, - x = 0.0, - primaryshunt=0.0, - tap = 1.0, - α = 0.0, - rate = 0.0 - ) = PhaseShiftingTransformer(name, available, connectionpoints, r, x, primaryshunt, tap, α, rate) \ No newline at end of file diff --git a/src/models/forecasts.jl b/src/models/forecasts.jl deleted file mode 100644 index e6867ae4d1..0000000000 --- a/src/models/forecasts.jl +++ /dev/null @@ -1,28 +0,0 @@ -abstract type Forecast <: Component end - -struct Deterministic <: Forecast - device::Device - horizon::Int - resolution::Dates.Period - interval::Dates.Period - initialtime::Dates.DateTime - data::Dict{Any,Dict{Int,TimeSeries.TimeArray}} -end - -struct Scenarios <: Forecast - horizon::Int - resolution::Dates.Period - interval::Dates.Period - initialtime::Dates.DateTime - scenarioquantity::Int - data::Dict{Any,Dict{Int,TimeSeries.TimeArray}} -end - -struct Probabilistic <: Forecast - horizon::Int - resolution::Dates.Period - interval::Dates.Period - initialtime::Dates.DateTime - percentilequantity::Int - data::Dict{Any,Dict{Int,TimeSeries.TimeArray}} -end diff --git a/src/models/generated/Arc.jl b/src/models/generated/Arc.jl new file mode 100644 index 0000000000..1ff68da10a --- /dev/null +++ b/src/models/generated/Arc.jl @@ -0,0 +1,26 @@ +#= +This file is auto-generated. Do not edit. +=# + +"""A topological Arc.""" +mutable struct Arc <: Topology + from::Bus + to::Bus + internal::InfrastructureSystemsInternal +end + +function Arc(from, to, ) + Arc(from, to, InfrastructureSystemsInternal()) +end + +function Arc(; from, to, ) + Arc(from, to, ) +end + + +"""Get Arc from.""" +get_from(value::Arc) = value.from +"""Get Arc to.""" +get_to(value::Arc) = value.to +"""Get Arc internal.""" +get_internal(value::Arc) = value.internal diff --git a/src/models/generated/Bus.jl b/src/models/generated/Bus.jl new file mode 100644 index 0000000000..3e3a75f3e5 --- /dev/null +++ b/src/models/generated/Bus.jl @@ -0,0 +1,68 @@ +#= +This file is auto-generated. Do not edit. +=# + +"""A power-system bus.""" +mutable struct Bus <: Topology + number::Int64 # number associated with the bus + name::String # the name of the bus + bustype::Union{Nothing, BusType} # bus type + angle::Union{Nothing, Float64} # angle of the bus in radians + voltage::Union{Nothing, Float64} # voltage as a multiple of basevoltage + voltagelimits::Union{Nothing, Min_Max} # limits on the voltage variation as multiples of basevoltage + basevoltage::Union{Nothing, Float64} # the base voltage in kV + internal::InfrastructureSystemsInternal + + function Bus(number, name, bustype, angle, voltage, voltagelimits, basevoltage, internal, ) + (number, name, bustype, angle, voltage, voltagelimits, basevoltage, internal, ) = CheckBusParams( + number, + name, + bustype, + angle, + voltage, + voltagelimits, + basevoltage, + internal, + ) + new(number, name, bustype, angle, voltage, voltagelimits, basevoltage, internal, ) + end +end + +function Bus(number, name, bustype, angle, voltage, voltagelimits, basevoltage, ) + Bus(number, name, bustype, angle, voltage, voltagelimits, basevoltage, InfrastructureSystemsInternal()) +end + +function Bus(; number, name, bustype, angle, voltage, voltagelimits, basevoltage, ) + Bus(number, name, bustype, angle, voltage, voltagelimits, basevoltage, ) +end + +# Constructor for demo purposes; non-functional. + +function Bus(::Nothing) + Bus(; + number=0, + name="init", + bustype=nothing, + angle=0.0, + voltage=0.0, + voltagelimits=(min=0.0, max=0.0), + basevoltage=nothing, + ) +end + +"""Get Bus number.""" +get_number(value::Bus) = value.number +"""Get Bus name.""" +get_name(value::Bus) = value.name +"""Get Bus bustype.""" +get_bustype(value::Bus) = value.bustype +"""Get Bus angle.""" +get_angle(value::Bus) = value.angle +"""Get Bus voltage.""" +get_voltage(value::Bus) = value.voltage +"""Get Bus voltagelimits.""" +get_voltagelimits(value::Bus) = value.voltagelimits +"""Get Bus basevoltage.""" +get_basevoltage(value::Bus) = value.basevoltage +"""Get Bus internal.""" +get_internal(value::Bus) = value.internal diff --git a/src/models/generated/FixedAdmittance.jl b/src/models/generated/FixedAdmittance.jl new file mode 100644 index 0000000000..c117146a73 --- /dev/null +++ b/src/models/generated/FixedAdmittance.jl @@ -0,0 +1,42 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct FixedAdmittance <: ElectricLoad + name::String + available::Bool + bus::Bus + Y::Complex{Float64} + internal::InfrastructureSystemsInternal +end + +function FixedAdmittance(name, available, bus, Y, ) + FixedAdmittance(name, available, bus, Y, InfrastructureSystemsInternal()) +end + +function FixedAdmittance(; name, available, bus, Y, ) + FixedAdmittance(name, available, bus, Y, ) +end + +# Constructor for demo purposes; non-functional. + +function FixedAdmittance(::Nothing) + FixedAdmittance(; + name="init", + available=false, + bus=Bus(nothing), + Y=0.0, + ) +end + +"""Get FixedAdmittance name.""" +get_name(value::FixedAdmittance) = value.name +"""Get FixedAdmittance available.""" +get_available(value::FixedAdmittance) = value.available +"""Get FixedAdmittance bus.""" +get_bus(value::FixedAdmittance) = value.bus +"""Get FixedAdmittance Y.""" +get_Y(value::FixedAdmittance) = value.Y +"""Get FixedAdmittance internal.""" +get_internal(value::FixedAdmittance) = value.internal diff --git a/src/models/generated/GenericBattery.jl b/src/models/generated/GenericBattery.jl new file mode 100644 index 0000000000..f6e025b952 --- /dev/null +++ b/src/models/generated/GenericBattery.jl @@ -0,0 +1,78 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct GenericBattery <: Storage + name::String + available::Bool + bus::Bus + primemover::PrimeMovers # PrimeMover Technology according to EIA 923 + energy::Float64 # State of Charge of the Battery p.u.-hr + capacity::Min_Max # Maximum and Minimum storage capacity in p.u.-hr + rating::Float64 + activepower::Float64 + inputactivepowerlimits::Min_Max + outputactivepowerlimits::Min_Max + efficiency::NamedTuple{(:in, :out), Tuple{Float64, Float64}} + reactivepower::Float64 + reactivepowerlimits::Union{Nothing, Min_Max} + internal::InfrastructureSystemsInternal +end + +function GenericBattery(name, available, bus, primemover, energy, capacity, rating, activepower, inputactivepowerlimits, outputactivepowerlimits, efficiency, reactivepower, reactivepowerlimits, ) + GenericBattery(name, available, bus, primemover, energy, capacity, rating, activepower, inputactivepowerlimits, outputactivepowerlimits, efficiency, reactivepower, reactivepowerlimits, InfrastructureSystemsInternal()) +end + +function GenericBattery(; name, available, bus, primemover, energy, capacity, rating, activepower, inputactivepowerlimits, outputactivepowerlimits, efficiency, reactivepower, reactivepowerlimits, ) + GenericBattery(name, available, bus, primemover, energy, capacity, rating, activepower, inputactivepowerlimits, outputactivepowerlimits, efficiency, reactivepower, reactivepowerlimits, ) +end + +# Constructor for demo purposes; non-functional. + +function GenericBattery(::Nothing) + GenericBattery(; + name="init", + available=false, + bus=Bus(nothing), + primemover=BA::PrimeMovers, + energy=0.0, + capacity=(min=0.0, max=0.0), + rating=0.0, + activepower=0.0, + inputactivepowerlimits=(min=0.0, max=0.0), + outputactivepowerlimits=(min=0.0, max=0.0), + efficiency=(in=0.0, out=0.0), + reactivepower=0.0, + reactivepowerlimits=(min=0.0, max=0.0), + ) +end + +"""Get GenericBattery name.""" +get_name(value::GenericBattery) = value.name +"""Get GenericBattery available.""" +get_available(value::GenericBattery) = value.available +"""Get GenericBattery bus.""" +get_bus(value::GenericBattery) = value.bus +"""Get GenericBattery primemover.""" +get_primemover(value::GenericBattery) = value.primemover +"""Get GenericBattery energy.""" +get_energy(value::GenericBattery) = value.energy +"""Get GenericBattery capacity.""" +get_capacity(value::GenericBattery) = value.capacity +"""Get GenericBattery rating.""" +get_rating(value::GenericBattery) = value.rating +"""Get GenericBattery activepower.""" +get_activepower(value::GenericBattery) = value.activepower +"""Get GenericBattery inputactivepowerlimits.""" +get_inputactivepowerlimits(value::GenericBattery) = value.inputactivepowerlimits +"""Get GenericBattery outputactivepowerlimits.""" +get_outputactivepowerlimits(value::GenericBattery) = value.outputactivepowerlimits +"""Get GenericBattery efficiency.""" +get_efficiency(value::GenericBattery) = value.efficiency +"""Get GenericBattery reactivepower.""" +get_reactivepower(value::GenericBattery) = value.reactivepower +"""Get GenericBattery reactivepowerlimits.""" +get_reactivepowerlimits(value::GenericBattery) = value.reactivepowerlimits +"""Get GenericBattery internal.""" +get_internal(value::GenericBattery) = value.internal diff --git a/src/models/generated/HVDCLine.jl b/src/models/generated/HVDCLine.jl new file mode 100644 index 0000000000..32516c7e99 --- /dev/null +++ b/src/models/generated/HVDCLine.jl @@ -0,0 +1,62 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct HVDCLine <: DCBranch + name::String + available::Bool + activepower_flow::Float64 + arc::Arc + activepowerlimits_from::Min_Max + activepowerlimits_to::Min_Max + reactivepowerlimits_from::Min_Max + reactivepowerlimits_to::Min_Max + loss::NamedTuple{(:l0, :l1), Tuple{Float64, Float64}} + internal::InfrastructureSystemsInternal +end + +function HVDCLine(name, available, activepower_flow, arc, activepowerlimits_from, activepowerlimits_to, reactivepowerlimits_from, reactivepowerlimits_to, loss, ) + HVDCLine(name, available, activepower_flow, arc, activepowerlimits_from, activepowerlimits_to, reactivepowerlimits_from, reactivepowerlimits_to, loss, InfrastructureSystemsInternal()) +end + +function HVDCLine(; name, available, activepower_flow, arc, activepowerlimits_from, activepowerlimits_to, reactivepowerlimits_from, reactivepowerlimits_to, loss, ) + HVDCLine(name, available, activepower_flow, arc, activepowerlimits_from, activepowerlimits_to, reactivepowerlimits_from, reactivepowerlimits_to, loss, ) +end + +# Constructor for demo purposes; non-functional. + +function HVDCLine(::Nothing) + HVDCLine(; + name="init", + available=false, + activepower_flow=0.0, + arc=Arc(Bus(nothing), Bus(nothing)), + activepowerlimits_from=(min=0.0, max=0.0), + activepowerlimits_to=(min=0.0, max=0.0), + reactivepowerlimits_from=(min=0.0, max=0.0), + reactivepowerlimits_to=(min=0.0, max=0.0), + loss=(l0=0.0, l1=0.0), + ) +end + +"""Get HVDCLine name.""" +get_name(value::HVDCLine) = value.name +"""Get HVDCLine available.""" +get_available(value::HVDCLine) = value.available +"""Get HVDCLine activepower_flow.""" +get_activepower_flow(value::HVDCLine) = value.activepower_flow +"""Get HVDCLine arc.""" +get_arc(value::HVDCLine) = value.arc +"""Get HVDCLine activepowerlimits_from.""" +get_activepowerlimits_from(value::HVDCLine) = value.activepowerlimits_from +"""Get HVDCLine activepowerlimits_to.""" +get_activepowerlimits_to(value::HVDCLine) = value.activepowerlimits_to +"""Get HVDCLine reactivepowerlimits_from.""" +get_reactivepowerlimits_from(value::HVDCLine) = value.reactivepowerlimits_from +"""Get HVDCLine reactivepowerlimits_to.""" +get_reactivepowerlimits_to(value::HVDCLine) = value.reactivepowerlimits_to +"""Get HVDCLine loss.""" +get_loss(value::HVDCLine) = value.loss +"""Get HVDCLine internal.""" +get_internal(value::HVDCLine) = value.internal diff --git a/src/models/generated/HydroDispatch.jl b/src/models/generated/HydroDispatch.jl new file mode 100644 index 0000000000..78ffc02446 --- /dev/null +++ b/src/models/generated/HydroDispatch.jl @@ -0,0 +1,54 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct HydroDispatch <: HydroGen + name::String + available::Bool + bus::Bus + activepower::Float64 + reactivepower::Float64 + tech::TechHydro + op_cost::TwoPartCost + internal::InfrastructureSystemsInternal +end + +function HydroDispatch(name, available, bus, activepower, reactivepower, tech, op_cost, ) + HydroDispatch(name, available, bus, activepower, reactivepower, tech, op_cost, InfrastructureSystemsInternal()) +end + +function HydroDispatch(; name, available, bus, activepower, reactivepower, tech, op_cost, ) + HydroDispatch(name, available, bus, activepower, reactivepower, tech, op_cost, ) +end + +# Constructor for demo purposes; non-functional. + +function HydroDispatch(::Nothing) + HydroDispatch(; + name="init", + available=false, + bus=Bus(nothing), + activepower=0.0, + reactivepower=0.0, + tech=TechHydro(nothing), + op_cost=TwoPartCost(nothing), + ) +end + +"""Get HydroDispatch name.""" +get_name(value::HydroDispatch) = value.name +"""Get HydroDispatch available.""" +get_available(value::HydroDispatch) = value.available +"""Get HydroDispatch bus.""" +get_bus(value::HydroDispatch) = value.bus +"""Get HydroDispatch activepower.""" +get_activepower(value::HydroDispatch) = value.activepower +"""Get HydroDispatch reactivepower.""" +get_reactivepower(value::HydroDispatch) = value.reactivepower +"""Get HydroDispatch tech.""" +get_tech(value::HydroDispatch) = value.tech +"""Get HydroDispatch op_cost.""" +get_op_cost(value::HydroDispatch) = value.op_cost +"""Get HydroDispatch internal.""" +get_internal(value::HydroDispatch) = value.internal diff --git a/src/models/generated/HydroFix.jl b/src/models/generated/HydroFix.jl new file mode 100644 index 0000000000..e650cb6ab1 --- /dev/null +++ b/src/models/generated/HydroFix.jl @@ -0,0 +1,50 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct HydroFix <: HydroGen + name::String + available::Bool + bus::Bus + activepower::Float64 + reactivepower::Float64 + tech::TechHydro + internal::InfrastructureSystemsInternal +end + +function HydroFix(name, available, bus, activepower, reactivepower, tech, ) + HydroFix(name, available, bus, activepower, reactivepower, tech, InfrastructureSystemsInternal()) +end + +function HydroFix(; name, available, bus, activepower, reactivepower, tech, ) + HydroFix(name, available, bus, activepower, reactivepower, tech, ) +end + +# Constructor for demo purposes; non-functional. + +function HydroFix(::Nothing) + HydroFix(; + name="init", + available=false, + bus=Bus(nothing), + activepower=0.0, + reactivepower=0.0, + tech=TechHydro(nothing), + ) +end + +"""Get HydroFix name.""" +get_name(value::HydroFix) = value.name +"""Get HydroFix available.""" +get_available(value::HydroFix) = value.available +"""Get HydroFix bus.""" +get_bus(value::HydroFix) = value.bus +"""Get HydroFix activepower.""" +get_activepower(value::HydroFix) = value.activepower +"""Get HydroFix reactivepower.""" +get_reactivepower(value::HydroFix) = value.reactivepower +"""Get HydroFix tech.""" +get_tech(value::HydroFix) = value.tech +"""Get HydroFix internal.""" +get_internal(value::HydroFix) = value.internal diff --git a/src/models/generated/HydroStorage.jl b/src/models/generated/HydroStorage.jl new file mode 100644 index 0000000000..889f66136c --- /dev/null +++ b/src/models/generated/HydroStorage.jl @@ -0,0 +1,62 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct HydroStorage <: HydroGen + name::String + available::Bool + bus::Bus + activepower::Float64 + reactivepower::Float64 + tech::TechHydro + op_cost::TwoPartCost + storagecapacity::Float64 + initial_storage::Float64 + internal::InfrastructureSystemsInternal +end + +function HydroStorage(name, available, bus, activepower, reactivepower, tech, op_cost, storagecapacity, initial_storage, ) + HydroStorage(name, available, bus, activepower, reactivepower, tech, op_cost, storagecapacity, initial_storage, InfrastructureSystemsInternal()) +end + +function HydroStorage(; name, available, bus, activepower, reactivepower, tech, op_cost, storagecapacity, initial_storage, ) + HydroStorage(name, available, bus, activepower, reactivepower, tech, op_cost, storagecapacity, initial_storage, ) +end + +# Constructor for demo purposes; non-functional. + +function HydroStorage(::Nothing) + HydroStorage(; + name="init", + available=false, + bus=Bus(nothing), + activepower=0.0, + reactivepower=0.0, + tech=TechHydro(nothing), + op_cost=TwoPartCost(nothing), + storagecapacity=0.0, + initial_storage=0.0, + ) +end + +"""Get HydroStorage name.""" +get_name(value::HydroStorage) = value.name +"""Get HydroStorage available.""" +get_available(value::HydroStorage) = value.available +"""Get HydroStorage bus.""" +get_bus(value::HydroStorage) = value.bus +"""Get HydroStorage activepower.""" +get_activepower(value::HydroStorage) = value.activepower +"""Get HydroStorage reactivepower.""" +get_reactivepower(value::HydroStorage) = value.reactivepower +"""Get HydroStorage tech.""" +get_tech(value::HydroStorage) = value.tech +"""Get HydroStorage op_cost.""" +get_op_cost(value::HydroStorage) = value.op_cost +"""Get HydroStorage storagecapacity.""" +get_storagecapacity(value::HydroStorage) = value.storagecapacity +"""Get HydroStorage initial_storage.""" +get_initial_storage(value::HydroStorage) = value.initial_storage +"""Get HydroStorage internal.""" +get_internal(value::HydroStorage) = value.internal diff --git a/src/models/generated/InterruptibleLoad.jl b/src/models/generated/InterruptibleLoad.jl new file mode 100644 index 0000000000..5c28c0fd39 --- /dev/null +++ b/src/models/generated/InterruptibleLoad.jl @@ -0,0 +1,62 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct InterruptibleLoad <: ControllableLoad + name::String + available::Bool + bus::Bus + model::LoadModel + activepower::Float64 + reactivepower::Float64 + maxactivepower::Float64 + maxreactivepower::Float64 + op_cost::TwoPartCost + internal::InfrastructureSystemsInternal +end + +function InterruptibleLoad(name, available, bus, model, activepower, reactivepower, maxactivepower, maxreactivepower, op_cost, ) + InterruptibleLoad(name, available, bus, model, activepower, reactivepower, maxactivepower, maxreactivepower, op_cost, InfrastructureSystemsInternal()) +end + +function InterruptibleLoad(; name, available, bus, model, activepower, reactivepower, maxactivepower, maxreactivepower, op_cost, ) + InterruptibleLoad(name, available, bus, model, activepower, reactivepower, maxactivepower, maxreactivepower, op_cost, ) +end + +# Constructor for demo purposes; non-functional. + +function InterruptibleLoad(::Nothing) + InterruptibleLoad(; + name="init", + available=false, + bus=Bus(nothing), + model=ConstantPower::LoadModel, + activepower=0.0, + reactivepower=0.0, + maxactivepower=0.0, + maxreactivepower=0.0, + op_cost=TwoPartCost(nothing), + ) +end + +"""Get InterruptibleLoad name.""" +get_name(value::InterruptibleLoad) = value.name +"""Get InterruptibleLoad available.""" +get_available(value::InterruptibleLoad) = value.available +"""Get InterruptibleLoad bus.""" +get_bus(value::InterruptibleLoad) = value.bus +"""Get InterruptibleLoad model.""" +get_model(value::InterruptibleLoad) = value.model +"""Get InterruptibleLoad activepower.""" +get_activepower(value::InterruptibleLoad) = value.activepower +"""Get InterruptibleLoad reactivepower.""" +get_reactivepower(value::InterruptibleLoad) = value.reactivepower +"""Get InterruptibleLoad maxactivepower.""" +get_maxactivepower(value::InterruptibleLoad) = value.maxactivepower +"""Get InterruptibleLoad maxreactivepower.""" +get_maxreactivepower(value::InterruptibleLoad) = value.maxreactivepower +"""Get InterruptibleLoad op_cost.""" +get_op_cost(value::InterruptibleLoad) = value.op_cost +"""Get InterruptibleLoad internal.""" +get_internal(value::InterruptibleLoad) = value.internal diff --git a/src/models/generated/Line.jl b/src/models/generated/Line.jl new file mode 100644 index 0000000000..02740ea510 --- /dev/null +++ b/src/models/generated/Line.jl @@ -0,0 +1,66 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct Line <: ACBranch + name::String + available::Bool + activepower_flow::Float64 + reactivepower_flow::Float64 + arc::Arc + r::Float64 # System per-unit value + x::Float64 # System per-unit value + b::NamedTuple{(:from, :to), Tuple{Float64, Float64}} # System per-unit value + rate::Float64 + anglelimits::NamedTuple{(:min, :max), Tuple{Float64, Float64}} + internal::InfrastructureSystemsInternal +end + +function Line(name, available, activepower_flow, reactivepower_flow, arc, r, x, b, rate, anglelimits, ) + Line(name, available, activepower_flow, reactivepower_flow, arc, r, x, b, rate, anglelimits, InfrastructureSystemsInternal()) +end + +function Line(; name, available, activepower_flow, reactivepower_flow, arc, r, x, b, rate, anglelimits, ) + Line(name, available, activepower_flow, reactivepower_flow, arc, r, x, b, rate, anglelimits, ) +end + +# Constructor for demo purposes; non-functional. + +function Line(::Nothing) + Line(; + name="init", + available=false, + activepower_flow=0.0, + reactivepower_flow=0.0, + arc=Arc(Bus(nothing), Bus(nothing)), + r=0.0, + x=0.0, + b=(from=0.0, to=0.0), + rate=0.0, + anglelimits=(min=-1.571, max=1.571), + ) +end + +"""Get Line name.""" +get_name(value::Line) = value.name +"""Get Line available.""" +get_available(value::Line) = value.available +"""Get Line activepower_flow.""" +get_activepower_flow(value::Line) = value.activepower_flow +"""Get Line reactivepower_flow.""" +get_reactivepower_flow(value::Line) = value.reactivepower_flow +"""Get Line arc.""" +get_arc(value::Line) = value.arc +"""Get Line r.""" +get_r(value::Line) = value.r +"""Get Line x.""" +get_x(value::Line) = value.x +"""Get Line b.""" +get_b(value::Line) = value.b +"""Get Line rate.""" +get_rate(value::Line) = value.rate +"""Get Line anglelimits.""" +get_anglelimits(value::Line) = value.anglelimits +"""Get Line internal.""" +get_internal(value::Line) = value.internal diff --git a/src/models/generated/LoadZones.jl b/src/models/generated/LoadZones.jl new file mode 100644 index 0000000000..7b8d09c163 --- /dev/null +++ b/src/models/generated/LoadZones.jl @@ -0,0 +1,46 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct LoadZones <: Topology + number::Int64 + name::String + buses::Vector{Bus} + maxactivepower::Float64 + maxreactivepower::Float64 + internal::InfrastructureSystemsInternal +end + +function LoadZones(number, name, buses, maxactivepower, maxreactivepower, ) + LoadZones(number, name, buses, maxactivepower, maxreactivepower, InfrastructureSystemsInternal()) +end + +function LoadZones(; number, name, buses, maxactivepower, maxreactivepower, ) + LoadZones(number, name, buses, maxactivepower, maxreactivepower, ) +end + +# Constructor for demo purposes; non-functional. + +function LoadZones(::Nothing) + LoadZones(; + number=0, + name="init", + buses=[Bus(nothing)], + maxactivepower=0.0, + maxreactivepower=0.0, + ) +end + +"""Get LoadZones number.""" +get_number(value::LoadZones) = value.number +"""Get LoadZones name.""" +get_name(value::LoadZones) = value.name +"""Get LoadZones buses.""" +get_buses(value::LoadZones) = value.buses +"""Get LoadZones maxactivepower.""" +get_maxactivepower(value::LoadZones) = value.maxactivepower +"""Get LoadZones maxreactivepower.""" +get_maxreactivepower(value::LoadZones) = value.maxreactivepower +"""Get LoadZones internal.""" +get_internal(value::LoadZones) = value.internal diff --git a/src/models/generated/MonitoredLine.jl b/src/models/generated/MonitoredLine.jl new file mode 100644 index 0000000000..ea320661a4 --- /dev/null +++ b/src/models/generated/MonitoredLine.jl @@ -0,0 +1,70 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct MonitoredLine <: ACBranch + name::String + available::Bool + activepower_flow::Float64 + reactivepower_flow::Float64 + arc::Arc + r::Float64 # System per-unit value + x::Float64 # System per-unit value + b::NamedTuple{(:from, :to), Tuple{Float64, Float64}} # System per-unit value + flowlimits::NamedTuple{(:from_to, :to_from), Tuple{Float64, Float64}} # TODO: throw warning above max SIL + rate::Float64 # TODO: compare to SIL (warn) (theoretical limit) + anglelimits::Min_Max + internal::InfrastructureSystemsInternal +end + +function MonitoredLine(name, available, activepower_flow, reactivepower_flow, arc, r, x, b, flowlimits, rate, anglelimits, ) + MonitoredLine(name, available, activepower_flow, reactivepower_flow, arc, r, x, b, flowlimits, rate, anglelimits, InfrastructureSystemsInternal()) +end + +function MonitoredLine(; name, available, activepower_flow, reactivepower_flow, arc, r, x, b, flowlimits, rate, anglelimits, ) + MonitoredLine(name, available, activepower_flow, reactivepower_flow, arc, r, x, b, flowlimits, rate, anglelimits, ) +end + +# Constructor for demo purposes; non-functional. + +function MonitoredLine(::Nothing) + MonitoredLine(; + name="init", + available=false, + activepower_flow=0.0, + reactivepower_flow=0.0, + arc=Arc(Bus(nothing), Bus(nothing)), + r=0.0, + x=0.0, + b=(from=0.0, to=0.0), + flowlimits=(from_to=0.0, to_from=0.0), + rate=0.0, + anglelimits=(min=-1.571, max=1.571), + ) +end + +"""Get MonitoredLine name.""" +get_name(value::MonitoredLine) = value.name +"""Get MonitoredLine available.""" +get_available(value::MonitoredLine) = value.available +"""Get MonitoredLine activepower_flow.""" +get_activepower_flow(value::MonitoredLine) = value.activepower_flow +"""Get MonitoredLine reactivepower_flow.""" +get_reactivepower_flow(value::MonitoredLine) = value.reactivepower_flow +"""Get MonitoredLine arc.""" +get_arc(value::MonitoredLine) = value.arc +"""Get MonitoredLine r.""" +get_r(value::MonitoredLine) = value.r +"""Get MonitoredLine x.""" +get_x(value::MonitoredLine) = value.x +"""Get MonitoredLine b.""" +get_b(value::MonitoredLine) = value.b +"""Get MonitoredLine flowlimits.""" +get_flowlimits(value::MonitoredLine) = value.flowlimits +"""Get MonitoredLine rate.""" +get_rate(value::MonitoredLine) = value.rate +"""Get MonitoredLine anglelimits.""" +get_anglelimits(value::MonitoredLine) = value.anglelimits +"""Get MonitoredLine internal.""" +get_internal(value::MonitoredLine) = value.internal diff --git a/src/models/generated/PhaseShiftingTransformer.jl b/src/models/generated/PhaseShiftingTransformer.jl new file mode 100644 index 0000000000..661b57373e --- /dev/null +++ b/src/models/generated/PhaseShiftingTransformer.jl @@ -0,0 +1,70 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct PhaseShiftingTransformer <: ACBranch + name::String + available::Bool + activepower_flow::Float64 + reactivepower_flow::Float64 + arc::Arc + r::Float64 # System per-unit value + x::Float64 # System per-unit value + primaryshunt::Float64 + tap::Float64 + α::Float64 + rate::Union{Nothing, Float64} + internal::InfrastructureSystemsInternal +end + +function PhaseShiftingTransformer(name, available, activepower_flow, reactivepower_flow, arc, r, x, primaryshunt, tap, α, rate, ) + PhaseShiftingTransformer(name, available, activepower_flow, reactivepower_flow, arc, r, x, primaryshunt, tap, α, rate, InfrastructureSystemsInternal()) +end + +function PhaseShiftingTransformer(; name, available, activepower_flow, reactivepower_flow, arc, r, x, primaryshunt, tap, α, rate, ) + PhaseShiftingTransformer(name, available, activepower_flow, reactivepower_flow, arc, r, x, primaryshunt, tap, α, rate, ) +end + +# Constructor for demo purposes; non-functional. + +function PhaseShiftingTransformer(::Nothing) + PhaseShiftingTransformer(; + name="init", + available=false, + activepower_flow=0.0, + reactivepower_flow=0.0, + arc=Arc(Bus(nothing), Bus(nothing)), + r=0.0, + x=0.0, + primaryshunt=0.0, + tap=1.0, + α=0.0, + rate=0.0, + ) +end + +"""Get PhaseShiftingTransformer name.""" +get_name(value::PhaseShiftingTransformer) = value.name +"""Get PhaseShiftingTransformer available.""" +get_available(value::PhaseShiftingTransformer) = value.available +"""Get PhaseShiftingTransformer activepower_flow.""" +get_activepower_flow(value::PhaseShiftingTransformer) = value.activepower_flow +"""Get PhaseShiftingTransformer reactivepower_flow.""" +get_reactivepower_flow(value::PhaseShiftingTransformer) = value.reactivepower_flow +"""Get PhaseShiftingTransformer arc.""" +get_arc(value::PhaseShiftingTransformer) = value.arc +"""Get PhaseShiftingTransformer r.""" +get_r(value::PhaseShiftingTransformer) = value.r +"""Get PhaseShiftingTransformer x.""" +get_x(value::PhaseShiftingTransformer) = value.x +"""Get PhaseShiftingTransformer primaryshunt.""" +get_primaryshunt(value::PhaseShiftingTransformer) = value.primaryshunt +"""Get PhaseShiftingTransformer tap.""" +get_tap(value::PhaseShiftingTransformer) = value.tap +"""Get PhaseShiftingTransformer α.""" +get_α(value::PhaseShiftingTransformer) = value.α +"""Get PhaseShiftingTransformer rate.""" +get_rate(value::PhaseShiftingTransformer) = value.rate +"""Get PhaseShiftingTransformer internal.""" +get_internal(value::PhaseShiftingTransformer) = value.internal diff --git a/src/models/generated/PowerLoad.jl b/src/models/generated/PowerLoad.jl new file mode 100644 index 0000000000..8c530e5c57 --- /dev/null +++ b/src/models/generated/PowerLoad.jl @@ -0,0 +1,58 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct PowerLoad <: StaticLoad + name::String + available::Bool + bus::Bus + model::Union{Nothing, LoadModel} + activepower::Float64 + reactivepower::Float64 + maxactivepower::Float64 + maxreactivepower::Float64 + internal::InfrastructureSystemsInternal +end + +function PowerLoad(name, available, bus, model, activepower, reactivepower, maxactivepower, maxreactivepower, ) + PowerLoad(name, available, bus, model, activepower, reactivepower, maxactivepower, maxreactivepower, InfrastructureSystemsInternal()) +end + +function PowerLoad(; name, available, bus, model, activepower, reactivepower, maxactivepower, maxreactivepower, ) + PowerLoad(name, available, bus, model, activepower, reactivepower, maxactivepower, maxreactivepower, ) +end + +# Constructor for demo purposes; non-functional. + +function PowerLoad(::Nothing) + PowerLoad(; + name="init", + available=false, + bus=Bus(nothing), + model=nothing, + activepower=0.0, + reactivepower=0.0, + maxactivepower=0.0, + maxreactivepower=0.0, + ) +end + +"""Get PowerLoad name.""" +get_name(value::PowerLoad) = value.name +"""Get PowerLoad available.""" +get_available(value::PowerLoad) = value.available +"""Get PowerLoad bus.""" +get_bus(value::PowerLoad) = value.bus +"""Get PowerLoad model.""" +get_model(value::PowerLoad) = value.model +"""Get PowerLoad activepower.""" +get_activepower(value::PowerLoad) = value.activepower +"""Get PowerLoad reactivepower.""" +get_reactivepower(value::PowerLoad) = value.reactivepower +"""Get PowerLoad maxactivepower.""" +get_maxactivepower(value::PowerLoad) = value.maxactivepower +"""Get PowerLoad maxreactivepower.""" +get_maxreactivepower(value::PowerLoad) = value.maxreactivepower +"""Get PowerLoad internal.""" +get_internal(value::PowerLoad) = value.internal diff --git a/src/models/generated/ProportionalReserve.jl b/src/models/generated/ProportionalReserve.jl new file mode 100644 index 0000000000..dc6c5a1efd --- /dev/null +++ b/src/models/generated/ProportionalReserve.jl @@ -0,0 +1,38 @@ +#= +This file is auto-generated. Do not edit. +=# + +"""Data Structure for a proportional reserve product for system simulations.""" +mutable struct ProportionalReserve <: Reserve + name::String + contributingdevices::Vector{Device} # devices from which the product can be procured + timeframe::Float64 # the relative saturation timeframe + internal::InfrastructureSystemsInternal +end + +function ProportionalReserve(name, contributingdevices, timeframe, ) + ProportionalReserve(name, contributingdevices, timeframe, InfrastructureSystemsInternal()) +end + +function ProportionalReserve(; name, contributingdevices, timeframe, ) + ProportionalReserve(name, contributingdevices, timeframe, ) +end + +# Constructor for demo purposes; non-functional. + +function ProportionalReserve(::Nothing) + ProportionalReserve(; + name="init", + contributingdevices=[ThermalStandard(nothing)], + timeframe=0.0, + ) +end + +"""Get ProportionalReserve name.""" +get_name(value::ProportionalReserve) = value.name +"""Get ProportionalReserve contributingdevices.""" +get_contributingdevices(value::ProportionalReserve) = value.contributingdevices +"""Get ProportionalReserve timeframe.""" +get_timeframe(value::ProportionalReserve) = value.timeframe +"""Get ProportionalReserve internal.""" +get_internal(value::ProportionalReserve) = value.internal diff --git a/src/models/generated/RenewableDispatch.jl b/src/models/generated/RenewableDispatch.jl new file mode 100644 index 0000000000..97cb2adee1 --- /dev/null +++ b/src/models/generated/RenewableDispatch.jl @@ -0,0 +1,54 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct RenewableDispatch <: RenewableGen + name::String + available::Bool + bus::Bus + activepower::Float64 + reactivepower::Float64 + tech::TechRenewable + op_cost::TwoPartCost + internal::InfrastructureSystemsInternal +end + +function RenewableDispatch(name, available, bus, activepower, reactivepower, tech, op_cost, ) + RenewableDispatch(name, available, bus, activepower, reactivepower, tech, op_cost, InfrastructureSystemsInternal()) +end + +function RenewableDispatch(; name, available, bus, activepower, reactivepower, tech, op_cost, ) + RenewableDispatch(name, available, bus, activepower, reactivepower, tech, op_cost, ) +end + +# Constructor for demo purposes; non-functional. + +function RenewableDispatch(::Nothing) + RenewableDispatch(; + name="init", + available=false, + bus=Bus(nothing), + activepower=0.0, + reactivepower=0.0, + tech=TechRenewable(nothing), + op_cost=TwoPartCost(nothing), + ) +end + +"""Get RenewableDispatch name.""" +get_name(value::RenewableDispatch) = value.name +"""Get RenewableDispatch available.""" +get_available(value::RenewableDispatch) = value.available +"""Get RenewableDispatch bus.""" +get_bus(value::RenewableDispatch) = value.bus +"""Get RenewableDispatch activepower.""" +get_activepower(value::RenewableDispatch) = value.activepower +"""Get RenewableDispatch reactivepower.""" +get_reactivepower(value::RenewableDispatch) = value.reactivepower +"""Get RenewableDispatch tech.""" +get_tech(value::RenewableDispatch) = value.tech +"""Get RenewableDispatch op_cost.""" +get_op_cost(value::RenewableDispatch) = value.op_cost +"""Get RenewableDispatch internal.""" +get_internal(value::RenewableDispatch) = value.internal diff --git a/src/models/generated/RenewableFix.jl b/src/models/generated/RenewableFix.jl new file mode 100644 index 0000000000..a0c90ff67c --- /dev/null +++ b/src/models/generated/RenewableFix.jl @@ -0,0 +1,50 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct RenewableFix <: RenewableGen + name::String + available::Bool + bus::Bus + activepower::Float64 + reactivepower::Float64 + tech::TechRenewable + internal::InfrastructureSystemsInternal +end + +function RenewableFix(name, available, bus, activepower, reactivepower, tech, ) + RenewableFix(name, available, bus, activepower, reactivepower, tech, InfrastructureSystemsInternal()) +end + +function RenewableFix(; name, available, bus, activepower, reactivepower, tech, ) + RenewableFix(name, available, bus, activepower, reactivepower, tech, ) +end + +# Constructor for demo purposes; non-functional. + +function RenewableFix(::Nothing) + RenewableFix(; + name="init", + available=false, + bus=Bus(nothing), + activepower=0.0, + reactivepower=0.0, + tech=TechRenewable(nothing), + ) +end + +"""Get RenewableFix name.""" +get_name(value::RenewableFix) = value.name +"""Get RenewableFix available.""" +get_available(value::RenewableFix) = value.available +"""Get RenewableFix bus.""" +get_bus(value::RenewableFix) = value.bus +"""Get RenewableFix activepower.""" +get_activepower(value::RenewableFix) = value.activepower +"""Get RenewableFix reactivepower.""" +get_reactivepower(value::RenewableFix) = value.reactivepower +"""Get RenewableFix tech.""" +get_tech(value::RenewableFix) = value.tech +"""Get RenewableFix internal.""" +get_internal(value::RenewableFix) = value.internal diff --git a/src/models/generated/StaticReserve.jl b/src/models/generated/StaticReserve.jl new file mode 100644 index 0000000000..1c35f17faf --- /dev/null +++ b/src/models/generated/StaticReserve.jl @@ -0,0 +1,42 @@ +#= +This file is auto-generated. Do not edit. +=# + +"""Data Structure for the procurement products for system simulations.""" +mutable struct StaticReserve <: Reserve + name::String + contributingdevices::Vector{Device} # devices from which the product can be procured + timeframe::Float64 # the relative saturation timeframe + requirement::Float64 # the required quantity of the product should be scaled by a Forecast + internal::InfrastructureSystemsInternal +end + +function StaticReserve(name, contributingdevices, timeframe, requirement, ) + StaticReserve(name, contributingdevices, timeframe, requirement, InfrastructureSystemsInternal()) +end + +function StaticReserve(; name, contributingdevices, timeframe, requirement, ) + StaticReserve(name, contributingdevices, timeframe, requirement, ) +end + +# Constructor for demo purposes; non-functional. + +function StaticReserve(::Nothing) + StaticReserve(; + name="init", + contributingdevices=[ThermalStandard(nothing)], + timeframe=0.0, + requirement=0.0, + ) +end + +"""Get StaticReserve name.""" +get_name(value::StaticReserve) = value.name +"""Get StaticReserve contributingdevices.""" +get_contributingdevices(value::StaticReserve) = value.contributingdevices +"""Get StaticReserve timeframe.""" +get_timeframe(value::StaticReserve) = value.timeframe +"""Get StaticReserve requirement.""" +get_requirement(value::StaticReserve) = value.requirement +"""Get StaticReserve internal.""" +get_internal(value::StaticReserve) = value.internal diff --git a/src/models/generated/TapTransformer.jl b/src/models/generated/TapTransformer.jl new file mode 100644 index 0000000000..2e0a3f329f --- /dev/null +++ b/src/models/generated/TapTransformer.jl @@ -0,0 +1,66 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct TapTransformer <: ACBranch + name::String + available::Bool + activepower_flow::Float64 + reactivepower_flow::Float64 + arc::Arc + r::Float64 # System per-unit value + x::Float64 # System per-unit value + primaryshunt::Float64 # System per-unit value + tap::Float64 + rate::Union{Nothing, Float64} + internal::InfrastructureSystemsInternal +end + +function TapTransformer(name, available, activepower_flow, reactivepower_flow, arc, r, x, primaryshunt, tap, rate, ) + TapTransformer(name, available, activepower_flow, reactivepower_flow, arc, r, x, primaryshunt, tap, rate, InfrastructureSystemsInternal()) +end + +function TapTransformer(; name, available, activepower_flow, reactivepower_flow, arc, r, x, primaryshunt, tap, rate, ) + TapTransformer(name, available, activepower_flow, reactivepower_flow, arc, r, x, primaryshunt, tap, rate, ) +end + +# Constructor for demo purposes; non-functional. + +function TapTransformer(::Nothing) + TapTransformer(; + name="init", + available=false, + activepower_flow=0.0, + reactivepower_flow=0.0, + arc=Arc(Bus(nothing), Bus(nothing)), + r=0.0, + x=0.0, + primaryshunt=0.0, + tap=1.0, + rate=0.0, + ) +end + +"""Get TapTransformer name.""" +get_name(value::TapTransformer) = value.name +"""Get TapTransformer available.""" +get_available(value::TapTransformer) = value.available +"""Get TapTransformer activepower_flow.""" +get_activepower_flow(value::TapTransformer) = value.activepower_flow +"""Get TapTransformer reactivepower_flow.""" +get_reactivepower_flow(value::TapTransformer) = value.reactivepower_flow +"""Get TapTransformer arc.""" +get_arc(value::TapTransformer) = value.arc +"""Get TapTransformer r.""" +get_r(value::TapTransformer) = value.r +"""Get TapTransformer x.""" +get_x(value::TapTransformer) = value.x +"""Get TapTransformer primaryshunt.""" +get_primaryshunt(value::TapTransformer) = value.primaryshunt +"""Get TapTransformer tap.""" +get_tap(value::TapTransformer) = value.tap +"""Get TapTransformer rate.""" +get_rate(value::TapTransformer) = value.rate +"""Get TapTransformer internal.""" +get_internal(value::TapTransformer) = value.internal diff --git a/src/models/generated/TechHydro.jl b/src/models/generated/TechHydro.jl new file mode 100644 index 0000000000..11411d8bbd --- /dev/null +++ b/src/models/generated/TechHydro.jl @@ -0,0 +1,50 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct TechHydro <: TechnicalParams + rating::Float64 # Thermal limited MVA Power Output of the unit. <= Capacity + primemover::PrimeMovers # PrimeMover Technology according to EIA 923 + activepowerlimits::Min_Max + reactivepowerlimits::Union{Nothing, Min_Max} + ramplimits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}} + timelimits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}} + internal::InfrastructureSystemsInternal +end + +function TechHydro(rating, primemover, activepowerlimits, reactivepowerlimits, ramplimits, timelimits, ) + TechHydro(rating, primemover, activepowerlimits, reactivepowerlimits, ramplimits, timelimits, InfrastructureSystemsInternal()) +end + +function TechHydro(; rating, primemover, activepowerlimits, reactivepowerlimits, ramplimits, timelimits, ) + TechHydro(rating, primemover, activepowerlimits, reactivepowerlimits, ramplimits, timelimits, ) +end + +# Constructor for demo purposes; non-functional. + +function TechHydro(::Nothing) + TechHydro(; + rating=0.0, + primemover=HY::PrimeMovers, + activepowerlimits=(min=0.0, max=0.0), + reactivepowerlimits=nothing, + ramplimits=nothing, + timelimits=nothing, + ) +end + +"""Get TechHydro rating.""" +get_rating(value::TechHydro) = value.rating +"""Get TechHydro primemover.""" +get_primemover(value::TechHydro) = value.primemover +"""Get TechHydro activepowerlimits.""" +get_activepowerlimits(value::TechHydro) = value.activepowerlimits +"""Get TechHydro reactivepowerlimits.""" +get_reactivepowerlimits(value::TechHydro) = value.reactivepowerlimits +"""Get TechHydro ramplimits.""" +get_ramplimits(value::TechHydro) = value.ramplimits +"""Get TechHydro timelimits.""" +get_timelimits(value::TechHydro) = value.timelimits +"""Get TechHydro internal.""" +get_internal(value::TechHydro) = value.internal diff --git a/src/models/generated/TechRenewable.jl b/src/models/generated/TechRenewable.jl new file mode 100644 index 0000000000..d633b3f8d5 --- /dev/null +++ b/src/models/generated/TechRenewable.jl @@ -0,0 +1,42 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct TechRenewable <: TechnicalParams + rating::Float64 # Thermal limited MVA Power Output of the unit. <= Capacity + primemover::PrimeMovers # PrimeMover Technology according to EIA 923 + reactivepowerlimits::Union{Nothing, Min_Max} + powerfactor::Float64 + internal::InfrastructureSystemsInternal +end + +function TechRenewable(rating, primemover, reactivepowerlimits, powerfactor, ) + TechRenewable(rating, primemover, reactivepowerlimits, powerfactor, InfrastructureSystemsInternal()) +end + +function TechRenewable(; rating, primemover, reactivepowerlimits, powerfactor, ) + TechRenewable(rating, primemover, reactivepowerlimits, powerfactor, ) +end + +# Constructor for demo purposes; non-functional. + +function TechRenewable(::Nothing) + TechRenewable(; + rating=0.0, + primemover=OT::PrimeMovers, + reactivepowerlimits=nothing, + powerfactor=1.0, + ) +end + +"""Get TechRenewable rating.""" +get_rating(value::TechRenewable) = value.rating +"""Get TechRenewable primemover.""" +get_primemover(value::TechRenewable) = value.primemover +"""Get TechRenewable reactivepowerlimits.""" +get_reactivepowerlimits(value::TechRenewable) = value.reactivepowerlimits +"""Get TechRenewable powerfactor.""" +get_powerfactor(value::TechRenewable) = value.powerfactor +"""Get TechRenewable internal.""" +get_internal(value::TechRenewable) = value.internal diff --git a/src/models/generated/TechThermal.jl b/src/models/generated/TechThermal.jl new file mode 100644 index 0000000000..553f66acec --- /dev/null +++ b/src/models/generated/TechThermal.jl @@ -0,0 +1,54 @@ +#= +This file is auto-generated. Do not edit. +=# + +"""Data Structure for the technical parameters of thermal generation technologies.""" +mutable struct TechThermal <: TechnicalParams + rating::Float64 # Thermal limited MVA Power Output of the unit. <= Capacity + primemover::PrimeMovers # PrimeMover Technology according to EIA 923 + fuel::ThermalFuels # PrimeMover Fuel according to EIA 923 + activepowerlimits::Min_Max + reactivepowerlimits::Union{Nothing, Min_Max} + ramplimits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}} + timelimits::Union{Nothing, NamedTuple{(:up, :down), Tuple{Float64, Float64}}} + internal::InfrastructureSystemsInternal +end + +function TechThermal(rating, primemover, fuel, activepowerlimits, reactivepowerlimits, ramplimits, timelimits, ) + TechThermal(rating, primemover, fuel, activepowerlimits, reactivepowerlimits, ramplimits, timelimits, InfrastructureSystemsInternal()) +end + +function TechThermal(; rating, primemover, fuel, activepowerlimits, reactivepowerlimits, ramplimits, timelimits, ) + TechThermal(rating, primemover, fuel, activepowerlimits, reactivepowerlimits, ramplimits, timelimits, ) +end + +# Constructor for demo purposes; non-functional. + +function TechThermal(::Nothing) + TechThermal(; + rating=0.0, + primemover=OT::PrimeMovers, + fuel=OTHER::ThermalFuels, + activepowerlimits=(min=0.0, max=0.0), + reactivepowerlimits=nothing, + ramplimits=nothing, + timelimits=nothing, + ) +end + +"""Get TechThermal rating.""" +get_rating(value::TechThermal) = value.rating +"""Get TechThermal primemover.""" +get_primemover(value::TechThermal) = value.primemover +"""Get TechThermal fuel.""" +get_fuel(value::TechThermal) = value.fuel +"""Get TechThermal activepowerlimits.""" +get_activepowerlimits(value::TechThermal) = value.activepowerlimits +"""Get TechThermal reactivepowerlimits.""" +get_reactivepowerlimits(value::TechThermal) = value.reactivepowerlimits +"""Get TechThermal ramplimits.""" +get_ramplimits(value::TechThermal) = value.ramplimits +"""Get TechThermal timelimits.""" +get_timelimits(value::TechThermal) = value.timelimits +"""Get TechThermal internal.""" +get_internal(value::TechThermal) = value.internal diff --git a/src/models/generated/ThermalStandard.jl b/src/models/generated/ThermalStandard.jl new file mode 100644 index 0000000000..251141643e --- /dev/null +++ b/src/models/generated/ThermalStandard.jl @@ -0,0 +1,54 @@ +#= +This file is auto-generated. Do not edit. +=# + +"""Data Structure for thermal generation technologies.""" +mutable struct ThermalStandard <: ThermalGen + name::String + available::Bool + bus::Bus + activepower::Float64 + reactivepower::Float64 + tech::Union{Nothing, TechThermal} + op_cost::ThreePartCost + internal::InfrastructureSystemsInternal +end + +function ThermalStandard(name, available, bus, activepower, reactivepower, tech, op_cost, ) + ThermalStandard(name, available, bus, activepower, reactivepower, tech, op_cost, InfrastructureSystemsInternal()) +end + +function ThermalStandard(; name, available, bus, activepower, reactivepower, tech, op_cost, ) + ThermalStandard(name, available, bus, activepower, reactivepower, tech, op_cost, ) +end + +# Constructor for demo purposes; non-functional. + +function ThermalStandard(::Nothing) + ThermalStandard(; + name="init", + available=false, + bus=Bus(nothing), + activepower=0.0, + reactivepower=0.0, + tech=TechThermal(nothing), + op_cost=ThreePartCost(nothing), + ) +end + +"""Get ThermalStandard name.""" +get_name(value::ThermalStandard) = value.name +"""Get ThermalStandard available.""" +get_available(value::ThermalStandard) = value.available +"""Get ThermalStandard bus.""" +get_bus(value::ThermalStandard) = value.bus +"""Get ThermalStandard activepower.""" +get_activepower(value::ThermalStandard) = value.activepower +"""Get ThermalStandard reactivepower.""" +get_reactivepower(value::ThermalStandard) = value.reactivepower +"""Get ThermalStandard tech.""" +get_tech(value::ThermalStandard) = value.tech +"""Get ThermalStandard op_cost.""" +get_op_cost(value::ThermalStandard) = value.op_cost +"""Get ThermalStandard internal.""" +get_internal(value::ThermalStandard) = value.internal diff --git a/src/models/generated/ThreePartCost.jl b/src/models/generated/ThreePartCost.jl new file mode 100644 index 0000000000..c5fb8c420a --- /dev/null +++ b/src/models/generated/ThreePartCost.jl @@ -0,0 +1,42 @@ +#= +This file is auto-generated. Do not edit. +=# + +"""Data Structure Operational Cost Data in Three parts fixed, variable cost and start - stop costs.""" +mutable struct ThreePartCost <: OperationalCost + variable::VariableCost + fixed::Float64 + startup::Float64 + shutdn::Float64 + internal::InfrastructureSystemsInternal +end + +function ThreePartCost(variable, fixed, startup, shutdn, ) + ThreePartCost(variable, fixed, startup, shutdn, InfrastructureSystemsInternal()) +end + +function ThreePartCost(; variable, fixed, startup, shutdn, ) + ThreePartCost(variable, fixed, startup, shutdn, ) +end + +# Constructor for demo purposes; non-functional. + +function ThreePartCost(::Nothing) + ThreePartCost(; + variable=VariableCost((0.0, 0.0)), + fixed=0.0, + startup=0.0, + shutdn=0.0, + ) +end + +"""Get ThreePartCost variable.""" +get_variable(value::ThreePartCost) = value.variable +"""Get ThreePartCost fixed.""" +get_fixed(value::ThreePartCost) = value.fixed +"""Get ThreePartCost startup.""" +get_startup(value::ThreePartCost) = value.startup +"""Get ThreePartCost shutdn.""" +get_shutdn(value::ThreePartCost) = value.shutdn +"""Get ThreePartCost internal.""" +get_internal(value::ThreePartCost) = value.internal diff --git a/src/models/generated/Transfer.jl b/src/models/generated/Transfer.jl new file mode 100644 index 0000000000..e1d819eb1f --- /dev/null +++ b/src/models/generated/Transfer.jl @@ -0,0 +1,42 @@ +#= +This file is auto-generated. Do not edit. +=# + + +mutable struct Transfer <: Service + name::String + contributingdevices::Vector{Device} + timeframe::Float64 + requirement::TimeSeries.TimeArray + internal::InfrastructureSystemsInternal +end + +function Transfer(name, contributingdevices, timeframe, requirement, ) + Transfer(name, contributingdevices, timeframe, requirement, InfrastructureSystemsInternal()) +end + +function Transfer(; name, contributingdevices, timeframe, requirement, ) + Transfer(name, contributingdevices, timeframe, requirement, ) +end + +# Constructor for demo purposes; non-functional. + +function Transfer(::Nothing) + Transfer(; + name="init", + contributingdevices=[ThermalStandard(nothing)], + timeframe=0.0, + requirement=[], + ) +end + +"""Get Transfer name.""" +get_name(value::Transfer) = value.name +"""Get Transfer contributingdevices.""" +get_contributingdevices(value::Transfer) = value.contributingdevices +"""Get Transfer timeframe.""" +get_timeframe(value::Transfer) = value.timeframe +"""Get Transfer requirement.""" +get_requirement(value::Transfer) = value.requirement +"""Get Transfer internal.""" +get_internal(value::Transfer) = value.internal diff --git a/src/models/generated/Transformer2W.jl b/src/models/generated/Transformer2W.jl new file mode 100644 index 0000000000..15108bfc6a --- /dev/null +++ b/src/models/generated/Transformer2W.jl @@ -0,0 +1,62 @@ +#= +This file is auto-generated. Do not edit. +=# + +"""The 2-W transformer model uses an equivalent circuit assuming the impedance is on the High Voltage Side of the transformer. The model allocates the iron losses and magnetizing susceptance to the primary side.""" +mutable struct Transformer2W <: ACBranch + name::String + available::Bool + activepower_flow::Float64 + reactivepower_flow::Float64 + arc::Arc + r::Float64 # System per-unit value + x::Float64 # System per-unit value + primaryshunt::Float64 # System per-unit value + rate::Union{Nothing, Float64} + internal::InfrastructureSystemsInternal +end + +function Transformer2W(name, available, activepower_flow, reactivepower_flow, arc, r, x, primaryshunt, rate, ) + Transformer2W(name, available, activepower_flow, reactivepower_flow, arc, r, x, primaryshunt, rate, InfrastructureSystemsInternal()) +end + +function Transformer2W(; name, available, activepower_flow, reactivepower_flow, arc, r, x, primaryshunt, rate, ) + Transformer2W(name, available, activepower_flow, reactivepower_flow, arc, r, x, primaryshunt, rate, ) +end + +# Constructor for demo purposes; non-functional. + +function Transformer2W(::Nothing) + Transformer2W(; + name="init", + available=false, + activepower_flow=0.0, + reactivepower_flow=0.0, + arc=Arc(Bus(nothing), Bus(nothing)), + r=0.0, + x=0.0, + primaryshunt=0.0, + rate=nothing, + ) +end + +"""Get Transformer2W name.""" +get_name(value::Transformer2W) = value.name +"""Get Transformer2W available.""" +get_available(value::Transformer2W) = value.available +"""Get Transformer2W activepower_flow.""" +get_activepower_flow(value::Transformer2W) = value.activepower_flow +"""Get Transformer2W reactivepower_flow.""" +get_reactivepower_flow(value::Transformer2W) = value.reactivepower_flow +"""Get Transformer2W arc.""" +get_arc(value::Transformer2W) = value.arc +"""Get Transformer2W r.""" +get_r(value::Transformer2W) = value.r +"""Get Transformer2W x.""" +get_x(value::Transformer2W) = value.x +"""Get Transformer2W primaryshunt.""" +get_primaryshunt(value::Transformer2W) = value.primaryshunt +"""Get Transformer2W rate.""" +get_rate(value::Transformer2W) = value.rate +"""Get Transformer2W internal.""" +get_internal(value::Transformer2W) = value.internal diff --git a/src/models/generated/TwoPartCost.jl b/src/models/generated/TwoPartCost.jl new file mode 100644 index 0000000000..8bf83fe474 --- /dev/null +++ b/src/models/generated/TwoPartCost.jl @@ -0,0 +1,34 @@ +#= +This file is auto-generated. Do not edit. +=# + +"""Data Structure Operational Cost Data in two parts fixed and variable cost.""" +mutable struct TwoPartCost <: OperationalCost + variable::VariableCost + fixed::Float64 + internal::InfrastructureSystemsInternal +end + +function TwoPartCost(variable, fixed, ) + TwoPartCost(variable, fixed, InfrastructureSystemsInternal()) +end + +function TwoPartCost(; variable, fixed, ) + TwoPartCost(variable, fixed, ) +end + +# Constructor for demo purposes; non-functional. + +function TwoPartCost(::Nothing) + TwoPartCost(; + variable=VariableCost((0.0, 0.0)), + fixed=0.0, + ) +end + +"""Get TwoPartCost variable.""" +get_variable(value::TwoPartCost) = value.variable +"""Get TwoPartCost fixed.""" +get_fixed(value::TwoPartCost) = value.fixed +"""Get TwoPartCost internal.""" +get_internal(value::TwoPartCost) = value.internal diff --git a/src/models/generated/VSCDCLine.jl b/src/models/generated/VSCDCLine.jl new file mode 100644 index 0000000000..e636d5a2e1 --- /dev/null +++ b/src/models/generated/VSCDCLine.jl @@ -0,0 +1,66 @@ +#= +This file is auto-generated. Do not edit. +=# + +"""As implemented in Milano's Book, Page 397""" +mutable struct VSCDCLine <: DCBranch + name::String + available::Bool + activepower_flow::Float64 + arc::Arc + rectifier_taplimits::Min_Max + rectifier_xrc::Float64 + rectifier_firing_angle::Min_Max + inverter_taplimits::Min_Max + inverter_xrc::Float64 + inverter_firing_angle::Min_Max + internal::InfrastructureSystemsInternal +end + +function VSCDCLine(name, available, activepower_flow, arc, rectifier_taplimits, rectifier_xrc, rectifier_firing_angle, inverter_taplimits, inverter_xrc, inverter_firing_angle, ) + VSCDCLine(name, available, activepower_flow, arc, rectifier_taplimits, rectifier_xrc, rectifier_firing_angle, inverter_taplimits, inverter_xrc, inverter_firing_angle, InfrastructureSystemsInternal()) +end + +function VSCDCLine(; name, available, activepower_flow, arc, rectifier_taplimits, rectifier_xrc, rectifier_firing_angle, inverter_taplimits, inverter_xrc, inverter_firing_angle, ) + VSCDCLine(name, available, activepower_flow, arc, rectifier_taplimits, rectifier_xrc, rectifier_firing_angle, inverter_taplimits, inverter_xrc, inverter_firing_angle, ) +end + +# Constructor for demo purposes; non-functional. + +function VSCDCLine(::Nothing) + VSCDCLine(; + name="init", + available=false, + activepower_flow=0.0, + arc=Arc(Bus(nothing), Bus(nothing)), + rectifier_taplimits=(min=0.0, max=0.0), + rectifier_xrc=0.0, + rectifier_firing_angle=(min=0.0, max=0.0), + inverter_taplimits=(min=0.0, max=0.0), + inverter_xrc=0.0, + inverter_firing_angle=(min=0.0, max=0.0), + ) +end + +"""Get VSCDCLine name.""" +get_name(value::VSCDCLine) = value.name +"""Get VSCDCLine available.""" +get_available(value::VSCDCLine) = value.available +"""Get VSCDCLine activepower_flow.""" +get_activepower_flow(value::VSCDCLine) = value.activepower_flow +"""Get VSCDCLine arc.""" +get_arc(value::VSCDCLine) = value.arc +"""Get VSCDCLine rectifier_taplimits.""" +get_rectifier_taplimits(value::VSCDCLine) = value.rectifier_taplimits +"""Get VSCDCLine rectifier_xrc.""" +get_rectifier_xrc(value::VSCDCLine) = value.rectifier_xrc +"""Get VSCDCLine rectifier_firing_angle.""" +get_rectifier_firing_angle(value::VSCDCLine) = value.rectifier_firing_angle +"""Get VSCDCLine inverter_taplimits.""" +get_inverter_taplimits(value::VSCDCLine) = value.inverter_taplimits +"""Get VSCDCLine inverter_xrc.""" +get_inverter_xrc(value::VSCDCLine) = value.inverter_xrc +"""Get VSCDCLine inverter_firing_angle.""" +get_inverter_firing_angle(value::VSCDCLine) = value.inverter_firing_angle +"""Get VSCDCLine internal.""" +get_internal(value::VSCDCLine) = value.internal diff --git a/src/models/generated/includes.jl b/src/models/generated/includes.jl new file mode 100644 index 0000000000..18149b1136 --- /dev/null +++ b/src/models/generated/includes.jl @@ -0,0 +1,95 @@ +include("TwoPartCost.jl") +include("ThreePartCost.jl") +include("TechHydro.jl") +include("TechRenewable.jl") +include("TechThermal.jl") +include("Bus.jl") +include("Arc.jl") +include("Line.jl") +include("MonitoredLine.jl") +include("PhaseShiftingTransformer.jl") +include("TapTransformer.jl") +include("Transformer2W.jl") +include("HVDCLine.jl") +include("VSCDCLine.jl") +include("InterruptibleLoad.jl") +include("FixedAdmittance.jl") +include("PowerLoad.jl") +include("HydroDispatch.jl") +include("HydroFix.jl") +include("HydroStorage.jl") +include("RenewableDispatch.jl") +include("RenewableFix.jl") +include("ThermalStandard.jl") +include("LoadZones.jl") +include("GenericBattery.jl") +include("ProportionalReserve.jl") +include("StaticReserve.jl") +include("Transfer.jl") + +export get_Y +export get_activepower +export get_activepower_flow +export get_activepowerlimits +export get_activepowerlimits_from +export get_activepowerlimits_to +export get_angle +export get_anglelimits +export get_arc +export get_available +export get_b +export get_basevoltage +export get_bus +export get_buses +export get_bustype +export get_capacity +export get_contributingdevices +export get_efficiency +export get_energy +export get_fixed +export get_flowlimits +export get_from +export get_fuel +export get_initial_storage +export get_inputactivepowerlimits +export get_internal +export get_inverter_firing_angle +export get_inverter_taplimits +export get_inverter_xrc +export get_loss +export get_maxactivepower +export get_maxreactivepower +export get_model +export get_name +export get_number +export get_op_cost +export get_outputactivepowerlimits +export get_powerfactor +export get_primaryshunt +export get_primemover +export get_r +export get_ramplimits +export get_rate +export get_rating +export get_reactivepower +export get_reactivepower_flow +export get_reactivepowerlimits +export get_reactivepowerlimits_from +export get_reactivepowerlimits_to +export get_rectifier_firing_angle +export get_rectifier_taplimits +export get_rectifier_xrc +export get_requirement +export get_shutdn +export get_startup +export get_storagecapacity +export get_tap +export get_tech +export get_timeframe +export get_timelimits +export get_to +export get_variable +export get_voltage +export get_voltagelimits +export get_x +export get_α diff --git a/src/models/generation.jl b/src/models/generation.jl index f8f13ffa0f..9af8008183 100644 --- a/src/models/generation.jl +++ b/src/models/generation.jl @@ -1,53 +1,15 @@ abstract type Generator <: Injection end const Generators = Array{<: Generator, 1} -include("generation/tech_common.jl") -include("generation/econ_common.jl") -include("generation/renewable_generation.jl") -include("generation/thermal_generation.jl") -include("generation/hydro_generation.jl") +abstract type HydroGen <: Generator end +abstract type RenewableGen <: Generator end +abstract type ThermalGen <: Generator end -struct GenClasses <: Component - thermal::Union{Nothing,Array{ <: ThermalGen,1}} - renewable::Union{Nothing,Array{ <: RenewableGen,1}} - hydro::Union{Nothing,Array{ <: HydroGen,1}} -end -# create iterator for GenClasses -function Base.iterate(iter::GenClasses, state=1) - if state > length(iter) - return nothing - else - return (getfield(iter, state), state+1) - end -end -Base.length(iter::GenClasses) = length(fieldnames(GenClasses)) -Base.eltype(iter::GenClasses) = Union{Nothing, - Array{ <: ThermalGen,1}, - Array{ <: RenewableGen,1}, - Array{ <: HydroGen,1}} - -# Generator Classifier -function genclassifier(gen::Array{T}) where T <: Generator - - t = [d for d in gen if isa(d, ThermalGen)] - r = [d for d in gen if isa(d, RenewableGen)] - h = [d for d in gen if isa(d, HydroGen)] - - #Check for data consistency - if isempty(t) - t = nothing - end - - if isempty(r) - r = nothing - end - - if isempty(h) - h = nothing - end - - generators = GenClasses(t, r, h) - return generators +function IS.get_limits(valid_range::Union{NamedTuple{(:min,:max)}, NamedTuple{(:max,:min)}}, + unused::T) where T <: Generator + # Gets min and max value defined for a field, + # e.g. "valid_range": {"min":-1.571, "max":1.571}. + return (min = valid_range.min, max = valid_range.max, zero = 0.0) end diff --git a/src/models/generation/econ_common.jl b/src/models/generation/econ_common.jl deleted file mode 100644 index 46256d8803..0000000000 --- a/src/models/generation/econ_common.jl +++ /dev/null @@ -1,46 +0,0 @@ -"""" -Data Structure for the economical parameters of renewable generation technologies. - The data structure can be called calling all the fields directly or using named fields. - All the limits are defined by NamedTuples and some fields can take ```nothing``` - - ## Examples - - - - -""" -struct EconRenewable <: TechnicalParams - curtailpenalty::Float64 # [$/MWh] - variablecost::Union{Float64,Nothing} # [$/MWh] -end - -EconRenewable(; curtailcost = 0.0, variablecost = nothing) = EconRenewable(curtailcost, variablecost) - - -"""" -Data Structure for the economical parameters of thermal generation technologies. - The data structure can be called calling all the fields directly or using named fields. - All the limits are defined by NamedTuples and some fields can take ```nothing``` - - ## Examples - - - - -""" -struct EconThermal <: TechnicalParams - capacity::Float64 # [MW] - variablecost::Union{Function,Array{Tuple{Float64, Float64}}} # [$/MWh] - fixedcost::Float64 # [$/h] - startupcost::Float64 # [$] - shutdncost::Float64 # [$] - annualcapacityfactor::Union{Float64,Nothing} # [0-1] -end - -EconThermal(; capacity = 0.0, - variablecost = [(0.0,1.0)], - fixedcost = 0.0, - startupcost = 0.0, - shutdncost = 0.0, - annualcapacityfactor = nothing - ) = EconThermal(capacity, variablecost, fixedcost, startupcost, shutdncost, annualcapacityfactor) diff --git a/src/models/generation/hydro_generation.jl b/src/models/generation/hydro_generation.jl deleted file mode 100644 index 6983879c40..0000000000 --- a/src/models/generation/hydro_generation.jl +++ /dev/null @@ -1,90 +0,0 @@ -abstract type - HydroGen <: Generator -end - - -struct TechHydro <: TechnicalParams - installedcapacity::Float64 - activepower::Float64 # [MW] - activepowerlimits::NamedTuple{(:min, :max),Tuple{Float64,Float64}} # [MW] - reactivepower::Union{Float64,Nothing} # [MVAr] - reactivepowerlimits::Union{NamedTuple{(:min, :max),Tuple{Float64,Float64}},Nothing} # [MVAr] - ramplimits::Union{NamedTuple{(:up, :down),Tuple{Float64,Float64}},Nothing} #MW/Hr - timelimits::Union{NamedTuple{(:up, :down),Tuple{Float64,Float64}},Nothing} # Hrs - function TechHydro(installedcapacity, activepower, activepowerlimits, reactivepower, reactivepowerlimits, ramplimits, timelimits) - - new(installedcapacity, activepower, PowerSystems.orderedlimits(activepowerlimits, "Real Power"), reactivepower, PowerSystems.orderedlimits(reactivepowerlimits, "Reactive Power"), ramplimits, timelimits) - - end -end - -TechHydro(;installedcapacity = 0.0, - activepower = 0.0, - activepowerlimits = (min = 0.0, max = 0.0), - reactivepower = nothing, - reactivepowerlimits = nothing, - ramplimits = nothing, - timelimits = nothing - ) = TechHydro(installedcapacity, activepower, activepowerlimits, reactivepower, reactivepowerlimits, ramplimits, timelimits) - - -struct EconHydro <: TechnicalParams - curtailpenalty::Float64 # [$/MWh] - variablecost::Union{Float64,Nothing} # [$/MWh] -end - -EconHydro(; cost = 0.0, curtailcost = 0.0) = EconHydro(cost, curtailcost) - -struct HydroFix <: HydroGen - name::String - available::Bool - bus::Bus - tech::TechHydro - scalingfactor::TimeSeries.TimeArray -end - -HydroFix(; name="init", - status = false, - bus = Bus(), - tech = TechHydro(), - scalingfactor = TimeSeries.TimeArray(Dates.today(),ones(1))) = HydroFix(name, status, bus, tech, scalingfactor) - - -struct HydroCurtailment <: HydroGen - name::String - available::Bool - bus::Bus - tech::TechHydro - econ::Union{EconHydro,Nothing} - scalingfactor::TimeSeries.TimeArray # [0-1] - function HydroCurtailment(name, status, bus, tech, curtailcost::Float64, scalingfactor) - econ = EconHydro(curtailcost, nothing) - new(name, status, bus, tech, econ, scalingfactor) - end -end - -HydroCurtailment(; name = "init", - status = false, - bus= Bus(), - tech = TechHydro(), - curtailcost = 0.0, - scalingfactor = TimeSeries.TimeArray(Dates.today(),ones(1))) = HydroCurtailment(name, status, bus, tech, curtailcost, scalingfactor) - - -struct HydroStorage <: HydroGen - name::String - available::Bool - bus::Bus - tech::TechHydro - econ::Union{EconHydro,Nothing} - storagecapacity::Float64 #[m^3] - scalingfactor::TimeSeries.TimeArray -end - -HydroStorage(; name = "init", - status = false, - bus= Bus(), - tech = TechHydro(), - econ = EconHydro(), - storagecapacity = 0.0, - scalingfactor = TimeSeries.TimeArray(Dates.today(),ones(1))) = HydroStorage(name, status, bus, tech, econ, storagecapacity, scalingfactor) diff --git a/src/models/generation/renewable_generation.jl b/src/models/generation/renewable_generation.jl deleted file mode 100644 index 65fc037b66..0000000000 --- a/src/models/generation/renewable_generation.jl +++ /dev/null @@ -1,59 +0,0 @@ -abstract type - RenewableGen <: Generator -end - -struct RenewableFix <: RenewableGen - name::String - available::Bool - bus::Bus - tech::TechRenewable - scalingfactor::TimeSeries.TimeArray - function RenewableFix(name, status, bus, installedcapacity::Float64, scalingfactor) - tech = TechRenewable(installedcapacity, nothing, 1.0) - new(name, status, bus, tech, scalingfactor) - end -end - -RenewableFix(; name="init", - status = false, - bus = Bus(), - installedcapacity = 0.0, - scalingfactor = TimeSeries.TimeArray(Dates.today(),ones(1))) = RenewableFix(name, status, bus, installedcapacity, scalingfactor) - -struct RenewableCurtailment <: RenewableGen - name::String - available::Bool - bus::Bus - tech::TechRenewable - econ::Union{EconRenewable,Nothing} - scalingfactor::TimeSeries.TimeArray # [0-1] -end - -function RenewableCurtailment(name::String, status::Bool, bus::Bus, installedcapacity::Float64, econ::Union{EconRenewable,Nothing}, scalingfactor::TimeSeries.TimeArray) - tech = TechRenewable(installedcapacity, nothing, 1.0) - return RenewableCurtailment(name, status, bus, tech, econ, scalingfactor) -end - -RenewableCurtailment(; name = "init", - status = false, - bus= Bus(), - installedcapacity = 0.0, - econ = EconRenewable(), - scalingfactor = TimeSeries.TimeArray(Dates.today(),ones(1))) = RenewableCurtailment(name, status, bus, installedcapacity, econ, scalingfactor) - -struct RenewableFullDispatch <: RenewableGen - name::String - available::Bool - bus::Bus - tech::TechRenewable - econ::Union{EconRenewable,Nothing} - scalingfactor::TimeSeries.TimeArray # [0-1] -end - - -RenewableFullDispatch(; name = "init", - status = false, - bus= Bus(), - installedcapacity = 0.0, - econ = EconRenewable(), - scalingfactor = TimeSeries.TimeArray(Dates.today(),ones(1))) = RenewableCurtailment(name, status, bus, installedcapacity, econ, scalingfactor) \ No newline at end of file diff --git a/src/models/generation/tech_common.jl b/src/models/generation/tech_common.jl deleted file mode 100644 index 104d843550..0000000000 --- a/src/models/generation/tech_common.jl +++ /dev/null @@ -1,65 +0,0 @@ -# DOCTODO add docstring for TechRenewable -struct TechRenewable <: TechnicalParams - installedcapacity::Float64 # [MW] - reactivepowerlimits::Union{NamedTuple{(:min, :max),Tuple{Float64,Float64}},Nothing} # [MVar] - powerfactor::Union{Float64,Nothing} # [-1. -1] -end - -# DOCTODO document this constructor for TechRenewable -TechRenewable(; InstalledCapacity = 0, - reactivepowerlimits = nothing, - powerfactor = nothing - ) = TechRenewable(InstalledCapacity, reactivepowerlimits, - powerfactor) - - -""" - TechThermal(activepower::Float64, - activepowerlimits::NamedTuple{(:min, :max),Tuple{Float64,Float64}}, - reactivepower::Union{Float64,Nothing}, - reactivepowerlimits::Union{(min::Float64,max::Float64),Nothing}, - ramplimits::Union{NamedTuple{(:up, :down),Tuple{Float64,Float64}},Nothing}, - timelimits::Union{NamedTuple{(:min, :max),Tuple{Float64,Float64}},Nothing}) - -Data Structure for the economical parameters of thermal generation technologies. - The data structure can be called calling all the fields directly or using named fields. - Two examples [DOCTODO only one example exists below] are provided one with minimal data definition and a more comprenhensive one - - # Examples - - ```jldoctest - - julia> Tech = TechThermal(activepower = 100.0, activepowerlimits = (min = 50.0, max = 200.0)) - [ Info: 'Reactive Power' limits defined as nothing - TechThermal: - activepower: 100.0 - activepowerlimits: (min = 50.0, max = 200.0) - reactivepower: nothing - reactivepowerlimits: nothing - ramplimits: nothing - timelimits: nothing - -""" -struct TechThermal <: TechnicalParams - activepower::Float64 # [MW] - activepowerlimits::NamedTuple{(:min, :max),Tuple{Float64,Float64}} # [MW] - reactivepower::Union{Float64,Nothing} # [MVAr] - reactivepowerlimits::Union{NamedTuple{(:min, :max),Tuple{Float64,Float64}},Nothing} # [MVAr] - ramplimits::Union{NamedTuple{(:up, :down),Tuple{Float64,Float64}},Nothing} #MW/Hr - timelimits::Union{NamedTuple{(:up, :down),Tuple{Float64,Float64}},Nothing} #Hr - function TechThermal(activepower, activepowerlimits, reactivepower, reactivepowerlimits, ramplimits, timelimits) - - new(activepower, PowerSystems.orderedlimits(activepowerlimits, "Real Power"), reactivepower, PowerSystems.orderedlimits(reactivepowerlimits, "Reactive Power"), ramplimits, timelimits) - - end -end - -# DOCTODO document this constructor -TechThermal(; activepower = 0.0, - activepowerlimits = (min = 0.0, max = 0.0), - reactivepower = nothing, - reactivepowerlimits = nothing, - ramplimits = nothing, - timelimits = nothing - ) = TechThermal(activepower, activepowerlimits, reactivepower, - reactivepowerlimits, ramplimits, timelimits) diff --git a/src/models/generation/thermal_generation.jl b/src/models/generation/thermal_generation.jl deleted file mode 100644 index 2c780a470c..0000000000 --- a/src/models/generation/thermal_generation.jl +++ /dev/null @@ -1,56 +0,0 @@ -"Abstract struct for thermal generation technologies" -abstract type - ThermalGen <: Generator -end - -"""" -Data Structure for thermal generation technologies. - The data structure contains all the information for technical and economical modeling. - The data fields can be filled using named fields or directly. - - Examples - - - - -""" -struct ThermalDispatch <: ThermalGen - name::String - available::Bool - bus::Bus - tech::Union{TechThermal,Nothing} - econ::Union{EconThermal,Nothing} -end - -ThermalDispatch(; name = "init", - status = false, - bus = Bus(), - tech = TechThermal(), - econ = EconThermal()) = ThermalDispatch(name, status, bus, tech, econ) - - - - -"""" -Data Structure for thermal generation technologies subjecto to seasonality constraints. - The data structure contains all the information for technical and economical modeling and an extra field for a time series. - The data fields can be filled using named fields or directly. - - Examples - -""" -struct ThermalGenSeason <: ThermalGen - name::String - available::Bool - bus::Bus - tech::Union{TechThermal,Nothing} - econ::Union{EconThermal,Nothing} - scalingfactor::TimeSeries.TimeArray -end - -ThermalGenSeason(; name = "init", - status = false, - bus = Bus(), - tech = TechThermal(), - econ = EconThermal(), - scalingfactor = TimeSeries.TimeArray(Dates.today(),ones(1))) = ThermalGenSeason(name, status, bus, tech, econ, scalingfactor) diff --git a/src/models/loads.jl b/src/models/loads.jl index e23746bc2c..3f71dd0c2d 100644 --- a/src/models/loads.jl +++ b/src/models/loads.jl @@ -1,5 +1,3 @@ abstract type ElectricLoad <: Injection end - -include("loads/electric_loads.jl") -include("loads/controllable_loads.jl") -include("loads/shunt_elements.jl") +abstract type StaticLoad <: ElectricLoad end +abstract type ControllableLoad <: ElectricLoad end diff --git a/src/models/loads/controllable_loads.jl b/src/models/loads/controllable_loads.jl deleted file mode 100644 index 5182888174..0000000000 --- a/src/models/loads/controllable_loads.jl +++ /dev/null @@ -1,15 +0,0 @@ -abstract type ControllableLoad <: ElectricLoad end - -struct InterruptibleLoad <: ControllableLoad - name::String - available::Bool - bus::Bus - model::String # [Z, I, P] - maxactivepower::Float64 # [MW] - maxreactivepower::Float64 # [MVAr] - sheddingcost::Float64 # $/MWh - scalingfactor::TimeSeries.TimeArray -end - -InterruptibleLoad(; name = "init", status = true, bus = Bus(), model = "0", maxactivepower = 0, maxreactivepower=0, sheddingcost = 999, - scalingfactor = TimeSeries.TimeArray(Dates.today(),ones(1))) = InterruptibleLoad(name, status, bus, model, maxactivepower, maxreactivepower, sheddingcost, scalingfactor) diff --git a/src/models/loads/electric_loads.jl b/src/models/loads/electric_loads.jl deleted file mode 100644 index 8cdcd421a0..0000000000 --- a/src/models/loads/electric_loads.jl +++ /dev/null @@ -1,33 +0,0 @@ -abstract type StaticLoad <: ElectricLoad end - -struct PowerLoad <: StaticLoad - name::String - available::Bool - bus::Bus - maxactivepower::Float64 # [MW] - maxreactivepower::Float64 # [MVAr] - scalingfactor::TimeSeries.TimeArray -end - -function PowerLoad(name::String, available::Bool, bus::Bus, maxactivepower::Float64, maxreactivepower::Float64, scalingfactor=nothing) - scalingfactor=TimeSeries.TimeArray(Dates.today(),ones(1)) - return PowerLoad(name, available, bus, maxactivepower, maxreactivepower, scalingfactor) -end - -function PowerLoadPF(name::String, available::Bool, bus::Bus, maxactivepower::Float64, power_factor::Float64, scalingfactor::TimeSeries.TimeArray) - maxreactivepower = maxactivepower*sin(acos(power_factor)) - return PowerLoad(name, available, bus, maxactivepower, maxreactivepower, scalingfactor) -end - -function PowerLoadPF(name::String, available::Bool, bus::Bus, maxactivepower::Float64, power_factor::Float64, scalingfactor=nothing) - scalingfactor=TimeSeries.TimeArray(Dates.today(),ones(1)) - maxreactivepower = maxactivepower*sin(acos(power_factor)) - return PowerLoad(name, available, bus, maxactivepower, maxreactivepower, scalingfactor) -end - - -PowerLoadPF(; name = "init", available = true, bus = Bus(), maxactivepower = 0.0, power_factor=1.0, - scalingfactor=TimeSeries.TimeArray(Dates.today(),ones(1))) = PowerLoadPF(name, available, bus, maxactivepower, power_factor, scalingfactor) - -PowerLoad(; name = "init", available = true, bus = Bus(), maxactivepower = 0.0, maxreactivepower=0.0, - scalingfactor=TimeSeries.TimeArray(Dates.today(),ones(1))) = PowerLoad(name, available, bus, maxactivepower, maxreactivepower, scalingfactor) \ No newline at end of file diff --git a/src/models/loads/shunt_elements.jl b/src/models/loads/shunt_elements.jl deleted file mode 100644 index f874109f91..0000000000 --- a/src/models/loads/shunt_elements.jl +++ /dev/null @@ -1,6 +0,0 @@ -struct FixedAdmittance <: ElectricLoad - name::String - available::Bool - bus::Bus - Y::Complex{Float64} # [Z] -end diff --git a/src/models/network.jl b/src/models/network.jl deleted file mode 100644 index 9a5bd4c64e..0000000000 --- a/src/models/network.jl +++ /dev/null @@ -1,17 +0,0 @@ -# TODO: Consider a dynamic rate problem, add time series in the rate calculations. - -struct Network - branches::Array{Branch} - ybus::SparseArrays.SparseMatrixCSC{Complex{Float64},Int64} - ptdf::Union{Array{Float64},Nothing} - incidence::Array{Int} -end - -function Network(branches::Array{T}, nodes::Array{Bus}) where {T<:Branch} - ybus = build_ybus(length(nodes),branches); - ptdf, A = buildptdf(branches, nodes) - - return Network(branches, ybus, ptdf, A) - -end - diff --git a/src/models/operational_cost.jl b/src/models/operational_cost.jl new file mode 100644 index 0000000000..10d14ea5ef --- /dev/null +++ b/src/models/operational_cost.jl @@ -0,0 +1,11 @@ +const VarCostArgs = Union{Float64, NTuple{2,Float64}, Vector{NTuple{2,Float64}}} + +abstract type OperationalCost <: TechnicalParams end + +mutable struct VariableCost{T} + cost::T +end + +get_cost(vc::PowerSystems.VariableCost) = vc.cost +Base.length(vc::PowerSystems.VariableCost) = length(vc.cost) +Base.getindex(vc::PowerSystems.VariableCost, ix::Int64) = getindex(vc.cost, ix) diff --git a/src/models/products/reserves.jl b/src/models/products/reserves.jl deleted file mode 100644 index 33722756cf..0000000000 --- a/src/models/products/reserves.jl +++ /dev/null @@ -1,98 +0,0 @@ - -abstract type - Reserve <: Service -end - -""" -ProportionalReserve(name::String, - contributingdevices::Device, - timeframe::Float64, - requirement::Dict{Any,Dict{Int,TimeSeries.TimeArray}}) - -Data Structure for a proportional reserve product for system simulations. -The data structure can be called calling all the fields directly or using named fields. -name - description -contributingdevices - devices from which the product can be procured -timeframe - the relative saturation timeframe -requirement - the required quantity of the product - -# Examples - -```jldoctest - - -""" -struct ProportionalReserve <: Reserve - name::String - contributingdevices::Array{Device} - timeframe::Float64 - requirement::TimeSeries.TimeArray -end - -function ProportionalReserve(name::String, - contributingdevices::Array{G}, - timeframe::Float64, - requirement::Float64, - loads::Array{T}) where {G <: Device, T <: ElectricLoad} - - totalload = zeros(0) - for i in TimeSeries.timestamp(loads[1].scalingfactor) - t = zeros(0) - for load in loads - push!(t,(load.maxactivepower*values(load.scalingfactor[i]))[1]) - end - push!(totalload,sum(t)) - end - requirement = TimeSeries.TimeArray(TimeSeries.timestamp(loads[1].scalingfactor),totalload*requirement) - - ProportionalReserve(name, contributingdevices, timeframe, requirement) -end - - -ProportionalReserve(;name = "init", - contributingdevices = [ThermalDispatch()], - timeframe = 0.0, - requirement = 0.03, - loads = [ PowerLoad()]) = ProportionalReserve(name, contributingdevices, timeframe, requirement, loads) - - -""" -StaticReserve(name::String, - contributingdevices::Device, - timeframe::Float64, - requirement::Float64}) - -Data Structure for the procurement products for system simulations. -The data structure can be called calling all the fields directly or using named fields. -name - description -contributingdevices - devices from which the product can be procured -timeframe - the relative saturation timeframe -requirement - the required quantity of the product - -# Examples - -```jldoctest - - -""" -struct StaticReserve <: Reserve - name::String - contributingdevices::Array{Device} - timeframe::Float64 - requirement::Float64 - - function StaticReserve(name::String, - contributingdevices::Array{Q}, - timeframe::Float64, - generators::Array{G}) where {Q <: Device, G <: TechThermal} - - requirement = maximum([gen.activepowerlimits[:max] for gen in generators]) - - new(name, contributingdevices, timeframe, requirement) - end -end - -StaticReserve(;name = "init", - contributingdevices = [ThermalDispatch()], - timeframe = 0.0, - generators = [TechThermal()]) = StaticReserve(name, contributingdevices, timeframe, generators) diff --git a/src/models/products/transfers.jl b/src/models/products/transfers.jl deleted file mode 100644 index da999a44e8..0000000000 --- a/src/models/products/transfers.jl +++ /dev/null @@ -1,7 +0,0 @@ - -struct Transfer <: Service - name::String - contributingdevices::Array{Device} - timeframe::Float64 - requirement::TimeSeries.TimeArray -end diff --git a/src/models/serialization.jl b/src/models/serialization.jl new file mode 100644 index 0000000000..85d52da320 --- /dev/null +++ b/src/models/serialization.jl @@ -0,0 +1,20 @@ + +"""Enables deserialization of VariableCost. The default implementation can't figure out the +variable Union. +""" +function JSON2.read(io::IO, ::Type{VariableCost}) + data = JSON2.read(io) + if data.cost isa Real + return VariableCost(Float64(data.cost)) + elseif data.cost[1] isa Array + variable = Vector{Tuple{Float64, Float64}}() + for array in data.cost + push!(variable, Tuple{Float64, Float64}(array)) + end + else + @assert data.cost isa Tuple || data.cost isa Array + variable = Tuple{Float64, Float64}(data.cost) + end + + return VariableCost(variable) +end diff --git a/src/models/services.jl b/src/models/services.jl index 981647dca7..c77c8e8b05 100644 --- a/src/models/services.jl +++ b/src/models/services.jl @@ -1,5 +1,78 @@ abstract type Service <: Component end +abstract type Reserve <: Service end -include("products/reserves.jl") -include("products/transfers.jl") +""" +All subtypes of Service define contributingdevices::Vector{Device}. The values get populated +with references to existing devices. The following functions override JSON encoding to +replace the devices with their UUIDs. This solves two problems: + +1. The information is redundant, consuming unnecessary space. Also, a user could modify + information in the JSON file in one place but not the other, which would be very + problematic. +2. The contributingdevices are stored in an vector of abstract types. JSON doesn't provide a + way to encode the actual concrete type, so deserialization would have to infer the type. + There is no guessing if the UUIDs are stored instead. The deserialization process can + replace the references with actual devices. + +These functions could be re-defined to accept subtypes of Component or PowerSystemType. +This is the minimum amount needed for now. +""" + +function JSON2.write(io::IO, service::T) where T <: Service + return JSON2.write(io, encode_for_json(service)) +end + +function JSON2.write(service::T) where T <: Service + return JSON2.write(encode_for_json(service)) +end + +function encode_for_json(service::T) where T <: Service + fields = fieldnames(T) + vals = [] + + for name in fields + val = getfield(service, name) + if val isa Vector{<:Device} + push!(vals, IS.get_uuid.(val)) + else + push!(vals, val) + end + end + + return NamedTuple{fields}(vals) +end + +"""Creates a Service object by decoding the data that was in JSON. This data stores the +values for the field contributingdevices as UUIDs, so this will lookup each device in +devices. +""" +function IS.convert_type( + ::Type{T}, + data::NamedTuple, + devices::Dict, + ) where T <: Service + @debug T data + values = [] + for (fieldname, fieldtype) in zip(fieldnames(T), fieldtypes(T)) + val = getfield(data, fieldname) + if fieldtype <: Vector{<:Device} + real_devices = [] + for item in val + uuid = Base.UUID(item.value) + service = devices[uuid] + push!(real_devices, service) + end + push!(values, real_devices) + else + obj = IS.convert_type(fieldtype, val) + push!(values, obj) + end + end + + return T(values...) +end + +function IS.convert_type(::Type{T}, data::Any) where T <: Service + error("This form of convert_type is not supported for Services") +end diff --git a/src/models/storage.jl b/src/models/storage.jl index f56353212a..c1b39d3e3c 100644 --- a/src/models/storage.jl +++ b/src/models/storage.jl @@ -1,3 +1 @@ abstract type Storage <: Injection end - -include("storage/batteries.jl") diff --git a/src/models/storage/batteries.jl b/src/models/storage/batteries.jl deleted file mode 100644 index 8b81c98be2..0000000000 --- a/src/models/storage/batteries.jl +++ /dev/null @@ -1,27 +0,0 @@ -struct GenericBattery <: Storage - name::String - available::Bool - bus::Bus - energy::Float64 # [MWh] - capacity::NamedTuple{(:min, :max),Tuple{Float64,Float64}} # [MWh] - activepower::Float64 # [MW] - inputactivepowerlimits::NamedTuple{(:min, :max),Tuple{Float64,Float64}} # [MW] - outputactivepowerlimits::NamedTuple{(:min, :max),Tuple{Float64,Float64}} # [MW] - efficiency::NamedTuple{(:in, :out),Tuple{Float64,Float64}} # [%] - reactivepower::Union{Float64,Nothing} # [MVAr] - reactivepowerlimits::Union{NamedTuple{(:min, :max),Tuple{Float64,Float64}},Nothing} # [MVAr] -end - -GenericBattery(; name = "init", - status = false, - bus = Bus(), - energy = 0.0, - capacity = (min = 0.0, max = 0.0), - activepower = 0.0, - inputactivepowerlimits = (min = 0.0, max = 0.0), - outputactivepowerlimits = (min = 0.0, max = 0.0), - efficiency = (in = 0.0, out = 0.0), - reactivepower = 0.0, - reactivepowerlimits = (min = 0.0, max = 0.0) - ) = GenericBattery(name, status, bus, energy, capacity, activepower, inputactivepowerlimits, - outputactivepowerlimits, efficiency, reactivepower, reactivepowerlimits) diff --git a/src/models/supplemental_constructors.jl b/src/models/supplemental_constructors.jl new file mode 100644 index 0000000000..8dd177c23b --- /dev/null +++ b/src/models/supplemental_constructors.jl @@ -0,0 +1,57 @@ +"""Accepts rating as a Float64 and then creates a TwoPartCost.""" +function TwoPartCost(variable_cost::T, args...) where {T <: VarCostArgs} + return TwoPartCost(VariableCost(variable_cost), args...) +end + +"""Accepts rating as a Float64 and then creates a ThreePartCost.""" +function ThreePartCost(variable_cost::T, args...) where {T <: VarCostArgs} + return ThreePartCost(VariableCost(variable_cost), args...) +end + +"""Accepts rating as a Float64 and then creates a TechRenewable.""" +function RenewableFix(name::String, available::Bool, bus::Bus, + activepower::Float64, reactivepower::Float64, + prime_mover::PrimeMovers, rating::Float64) + tech = TechRenewable(rating, prime_mover, nothing, 1.0) + RenewableFix(name, available, bus, activepower, reactivepower, tech) +end + +"""Accepts rating as a Float64 and then creates a TechRenewable.""" +function RenewableDispatch(name::String, available::Bool, bus::Bus, + activepower::Float64, reactivepower::Float64, + prime_mover::PrimeMovers, rating::Float64, op_cost::TwoPartCost) + tech = TechRenewable(rating, prime_mover, nothing, 1.0) + return RenewableDispatch(name, available, bus, activepower, reactivepower, tech, op_cost) +end + +function PowerLoadPF(name::String, available::Bool, bus::Bus, + model::Union{Nothing, LoadModel}, activepower::Float64, + maxactivepower::Float64, power_factor::Float64) + maxreactivepower = maxactivepower * sin(acos(power_factor)) + reactivepower = activepower * sin(acos(power_factor)) + return PowerLoad(name, + available, + bus, + model, + activepower, + reactivepower, + maxactivepower, + maxreactivepower) +end + +function PowerLoadPF(::Nothing) + return PowerLoadPF("init", true, Bus(nothing), nothing, 0.0, 0.0, 1.0) +end + +"""Accepts anglelimits as a Float64.""" +function Line(name, available::Bool, activepower_flow::Float64, + reactivepower_flow::Float64, arc::Arc, r, x, b, rate, anglelimits::Float64) + return Line(name, available, activepower_flow, reactivepower_flow, arc::Arc, r, x, b, rate, + (min=-anglelimits, max=anglelimits)) +end + +"""Allows construction with bus type specified as a string for legacy code.""" +function Bus(number, name, bustype::String, angle, voltage, voltagelimits, basevoltage) + return Bus(number, name, get_enum_value(BusType, bustype), angle, voltage, + voltagelimits, basevoltage, InfrastructureSystemsInternal()) +end diff --git a/src/models/topological_elements.jl b/src/models/topological_elements.jl index dc9157c2ec..291e2dda47 100644 --- a/src/models/topological_elements.jl +++ b/src/models/topological_elements.jl @@ -1,67 +1,15 @@ -""" - Bus - -A power-system bus. - -# Constructor -```julia -Bus(number, name, bustype, angle, voltage, voltagelimits, basevoltage) -``` - -# Arguments -* `number`::Int64 : number associated with the bus -* `name`::String : the name of the bus -* `bustype`::String : type of bus, [PV, PQ, SF]; may be `nothing` -* `angle`::Float64 : angle of the bus in degrees; may be `nothing` -* `voltage`::Float64 : voltage as a multiple of basevoltage; may be `nothing` -* `voltagelimits`::NamedTuple(min::Float64, max::Float64) : limits on the voltage variation as multiples of basevoltage; may be `nothing` -* `basevoltage`::Float64 : the base voltage in kV; may be `nothing` - -""" -struct Bus <: Injection - # field docstrings work here! (they are not for System) - """ number associated with the bus """ - number::Int64 - """ the name of the bus """ - name::String - """ bus type, [PV, PQ, SF] """ - bustype::Union{String,Nothing} # [PV, PQ, SF] - """ angle of the bus in degrees """ - angle::Union{Float64,Nothing} # [degrees] - """ voltage as a multiple of basevoltage """ - voltage::Union{Float64,Nothing} # [pu] - """ limits on the voltage variation as multiples of basevoltage """ - voltagelimits::Union{NamedTuple{(:min, :max),Tuple{Float64,Float64}}, - Nothing} # [pu] - """ - the base voltage in kV - """ - basevoltage::Union{Float64,Nothing} # [kV] +abstract type Topology <: Component end + +function CheckBusParams(number, name, bustype, angle, voltage, voltagelimits, basevoltatge, + internal) + if !isnothing(bustype) + if bustype == SLACK::BusType + bustype = REF::BusType + @debug "Changed bus type from SLACK to" bustype + elseif bustype == ISOLATED::BusType + throw(DataFormatError("isolated buses are not supported; name=$name")) + end + end + + return number, name, bustype, angle, voltage, voltagelimits, basevoltatge, internal end - -# DOCTODO add this constructor type to docstring for Bus -Bus(; number = 0, - name = "init", - bustype = nothing, - angle = 0.0, - voltage = 0.0, - voltagelimits = (min = 0.0, max = 0.0), - basevoltage = nothing - ) = Bus(number, name, bustype, angle, voltage, - orderedlimits(voltagelimits, "Voltage"), basevoltage) - -# DOCTODO What are LoadZones? JJS 1/18/19 -struct LoadZones <: Injection - number::Int - name::String - buses::Array{Bus,1} - maxactivepower::Float64 - maxreactivepower::Float64 -end - -LoadZones(; number = 0, - name = "init", - buses = [Bus()], - maxactivepower = 0.0, - maxreactivepower = 0.0 - ) = LoadZones(number, name, buses, maxactivepower, maxreactivepower) diff --git a/src/parsers/cdm_parser.jl b/src/parsers/cdm_parser.jl deleted file mode 100644 index cdbfe2ade4..0000000000 --- a/src/parsers/cdm_parser.jl +++ /dev/null @@ -1,755 +0,0 @@ -""" -Reads in all the data stored in csv files -The general format for data is - folder: - gen.csv - branch.csv - bus.csv - .. - load.csv -Args: - Path to folder with all the System data CSV files - -Returns: - Nested Data dictionary with key values as folder/file names and dataframes - as values - -""" -function read_csv_data(file_path::String, baseMVA::Float64) - files = readdir(file_path) - REGEX_DEVICE_TYPE = r"(.*?)\.csv" - REGEX_IS_FOLDER = r"^[A-Za-z]+$" - data =Dict{String,Any}() - - if length(files) == 0 - error("No files in the folder") - else - data["baseMVA"] = baseMVA - end - - encountered_files = 0 - for d_file in files - try - if match(REGEX_IS_FOLDER, d_file) != nothing - @info "Parsing csv files in $d_file ..." - d_file_data = Dict{String,Any}() - for file in readdir(joinpath(file_path,d_file)) - if match(REGEX_DEVICE_TYPE, file) != nothing - @info "Parsing csv data in $file ..." - encountered_files += 1 - fpath = joinpath(file_path,d_file,file) - raw_data = CSV.File(fpath) |> DataFrames.DataFrame - d_file_data[split(file,r"[.]")[1]] = raw_data - end - end - - if length(d_file_data) > 0 - data[d_file] = d_file_data - @info "Successfully parsed $d_file" - end - - elseif match(REGEX_DEVICE_TYPE, d_file) != nothing - @info "Parsing csv data in $d_file ..." - encountered_files += 1 - fpath = joinpath(file_path,d_file) - raw_data = CSV.File(fpath)|> DataFrames.DataFrame - data[split(d_file,r"[.]")[1]] = raw_data - @info "Successfully parsed $d_file" - end - catch ex - @error "Error occurred while parsing $d_file" exception=ex - end - end - if encountered_files == 0 - error("No csv files or folders in $file_path") - end - - if "timeseries_pointers" in keys(data) - @info "parsing timeseries data" - tsp_raw = data["timeseries_pointers"] - data["timeseries_data"] = Dict() - if :Simulation in names(tsp_raw) - for sim in unique(tsp_raw[:Simulation]) - data["timeseries_data"][String(sim)] = Dict() - end - end - - for r in eachrow(tsp_raw) - fpath = joinpath(file_path,r[Symbol("Data File")]) - if isfile(fpath) - # read data and insert into dict - @info "parsing timeseries data in $fpath for $(r.Object)" - raw_data = CSV.File(fpath) |> DataFrames.DataFrame |> read_datetime - - if length([c for c in names(raw_data) if String(c) == String(r.Object)]) == 1 - raw_data = TimeSeries.TimeArray(raw_data[:DateTime],raw_data[Symbol(r.Object)]) - end - - if :Simulation in names(tsp_raw) - data["timeseries_data"][String(r.Simulation)][String(r.Object)] = raw_data - else - data["timeseries_data"][String(r.Object)] = raw_data - end - else - @warn "File referenced in timeseries_pointers.csv doesn't exist : $fpath" - end - end - end - - return data -end - - -""" -Args: - Dict with all the System data from CSV files ... see `read_csv_data()`' -Returns: - A Power Systems Nested dictionary with keys as devices and values as data - dictionary necessary to construct the device structs - PS dictionary: - "Bus" => Dict(bus_no => Dict("name" => - "number" => ... ) ) - "Generator" => Dict( "Thermal" => Dict( "name" => - "tech" => ...) - "Hydro" => .. - "Renewable" => .. ) - "Branch" => ... - "Load" => ... - "LoadZones" => ... - "BaseKV" => .. - ... -""" -function csv2ps_dict(data::Dict{String,Any}) - ps_dict =Dict{String,Any}() - - if haskey(data,"baseMVA") - ps_dict["baseMVA"] = data["baseMVA"] - else - @warn "Key error : key 'baseMVA' not found in PowerSystems dictionary, this will result in a ps_dict['baseMVA'] = 100.0" - ps_dict["baseMVA"] = 100.0 - end - - if haskey(data,"bus") - ps_dict["bus"] = PowerSystems.bus_csv_parser(data["bus"]) - if :Area in names(data["bus"]) - ps_dict["loadzone"] = PowerSystems.loadzone_csv_parser(data["bus"], ps_dict["bus"]) - ps_dict["load"] = PowerSystems.load_csv_parser(data["bus"], ps_dict["bus"], ps_dict["loadzone"], ps_dict["baseMVA"], haskey(data,"load") ? data["load"] : nothing) - else - @warn "Missing Data : no 'Area' information for buses, cannot create loads based on areas" - ps_dict["load"] = PowerSystems.load_csv_parser(data["bus"], ps_dict["bus"], ps_dict["baseMVA"], haskey(data,"load") ? data["load"] : nothing) - end - else - error("Key error : key 'bus' not found in PowerSystems dictionary, cannot construct any System Struct") - end - if haskey(data,"gen") - ps_dict["gen"] = PowerSystems.gen_csv_parser(data["gen"], ps_dict["bus"], ps_dict["baseMVA"]) - else - @warn "Key error : key 'gen' not found in PowerSystems dictionary, this will result in an ps_dict['gen'] = nothing" - ps_dict["gen"] = nothing - end - if haskey(data,"branch") - ps_dict["branch"] = PowerSystems.branch_csv_parser(data["branch"], ps_dict["bus"], ps_dict["baseMVA"]) - else - @warn "Key error : key 'branch' not found in PowerSystems dictionary, - \n This will result in an ps_dict['branch'] = nothing" - ps_dict["branch"] = nothing - end - if haskey(data,"dc_branch") - ps_dict["dcline"] = PowerSystems.dc_branch_csv_parser(data["dc_branch"], ps_dict["bus"], ps_dict["baseMVA"]) - else - @warn "Key error : key 'dc_branch' not found in PowerSystems dictionary, - \n This will result in an ps_dict['dcline'] = nothing" - ps_dict["dcline"] = nothing - end - if haskey(data,"reserves") - ps_dict["services"] = PowerSystems.services_csv_parser(data["reserves"],data["gen"],data["bus"]) - else - @warn "Key error : key 'reserves' not found in PowerSystems dictionary, this will result in a ps_dict['services'] = nothing" - ps_dict["services"] = nothing - end - - if haskey(data,"timeseries_data") - gen_map = _retrieve(ps_dict["gen"],"name",Dict()) - if haskey(data["timeseries_data"],"DAY_AHEAD") - @info "adding DAY-AHEAD generator forcasats" - _add_nested_dict!(ps_dict,["forecast","DA","gen"],_format_fcdict(data["timeseries_data"]["DAY_AHEAD"],gen_map)) - @info "adding DAY-AHEAD load forcasats" - ps_dict["forecast"]["DA"]["load"] = data["timeseries_data"]["DAY_AHEAD"]["Load"] - end - if haskey(data["timeseries_data"],"REAL_TIME") - @info "adding REAL-TIME generator forcasats" - _add_nested_dict!(ps_dict,["forecast","RT","gen"],_format_fcdict(data["timeseries_data"]["REAL_TIME"],gen_map)) - @info "adding REAL-TIME load forcasats" - ps_dict["forecast"]["RT"]["load"] = data["timeseries_data"]["REAL_TIME"]["Load"] - end - end - - return ps_dict -end - -function _add_nested_dict!(d,keylist,value = Dict()) - if !haskey(d,keylist[1]) - d[keylist[1]] = length(keylist)==1 ? value : Dict{String,Any}() - end - if length(keylist) > 1 - _add_nested_dict!(d[keylist[1]],keylist[2:end],value) - end -end - -function _format_fcdict(fc,obj_map) - paths = Dict() - for (k,d) in fc - if haskey(obj_map, k) - _add_nested_dict!(paths,obj_map[k],d) - end - end - return paths -end - - -""" -Args: - Path to folder with all the System data CSV's files -Returns: - A Power Systems Nested dictionary with keys as devices and values as data - dictionary necessary to construct the device structs - PS dictionary: - "Bus" => Dict(bus_no => Dict("name" => - "number" => ... ) ) - "Generator" => Dict( "Thermal" => Dict( "name" => - "tech" => ...) - "Hydro" => .. - "Renewable" => .. ) - "Branch" => ... - "Load" => ... - "LoadZones" => ... - "BaseKV" => .. - ... -""" -function csv2ps_dict(file_path::String, baseMVA::Float64) - data = read_csv_data(file_path, baseMVA) - ps_dict = csv2ps_dict(data) - return ps_dict -end - - -########### -#Bus data parser -########### - - -""" -Args: - A DataFrame with the same column names as in RTS_GMLC bus.csv file - - "Bus ID" "Bus Name" "BaseKV" "Bus Type" "MW Load" "MVAR Load" "V Mag" "V - Angle" "MW Shunt G" "MVAR Shunt B" "Area" - -Returns: - A Nested Dictionary with keys as Bus number and values as bus data - dictionary with same keys as the device struct -""" -function bus_csv_parser(bus_raw::DataFrames.DataFrame,colnames = nothing) - - if colnames isa Nothing - need_cols = ["Bus ID", "Bus Name", "BaseKV", "Bus Type", "V Mag", "V Angle"] - tbl_cols = string.(names(bus_raw)) - colnames = Dict(zip(need_cols,[findall(tbl_cols.==c)[1] for c in need_cols])) - end - - Buses_dict = Dict{Int64,Any}() - for i in 1:DataFrames.nrow(bus_raw) - Buses_dict[bus_raw[i,1]] = Dict{String,Any}("number" =>bus_raw[i,colnames["Bus ID"]] , - "name" => bus_raw[i,colnames["Bus Name"]], - "bustype" => bus_raw[i,colnames["Bus Type"]], - "angle" => bus_raw[i,colnames["V Angle"]], - "voltage" => bus_raw[i,colnames["V Mag"]], - "voltagelimits" => (min=0.95,max=1.05), - "basevoltage" => bus_raw[i,colnames["BaseKV"]] - ) - end - return Buses_dict -end - -function _get_value_or_nothing(value::Union{Real, String})::Union{Float64, Nothing} - if value == "NA" - return nothing - end - - return Float64(value) -end - -########### -#Generator data parser -########### - -""" -Args: - A DataFrame with the same column names as in RTS_GMLC gen.csv file - Parsed Bus PowerSystems dictionary -Returns: - A Nested Dictionary with keys as generator types/names and values as - generator data dictionary with same keys as the device struct -""" -function gen_csv_parser(gen_raw::DataFrames.DataFrame, Buses::Dict{Int64,Any}, baseMVA::Float64, colnames = nothing) - Generators_dict = Dict{String,Any}() - Generators_dict["Thermal"] = Dict{String,Any}() - Generators_dict["Hydro"] = Dict{String,Any}() - Generators_dict["Renewable"] = Dict{String,Any}() - Generators_dict["Renewable"]["PV"]= Dict{String,Any}() - Generators_dict["Renewable"]["RTPV"]= Dict{String,Any}() - Generators_dict["Renewable"]["WIND"]= Dict{String,Any}() - Generators_dict["Storage"] = Dict{String,Any}() - - if colnames isa Nothing - need_cols = ["GEN UID", "Bus ID", "Fuel", "Unit Type", - "MW Inj", "MVAR Inj", "PMax MW", "PMin MW", "QMax MVAR", "QMin MVAR", - "Min Down Time Hr", "Min Up Time Hr", "Ramp Rate MW/Min", - "Start Heat Cold MBTU", "Non Fuel Start Cost \$", "Non Fuel Shutdown Cost \$", "Fuel Price \$/MMBTU", - "Output_pct_0", "Output_pct_1", "Output_pct_2", "Output_pct_3", "Output_pct_4", "HR_avg_0", "HR_incr_1", "HR_incr_2", "HR_incr_3", "HR_incr_4", - "Base MVA"] - tbl_cols = string.(names(gen_raw)) - colnames = Dict(zip(need_cols,[findall(tbl_cols.==c)[1] for c in need_cols])) - end - - cost_colnames = [] - for i in 0:length([n for n in names(gen_raw) if occursin("Output_pct_",String(n))])-1 - hr = [n for n in names(gen_raw) if !isa(match(Regex("HR_.*_$i"),String(n)),Nothing)][1] - mw = Symbol("Output_pct_$i") - push!(cost_colnames, (hr,mw)) - end - - pu_cols = ["PMin MW", "PMax MW", "MVAR Inj", "MW Inj", "QMin MVAR", "QMax MVAR", "Ramp Rate MW/Min"] - [gen_raw[colnames[c]] = gen_raw[colnames[c]]./baseMVA for c in pu_cols] # P.U. conversion - - for gen in 1:DataFrames.nrow(gen_raw) - pmax = _get_value_or_nothing(gen_raw[gen,colnames["PMax MW"]]) - - if gen_raw[gen,colnames["Fuel"]] in ["Oil","Coal","NG","Nuclear"] - - fuel_cost = gen_raw[gen,colnames["Fuel Price \$/MMBTU"]]./1000 - - var_cost = [(_get_value_or_nothing(gen_raw[gen,cn[1]]), _get_value_or_nothing(gen_raw[gen,cn[2]])) for cn in cost_colnames] - var_cost = [(c[1]*c[2]*fuel_cost*baseMVA, c[2]).*pmax for c in var_cost if !in(nothing,c)] - var_cost[2:end] = [(var_cost[i-1][1]+var_cost[i][1], var_cost[i][2]) for i in 2:length(var_cost)] - - bus_id =[Buses[i] for i in keys(Buses) if Buses[i]["number"] == gen_raw[gen,colnames["Bus ID"]]] - - Generators_dict["Thermal"][gen_raw[gen,colnames["GEN UID"]]] = Dict{String,Any}("name" => gen_raw[gen,colnames["GEN UID"]], - "available" => true, - "bus" => make_bus(bus_id[1]), - "tech" => Dict{String,Any}("activepower" => gen_raw[gen,colnames["MW Inj"]], - "activepowerlimits" => (min=_get_value_or_nothing(gen_raw[gen, colnames["PMin MW"]]), max=pmax), - "reactivepower" => _get_value_or_nothing(gen_raw[gen, colnames["MVAR Inj"]]), - "reactivepowerlimits" => (min=_get_value_or_nothing(gen_raw[gen, colnames["QMin MVAR"]]), max=_get_value_or_nothing(gen_raw[gen, colnames["QMax MVAR"]])), - "ramplimits" => (up=gen_raw[gen,colnames["Ramp Rate MW/Min"]], down=gen_raw[gen, colnames["Ramp Rate MW/Min"]]), - "timelimits" => (up=gen_raw[gen,colnames["Min Up Time Hr"]], down=gen_raw[gen, colnames["Min Down Time Hr"]])), - "econ" => Dict{String,Any}("capacity" => pmax, - "variablecost" => var_cost, - "fixedcost" => 0.0, - "startupcost" => gen_raw[gen,colnames["Start Heat Cold MBTU"]]*fuel_cost*1000, - "shutdncost" => 0.0, - "annualcapacityfactor" => nothing) - ) - - elseif gen_raw[gen,colnames["Fuel"]] in ["Hydro"] - bus_id =[Buses[i] for i in keys(Buses) if Buses[i]["number"] == gen_raw[gen,colnames["Bus ID"]]] - Generators_dict["Hydro"][gen_raw[gen,colnames["GEN UID"]]] = Dict{String,Any}("name" => gen_raw[gen,colnames["GEN UID"]], - "available" => true, # change from staus to available - "bus" => make_bus(bus_id[1]), - "tech" => Dict{String,Any}( "installedcapacity" => pmax, - "activepower" => gen_raw[gen, colnames["MW Inj"]], - "activepowerlimits" => (min=_get_value_or_nothing(gen_raw[gen, colnames["PMin MW"]]), max=pmax), - "reactivepower" => gen_raw[gen, colnames["MVAR Inj"]], - "reactivepowerlimits" => (min=_get_value_or_nothing(gen_raw[gen, colnames["QMin MVAR"]]), max=_get_value_or_nothing(gen_raw[gen, colnames["QMax MVAR"]])), - "ramplimits" => (up=gen_raw[gen, colnames["Ramp Rate MW/Min"]], down=gen_raw[gen, colnames["Ramp Rate MW/Min"]]), - "timelimits" => (up=gen_raw[gen, colnames["Min Down Time Hr"]], down=gen_raw[gen, colnames["Min Down Time Hr"]])), - "econ" => Dict{String,Any}("curtailcost" => 0.0, - "interruptioncost" => nothing), - "scalingfactor" => TimeSeries.TimeArray(collect(Dates.DateTime(Dates.today()):Dates.Hour(1):Dates.DateTime(Dates.today()+Dates.Day(1))), ones(25)) # TODO: connect scaling factors - ) - - elseif gen_raw[gen,colnames["Fuel"]] in ["Solar","Wind"] - bus_id =[Buses[i] for i in keys(Buses) if Buses[i]["number"] == gen_raw[gen,colnames["Bus ID"]]] - if gen_raw[gen,colnames["Unit Type"]] == "PV" - Generators_dict["Renewable"]["PV"][gen_raw[gen,colnames["GEN UID"]]] = Dict{String, Any}("name" => gen_raw[gen,colnames["GEN UID"]], - "available" => true, # change from staus to available - "bus" => make_bus(bus_id[1]), - "tech" => Dict{String, Any}("installedcapacity" => pmax, - "reactivepowerlimits" => (min=_get_value_or_nothing(gen_raw[gen, colnames["QMin MVAR"]]), max=_get_value_or_nothing(gen_raw[gen, colnames["QMax MVAR"]])), - "powerfactor" => 1), - "econ" => Dict{String, Any}("curtailcost" => 0.0, - "interruptioncost" => nothing), - "scalingfactor" => TimeSeries.TimeArray(collect(Dates.DateTime(Dates.today()):Dates.Hour(1):Dates.DateTime(Dates.today()+Dates.Day(1))), ones(25)) # TODO: connect scaling factors - ) - elseif gen_raw[gen,colnames["Unit Type"]] == "RTPV" - Generators_dict["Renewable"]["RTPV"][gen_raw[gen,colnames["GEN UID"]]] = Dict{String,Any}("name" => gen_raw[gen,colnames["GEN UID"]], - "available" => true, # change from staus to available - "bus" => make_bus(bus_id[1]), - "tech" => Dict{String, Any}("installedcapacity" => pmax, - "reactivepowerlimits" => (min=_get_value_or_nothing(gen_raw[gen, colnames["QMin MVAR"]]), max=_get_value_or_nothing(gen_raw[gen, colnames["QMax MVAR"]])), - "powerfactor" => 1), - "econ" => Dict{String, Any}("curtailcost" => 0.0, - "interruptioncost" => nothing), - "scalingfactor" => TimeSeries.TimeArray(collect(Dates.DateTime(Dates.today()):Dates.Hour(1):Dates.DateTime(Dates.today()+Dates.Day(1))), ones(25)) # TODO: Connect scaling factors - ) - elseif gen_raw[gen,colnames["Unit Type"]] == "WIND" - Generators_dict["Renewable"]["WIND"][gen_raw[gen, colnames["GEN UID"]]] = Dict{String, Any}("name" => gen_raw[gen, colnames["GEN UID"]], - "available" => true, # change from staus to available - "bus" => make_bus(bus_id[1]), - "tech" => Dict{String, Any}("installedcapacity" => pmax, - "reactivepowerlimits" => (min=_get_value_or_nothing(gen_raw[gen, colnames["QMin MVAR"]]), max=_get_value_or_nothing(gen_raw[gen, colnames["QMax MVAR"]])), - "powerfactor" => 1), - "econ" => Dict{String, Any}("curtailcost" => 0.0, - "interruptioncost" => nothing), - "scalingfactor" => TimeSeries.TimeArray(collect(Dates.DateTime(Dates.today()):Dates.Hour(1):Dates.DateTime(Dates.today()+Dates.Day(1))), ones(25)) # TODO: connect scaling factors - ) - end - elseif gen_raw[gen,colnames["Fuel"]] in ["Storage"] - bus_id =[Buses[i] for i in keys(Buses) if Buses[i]["number"] == gen_raw[gen,colnames["Bus ID"]]] - Generators_dict["Storage"][gen_raw[gen,colnames["GEN UID"]]] = Dict{String,Any}("name" => gen_raw[gen,colnames["GEN UID"]], - "available" => true, # change from staus to available - "bus" => make_bus(bus_id[1]), - "energy" => 0.0, - "capacity" => (min=_get_value_or_nothing(gen_raw[gen,colnames["PMin MW"]]),max=pmax), - "activepower" => gen_raw[gen,colnames["MW Inj"]], - "inputactivepowerlimits" => (min=0.0,max=pmax), - "outputactivepowerlimits" => (min=0.0,max=pmax), - "efficiency" => (in= 0.0, out = 0.0), - "reactivepower" => gen_raw[gen,colnames["MVAR Inj"]], - "reactivepowerlimits" => (min = 0.0, max = 0.0), - ) - end - end - return Generators_dict -end - -########### -#Branch data parser -########### - -""" -Args: - A DataFrame with the same column names as in RTS_GMLC branch.csv file - Parsed Bus PowerSystems dictionary -Returns: - A Nested Dictionary with keys as branch types/names and values as - line/transformer data dictionary with same keys as the device struct -""" -function branch_csv_parser(branch_raw::DataFrames.DataFrame, Buses::Dict, baseMVA::Float64, colnames=nothing) - - if colnames isa Nothing - need_cols = ["From Bus", "To Bus", "Tr Ratio", "Cont Rating","UID","R","X","B"] - tbl_cols = string.(names(branch_raw)) - colnames = Dict(zip(need_cols,[findall(tbl_cols.==c)[1] for c in need_cols])) - end - - pu_cols = ["Cont Rating"] - [branch_raw[colnames[c]] = branch_raw[colnames[c]]./baseMVA for c in pu_cols] # P.U. conversion - - Branches_dict = Dict{String,Any}() - Branches_dict["Transformers"] = Dict{String,Any}() - Branches_dict["Lines"] = Dict{String,Any}() - for i in 1:DataFrames.nrow(branch_raw) - bus_f = [Buses[f] for f in keys(Buses) if Buses[f]["number"] == branch_raw[i,colnames["From Bus"]]] - bus_t = [Buses[t] for t in keys(Buses) if Buses[t]["number"] == branch_raw[i,colnames["To Bus"]]] - if branch_raw[i,Symbol("Tr Ratio")] > 0.0 - Branches_dict["Transformers"][branch_raw[i,colnames["UID"]]] = Dict{String,Any}("name" => branch_raw[i,colnames["UID"]], - "available" => true, - "connectionpoints" => (from=make_bus(bus_f[1]),to=make_bus(bus_t[1])), - "r" => branch_raw[i,colnames["R"]], - "x" => branch_raw[i,colnames["X"]], - "primaryshunt" => branch_raw[i,colnames["B"]] , #TODO: add field in CSV - "alpha" => (branch_raw[i,colnames["B"]]/2) - (branch_raw[i,colnames["B"]]/2), #TODO: Phase-Shifting Transformer angle - "tap" => branch_raw[i,colnames["Tr Ratio"]], - "rate" => branch_raw[i,colnames["Cont Rating"]], - ) - else - Branches_dict["Lines"][branch_raw[i,:UID]] = Dict{String,Any}("name" => branch_raw[i,:UID], - "available" => true, - "connectionpoints" => (from=make_bus(bus_f[1]),to=make_bus(bus_t[1])), - "r" => branch_raw[i,colnames["R"]], - "x" => branch_raw[i,colnames["X"]], - "b" => (from=(branch_raw[i,colnames["B"]]/2),to=(branch_raw[i,colnames["B"]]/2)), - "rate" => branch_raw[i,colnames["Cont Rating"]], - "anglelimits" => (min=-60.0,max =60.0) #TODO: add field in CSV - ) - - end - end - return Branches_dict -end - - -########### -#DC Branch data parser -########### - -""" -Args: - A DataFrame with the same column names as in RTS_GMLC dc_branch.csv file - Parsed Bus PowerSystems dictionary -Returns: - A Nested Dictionary with keys as dc_branch types/names and values as - dc_branch data dictionary with same keys as the device struct -""" -function dc_branch_csv_parser(dc_branch_raw::DataFrames.DataFrame, Buses::Dict, baseMVA::Float64, colnames=nothing) - - if colnames isa Nothing - need_cols = ["UID","From Bus", "To Bus","From X Commutating", "From Tap Min", "From Tap Max","From Min Firing Angle","From Max Firing Angle", "To X Commutating", "To Tap Min", "To Tap Max","To Min Firing Angle","To Max Firing Angle", "MW Load"] - tbl_cols = string.(names(dc_branch_raw)) - colnames = Dict(zip(need_cols,[findall(tbl_cols.==c)[1] for c in need_cols if c in tbl_cols])) - end - - pu_cols = ["MW Load"] - [dc_branch_raw[colnames[c]] = dc_branch_raw[colnames[c]]./baseMVA for c in pu_cols] # P.U. conversion - - DCBranches_dict = Dict{String,Any}() - DCBranches_dict["HVDCLine"] = Dict{String,Any}() - DCBranches_dict["VSCDCLine"] = Dict{String,Any}() - - for i in 1:DataFrames.nrow(dc_branch_raw) - bus_f = [Buses[f] for f in keys(Buses) if Buses[f]["number"] == dc_branch_raw[i,colnames["From Bus"]]] - bus_t = [Buses[t] for t in keys(Buses) if Buses[t]["number"] == dc_branch_raw[i,colnames["To Bus"]]] - if (dc_branch_raw[i,colnames["From Max Firing Angle"]] + dc_branch_raw[i,colnames["To Max Firing Angle"]])/2 != 0.0 #TODO: Replace this with the correct conditional to create VSCDC or HVDC lines - DCBranches_dict["VSCDCLine"][dc_branch_raw[i,colnames["UID"]]] = Dict{String,Any}("name" => dc_branch_raw[i,colnames["UID"]], - "available" => true, - "connectionpoints" => (from=make_bus(bus_f[1]),to=make_bus(bus_t[1])), - "rectifier_taplimits" => (min=dc_branch_raw[i,colnames["From Tap Min"]],max=dc_branch_raw[i,colnames["From Tap Max"]]), - "rectifier_xrc" => dc_branch_raw[i,colnames["From X Commutating"]], #TODO: What is this? - "rectifier_firingangle" => (min=dc_branch_raw[i,colnames["From Min Firing Angle"]],max=dc_branch_raw[i,colnames["From Max Firing Angle"]]), - "inverter_taplimits" => (min=dc_branch_raw[i,colnames["To Tap Min"]],max=dc_branch_raw[i,colnames["To Tap Max"]]), - "inverter_xrc" => dc_branch_raw[i,colnames["To X Commutating"]], #TODO: What is this? - "inverter_firingangle" => (min=dc_branch_raw[i,colnames["To Min Firing Angle"]],max=dc_branch_raw[i,colnames["To Max Firing Angle"]]) - ) - else - DCBranches_dict["HVDCLine"][dc_branch_raw[i,colnames["UID"]]] = Dict{String,Any}("name" => dc_branch_raw[i,colnames["UID"]], - "available" => true, - "connectionpoints" => (from=make_bus(bus_f[1]),to=make_bus(bus_t[1])), - "activepowerlimits_from" => (min=-1*dc_branch_raw[i,colnames["MW Load"]], max=dc_branch_raw[i,colnames["MW Load"]]), #TODO: is there a better way to calculate this? - "activepowerlimits_to" => (min=-1*dc_branch_raw[i,colnames["Rating"]], max=dc_branch_raw[i,colnames["MW Load"]]), #TODO: is there a better way to calculate this? - "reactivepowerlimits_from" => (min=-1*dc_branch_raw[i,colnames["MW Load"]], max=dc_branch_raw[i,colnames["MW Load"]]), #TODO: is there a better way to calculate this? - "reactivepowerlimits_to" => (min=-1*dc_branch_raw[i,colnames["MW Load"]], max=dc_branch_raw[i,colnames["MW Load"]]), #TODO: is there a better way to calculate this? - "loss" => 0.0, #TODO: Can we infer this from the other data? - ) - - end - end - return DCBranches_dict -end - - -########### -#Load data parser -########### - -""" -Args: - A DataFrame with the same column names as in RTS_GMLC bus.csv file - Parsed Bus entry of PowerSystems dictionary - Parsed LoadZone entry of PowerSystems dictionary -Optional Args: - DataFrame of LoadZone timeseries data - Dict of bus column names - Dict of load LoadZone timeseries column names -Returns: - A Nested Dictionary with keys as load names and values as load data - dictionary with same keys as the device struct -""" -function load_csv_parser(bus_raw::DataFrames.DataFrame, Buses::Dict, LoadZone::Dict, baseMVA::Float64, load_raw=nothing,bus_colnames=nothing,load_colnames=nothing) - Loads_dict = Dict{String,Any}() - load_zone = nothing - - if bus_colnames isa Nothing - need_cols = ["MW Load", "MVAR Load", "Bus ID"] - tbl_cols = string.(names(bus_raw)) - bus_colnames = Dict(zip(need_cols,[findall(tbl_cols.==c)[1] for c in need_cols])) - end - - if !isa(load_raw,Nothing) - load_raw = read_datetime(load_raw) - if load_colnames isa Nothing - need_cols = ["DateTime",] - tbl_cols = string.(names(load_raw)) - load_colnames = Dict(zip(need_cols,[findall(tbl_cols.==c)[1] for c in need_cols])) - end - end - - pu_cols = ["MW Load", "MVAR Load"] - [bus_raw[bus_colnames[c]] = bus_raw[bus_colnames[c]]./baseMVA for c in pu_cols] # P.U. conversion - - for (k_b,b) in Buses - for (k_l,l) in LoadZone - bus_numbers = [b.number for b in l["buses"] ] - if b["number"] in bus_numbers - load_zone = k_l - end - end - p = [bus_raw[n,bus_colnames["MW Load"]] for n in 1:DataFrames.nrow(bus_raw) if bus_raw[n,bus_colnames["Bus ID"]] == b["number"]] - q = [bus_raw[m,bus_colnames["MVAR Load"]] for m in 1:DataFrames.nrow(bus_raw) if bus_raw[m,bus_colnames["Bus ID"]] == b["number"]] - ts = isa(load_raw,Nothing) ? nothing : TimeSeries.TimeArray(load_raw[load_colnames["DateTime"]],load_raw[load_zone]*(p[1]/LoadZone[load_zone]["maxactivepower"])) - Loads_dict[b["name"]] = Dict{String,Any}("name" => b["name"], - "available" => true, - "bus" => make_bus(b), - "maxactivepower" => p[1], - "maxreactivepower" => q[1], - "scalingfactor" => ts #TODO remove TS - ) - end - return Loads_dict -end - - -""" -Args: - A DataFrame with the same column names as in RTS_GMLC bus.csv file - Parsed Bus entry of PowerSystems dictionary -Optional Args: - DataFrame of LoadZone timeseries data - Dict of bus column names - Dict of load LoadZone timeseries column names -Returns: - A Nested Dictionary with keys as load names and values as load data - dictionary with same keys as the device struct -""" -function load_csv_parser(bus_raw::DataFrames.DataFrame, Buses::Dict, baseMVA::Float64, load_raw=nothing, bus_colnames=nothing, load_colnames=nothing) - Loads_dict = Dict{String,Any}() - load_zone = nothing - - if bus_colnames isa Nothing - need_cols = ["MW Load", "MVAR Load", "Bus ID"] - tbl_cols = string.(names(bus_raw)) - bus_colnames = Dict(zip(need_cols,[findall(tbl_cols.==c)[1] for c in need_cols])) - end - - if !isa(load_raw,nothing) - load_raw = read_datetime(load_raw) - if load_colnames isa Nothing - need_cols = ["DateTime"] - tbl_cols = string.(names(load_raw)) - load_colnames = Dict(zip(need_cols,[findall(tbl_cols.==c)[1] for c in need_cols])) - end - end - - pu_cols = ["MW Load", "MVAR Load"] - [bus_raw[colnames[c]] = bus_raw[colnames[c]]./baseMVA for c in pu_cols] # P.U. conversion - - for (k_b,b) in Buses - p = [bus_raw[n,bus_colnames["MW Load"]] for n in 1:DataFrames.nrow(bus_raw) if bus_raw[n,bus_colnames["Bus ID"]] == b["number"]] - q = [bus_raw[m,bus_colnames["MVAR Load"]] for m in 1:DataFrames.nrow(bus_raw) if bus_raw[m,bus_colnames["Bus ID"]] == b["number"]] - ts = isa(load_raw,nothing) ? nothing : TimeSeries.TimeArray(load_raw[load_colnames["DateTime"]],load_raw[b["number"]]/(p[1]*baseMVA)) - Loads_dict[b["name"]] = Dict{String,Any}("name" => b["name"], - "available" => true, - "bus" => make_bus(b), - "maxactivepower" => p[1], - "maxreactivepower" => q[1], - "scalingfactor" => ts #TODO remove TS - ) - end - return Loads_dict -end - -########### -#LoadZone data parser -########### - -""" -Args: - A DataFrame with the same column names as in RTS_GMLC bus.csv file - Parsed Bus PowerSystems dictionary -Returns: - A Nested Dictionary with keys as loadzone names and values as loadzone data - dictionary with same keys as the device struct -""" -function loadzone_csv_parser(bus_raw::DataFrames.DataFrame, Buses::Dict, colnames=nothing) - if colnames isa Nothing - need_cols = ["MW Load", "MVAR Load", "Bus ID", "Area"] - tbl_cols = string.(names(bus_raw)) - colnames = Dict(zip(need_cols,[findall(tbl_cols.==c)[1] for c in need_cols])) - end - - LoadZone_dict = Dict{Int64,Any}() - lbs = zip(unique(bus_raw[colnames["Area"]]),[sum(bus_raw[colnames["Area"]].==a) for a in unique(bus_raw[colnames["Area"]])]) - for (zone,count) in lbs - b_numbers = [bus_raw[b,colnames["Bus ID"]] for b in 1:DataFrames.nrow(bus_raw) if bus_raw[b,colnames["Area"]] == zone ] - buses = [make_bus(Buses[i]) for i in keys(Buses) if Buses[i]["number"] in b_numbers] - activepower = [bus_raw[b,colnames["MW Load"]] for b in 1:DataFrames.nrow(bus_raw) if bus_raw[b,colnames["Area"]] == zone] - reactivepower = [bus_raw[b,colnames["MVAR Load"]] for b in 1:DataFrames.nrow(bus_raw) if bus_raw[b,colnames["Area"]] == zone] - LoadZone_dict[zone] = Dict{String,Any}("number" => zone, - "name" => zone , - "buses" => buses, - "maxactivepower" => sum(activepower), - "maxreactivepower" => sum(reactivepower) - ) - end - return LoadZone_dict -end - - -########### -#reserves data parser -########### - -""" -Args: - A DataFrame with the same column names as in RTS_GMLC reserves.csv file - A DataFrame with the same column names as in RTS_GMLC gen.csv file - A DataFrame with the same column names as in RTS_GMLC bus.csv file -Returns: - A Nested Dictionary with keys as loadzone names and values as loadzone data - dictionary with same keys as the device struct -""" -function services_csv_parser(rsv_raw::DataFrames.DataFrame,gen_raw::DataFrames.DataFrame,bus_raw::DataFrames.DataFrame,colnames=nothing, gen_colnames = nothing,bus_colnames = nothing) - if colnames isa Nothing - need_cols = ["Reserve Product", "Timeframe (sec)", "Eligible Regions", "Eligible Gen Categories"] - tbl_cols = string.(names(rsv_raw)) - colnames = Dict(zip(need_cols,[findall(tbl_cols.==c)[1] for c in need_cols])) - end - - if gen_colnames isa Nothing - need_cols = ["GEN UID", "Bus ID", "Category"] - tbl_cols = string.(names(gen_raw)) - gen_colnames = Dict(zip(need_cols,[findall(tbl_cols.==c)[1] for c in need_cols])) - end - - if bus_colnames isa Nothing - need_cols = ["Bus ID", "Area"] - tbl_cols = string.(names(bus_raw)) - bus_colnames = Dict(zip(need_cols,[findall(tbl_cols.==c)[1] for c in need_cols])) - end - - services_dict = Dict{String,Any}() - for r in 1:DataFrames.nrow(rsv_raw) - gen_cats = strip(rsv_raw[r,colnames["Eligible Gen Categories"]],[i for i in "()"]) |> (x->split(x, ",")) - regions = strip(rsv_raw[r,colnames["Eligible Regions"]],[i for i in "()"]) |> (x->split(x,",")) - contributingdevices = [] - [push!(contributingdevices,gen_raw[g,gen_colnames["GEN UID"]]) for g in 1:DataFrames.nrow(gen_raw) if - (gen_raw[g,gen_colnames["Category"]] in gen_cats) & - (string(bus_raw[bus_raw[bus_colnames["Bus ID"]] .== gen_raw[g,gen_colnames["Bus ID"]],bus_colnames["Area"]][1]) in regions)]; - - - services_dict[rsv_raw[r,colnames["Reserve Product"]]] = Dict{String,Any}("name" => rsv_raw[r,colnames["Reserve Product"]], - "contributingdevices" => contributingdevices, - "timeframe" => rsv_raw[r,colnames["Timeframe (sec)"]]) - end - - - return services_dict -end - - -# Remove missing values form dataframes -#TODO : Remove "NA" Strings from the data created by CSV.read() -""" -Arg: - Any DataFrame with Missing values / "NA" strings that are either created by - readtable() or CSV.read() -Returns: - DataFrame with missing values replaced by 0 -""" -function remove_missing(df) - for col in names(df) - df[ismissing.(df[col]), col] = 0 - end - return df -end diff --git a/src/parsers/common.jl b/src/parsers/common.jl new file mode 100644 index 0000000000..901dc76da7 --- /dev/null +++ b/src/parsers/common.jl @@ -0,0 +1,121 @@ +const GENERATOR_MAPPING_FILE = joinpath(dirname(pathof(PowerSystems)), "parsers", + "generator_mapping.yaml") + +"""Return a dict where keys are a tuple of input parameters (fuel, unit_type) and values are +generator types.""" +function get_generator_mapping(filename=nothing) + if isnothing(filename) + filename = GENERATOR_MAPPING_FILE + end + genmap = open(filename) do file + YAML.load(file) + end + + mappings = Dict{NamedTuple, DataType}() + for (gen_type, vals) in genmap + gen = getfield(PowerSystems, Symbol(gen_type)) + for val in vals + key = (fuel=val["fuel"], unit_type=val["type"]) + if haskey(mappings, key) + error("duplicate generator mappings: $gen $(key.fuel) $(key.unit_type)") + end + mappings[key] = gen + end + end + + return mappings +end + +"""Return the PowerSystems generator type for this fuel and unit_type.""" +function get_generator_type(fuel, unit_type, mappings::Dict{NamedTuple, DataType}) + fuel = uppercase(fuel) + unit_type = uppercase(unit_type) + generator = nothing + + # Try to match the unit_type if it's defined. If it's nothing then just match on fuel. + for ut in (unit_type, nothing) + key = (fuel=fuel, unit_type=ut) + if haskey(mappings, key) + generator = mappings[key] + break + end + end + + if isnothing(generator) + @error "No mapping for generator fuel=$fuel unit_type=$unit_type" + end + + return generator +end + +function get_branch_type(tap::Float64, alpha::Float64) + if tap <= 0.0 + branch_type = Line + elseif tap == 1.0 + branch_type = Transformer2W + else + if alpha == 0.0 + branch_type = TapTransformer + else + branch_type = PhaseShiftingTransformer + end + end + + return branch_type +end + +function calculate_rating(active_power_max::Float64, reactive_power_max::Float64) + return sqrt(active_power_max^2 + reactive_power_max^2) +end + +function convert_units!(value::Float64, + unit_conversion::NamedTuple{(:From,:To),Tuple{String,String}}) + + if unit_conversion.From == "degree" && unit_conversion.To == "radian" + value = deg2rad(value) + elseif unit_conversion.From == "radian" && unit_conversion.To == "degree" + value = rad2deg(value) + else + throw(DataFormatError("Unit conversion from $(unit_conversion.From) to $(unit_conversion.To) not supported")) + end + return value +end + +const STRING2FUEL = Dict((string(e) => e) for e in instances(ThermalFuels)) +merge!(STRING2FUEL, Dict("NG" => NATURAL_GAS::ThermalFuels, + "NUC" => NUCLEAR::ThermalFuels, + "GAS" => NATURAL_GAS::ThermalFuels, + "OIL" => DISTILLATE_FUEL_OIL::ThermalFuels, + "SYNC_COND" => OTHER::ThermalFuels, + )) + +function Base.convert(::Type{ThermalFuels}, fuel::AbstractString) + return STRING2FUEL[uppercase(fuel)] +end + +function Base.convert(::Type{ThermalFuels}, fuel::Symbol) + return convert(ThermalFuels, string(fuel)) +end + +const STRING2PRIMEMOVER = Dict((string(e) => e) for e in instances(PrimeMovers)) +merge!(STRING2PRIMEMOVER, Dict("W2" => WT::PrimeMovers, + "WIND" => WT::PrimeMovers, + "PV" => PVe::PrimeMovers, + "RTPV" => PVe::PrimeMovers, + "NB" => ST::PrimeMovers, + "STEAM" => ST::PrimeMovers, + "HYDRO" => HY::PrimeMovers, + "NUCLEAR" => ST::PrimeMovers, + "SYNC_COND" => OT::PrimeMovers, + "CSP" => CP::PrimeMovers, + "UN" => OT::PrimeMovers, + "STORAGE" => BA::PrimeMovers, + )) + +function Base.convert(::Type{PrimeMovers}, primemover::AbstractString) + return STRING2PRIMEMOVER[uppercase(primemover)] +end + +function Base.convert(::Type{PrimeMovers}, primemover::Symbol) + return convert(PrimeMovers, string(primemover)) +end diff --git a/src/parsers/dict_to_struct.jl b/src/parsers/dict_to_struct.jl deleted file mode 100644 index c3779059ac..0000000000 --- a/src/parsers/dict_to_struct.jl +++ /dev/null @@ -1,514 +0,0 @@ -# Global method definition needs to be at top level in .7 - -""" -Takes a PowerSystems dictionary and return an array of PowerSystems struct for -Bus, Generator, Branch and load -""" -function ps_dict2ps_struct(data::Dict{String,Any}) - generators = Array{G where {G<:Generator},1}() - storages = Array{S where {S<:Storage},1}() - buses = Array{Bus,1}() - branches = Array{B where {B<:Branch},1}() - loads = Array{E where {E<:ElectricLoad},1}() - shunts = Array{FixedAdmittance,1}() - loadZones = Array{D where {D<:Device},1}() - services = Array{S where {S<:Service},1}() - - # TODO: should we raise an exception in the following? - - if haskey(data, "bus") - buses = PowerSystems.bus_dict_parse(data["bus"]) - else - @warn "key 'bus' not found in PowerSystems dictionary, this will result in an empty Bus array" - end - if haskey(data, "gen") - (generators, storage) = PowerSystems.gen_dict_parser(data["gen"]) - else - @warn "key 'gen' not found in PowerSystems dictionary, this will result in an empty Generators and Storage array" - end - if haskey(data, "branch") - branches = PowerSystems.branch_dict_parser(data["branch"], branches) - else - @warn "key 'branch' not found in PowerSystems dictionary, this will result in an empty Branches array" - end - if haskey(data, "load") - loads = PowerSystems.load_dict_parser(data["load"]) - else - @warn "key 'load' not found in PowerSystems dictionary, this will result in an empty Loads array" - end - if haskey(data, "loadzone") - loadZones = PowerSystems.loadzone_dict_parser(data["loadzone"]) - else - @warn "key 'loadzone' not found in PowerSystems dictionary, this will result in an empty LoadZones array" - end - if haskey(data, "shunt") - shunts = PowerSystems.shunt_dict_parser(data["shunt"]) - else - @warn "key 'shunt' not found in PowerSystems dictionary, this will result in an empty Shunts array" - end - if haskey(data, "dcline") - branches = PowerSystems.dclines_dict_parser(data["dcline"], branches) - else - @warn "key 'dcline' not found in PowerSystems dictionary, this will result in an empty DCLines array" - end - if haskey(data, "services") - services = PowerSystems.services_dict_parser(data["services"],generators) - else - @warn "key 'services' not found in PowerSystems dictionary, this will result in an empty services array" - end - - return sort!(buses, by = x -> x.number), generators, storage, sort!(branches, by = x -> x.connectionpoints.from.number), loads, loadZones, shunts, services - -end - - -function _retrieve(dict::T, key_of_interest::String, output = Dict(), path = []) where T<:AbstractDict - iter_result = Base.iterate(dict) - last_element = length(path) - while iter_result !== nothing - ((key,value), state) = iter_result - if key == key_of_interest - output[value] = !haskey(output,value) ? path[1:end] : push!(output[value],path[1:end]) - end - if value isa AbstractDict - push!(path,key) - _retrieve(value, key_of_interest, output, path) - path = path[1:last_element] - end - iter_result = Base.iterate(dict, state) - end - return output -end - -function _retrieve(dict::T, type_of_interest, output = Dict(), path = []) where T<:AbstractDict - iter_result = Base.iterate(dict) - last_element = length(path) - while iter_result !== nothing - ((key,value), state) = iter_result - if typeof(value) <: type_of_interest - output[key] = !haskey(output,value) ? path[1:end] : push!(output[value],path[1:end]) - end - if value isa AbstractDict - push!(path,key) - _retrieve(value, type_of_interest, output, path) - path = path[1:last_element] - end - iter_result = Base.iterate(dict, state) - end - return output -end - -function _access(nesteddict::T,keylist) where T<:AbstractDict - if !haskey(nesteddict,keylist[1]) - @error "$(keylist[1]) not found in dict" - end - if length(keylist) > 1 - nesteddict = _access(nesteddict[keylist[1]],keylist[2:end]) - else - nesteddict = nesteddict[keylist[1]] - end -end - -function _get_device(name, collection, devices = []) - if isa(collection,Array) - fn = fieldnames(typeof(collection[1])) - if :name in fn - [push!(devices,d) for d in collection if d.name == name] - end - else - fn = fieldnames(typeof(collection)) - for f in fn - _get_device(name,getfield(collection,f),devices) - end - end - return devices -end - - -""" -Arg: - Dataframes which includes a timerseries columns of either: - Year, Month, Day, Period - or - DateTime - or - nothing (creates a today referenced DateTime Column) -Returns: - Dataframe with a DateTime columns -""" -function read_datetime(df; kwargs...) - if [c for c in [:Year,:Month,:Day,:Period] if c in names(df)] == [:Year,:Month,:Day,:Period] - if Dates.Hour(DataFrames.maximum(df[:Period])) <= Dates.Hour(25) - df[:DateTime] = collect(Dates.DateTime(df[1,:Year],df[1,:Month],df[1,:Day],(df[1,:Period]-1)) :Dates.Hour(1) : - Dates.DateTime(df[end,:Year],df[end,:Month],df[end,:Day],(df[end,:Period]-1))) - elseif (Dates.Minute(5) * DataFrames.maximum(df[:Period]) >= Dates.Minute(1440))& (Dates.Minute(5) * DataFrames.maximum(df[:Period]) <= Dates.Minute(1500)) - df[:DateTime] = collect(Dates.DateTime(df[1,:Year],df[1,:Month],df[1,:Day],floor(df[1,:Period]/12),Int(df[1,:Period])-1) :Dates.Minute(5) : - Dates.DateTime(df[end,:Year],df[end,:Month],df[end,:Day],floor(df[end,:Period]/12)-1,5*(Int(df[end,:Period])-(floor(df[end,:Period]/12)-1)*12) -5)) - else - @error "I don't know what the period length is, reformat timeseries" - end - DataFrames.deletecols!(df, [:Year,:Month,:Day,:Period]) - - elseif :DateTime in names(df) - df[:DateTime] = Dates.DateTime(df[:DateTime]) - else - if :startdatetime in keys(kwargs) - startdatetime = kwargs[:startdatetime] - else - @warn "No reference date given, assuming today" - startdatetime = Dates.today() - end - df[:DateTime] = collect(Dates.DateTime(startdatetime):Dates.Hour(1):Dates.DateTime(startdatetime)+Dates.Hour(size(df)[1]-1)) - end - return df -end - -""" -Arg: - Device dictionary - Generators - Dataframe contains device Realtime/Forecast TimeSeries -Returns: - Device dictionary with timeseries added -""" -function add_time_series(Device_dict::Dict{String,Any}, df::DataFrames.DataFrame) - for (device_key,device) in Device_dict - if device_key in map(string, names(df)) - ts_raw = df[Symbol(device_key)] - if maximum(ts_raw) <= 1.0 - @info "assumed time series is a scaling factor for $device_key" - Device_dict[device_key]["scalingfactor"] = TimeSeries.TimeArray(df[:DateTime],ts_raw) - else - @info "assumed time series is MW for $device_key" - Device_dict[device_key]["scalingfactor"] = TimeSeries.TimeArray(df[:DateTime],ts_raw/device["tech"]["installedcapacity"]) - end - end - end - return Device_dict -end - -function add_time_series(Device_dict::Dict{String,Any}, ts_raw::TimeSeries.TimeArray) - """ - Arg: - Device dictionary - Generators - Dict contains device Realtime/Forecast TimeSeries.TimeArray - Returns: - Device dictionary with timeseries added - """ - - name = get(Device_dict, "name", "") - if name == "" - throw(DataFormatError("input dict to add_time_series in wrong format")) - end - - if maximum(values(ts_raw)) > 1.0 - @warn "Time series for $name has values > 1.0, expected values in range {0.0,1.0}" - end - Device_dict["scalingfactor"] = ts_raw - - - return Device_dict -end - -""" -Arg: - Load dictionary - LoadZones dictionary - Dataframe contains device Realtime/Forecast TimeSeries -Returns: - Device dictionary with timeseries added -""" -function add_time_series_load(data::Dict{String,Any}, df::DataFrames.DataFrame) - load_dict = data["load"] - - load_names = [string(l["name"]) for (k,l) in load_dict] - ts_names = [string(n) for n in names(df) if n != :DateTime] - - write_sf_by_lz = false - lzkey = [k for k in ["loadzone","load_zone"] if haskey(data,k)][1] - if lzkey in keys(data) - load_zone_dict = data[lzkey] - z_names = [string(z["name"]) for (k,z) in load_zone_dict] - if length([n for n in z_names if n in ts_names]) > 0 - write_sf_by_lz = true - end - end - - assigned_loads = [] - if write_sf_by_lz - @info "assigning load scaling factors by load_zone" - # TODO: make this faster/better - for (l_key,l) in load_dict - for (lz_key,lz) in load_zone_dict - if l["bus"] in lz["buses"] - ts_raw = df[lz_key]/lz["maxactivepower"] - load_dict[l_key]["scalingfactor"] = TimeSeries.TimeArray(df[:DateTime],ts_raw) - push!(assigned_loads,l_key) - end - end - - end - else - @info "assigning load scaling factors by bus" - for (l_key,l) in load_dict - load_dict[l_key]["scalingfactor"] = TimeSeries.TimeArray(df[:DateTime],df[Symbol(l["name"])]) - push!(assigned_loads,l["name"]) - end - - end - - for l in [l for l in load_names if !(l in assigned_loads)] - @warn "No load scaling factor assigned for $l" maxlog=PS_MAX_LOG - end - - return load_dict -end - -## - Parse Dict to Struct -function bus_dict_parse(dict::Dict{Int,Any}) - Buses = [Bus(b["number"],b["name"], b["bustype"],b["angle"],b["voltage"],b["voltagelimits"],b["basevoltage"]) for (k_b,b) in dict ] - return Buses -end - - -## - Parse Dict to Array -function gen_dict_parser(dict::Dict{String,Any}) - Generators = Array{G where {G<:Generator},1}() - Storage_gen = Array{S where {S<:Storage},1}() - for (gen_type_key,gen_type_dict) in dict - if gen_type_key =="Thermal" - for (thermal_key,thermal_dict) in gen_type_dict - push!(Generators,ThermalDispatch(string(thermal_dict["name"]), - Bool(thermal_dict["available"]), - thermal_dict["bus"], - TechThermal(thermal_dict["tech"]["activepower"], - thermal_dict["tech"]["activepowerlimits"], - thermal_dict["tech"]["reactivepower"], - thermal_dict["tech"]["reactivepowerlimits"], - thermal_dict["tech"]["ramplimits"], - thermal_dict["tech"]["timelimits"]), - EconThermal(thermal_dict["econ"]["capacity"], - thermal_dict["econ"]["variablecost"], - thermal_dict["econ"]["fixedcost"], - thermal_dict["econ"]["startupcost"], - thermal_dict["econ"]["shutdncost"], - thermal_dict["econ"]["annualcapacityfactor"]) - )) - end - elseif gen_type_key =="Hydro" - for (hydro_key,hydro_dict) in gen_type_dict - push!(Generators,HydroCurtailment(string(hydro_dict["name"]), - hydro_dict["available"], - hydro_dict["bus"], - TechHydro( hydro_dict["tech"]["installedcapacity"], - hydro_dict["tech"]["activepower"], - hydro_dict["tech"]["activepowerlimits"], - hydro_dict["tech"]["reactivepower"], - hydro_dict["tech"]["reactivepowerlimits"], - hydro_dict["tech"]["ramplimits"], - hydro_dict["tech"]["timelimits"]), - hydro_dict["econ"]["curtailcost"], - hydro_dict["scalingfactor"] - )) - end - elseif gen_type_key =="Renewable" - for (ren_key,ren_dict) in gen_type_dict - if ren_key == "PV" - for (pv_key,pv_dict) in ren_dict - push!(Generators,RenewableCurtailment(string(pv_dict["name"]), - Bool( pv_dict["available"]), - pv_dict["bus"], - pv_dict["tech"]["installedcapacity"], - EconRenewable(pv_dict["econ"]["curtailcost"], - pv_dict["econ"]["interruptioncost"]), - pv_dict["scalingfactor"] - )) - end - elseif ren_key == "RTPV" - for (rtpv_key,rtpv_dict) in ren_dict - push!(Generators,RenewableFix(string(rtpv_dict["name"]), - Bool(rtpv_dict["available"]), - rtpv_dict["bus"], - rtpv_dict["tech"]["installedcapacity"], - rtpv_dict["scalingfactor"] - )) - end - elseif ren_key == "WIND" - for (wind_key,wind_dict) in ren_dict - push!(Generators,RenewableCurtailment(string(wind_dict["name"]), - Bool(wind_dict["available"]), - wind_dict["bus"], - wind_dict["tech"]["installedcapacity"], - EconRenewable(wind_dict["econ"]["curtailcost"], - wind_dict["econ"]["interruptioncost"]), - wind_dict["scalingfactor"] - )) - end - end - end - elseif gen_type_key =="Storage" - for (storage_key,storage_dict) in gen_type_dict - push!(Storage_gen,GenericBattery(string(storage_dict["name"]), - Bool(storage_dict["available"]), - storage_dict["bus"], - storage_dict["energy"], - storage_dict["capacity"], - storage_dict["activepower"], - storage_dict["inputactivepowerlimits"], - storage_dict["outputactivepowerlimits"], - storage_dict["efficiency"], - storage_dict["reactivepower"], - storage_dict["reactivepowerlimits"] - )) - end - end - end - return (Generators, Storage_gen) -end - -# - Parse Dict to Array - -function branch_dict_parser(dict::Dict{String,Any},Branches::Array{B,1}) where {B<:Branch} - for (branch_key,branch_dict) in dict - if branch_key == "Transformers" - for (trans_key,trans_dict) in branch_dict - if trans_dict["tap"] ==1.0 - push!(Branches,Transformer2W(string(trans_dict["name"]), - Bool(trans_dict["available"]), - trans_dict["connectionpoints"], - trans_dict["r"], - trans_dict["x"], - trans_dict["primaryshunt"], - trans_dict["rate"] - )) - elseif trans_dict["tap"] !=1.0 - alpha = "α" in keys(trans_dict) ? trans_dict["α"] : 0.0 - if alpha !=0.0 #TODO : 3W Transformer - push!(Branches,PhaseShiftingTransformer(string(trans_dict["name"]), - Bool(trans_dict["available"]), - trans_dict["connectionpoints"], - trans_dict["r"], - trans_dict["x"], - trans_dict["primaryshunt"], - trans_dict["tap"], - trans_dict["α"], - trans_dict["rate"] - )) - else - push!(Branches,TapTransformer(string(trans_dict["name"]), - Bool(trans_dict["available"]), - trans_dict["connectionpoints"], - trans_dict["r"], - trans_dict["x"], - trans_dict["primaryshunt"], - trans_dict["tap"], - trans_dict["rate"] - )) - end - end - end - else branch_key == "Lines" - for (line_key,line_dict) in branch_dict - push!(Branches,Line(string(line_dict["name"]), - Bool(line_dict["available"]), - line_dict["connectionpoints"], - line_dict["r"], - line_dict["x"], - line_dict["b"], - float(line_dict["rate"]), - line_dict["anglelimits"] - )) - end - end - end - return Branches -end - - -function load_dict_parser(dict::Dict{String,Any}) - Loads =Array{L where {L<:ElectricLoad},1}() - for (load_key,load_dict) in dict - push!(Loads,PowerLoad(string(load_dict["name"]), - Bool(load_dict["available"]), - load_dict["bus"], - load_dict["maxactivepower"], - load_dict["maxreactivepower"], - load_dict["scalingfactor"] - )) - end - return Loads -end - -function loadzone_dict_parser(dict::Dict{Int64,Any}) - LoadZs =Array{D where {D<:Device},1}() - for (lz_key,lz_dict) in dict - push!(LoadZs,LoadZones(lz_dict["number"], - string(lz_dict["name"]), - lz_dict["buses"], - lz_dict["maxactivepower"], - lz_dict["maxreactivepower"] - )) - end - return LoadZs -end - -function shunt_dict_parser(dict::Dict{String,Any}) - Shunts = Array{FixedAdmittance,1}() - for (s_key,s_dict) in dict - push!(Shunts,FixedAdmittance(string(s_dict["name"]), - Bool(s_dict["available"]), - s_dict["bus"], - s_dict["Y"] - ) - ) - end - return Shunts -end - - -function dclines_dict_parser(dict::Dict{String,Any},Branches::Array{Branch,1}) - for (dct_key,dct_dict) in dict - if dct_key == "HVDCLine" - for (dcl_key,dcl_dict) in dct_dict - push!(Branches,HVDCLine(string(dcl_dict["name"]), - Bool(dcl_dict["available"]), - dcl_dict["connectionpoints"], - dcl_dict["activepowerlimits_from"], - dcl_dict["activepowerlimits_to"], - dcl_dict["reactivepowerlimits_from"], - dcl_dict["reactivepowerlimits_to"], - dcl_dict["loss"] - )) - end - elseif dct_key == "VSCDCLine" - for (dcl_key,dcl_dict) in dct_dict - push!(Branches,VSCDCLine(string(dcl_dict["name"]), - Bool(dcl_dict["available"]), - dcl_dict["connectionpoints"], - dcl_dict["rectifier_taplimits"], - dcl_dict["rectifier_xrc"], - dcl_dict["rectifier_firingangle"], - dcl_dict["inverter_taplimits"], - dcl_dict["inverter_xrc"], - dcl_dict["inverter_firingangle"] - )) - end - end - end - return Branches -end - - -function services_dict_parser(dict::Dict{String,Any},generators::Array{Generator,1}) - Services = Array{D where {D <: Service},1}() - - for (k,d) in dict - contributingdevices = Array{D where {D<:PowerSystems.Device},1}() - [PowerSystems._get_device(dev,generators) for dev in d["contributingdevices"]] |> (x->[push!(contributingdevices,d[1]) for d in x if length(d)==1]) - push!(Services,ProportionalReserve(d["name"], - contributingdevices, - Float64(d["timeframe"]), - TimeSeries.TimeArray(Dates.today(),ones(1)) #TODO : fix requirement - )) - end - return Services -end diff --git a/src/parsers/enums.jl b/src/parsers/enums.jl new file mode 100644 index 0000000000..08f1566197 --- /dev/null +++ b/src/parsers/enums.jl @@ -0,0 +1,34 @@ + +@enum InputCategory begin + BRANCH + BUS + DC_BRANCH + GENERATOR + LOAD + RESERVES + SIMULATION_OBJECTS + STORAGE +end + +ENUM_MAPPINGS = Dict() +for enum in (AngleUnit, BusType, InputCategory) + ENUM_MAPPINGS[enum] = Dict() + for value in instances(enum) + ENUM_MAPPINGS[enum][lowercase(string(value))] = value + end +end + +"""Get the enum value for the string. Case insensitive.""" +function get_enum_value(enum, value::String) + if !haskey(ENUM_MAPPINGS, enum) + throw(ArgumentError("enum=$enum is not valid")) + end + + val = lowercase(value) + if !haskey(ENUM_MAPPINGS[enum], val) + throw(ArgumentError("enum=$enum does not have value=$val")) + end + + return ENUM_MAPPINGS[enum][val] +end + diff --git a/src/parsers/forecast_parser.jl b/src/parsers/forecast_parser.jl index d5e020ee36..c958cbce9b 100644 --- a/src/parsers/forecast_parser.jl +++ b/src/parsers/forecast_parser.jl @@ -1,242 +1,7 @@ -function get_name_and_csv(path_to_filename) - df = CSV.File(path_to_filename) |> DataFrames.DataFrame - folder = splitdir(splitdir(path_to_filename)[1])[2] - return folder, df -end - - -# Parser for Forecasts dat files -""" -Read all forecast CSV's in the path provided, the struct of the data should -follow this format -folder : PV - file : DAY_AHEAD - file : REAL_TIME -Folder name should be the device type -Files should only contain one real-time and day-ahead forecast -Args: - files: A string -Returns: - A dictionary with the CSV files as dataframes and folder names as keys -# TODO : Stochasti/Multiple scenarios -""" - function read_data_files(rootpath::String; kwargs...) - if :REGEX_FILE in keys(kwargs) - REGEX_FILE = kwargs[:REGEX_FILE] - else - REGEX_FILE = r"(.*?)\.csv" - end - - DATA = Dict{String, Any}() - data = Dict{String, Any}() - DATA["gen"] = data - - for (root, dirs, files) in walkdir(rootpath) - - for filename in files - - path_to_filename = joinpath(root, filename) - if match(REGEX_FILE, path_to_filename) != nothing - folder_name, csv_data = get_name_and_csv(path_to_filename) - if folder_name == "load" - DATA["load"] = read_datetime(csv_data; kwargs...) - else - data[folder_name] = read_datetime(csv_data; kwargs...) - end - @info "Successfully parsed $rootpath" - else - @warn "Unable to match regex with $path_to_filename" - end - end - - end - - return DATA -end - -""" -Args: - PowerSystems Dictionary - Dictionary of all the data files -Returns: - Returns an dictionary with Device name as key and PowerSystems Forecasts - dictionary as values -""" -function assign_ts_data(ps_dict::Dict{String,Any},ts_dict::Dict{String,Any}) - if haskey(ts_dict,"load") - ps_dict["load"] = PowerSystems.add_time_series_load(ps_dict,ts_dict["load"]) - else - @warn "Not assigning time series to loads" - end - - if haskey(ts_dict,"gen") - ts_map = _retrieve(ts_dict["gen"], Union{TimeSeries.TimeArray,DataFrames.DataFrame}) - for (key,val) in ts_map - ts = _access(ts_dict["gen"],vcat(val,key)) - dd_map = _retrieve(ps_dict["gen"],key) - if length(dd_map) > 0 - dd_map = string.(unique(values(dd_map))[1]) - else - @warn("no $key entries in psdict") - continue - end - dd = _access(ps_dict["gen"],vcat(dd_map,key)) - dd = PowerSystems.add_time_series(dd,ts) - end - else - @warn "Not assigning time series to gens" - end - - return ps_dict -end - -function make_device_forecast(device::D, df::DataFrames.DataFrame, resolution::Dates.Period,horizon::Int) where {D<:Device} - time_delta = Dates.Minute(df[2,:DateTime]-df[1,:DateTime]) - initialtime = df[1,:DateTime] # TODO :read the correct date/time when that was issued forecast - last_date = df[end,:DateTime] - ts_dict = Dict{Any,Dict{Int,TimeSeries.TimeArray}}() - ts_raw = TimeSeries.TimeArray(df[1],df[2]) - for ts in initialtime:resolution:last_date - ts_dict[ts] = Dict{Int,TimeSeries.TimeArray}(1 => ts_raw[ts:time_delta:(ts+resolution)]) - end - forecast = Dict{String,Any}("horizon" =>horizon, - "resolution" => resolution, #TODO : fix type conversion to JSON - "interval" => time_delta, #TODO : fix type conversion to JSON - "initialtime" => initialtime, - "device" => device, - "data" => ts_dict - ) - return forecast -end - - # -Parse csv file to dict -""" -Args: - Dictionary of all the data files - Length of the forecast - Week()/Dates.Day()/Dates.Hour() - Forecast horizon in hours - Int64 - Array of PowerSystems devices in the systems - Renewable Generators and - Loads -Returns: - Returns an dictionary with Device name as key and PowerSystems Forecasts - dictionary as values -""" -function make_forecast_dict(time_series::Dict{String,Any}, - resolution::Dates.Period, horizon::Int, - Devices::Array{Generator,1}) - forecast = Dict{String,Any}() - for device in Devices - for (key_df,df) in time_series - if device.name in map(String, names(df)) - for name in map(String, names(df)) - if name == device.name - forecast[device.name] = make_device_forecast(device, df[[:DateTime, Symbol(device.name)]], resolution, horizon) - end - end - end - end - if !haskey(forecast,device.name) - @info "No forecast found for $(device.name) " - end - end - return forecast -end - -""" -Args: - Dictionary of all the data files - Length of the forecast - Week()/Dates.Day()/Dates.Hour() - Forecast horizon in hours - Int64 - Array of PowerSystems devices in the systems- Loads -Returns: - Returns an dictionary with Device name as key and PowerSystems Forecasts - dictionary as values -""" -function make_forecast_dict(time_series::Dict{String,Any}, - resolution::Dates.Period, horizon::Int, - Devices::Array{ElectricLoad,1}) - forecast = Dict{String,Any}() - for device in Devices - if haskey(time_series,"load") - if device.bus.name in map(String, names(time_series["load"])) - df = time_series["load"][[:DateTime,Symbol(device.bus.name)]] - forecast[device.name] = make_device_forecast(device, df, resolution, horizon) - end - else - @warn "No forecast found for Loads" - end - end - return forecast -end - - -""" -Args: - Dictionary of all the data files - Length of the forecast - Week()/Dates.Day()/Dates.Hour() - Forecast horizon in hours - Int64 - Array of PowerSystems devices in the systems- Loads - Array of PowerSystems LoadZones - -Returns: - Returns an dictionary with Device name as key and PowerSystems Forecasts - dictionary as values -""" -function make_forecast_dict(time_series::Dict{String,Any}, - resolution::Dates.Period, horizon::Int, - Devices::Array{ElectricLoad,1}, - LoadZones::Array{Device,1}) - forecast = Dict{String,Any}() - for device in Devices - if haskey(time_series,"load") - for lz in LoadZones - if device.bus in lz.buses - df = time_series["load"][[:DateTime,Symbol(lz.name)]] - forecast[device.name] = make_device_forecast(device, df, resolution, horizon) - end - end - else - @warn "No forecast found for Loads" - end - end - return forecast -end -# - Parse Dict to Forecast Struct - -""" -Args: - A PowerSystems forecast dictionary -Returns: - A PowerSystems forecast stuct array -""" -function make_forecast_array(dict) - Forecasts = Array{Forecast}(undef, 0) - for (device_key,device_dict) in dict - push!(Forecasts,Deterministic(device_dict["device"],device_dict["horizon"], - device_dict["resolution"],device_dict["interval"], - device_dict["initialtime"], - device_dict["data"] - )) - end - return Forecasts -end - -# Write dict to Json - -function write_to_json(filename,Forecasts_dict) - for (type_key,type_fc) in Forecasts_dict - for (device_key,device_dicts) in type_fc - stringdata =JSON.json(device_dicts, 3) - open("$filename/$device_key.json", "w") do f - write(f, stringdata) - end - end - end -end - +#= # Parse json to dict #TODO : fix broken data formats function parse_json(filename,device_names) @@ -247,10 +12,11 @@ function parse_json(filename,device_names) open("$filename/x$name.json", "r") do f global temp dicttxt = readstring(f) # file information to string - temp=JSON.parse(dicttxt) # parse and transform data + temp=JSON2.read(dicttxt,Dict{Any,Array{Dict}}) # parse and transform data Devices[name] = temp end end end return Devices end +=# diff --git a/src/parsers/generator_mapping.yaml b/src/parsers/generator_mapping.yaml index 13b0ff1bed..d0cab230ab 100644 --- a/src/parsers/generator_mapping.yaml +++ b/src/parsers/generator_mapping.yaml @@ -1,22 +1,27 @@ -# Default configuration file for mapping generator fuel and type strings to -# generator categories in PowerSystems. All mapping strings should be in -# uppercase because that is what is assumed in read_gen(). +# Parsing code ignores type=null. -Hydro: - fuel: [HYDRO] - type: [HYDRO, HY] +HydroDispatch: +- {fuel: HYDRO, type: null} -WIND: - fuel: [WIND] - type: [WIND, W2] +RenewableDispatch: +- {fuel: SOLAR, type: PV} +- {fuel: SOLAR, type: UN} +- {fuel: WIND, type: WIND} +- {fuel: WIND, type: null} +- {fuel: SOLAR, type: CSP} # TODO: may need a new struct -PV: - fuel: [SOLAR, PV] - type: [PV] +RenewableFix: +- {fuel: SOLAR, type: RTPV} -RTPV: - fuel: [SOLAR, PV] - type: [RTPV] +ThermalStandard: +- {fuel: OIL, type: null} +- {fuel: COAL, type: null} +- {fuel: NG, type: null} +- {fuel: GAS, type: null} +- {fuel: NUCLEAR, type: null} +- {fuel: NUC, type: null} +- {fuel: SYNC_COND, type: SYNC_COND} +- {fuel: OTHER, type: OT} -# right now, everything else is classified as thermal, but could create -# mappings for other types if needed/wanted +GenericBattery: +- {fuel: STORAGE, type: null} diff --git a/src/parsers/im_io/common.jl b/src/parsers/im_io/common.jl index a4a052cf23..63c33c083e 100644 --- a/src/parsers/im_io/common.jl +++ b/src/parsers/im_io/common.jl @@ -1,7 +1,7 @@ "turns top level arrays into dicts" -function arrays_to_dicts!(data::Dict{String,Any}) +function arrays_to_dicts!(data::Dict{String,<:Any}) # update lookup structure for (k,v) in data if isa(v, Array) && length(v) > 0 && isa(v[1], Dict) diff --git a/src/parsers/im_io/data.jl b/src/parsers/im_io/data.jl index 7784355ec5..67098cf6d8 100644 --- a/src/parsers/im_io/data.jl +++ b/src/parsers/im_io/data.jl @@ -1,7 +1,7 @@ export update_data! "recursively applies new_data to data, overwriting information" -function update_data!(data::Dict{String,Any}, new_data::Dict{String,Any}) +function update_data!(data::Dict{String,<:Any}, new_data::Dict{String,<:Any}) if haskey(data, "per_unit") && haskey(new_data, "per_unit") if data["per_unit"] != new_data["per_unit"] error("update_data requires datasets in the same units, try make_per_unit and make_mixed_units") @@ -14,7 +14,7 @@ end "recursive call of _update_data" -function _update_data!(data::Dict{String,Any}, new_data::Dict{String,Any}) +function _update_data!(data::Dict{String,<:Any}, new_data::Dict{String,<:Any}) for (key, new_v) in new_data if haskey(data, key) v = data[key] @@ -30,26 +30,15 @@ function _update_data!(data::Dict{String,Any}, new_data::Dict{String,Any}) end "checks if a given network data is a multinetwork" -ismultinetwork(data::Dict{String,Any}) = (haskey(data, "multinetwork") && data["multinetwork"] == true) +ismultinetwork(data::Dict{String,<:Any}) = (haskey(data, "multinetwork") && data["multinetwork"] == true) "Transforms a single network into a multinetwork with several deepcopies of the original network" -function im_replicate(sn_data::Dict{String,Any}, count::Int; global_keys::Set{String} = Set{String}()) +function im_replicate(sn_data::Dict{String,<:Any}, count::Int, global_keys::Set{String}) @assert count > 0 if ismultinetwork(sn_data) error("replicate can only be used on single networks") end - if length(global_keys) <= 0 - @warn "deprecation warning, calls to replicate should explicitly specify a set of global_keys" - # old default - for (k,v) in sn_data - if !(typeof(v) <: Dict) - @warn "adding global key $(k)" - push!(global_keys, k) - end - end - end - name = get(sn_data, "name", "anonymous") mn_data = Dict{String,Any}( @@ -79,198 +68,12 @@ end -"builds a table of component data" -function component_table(data::Dict{String,Any}, component::String, fields::Vector{String}) - if ismultinetwork(data) - return Dict((i, _component_table(nw_data, component, fields)) for (i,nw_data) in data["nw"]) - else - return _component_table(data, component, fields) - end -end -component_table(data::Dict{String,Any}, component::String, field::String) = component_table(data, component, [field]) - -function _component_table(data::Dict{String,Any}, component::String, fields::Vector{String}) - comps = data[component] - if !_iscomponentdict(comps) - @error "$(component) does not appear to refer to a component list" - end - - items = [] - sorted_comps = sort(collect(comps); by=x->parse(Int, x[1])) - for (i,comp) in sorted_comps - push!(items, parse(Int, i)) - end - for key in fields - for (i,comp) in sorted_comps - if haskey(comp, key) - push!(items, comp[key]) - else - push!(items, NaN) - end - end - end - - return reshape(items, length(comps), length(fields)+1) -end - - -"prints the text summary for a data dictionary to stdout" -function print_summary(obj::Dict{String,Any}; kwargs...) - summary(stdout, obj; kwargs...) -end - - -"prints the text summary for a data dictionary to IO" -function summary(io::IO, data::Dict{String,Any}; - float_precision::Int = 3, - component_types_order = Dict(), - component_parameter_order = Dict(), - max_parameter_value = 999.0, - component_status_parameters = Set(["status"]) - ) - - if ismultinetwork(data) - error("summary does not yet support multinetwork data") - end - - component_types = [] - other_types = [] - - println(io, _bold("Metadata")) - for (k,v) in sort(collect(data); by=x->x[1]) - if typeof(v) <: Dict && _iscomponentdict(v) - push!(component_types, k) - continue - end - - println(io, " $(k): $(_value2string(v, float_precision))") - end - - - if length(component_types) > 0 - println(io, "") - println(io, _bold("Table Counts")) - end - for k in sort(component_types, by=x->get(component_types_order, x, max_parameter_value)) - println(io, " $(k): $(length(data[k]))") - end - - for comp_type in sort(component_types, by=x->get(component_types_order, x, max_parameter_value)) - if length(data[comp_type]) <= 0 - continue - end - println(io, "") - println(io, "") - println(io, _bold("Table: $(comp_type)")) - - components = data[comp_type] - - display_components = Dict() - active_components = Set() - for (i, component) in components - disp_comp = copy(component) - - status_found = false - for (k, v) in disp_comp - if k in component_status_parameters - status_found = true - if !(v == 0) - push!(active_components, i) - end - end - - disp_comp[k] = _value2string(v, float_precision) - end - if !status_found - push!(active_components, i) - end - - display_components[i] = disp_comp - end - - - comp_key_sizes = Dict{String, Int}() - default_values = Dict{String, Any}() - for (i, component) in display_components - # a special case for "index", for example when reading solution data - if haskey(comp_key_sizes, "index") - comp_key_sizes["index"] = max(comp_key_sizes["index"], length(i)) - else - comp_key_sizes["index"] = length(i) - end - - for (k, v) in component - if haskey(comp_key_sizes, k) - comp_key_sizes[k] = max(comp_key_sizes[k], length(v)) - else - comp_key_sizes[k] = length(v) - end - - if haskey(default_values, k) - if default_values[k] != v - default_values[k] = nothing - end - else - default_values[k] = v - end - end - end - - # when there is only one component nothing is default - if length(display_components) == 1 - default_values = Dict{String, Any}() - else - default_values = Dict{String, Any}([x for x in default_values if !isa(x.second, Nothing)]) - end - - #display(default_values) - - # account for header width - for (k, v) in comp_key_sizes - comp_key_sizes[k] = max(length(k), v) - end - - comp_id_pad = comp_key_sizes["index"] # not clear why this is offset so much - delete!(comp_key_sizes, "index") - comp_keys_ordered = sort([k for k in keys(comp_key_sizes) if !(haskey(default_values, k))], by=x->(get(component_parameter_order, x, max_parameter_value), x)) - - header = join([lpad(k, comp_key_sizes[k]) for k in comp_keys_ordered], ", ") - - pad = " "^(comp_id_pad+2) - println(io, " $(pad)$(header)") - for k in sort([k for k in keys(display_components)]; by=x->parse(Int, x)) - comp = display_components[k] - items = [] - for ck in comp_keys_ordered - if haskey(comp, ck) - push!(items, lpad("$(comp[ck])", comp_key_sizes[ck])) - else - push!(items, lpad("--", comp_key_sizes[ck])) - end - end - line = " $(lpad(k, comp_id_pad)): $(join(items, ", "))" - if k in active_components - println(io, line) - else - println(io, _grey(line)) - end - end - - if length(default_values) > 0 - println(io, "") - println(io, " default values:") - for k in sort([k for k in keys(default_values)], by=x->(get(component_parameter_order, x, max_parameter_value), x)) - println(io, " $(k): $(default_values[k])") - end - end - end - -end - +#= "Attempts to determine if the given data is a component dictionary" function _iscomponentdict(data::Dict) return all( typeof(comp) <: Dict for (i, comp) in data ) end +=# "Makes a string bold in the terminal" function _bold(s::String) @@ -326,7 +129,7 @@ function compare_dict(d1, d2) v2 = d2[k1] if isa(v1, Number) - if !compare_numbers(v1, v2) + if !_compare_numbers(v1, v2) return false end elseif isa(v1, Array) @@ -335,7 +138,7 @@ function compare_dict(d1, d2) end for i in 1:length(v1) if isa(v1[i], Number) - if !compare_numbers(v1[i], v2[i]) + if !_compare_numbers(v1[i], v2[i]) return false end else @@ -361,12 +164,9 @@ function compare_dict(d1, d2) return true end -function Base.isapprox(a::Any, b::Any; kwargs...) - return a == b -end "tests if two numbers are equal, up to floating point precision" -function compare_numbers(v1, v2) +function _compare_numbers(v1, v2) if isnan(v1) #println("1.1") if !isnan(v2) diff --git a/src/parsers/im_io/matlab.jl b/src/parsers/im_io/matlab.jl index b8cc00d1b0..df69a7d7fb 100644 --- a/src/parsers/im_io/matlab.jl +++ b/src/parsers/im_io/matlab.jl @@ -9,15 +9,13 @@ export parse_matlab_file, parse_matlab_string function parse_matlab_file(file_string::String; kwargs...) - result = open(file_string) do io - lines = readlines(io) - return parse_matlab_string(lines; kwargs...) - end - - return result + data_string = read(open(file_string),String) + return parse_matlab_string(data_string; kwargs...) end -function parse_matlab_string(data_lines::Array{String}; extended=false) +function parse_matlab_string(data_string::String; extended=false) + data_lines = split(data_string, '\n') + matlab_dict = Dict{String,Any}() struct_name = nothing function_name = nothing @@ -35,7 +33,7 @@ function parse_matlab_string(data_lines::Array{String}; extended=false) end if occursin("function", line) - func, value = extract_matlab_assignment(line) + func, value = _extract_matlab_assignment(line) struct_name = strip(replace(func, "function" => "")) function_name = value elseif occursin("=",line) @@ -44,22 +42,22 @@ function parse_matlab_string(data_lines::Array{String}; extended=false) end if occursin("[", line) - matrix_dict = parse_matlab_matrix(data_lines, index) + matrix_dict = _parse_matlab_matrix(data_lines, index) matlab_dict[matrix_dict["name"]] = matrix_dict["data"] - if haskey(matrix_dict, "column_names") + if haskey(matrix_dict, "column_names") column_names[matrix_dict["name"]] = matrix_dict["column_names"] end - index = index + matrix_dict["line_count"] + index = index + matrix_dict["line_count"]-1 elseif occursin("{", line) - cell_dict = parse_matlab_cells(data_lines, index) + cell_dict = _parse_matlab_cells(data_lines, index) matlab_dict[cell_dict["name"]] = cell_dict["data"] if haskey(cell_dict, "column_names") column_names[cell_dict["name"]] = cell_dict["column_names"] end - index = index + cell_dict["line_count"] + index = index + cell_dict["line_count"]-1 else - name, value = extract_matlab_assignment(line) - value = type_value(value) + name, value = _extract_matlab_assignment(line) + value = _type_value(value) matlab_dict[name] = value end else @@ -78,7 +76,7 @@ end "breaks up matlab strings of the form 'name = value;'" -function extract_matlab_assignment(string::AbstractString) +function _extract_matlab_assignment(string::AbstractString) statement = split(string, ';')[1] statement_parts = split(statement, '=') @assert(length(statement_parts) == 2) @@ -89,7 +87,7 @@ end "Attempts to determine the type of a string extracted from a matlab file" -function type_value(value_string::AbstractString) +function _type_value(value_string::AbstractString) value_string = strip(value_string) if occursin("'", value_string) # value is a string @@ -107,7 +105,7 @@ function type_value(value_string::AbstractString) end "Attempts to determine the type of an array of strings extracted from a matlab file" -function type_array(string_array::Vector{T}) where {T <: AbstractString} +function _type_array(string_array::Vector{T}) where {T <: AbstractString} value_string = [strip(value_string) for value_string in string_array] return if any(occursin("'",value_string) for value_string in string_array) @@ -121,13 +119,13 @@ end "" -parse_matlab_cells(lines, index) = parse_matlab_data(lines, index, '{', '}') +_parse_matlab_cells(lines, index) = _parse_matlab_data(lines, index, '{', '}') "" -parse_matlab_matrix(lines, index) = parse_matlab_data(lines, index, '[', ']') +_parse_matlab_matrix(lines, index) = _parse_matlab_data(lines, index, '[', ']') "" -function parse_matlab_data(lines, index, start_char, end_char) +function _parse_matlab_data(lines, index, start_char, end_char) last_index = length(lines) line_count = 0 columns = -1 @@ -169,7 +167,7 @@ function parse_matlab_data(lines, index, start_char, end_char) end #print(matrix_body_lines) - matrix_body_lines = [add_line_delimiter(line, start_char, end_char) for line in matrix_body_lines] + matrix_body_lines = [_add_line_delimiter(line, start_char, end_char) for line in matrix_body_lines] #print(matrix_body_lines) matrix_body = join(matrix_body_lines, ' ') @@ -190,7 +188,7 @@ function parse_matlab_data(lines, index, start_char, end_char) end rows = length(matrix) - typed_columns = [type_array([ matrix[r][c] for r in 1:rows ]) for c in 1:columns] + typed_columns = [_type_array([ matrix[r][c] for r in 1:rows ]) for c in 1:columns] for r in 1:rows matrix[r] = [typed_columns[c][r] for c in 1:columns] end @@ -260,7 +258,7 @@ function split_line(mp_line::AbstractString) end "" -function add_line_delimiter(mp_line::AbstractString, start_char, end_char) +function _add_line_delimiter(mp_line::AbstractString, start_char, end_char) if strip(mp_line) == string(start_char) return mp_line end @@ -303,6 +301,3 @@ function check_type(typ, value) end end end - - - diff --git a/src/parsers/json2ps_parser.jl b/src/parsers/json2ps_parser.jl index 042eb0ef05..97e154b269 100644 --- a/src/parsers/json2ps_parser.jl +++ b/src/parsers/json2ps_parser.jl @@ -54,7 +54,7 @@ function gen_json_parser(dict::Dict{String,Any}) for (gen_type_key,gen_type_dict) in dict if gen_type_key =="Thermal" for (thermal_key,thermal_dict) in gen_type_dict - push!(Generators,ThermalDispatch(thermal_dict["name"], + push!(Generators,ThermalStandard(thermal_dict["name"], thermal_dict["available"], Bus(thermal_dict["bus"]["number"], thermal_dict["bus"]["name"], @@ -69,17 +69,15 @@ function gen_json_parser(dict::Dict{String,Any}) (min =thermal_dict["tech"]["reactivepowerlimits"]["min"],max =thermal_dict["tech"]["reactivepowerlimits"]["min"]), (up=thermal_dict["tech"]["ramplimits"]["up"],down=thermal_dict["tech"]["ramplimits"]["down"]), (up=thermal_dict["tech"]["timelimits"]["up"],down=thermal_dict["tech"]["timelimits"]["down"])), - EconThermal(thermal_dict["econ"]["capacity"], - json_var_cost(thermal_dict["econ"]["variablecost"]), - thermal_dict["econ"]["fixedcost"], - thermal_dict["econ"]["startupcost"], - thermal_dict["econ"]["shutdncost"], - thermal_dict["econ"]["annualcapacityfactor"]) + ThreePartCost(json_var_cost(thermal_dict["econ"]["variable"]), + thermal_dict["econ"]["fixedcost"], + thermal_dict["econ"]["startup"], + thermal_dict["econ"]["shutdn"]) )) end elseif gen_type_key =="Hydro" for (hydro_key,hydro_dict) in gen_type_dict - push!(Generators,HydroCurtailment(hydro_dict["name"], + push!(Generators,HydroDispatch(hydro_dict["name"], hydro_dict["available"], Bus(hydro_dict["bus"]["number"], hydro_dict["bus"]["name"], @@ -88,7 +86,7 @@ function gen_json_parser(dict::Dict{String,Any}) hydro_dict["bus"]["voltage"], (min =hydro_dict["bus"]["voltagelimits"]["min"],max=hydro_dict["bus"]["voltagelimits"]["max"]), hydro_dict["bus"]["basevoltage"] ), - TechHydro( hydro_dict["tech"]["installedcapacity"], + TechHydro( hydro_dict["tech"]["rating"], hydro_dict["tech"]["activepower"], (min =hydro_dict["tech"]["activepowerlimits"]["min"],max =hydro_dict["tech"]["activepowerlimits"]["max"]), hydro_dict["tech"]["reactivepower"], @@ -103,7 +101,7 @@ function gen_json_parser(dict::Dict{String,Any}) for (ren_key,ren_dict) in gen_type_dict if ren_key == "PV" for (pv_key,pv_dict) in ren_dict - push!(Generators,RenewableCurtailment(pv_dict["name"], + push!(Generators,RenewableDispatch(pv_dict["name"], pv_dict["available"], Bus(pv_dict["bus"]["number"], pv_dict["bus"]["name"], @@ -112,8 +110,8 @@ function gen_json_parser(dict::Dict{String,Any}) pv_dict["bus"]["voltage"], (min =pv_dict["bus"]["voltagelimits"]["min"],max=pv_dict["bus"]["voltagelimits"]["max"]), pv_dict["bus"]["basevoltage"] ), - pv_dict["tech"]["installedcapacity"], - EconRenewable(pv_dict["econ"]["curtailcost"], + pv_dict["tech"]["rating"], + TwoPartCost(pv_dict["econ"]["curtailcost"], pv_dict["econ"]["interruptioncost"]), dict_to_timearray(pv_dict["scalingfactor"]) )) @@ -129,13 +127,13 @@ function gen_json_parser(dict::Dict{String,Any}) rtpv_dict["bus"]["voltage"], (min =rtpv_dict["bus"]["voltagelimits"]["min"],max=rtpv_dict["bus"]["voltagelimits"]["max"]), rtpv_dict["bus"]["basevoltage"] ), - rtpv_dict["tech"]["installedcapacity"], + rtpv_dict["tech"]["rating"], dict_to_timearray(rtpv_dict["scalingfactor"]) )) end elseif ren_key == "WIND" for (wind_key,wind_dict) in ren_dict - push!(Generators,RenewableCurtailment(wind_dict["name"], + push!(Generators,RenewableDispatch(wind_dict["name"], wind_dict["available"], Bus(wind_dict["bus"]["number"], wind_dict["bus"]["name"], @@ -144,8 +142,8 @@ function gen_json_parser(dict::Dict{String,Any}) wind_dict["bus"]["voltage"], (min =wind_dict["bus"]["voltagelimits"]["min"],max=wind_dict["bus"]["voltagelimits"]["max"]), wind_dict["bus"]["basevoltage"] ), - wind_dict["tech"]["installedcapacity"], - EconRenewable(wind_dict["econ"]["curtailcost"], + wind_dict["tech"]["rating"], + TwoPartCost(wind_dict["econ"]["curtailcost"], wind_dict["econ"]["interruptioncost"]), dict_to_timearray(wind_dict["scalingfactor"]) )) @@ -184,20 +182,20 @@ function branch_json_parser(dict) for (branch_key,branch_dict) in dict if branch_key == "Transformers" for (trans_key,trans_dict) in branch_dict - bus_f =Bus(trans_dict["connectionpoints"]["from"]["number"], - trans_dict["connectionpoints"]["from"]["name"], - trans_dict["connectionpoints"]["from"]["bustype"], - trans_dict["connectionpoints"]["from"]["angle"], - trans_dict["connectionpoints"]["from"]["voltage"], - (min =trans_dict["connectionpoints"]["from"]["voltagelimits"]["min"],max=trans_dict["connectionpoints"]["from"]["voltagelimits"]["max"]), - trans_dict["connectionpoints"]["from"]["basevoltage"] ) - bus_t =Bus(trans_dict["connectionpoints"]["to"]["number"], - trans_dict["connectionpoints"]["to"]["name"], - trans_dict["connectionpoints"]["to"]["bustype"], - trans_dict["connectionpoints"]["to"]["angle"], - trans_dict["connectionpoints"]["to"]["voltage"], - (min =trans_dict["connectionpoints"]["to"]["voltagelimits"]["min"],max=trans_dict["connectionpoints"]["to"]["voltagelimits"]["max"]), - trans_dict["connectionpoints"]["to"]["basevoltage"] ) + bus_f =Bus(trans_dict["arc"]["from"]["number"], + trans_dict["arc"]["from"]["name"], + trans_dict["arc"]["from"]["bustype"], + trans_dict["arc"]["from"]["angle"], + trans_dict["arc"]["from"]["voltage"], + (min =trans_dict["arc"]["from"]["voltagelimits"]["min"],max=trans_dict["arc"]["from"]["voltagelimits"]["max"]), + trans_dict["arc"]["from"]["basevoltage"] ) + bus_t =Bus(trans_dict["arc"]["to"]["number"], + trans_dict["arc"]["to"]["name"], + trans_dict["arc"]["to"]["bustype"], + trans_dict["arc"]["to"]["angle"], + trans_dict["arc"]["to"]["voltage"], + (min =trans_dict["arc"]["to"]["voltagelimits"]["min"],max=trans_dict["arc"]["to"]["voltagelimits"]["max"]), + trans_dict["arc"]["to"]["basevoltage"] ) if trans_dict["tap"] ==1.0 push!(Branches,Transformer2W(trans_dict["name"], trans_dict["available"], @@ -221,20 +219,20 @@ function branch_json_parser(dict) end elseif branch_key == "Lines" for (line_key,line_dict) in branch_dict - bus_t =Bus(line_dict["connectionpoints"]["to"]["number"], - line_dict["connectionpoints"]["to"]["name"], - line_dict["connectionpoints"]["to"]["bustype"], - line_dict["connectionpoints"]["to"]["angle"], - line_dict["connectionpoints"]["to"]["voltage"], - (min =line_dict["connectionpoints"]["to"]["voltagelimits"]["min"],max=line_dict["connectionpoints"]["to"]["voltagelimits"]["max"]), - line_dict["connectionpoints"]["to"]["basevoltage"] ) - bus_f =Bus(line_dict["connectionpoints"]["from"]["number"], - line_dict["connectionpoints"]["from"]["name"], - line_dict["connectionpoints"]["from"]["bustype"], - line_dict["connectionpoints"]["from"]["angle"], - line_dict["connectionpoints"]["from"]["voltage"], - (min =line_dict["connectionpoints"]["from"]["voltagelimits"]["min"],max=line_dict["connectionpoints"]["from"]["voltagelimits"]["max"]), - line_dict["connectionpoints"]["from"]["basevoltage"] ) + bus_t =Bus(line_dict["arc"]["to"]["number"], + line_dict["arc"]["to"]["name"], + line_dict["arc"]["to"]["bustype"], + line_dict["arc"]["to"]["angle"], + line_dict["arc"]["to"]["voltage"], + (min =line_dict["arc"]["to"]["voltagelimits"]["min"],max=line_dict["arc"]["to"]["voltagelimits"]["max"]), + line_dict["arc"]["to"]["basevoltage"] ) + bus_f =Bus(line_dict["arc"]["from"]["number"], + line_dict["arc"]["from"]["name"], + line_dict["arc"]["from"]["bustype"], + line_dict["arc"]["from"]["angle"], + line_dict["arc"]["from"]["voltage"], + (min =line_dict["arc"]["from"]["voltagelimits"]["min"],max=line_dict["arc"]["from"]["voltagelimits"]["max"]), + line_dict["arc"]["from"]["basevoltage"] ) push!(Branches,Line(line_dict["name"], line_dict["available"], (from = bus_f, to = bus_t), @@ -274,7 +272,7 @@ end # Write dict to json file function dict_to_json(dict,filename) - stringdata =JSON.json(dict, 3) + stringdata =JSON2.write(dict) open("$filename.json", "w") do f write(f, stringdata) end @@ -288,7 +286,7 @@ function json_parser(filename) open("../data/CDM/RTS/JSON/RTS-GMLC_Test_Case.json", "r") do f global temp dicttxt = readstring(f) # file information to string - temp = JSON.parse(dicttxt) # parse and transform data + temp = JSON2.read(dicttxt, Dict{Any,Array{Dict}}) # parse and transform data data = temp end else diff --git a/src/parsers/pm2ps_parser.jl b/src/parsers/pm2ps_parser.jl index dad91adda0..76aad9ee35 100644 --- a/src/parsers/pm2ps_parser.jl +++ b/src/parsers/pm2ps_parser.jl @@ -1,64 +1,27 @@ -MAPPING_BUSNUMBER2INDEX = Dict{Int64, Int64}() - """ -Takes a dictionary parsed by PowerModels and returns a PowerSystems -dictionary. Currently Supports MATPOWER and PSSE data files parsed by -PowerModels +Converts a dictionary parsed by PowerModels to a System. +Currently Supports MATPOWER and PSSE data files parsed by PowerModels. """ function pm2ps_dict(data::Dict{String,Any}; kwargs...) if length(data["bus"]) < 1 throw(DataFormatError("There are no buses in this file.")) end - ps_dict = Dict{String,Any}() - ps_dict["name"] = data["name"] - ps_dict["baseMVA"] = data["baseMVA"] - ps_dict["source_type"] = data["source_type"] - @info "Reading bus data" - Buses = read_bus(data) - if !isa(Buses,Nothing) - ps_dict["bus"] = Buses - else - @error "No bus data found" # TODO : need for a model without a bus - end - @info "Reading load data" - Loads= read_loads(data,ps_dict["bus"]) - LoadZones= read_loadzones(data,ps_dict["bus"]) - @info "Reading generator data" - Generators= read_gen(data, ps_dict["bus"]; kwargs...) - @info "Reading branch data" - Branches= read_branch(data,ps_dict["bus"]) - Shunts = read_shunt(data,ps_dict["bus"]) - DCLines= read_dcline(data,ps_dict["bus"]) - ps_dict["load"] = Loads - if !isa(LoadZones,Nothing) - ps_dict["loadzone"] = LoadZones - else - @info "There are no Load Zones data in this file" - end - if !isa(Generators,Nothing) - ps_dict["gen"] = Generators - else - @error "There are no Generators in this file" - end - if !isa(Branches,Nothing) - ps_dict["branch"] = Branches - else - @info "There is no Branch data in this file" - end - if !isa(Shunts,Nothing) - ps_dict["shunt"] = Shunts - else - @info "There is no shunt data in this file" - end - if !isa(DCLines,Nothing) - ps_dict["dcline"] = DCLines - else - @info "There is no DClines data in this file" - end + @info "Constructing System from Power Models" data["name"] data["source_type"] + + sys = System(data["baseMVA"]) + + bus_number_to_bus = read_bus!(sys, data) + read_loads!(sys, data, bus_number_to_bus) + read_loadzones!(sys, data, bus_number_to_bus) + read_gen!(sys, data, bus_number_to_bus; kwargs...) + read_branch!(sys, data, bus_number_to_bus) + read_shunt!(sys, data, bus_number_to_bus) + read_dcline!(sys, data, bus_number_to_bus) - return ps_dict + check!(sys) + return sys end @@ -77,373 +40,500 @@ function make_bus(bus_dict::Dict{String,Any}) return bus end -""" -Finds the bus dictionary where a Generator/Load is located or the from & to bus -for a line/transformer -""" -function find_bus(Buses::Dict{Int64,Any},device_dict::Dict{String,Any}) - if haskey(device_dict, "t_bus") - if haskey(device_dict, "f_bus") - t_bus = Buses[MAPPING_BUSNUMBER2INDEX[Int(device_dict["t_bus"])]] - f_bus = Buses[MAPPING_BUSNUMBER2INDEX[Int(device_dict["f_bus"])]] - value =(f_bus,t_bus) - end - elseif haskey(device_dict, "gen_bus") - bus = Buses[MAPPING_BUSNUMBER2INDEX[Int(device_dict["gen_bus"])]] - value =bus - elseif haskey(device_dict, "load_bus") - bus = Buses[MAPPING_BUSNUMBER2INDEX[Int(device_dict["load_bus"])]] - value =bus - elseif haskey(device_dict,"shunt_bus") - bus = Buses[MAPPING_BUSNUMBER2INDEX[Int(device_dict["shunt_bus"])]] - value =bus - else - throw(DataFormatError("Provided Dict missing key/s: gen_bus or f_bus/t_bus or load_bus")) - end - return value -end - -function make_bus(bus_name, d, bus_types) - bus = Dict{String,Any}("name" => bus_name , - "number" => MAPPING_BUSNUMBER2INDEX[d["bus_i"]], +function make_bus(bus_name, bus_number, d, bus_types) + bus = make_bus(Dict{String,Any}("name" => bus_name , + "number" => bus_number, "bustype" => bus_types[d["bus_type"]], - "angle" => 0, # NOTE: angle 0, tuple(min, max) + "angle" => d["va"], "voltage" => d["vm"], "voltagelimits" => (min=d["vmin"], max=d["vmax"]), "basevoltage" => d["base_kv"] - ) + )) return bus end -function read_bus(data) - Buses = Dict{Int64,Any}() - bus_types = ["PV", "PQ", "SF","isolated"] - data = sort(collect(data["bus"]), by = x->parse(Int64,x[1])) +# "From http://www.pserc.cornell.edu/matpower/MATPOWER-manual.pdf Table B-1" +@enum MatpowerBusType begin + MATPOWER_PQ = 1 + MATPOWER_PV = 2 + MATPOWER_REF = 3 + MATPOWER_ISOLATED = 4 +end + +function Base.convert(::Type{BusType}, x::MatpowerBusType) + map = Dict(MATPOWER_ISOLATED => ISOLATED, + MATPOWER_PQ => PQ, + MATPOWER_PV => PV, + MATPOWER_REF => REF) + return map[x] +end + +function read_bus!(sys::System, data) + @info "Reading bus data" + bus_number_to_bus = Dict{Int, Bus}() + + bus_types = instances(MatpowerBusType) + data = sort(collect(data["bus"]), by = x->parse(Int64,x[1])) + + if length(data) == 0 + @error "No bus data found" # TODO : need for a model without a bus + end + for (i, (d_key, d)) in enumerate(data) # d id the data dict for each bus # d_key is bus key - haskey(d,"bus_name") ? bus_name = d["bus_name"] : bus_name = string(d["bus_i"]) + bus_name = haskey(d,"name") ? d["name"] : string(d["bus_i"]) bus_number = Int(d["bus_i"]) - MAPPING_BUSNUMBER2INDEX[bus_number] = i - Buses[MAPPING_BUSNUMBER2INDEX[bus_number]] = make_bus(bus_name, d, bus_types) + bus = make_bus(bus_name, bus_number, d, bus_types) + bus_number_to_bus[bus.number] = bus + add_component!(sys, bus; skip_validation=SKIP_PM_VALIDATION) end - return Buses + + return bus_number_to_bus end -function make_load(d,bus) - load =Dict{String,Any}("name" => bus["name"], - "available" => true, - "bus" => make_bus(bus), - "maxactivepower" => d["pd"], - "maxreactivepower" => d["qd"], - "scalingfactor" => TimeSeries.TimeArray(collect(Dates.DateTime(Dates.today()):Dates.Hour(1):Dates.DateTime(Dates.today()+Dates.Day(1))), ones(25)) - ) - return load +function make_load(d, bus) + return PowerLoad(; + name=bus.name, + available=true, + model = ConstantPower::LoadModel, + bus=bus, + activepower=d["pd"], + reactivepower=d["qd"], + maxactivepower=d["pd"], + maxreactivepower=d["qd"], + ) end -function read_loads(data,Buses) - if haskey(data,"load") - Loads = Dict{String,Any}() # Using least constrained Load - for d_key in keys(data["load"]) - d = data["load"][d_key] - if d["pd"] != 0.0 - # NOTE: access nodes using index i in case numbering of original data not sequential/consistent - bus = find_bus(Buses,d) - Loads[string(d["index"])] = make_load(d,bus) - end - end - return Loads - else +function read_loads!(sys::System, data, bus_number_to_bus::Dict{Int, Bus}) + if !haskey(data, "load") @error "There are no loads in this file" + return + end + + for d_key in keys(data["load"]) + d = data["load"][d_key] + if d["pd"] != 0.0 + bus = bus_number_to_bus[d["load_bus"]] + load = make_load(d, bus) + + add_component!(sys, load; skip_validation=SKIP_PM_VALIDATION) + end end end -function make_loadzones(d_key,d,bus_l,activepower, reactivepower) - loadzone = Dict{String,Any}("number" => d["index"], - "name" => d_key , - "buses" => bus_l, - "maxactivepower" => sum(activepower), - "maxreactivepower" => sum(reactivepower) - ) - return loadzone +function make_loadzones(d_key, d, bus_l, activepower, reactivepower) + return LoadZones(; + number=d["index"], + name=d_key, + buses=bus_l, + maxactivepower=sum(activepower), + maxreactivepower=sum(reactivepower), + ) end -function read_loadzones(data,Buses) - if haskey(data,"areas") - LoadZones = Dict{Int64,Any}() - for (d_key,d) in data["areas"] - b_array = [MAPPING_BUSNUMBER2INDEX[b["bus_i"]] for (b_key, b) in data["bus"] if b["area"] == d["index"] ] - bus_l = [make_bus(Buses[Int(b_key)]) for b_key in b_array] - activepower = [ l["pd"] for (l_key, l) in data["load"] if MAPPING_BUSNUMBER2INDEX[l["load_bus"]] in b_array ] #TODO: Fast Implementations - reactivepower = [ l["qd"] for (l_key, l) in data["load"] if MAPPING_BUSNUMBER2INDEX[l["load_bus"]] in b_array] - LoadZones[d["index"]] = make_loadzones(d_key,d,bus_l,activepower, reactivepower) +function read_loadzones!(sys::System, data, bus_number_to_bus::Dict{Int, Bus}) + if !haskey(data, "areas") + @info "There are no Load Zones data in this file" + return + end + + for (d_key, d) in data["areas"] + buses = [bus_number_to_bus[b["bus_i"]] for (b_key, b) in data["bus"] + if b["area"] == d["index"]] + bus_names = Set{String}() + for bus in buses + push!(bus_names, get_name(bus)) end - return LoadZones - else - return nothing + + active_power = Vector{Float64}() + reactive_power = Vector{Float64}() + + for (key, load) in data["load"] + load_bus = bus_number_to_bus[load["load_bus"]] + if get_name(load_bus) in bus_names + push!(active_power, load["pd"]) + push!(reactive_power, load["qd"]) + end + end + + load_zones = make_loadzones(d_key, d, buses, active_power, reactive_power) + add_component!(sys, load_zones; skip_validation=SKIP_PM_VALIDATION) end end function make_hydro_gen(gen_name, d, bus) ramp_agc = get(d, "ramp_agc", get(d, "ramp_10", get(d, "ramp_30", d["pmax"]))) - hydro = Dict{String,Any}("name" => gen_name, - "available" => d["gen_status"], # change from staus to available - "bus" => make_bus(bus), - "tech" => Dict{String,Any}( "installedcapacity" => float(d["pmax"]), - "activepower" => d["pg"], - "activepowerlimits" => (min=d["pmin"], max=d["pmax"]), - "reactivepower" => d["qg"], - "reactivepowerlimits" => (min=d["qmin"], max=d["qmax"]), - "ramplimits" => (up=ramp_agc/d["mbase"],down=ramp_agc/d["mbase"]), - "timelimits" => nothing), - "econ" => Dict{String,Any}("curtailcost" => 0.0, - "interruptioncost" => nothing), - "scalingfactor" => TimeSeries.TimeArray(collect(Dates.DateTime(Dates.today()):Dates.Hour(1):Dates.DateTime(Dates.today()+Dates.Day(1))), ones(25)) - ) - return hydro + tech = TechHydro(; + rating=calculate_rating(d["pmax"], d["qmax"]), + primemover=convert(PrimeMovers, d["type"]), + activepowerlimits=(min=d["pmin"], max=d["pmax"]), + reactivepowerlimits=(min=d["qmin"], max=d["qmax"]), + ramplimits=(up=ramp_agc / d["mbase"], down=ramp_agc / d["mbase"]), + timelimits=nothing, + ) + + curtailcost = TwoPartCost(0.0, 0.0) + + return HydroDispatch(name = gen_name, + available = Bool(d["gen_status"]), + bus = bus, + activepower = d["pg"], + reactivepower = d["qg"], + tech = tech, + op_cost = curtailcost) +end + +function make_tech_renewable(d) + tech = TechRenewable(; + rating=float(d["pmax"]), + primemover=convert(PrimeMovers, d["type"]), + reactivepowerlimits=(min=d["qmin"], max=d["qmax"]), + powerfactor=1.0, + ) + + return tech +end + +function make_renewable_dispatch(gen_name, d, bus) + tech = make_tech_renewable(d) + cost = TwoPartCost(0.0, 0.0) + generator = RenewableDispatch(; + name=gen_name, + available=Bool(d["gen_status"]), + bus=bus, + activepower = d["pg"], + reactivepower = d["qg"], + tech=tech, + op_cost=cost, + ) + + return generator end -function make_ren_gen(gen_name, d, bus) - gen_re = Dict{String,Any}("name" => gen_name, - "available" => d["gen_status"], # change from staus to available - "bus" => make_bus(bus), - "tech" => Dict{String,Any}("installedcapacity" => float(d["pmax"]), - "reactivepowerlimits" => (min=d["pmin"], max=d["pmax"]), - "powerfactor" => 1), - "econ" => Dict{String,Any}("curtailcost" => 0.0, - "interruptioncost" => nothing), - "scalingfactor" => TimeSeries.TimeArray(collect(Dates.DateTime(Dates.today()):Dates.Hour(1):Dates.DateTime(Dates.today()+Dates.Day(1))), ones(25)) - ) - return gen_re +function make_renewable_fix(gen_name, d, bus) + tech = make_tech_renewable(d) + generator = RenewableFix(; + name=gen_name, + available=Bool(d["gen_status"]), + bus=bus, + activepower = d["pg"], + reactivepower = d["qg"], + tech=tech, + ) + + return generator +end + +function make_generic_battery(gen_name, d, bus) + + # TODO: placeholder + #battery=GenericBattery(; + # name=gen_name, + # available=Bool(d["gen_status"]), + # bus=bus, + # energy=, + # capacity=, + # rating=, + # activepower=, + # inputactivepowerlimits=, + # outputactivepowerlimits=, + # efficiency=, + # reactivepower=, + # reactivepowerlimits=, + #) + #return battery end -function make_thermal_gen(gen_name, d, bus) - if haskey(d,"model") +""" +The polynomial term follows the convention that for an n-degree polynomial, at least n + 1 components are needed. + c(p) = c_n*p^n+...+c_1p+c_0 + c_o is stored in the field in of the Econ Struct +""" +function make_thermal_gen(gen_name::AbstractString, d::Dict, bus::Bus) + if haskey(d, "model") model = GeneratorCostModel(d["model"]) if model == PIECEWISE_LINEAR::GeneratorCostModel cost_component = d["cost"] power_p = [i for (ix,i) in enumerate(cost_component) if isodd(ix)] - cost_p = [i for (ix,i) in enumerate(cost_component) if iseven(ix)]./power_p + cost_p = [i for (ix,i) in enumerate(cost_component) if iseven(ix)] cost = [(p,c) for (p,c) in zip(cost_p,power_p)] - fixedcost = cost[1][2] + fixed = cost[1][2] elseif model == POLYNOMIAL::GeneratorCostModel if d["ncost"] == 0 - cost = x-> 0 + cost = (0.0, 0.0) + fixed = 0.0 elseif d["ncost"] == 1 - cost = x-> d["cost"][1] + cost = (0.0, 0.0) + fixed = d["cost"][1] elseif d["ncost"] == 2 - cost = x-> d["cost"][1]*x + d["cost"][2] + cost = (0.0, d["cost"][1]) + fixed = d["cost"][2] elseif d["ncost"] == 3 - cost = x-> d["cost"][1]*x^2 + d["cost"][2]*x + d["cost"][3] - elseif d["ncost"] == 4 - cost = x-> d["cost"][1]*x^3 + d["cost"][2]*x^2 + d["cost"][3]*x + d["cost"][4] + cost = (d["cost"][1], d["cost"][2]) + fixed = d["cost"][3] else - throw(DataFormatError("invalid value for ncost: $(d["ncost"])")) + throw(DataFormatError("invalid value for ncost: $(d["ncost"]). PowerSystems only supports polynomials up to second degree")) end - - # TODO: Reviewers: Is this correct? - fixedcost = cost(d["pmin"]) end - startupcost = d["startup"] - shutdncost = d["shutdown"] + startup = d["startup"] + shutdn = d["shutdown"] else @warn "Generator cost data not included for Generator: $gen_name" - tmpcost = EconThermal() - cost = tmpcost.variablecost - fixedcost = tmpcost.fixedcost - startupcost = tmpcost.startupcost - shutdncost = tmpcost.shutdncost + tmpcost = ThreePartCost(nothing) + cost = tmpcost.variable + fixed = tmpcost.fixed + startup = tmpcost.startup + shutdn = tmpcost.shutdn end # TODO GitHub #148: ramp_agc isn't always present. This value may not be correct. ramp_agc = get(d, "ramp_agc", get(d, "ramp_10", get(d, "ramp_30", d["pmax"]))) - thermal_gen = Dict{String,Any}("name" => gen_name, - "available" => d["gen_status"], - "bus" => make_bus(bus), - "tech" => Dict{String,Any}("activepower" => d["pg"], - "activepowerlimits" => (min=d["pmin"], max=d["pmax"]), - "reactivepower" => d["qg"], - "reactivepowerlimits" => (min=d["qmin"], max=d["qmax"]), - "ramplimits" => (up=ramp_agc/d["mbase"], down=ramp_agc/d["mbase"]), - "timelimits" => nothing), - "econ" => Dict{String,Any}("capacity" => d["pmax"], - "variablecost" => cost, - "fixedcost" => fixedcost, - "startupcost" => startupcost, - "shutdncost" => shutdncost, - "annualcapacityfactor" => nothing) - ) + + tech = TechThermal(; + rating = sqrt(d["pmax"]^2 + d["qmax"]^2), + primemover = convert(PrimeMovers, d["type"]), + fuel = convert(ThermalFuels, d["fuel"]), + activepowerlimits = (min = d["pmin"], max = d["pmax"]), + reactivepowerlimits = (min = d["qmin"], max = d["qmax"]), + ramplimits = (up = ramp_agc / d["mbase"], down = ramp_agc / d["mbase"]), + timelimits = nothing, + ) + op_cost = ThreePartCost(; + variable=cost, + fixed=fixed, + startup=startup, + shutdn=shutdn, + ) + + thermal_gen = ThermalStandard( + name=gen_name, + available=Bool(d["gen_status"]), + bus=bus, + activepower = d["pg"], + reactivepower = d["qg"], + tech=tech, + op_cost=op_cost, + ) + return thermal_gen end """ Transfer generators to ps_dict according to their classification """ -function read_gen(data, Buses; kwargs...) +function read_gen!(sys::System, data, bus_number_to_bus::Dict{Int, Bus}; kwargs...) + @info "Reading generator data" - if :genmap_file in keys(kwargs) - genmap_file = kwargs[:genmap_file] - else # use default generator mapping config file - genmap_file = joinpath(dirname(dirname(pathof(PowerSystems))), - "src/parsers/generator_mapping.yaml") - end - genmap_dict = open(genmap_file) do file - YAML.load(file) - end - - generators = Dict{String,Any}() - generators["Thermal"] = Dict{String,Any}() - generators["Hydro"] = Dict{String,Any}() - generators["Renewable"] = Dict{String,Any}() - generators["Renewable"]["PV"]= Dict{String,Any}() - generators["Renewable"]["RTPV"]= Dict{String,Any}() - generators["Renewable"]["WIND"]= Dict{String,Any}() - generators["Storage"] = Dict{String,Any}() # not currently used? JJS 3/13/19 - if !haskey(data, "gen") + @error "There are no Generators in this file" return nothing end - - fuel = [] - gen_name =[] - type_gen =[] - for (d_key,d) in data["gen"] - - fuel = uppercase(get(d, "fuel", "generic")) - type_gen = uppercase(get(d, "type", "generic")) - if haskey(d, "name") - gen_name = d["name"] - elseif haskey(d, "source_id") - gen_name = strip(string(d["source_id"][1])*"-"*d["source_id"][2]) + + genmap_file = get(kwargs, :genmap_file, nothing) + genmap = get_generator_mapping(genmap_file) + + for (name, pm_gen) in data["gen"] + if haskey(pm_gen, "name") + gen_name = pm_gen["name"] + elseif haskey(pm_gen, "source_id") + gen_name = strip(string(pm_gen["source_id"][1]) * "-" * string(pm_gen["source_id"][2]) * "-" * name) else - gen_name = d_key + gen_name = name end - - bus = find_bus(Buses, d) - - assigned = false - for (rkey, rval) in generators["Renewable"] - fuelkeys = genmap_dict[rkey]["fuel"] - typekeys = genmap_dict[rkey]["type"] - if fuel in fuelkeys && type_gen in typekeys - generators["Renewable"][rkey][gen_name] = make_ren_gen(gen_name, d, bus) - assigned = true - break - end - end - if !assigned - fuelkeys = genmap_dict["Hydro"]["fuel"] - typekeys = genmap_dict["Hydro"]["type"] - if fuel in fuelkeys && type_gen in typekeys - generators["Hydro"][gen_name] = make_hydro_gen(gen_name, d, bus) - assigned = true - end + + bus = bus_number_to_bus[pm_gen["gen_bus"]] + pm_gen["fuel"] = get(pm_gen, "fuel", "OTHER") + pm_gen["type"] = get(pm_gen, "type", "OT") + @debug "Found generator" gen_name bus pm_gen["fuel"] pm_gen["type"] + + gen_type = get_generator_type(pm_gen["fuel"], pm_gen["type"], genmap) + if gen_type == ThermalStandard + generator = make_thermal_gen(gen_name, pm_gen, bus) + elseif gen_type == HydroDispatch + generator = make_hydro_gen(gen_name, pm_gen, bus) + elseif gen_type == RenewableDispatch + generator = make_renewable_dispatch(gen_name, pm_gen, bus) + elseif gen_type == RenewableFix + generator = make_renewable_fix(gen_name, pm_gen, bus) + elseif gen_type == GenericBattery + @warn "Skipping GenericBattery" + continue + # TODO + #generator = make_generic_battery(gen_name, pm_gen, bus) + else + @error "Skipping unsupported generator" gen_type + continue end - if !assigned - # default to Thermal type if not already assigned - generators["Thermal"][gen_name] = make_thermal_gen(gen_name, d, bus) + + add_component!(sys, generator; skip_validation=SKIP_PM_VALIDATION) + end +end + +function make_branch(name, d, bus_f, bus_t) + primary_shunt = d["b_fr"] + alpha = d["shift"] + branch_type = get_branch_type(d["tap"], alpha) + + if d["transformer"] + if branch_type == Line + throw(DataFormatError("Data is mismatched; this cannot be a line. $d")) + elseif branch_type == Transformer2W + value = make_transformer_2w(name, d, bus_f, bus_t) + elseif branch_type == TapTransformer + value = make_tap_transformer(name, d, bus_f, bus_t) + elseif branch_type == PhaseShiftingTransformer + value = make_phase_shifting_transformer(name, d, bus_f, bus_t, alpha) + else + error("Unsupported branch type $branch_type") end + else + # The get_branch_type() logic doesn't work for this data. + # tap can be 1.0 for this data. + value = make_line(name, d, bus_f, bus_t) + end + + return value +end - end # for (d_key,d) in data["gen"] +function make_line(name, d, bus_f, bus_t) + pf = get(d,"pf", 0.0) + qf = get(d,"qf", 0.0) - return generators + return Line(; + name=name, + available=Bool(d["br_status"]), + activepower_flow = pf, + reactivepower_flow = qf, + arc=Arc(bus_f, bus_t), + r=d["br_r"], + x=d["br_x"], + b=(from=d["b_fr"], to=d["b_to"]), + rate=d["rate_a"], + anglelimits=(min=d["angmin"], max=d["angmax"]), + ) end -function make_transformer(b_name, d, bus_f, bus_t) - trans = Dict{String,Any}("name" => b_name, - "available" => Bool(d["br_status"]), - "connectionpoints" => (from=make_bus(bus_f),to=make_bus(bus_t)), - "r" => d["br_r"], - "x" => d["br_x"], - "primaryshunt" => d["b_fr"] , #TODO: which b ?? - "tap" => d["tap"], - "rate" => d["rate_a"], - "α" => d["shift"] - ) - return trans +function make_transformer_2w(name, d, bus_f, bus_t) + pf = get(d,"pf", 0.0) + qf = get(d,"qf", 0.0) + return Transformer2W(; + name=name, + available=Bool(d["br_status"]), + activepower_flow = pf, + reactivepower_flow = qf, + arc=Arc(bus_f, bus_t), + r=d["br_r"], + x=d["br_x"], + primaryshunt=d["b_fr"], # TODO: which b ?? + rate=d["rate_a"], + ) end -function make_lines(b_name, d, bus_f, bus_t) - line = Dict{String,Any}("name" => b_name, - "available" => Bool(d["br_status"]), - "connectionpoints" => (from=make_bus(bus_f),to=make_bus(bus_t)), - "r" => d["br_r"], - "x" => d["br_x"], - "b" => (from=d["b_fr"],to=d["b_to"]), - "rate" => d["rate_a"], - "anglelimits" => (min=rad2deg(d["angmin"]),max =rad2deg(d["angmax"])) - ) - return line +function make_tap_transformer(name, d, bus_f, bus_t) + pf = get(d,"pf", 0.0) + qf = get(d,"qf", 0.0) + return TapTransformer(; + name=name, + available=Bool(d["br_status"]), + activepower_flow = pf, + reactivepower_flow = qf, + arc=Arc(bus_f, bus_t), + r=d["br_r"], + x=d["br_x"], + tap=d["tap"], + primaryshunt=d["b_fr"], # TODO: which b ?? + rate=d["rate_a"], + ) end -function read_branch(data,Buses) - Branches = Dict{String,Any}() - Branches["Transformers"] = Dict{String,Any}() - Branches["Lines"] = Dict{String,Any}() - if haskey(data,"branch") - b_name = [] - for (d_key,d) in data["branch"] - haskey(d,"name") ? b_name = d["name"] : b_name = d_key - (bus_f,bus_t) = find_bus(Buses,d) - if d["transformer"] #TODO : 3W Transformer - Branches["Transformers"][b_name] = make_transformer(b_name, d, bus_f, bus_t) - else - Branches["Lines"][b_name] = make_lines(b_name, d, bus_f, bus_t) - end - end - return Branches - else - return nothing +function make_phase_shifting_transformer(name, d, bus_f, bus_t, alpha) + pf = get(d,"pf", 0.0) + qf = get(d,"qf", 0.0) + return PhaseShiftingTransformer(; + name=name, + available=Bool(d["br_status"]), + activepower_flow = pf, + reactivepower_flow = qf, + arc=Arc(bus_f, bus_t), + r=d["br_r"], + x=d["br_x"], + tap=d["tap"], + primaryshunt=d["b_fr"], # TODO: which b ?? + α=alpha, + rate=d["rate_a"], + ) +end + +function read_branch!(sys::System, data, bus_number_to_bus::Dict{Int, Bus}) + @info "Reading branch data" + if !haskey(data, "branch") + @info "There is no Branch data in this file" + return + end + + for (d_key, d) in data["branch"] + name = get(d, "name", d_key) + bus_f = bus_number_to_bus[d["f_bus"]] + bus_t = bus_number_to_bus[d["t_bus"]] + value = make_branch(name, d, bus_f, bus_t) + + add_component!(sys, value; skip_validation=SKIP_PM_VALIDATION) end end -function make_dcline(l_name, d, bus_f, bus_t) - dcline = Dict{String,Any}("name" => l_name, - "available" =>d["br_status"] , - "connectionpoints" => (from = make_bus(bus_f),to = make_bus(bus_t) ) , - "activepowerlimits_from" => (min= d["pminf"] , max = d["pmaxf"]) , - "activepowerlimits_to" => (min= d["pmint"] , max =d["pmaxt"] ) , - "reactivepowerlimits_from" => (min= d["qminf"], max =d["qmaxf"] ), - "reactivepowerlimits_to" => (min=d["qmint"] , max =d["qmaxt"] ), - "loss" => (l0=d["loss0"] , l1 =d["loss1"] ) - ) - return dcline +function make_dcline(name, d, bus_f, bus_t) + return HVDCLine(; + name=name, + available=Bool(d["br_status"]), + activepower_flow = get(d,"pf", 0.0), + arc=Arc(bus_f, bus_t), + activepowerlimits_from=(min=d["pminf"] , max=d["pmaxf"]), + activepowerlimits_to=(min=d["pmint"], max=d["pmaxt"]), + reactivepowerlimits_from=(min=d["qminf"], max=d["qmaxf"]), + reactivepowerlimits_to=(min=d["qmint"], max=d["qmaxt"]), + loss=(l0=d["loss0"], l1 =d["loss1"]), + ) end -function read_dcline(data,Buses) - DCLines = Dict{String,Any}() - if haskey(data,"dcline") - for (d_key,d) in data["dcline"] - haskey(d,"name") ? l_name =d["name"] : l_name = d_key - (bus_f,bus_t) = find_bus(Buses,d) - DCLines[l_name] = make_dcline(l_name, d, bus_f, bus_t) - end - return DCLines - else - return nothing +function read_dcline!(sys::System, data, bus_number_to_bus::Dict{Int, Bus}) + @info "Reading DC Line data" + if !haskey(data,"dcline") + @info "There is no DClines data in this file" + return + end + + for (d_key, d) in data["dcline"] + name = get(d, "name", d_key) + bus_f = bus_number_to_bus[d["f_bus"]] + bus_t = bus_number_to_bus[d["t_bus"]] + + dcline = make_dcline(name, d, bus_f, bus_t) + add_component!(sys, dcline, skip_validation=SKIP_PM_VALIDATION) end end -function make_shunt(s_name, d, bus) - shunt = Dict{String,Any}("name" => s_name, - "available" => d["status"], - "bus" => make_bus(bus), - "Y" => (-d["gs"] + d["bs"]im) - ) - return shunt +function make_shunt(name, d, bus) + return FixedAdmittance(; + name=name, + available=Bool(d["status"]), + bus=bus, + Y=(-d["gs"] + d["bs"]im), + ) end -function read_shunt(data,Buses) - Shunts = Dict{String,Any}() - if haskey(data,"shunt") - s_name =[] - for (d_key,d) in data["shunt"] - haskey(d,"name") ? s_name =d["name"] : s_name = d_key - bus = find_bus(Buses,d) - Shunts[s_name] = make_shunt(s_name, d, bus) - end - return Shunts - else - return nothing +function read_shunt!(sys::System, data, bus_number_to_bus::Dict{Int, Bus}) + @info "Reading branch data" + if !haskey(data,"shunt") + @info "There is no shunt data in this file" + return + end + + for (d_key,d) in data["shunt"] + name = get(d, "name", d_key) + bus = bus_number_to_bus[d["shunt_bus"]] + shunt = make_shunt(name, d, bus) + + add_component!(sys, shunt; skip_validation=SKIP_PM_VALIDATION) end end diff --git a/src/parsers/pm_io.jl b/src/parsers/pm_io.jl index c9517427ad..e7c217f360 100644 --- a/src/parsers/pm_io.jl +++ b/src/parsers/pm_io.jl @@ -3,4 +3,3 @@ include("pm_io/common.jl") include("pm_io/pti.jl") include("pm_io/psse.jl") include("pm_io/data.jl") -include("pm_io/multiconductor.jl") \ No newline at end of file diff --git a/src/parsers/pm_io/common.jl b/src/parsers/pm_io/common.jl index 35c5b50bff..f542afb743 100644 --- a/src/parsers/pm_io/common.jl +++ b/src/parsers/pm_io/common.jl @@ -5,39 +5,31 @@ Parses a Matpower .m `file` or PTI (PSS(R)E-v33) .raw `file` into a PowerModels data structure. All fields from PTI files will be imported if `import_all` is true (Default: false). """ + function parse_file(file::String; import_all=false, validate=true) - if endswith(file, ".m") - pm_data = parse_matpower(file, validate=validate) - elseif endswith(lowercase(file), ".raw") - @info("The PSS(R)E parser currently supports buses, loads, shunts, generators, branches, transformers, and dc lines") - pm_data = parse_psse(file; import_all=import_all, validate=validate) - else - pm_data = parse_json(file, validate=validate) + pm_data = open(file) do io + pm_data = parse_file(io; import_all=import_all, validate=validate, filetype=split(lowercase(file), '.')[end]) end - - # TODO: not sure if this relevant for all three file types, or only .m, JJS 3/7/19 - move_genfuel_and_gentype!(pm_data) - return pm_data end -"" -function parse_json(file_string::String; kwargs...) - open(file_string) do f - pm_data = parse_json(f, kwargs...) +"Parses the iostream from a file" +function parse_file(io::IO; import_all=false, validate=true, filetype="json") + if filetype == "m" + pm_data = parse_matpower(io, validate=validate) + elseif filetype == "raw" + @info("The PSS(R)E parser currently supports buses, loads, shunts, generators, branches, transformers, and dc lines") + pm_data = parse_psse(io; import_all=import_all, validate=validate) + elseif filetype == "json" + pm_data = parse_json(io; validate=validate) + else + @info("Unrecognized filetype") end - return pm_data -end + # TODO: not sure if this relevant for all three file types, or only .m, JJS 3/7/19 + move_genfuel_and_gentype!(pm_data) -"" -function parse_json(io::IO; validate=true) - data_string = read(io, String) - pm_data = JSON.parse(data_string) - if validate - check_network_data(pm_data) - end return pm_data end @@ -46,34 +38,38 @@ end Runs various data quality checks on a PowerModels data dictionary. Applies modifications in some cases. Reports modified component ids. """ -function check_network_data(data::Dict{String,Any}) +function correct_network_data!(data::Dict{String,<:Any}) mod_bus = Dict{Symbol,Set{Int}}() mod_gen = Dict{Symbol,Set{Int}}() mod_branch = Dict{Symbol,Set{Int}}() mod_dcline = Dict{Symbol,Set{Int}}() check_conductors(data) - make_per_unit(data) check_connectivity(data) + check_status(data) + check_reference_bus(data) + + make_per_unit!(data) - mod_branch[:xfer_fix] = check_transformer_parameters(data) - mod_branch[:vad_bounds] = check_voltage_angle_differences(data) - mod_branch[:mva_zero] = check_thermal_limits(data) - mod_branch[:orientation] = check_branch_directions(data) + mod_branch[:xfer_fix] = correct_transformer_parameters!(data) + # mod_branch[:vad_bounds] = correct_voltage_angle_differences!(data) + mod_branch[:mva_zero] = correct_thermal_limits!(data) + mod_branch[:orientation] = correct_branch_directions!(data) check_branch_loops(data) - mod_dcline[:losses] = check_dcline_limits(data) + mod_dcline[:losses] = correct_dcline_limits!(data) - mod_bus[:type] = check_bus_types(data) + mod_bus[:type] = correct_bus_types!(data) check_voltage_setpoints(data) check_storage_parameters(data) + check_switch_parameters(data) - gen, dcline = check_cost_functions(data) + gen, dcline = correct_cost_functions!(data) mod_gen[:cost_pwl] = gen mod_dcline[:cost_pwl] = dcline - simplify_cost_terms(data) + simplify_cost_terms!(data) return Dict( "bus" => mod_bus, @@ -82,19 +78,3 @@ function check_network_data(data::Dict{String,Any}) "dcline" => mod_dcline ) end - - -#= -function row_to_typed_dict(row_data, columns) - @info("call to depreciated function row_to_typed_dict, use row_to_typed_dict") - return row_to_typed_dict(row_data, columns) -end - -function row_to_dict(row_data, columns) - @info("call to depreciated function row_to_dict, use row_to_dict") - return row_to_dict(row_data, columns) -end -=# - - - diff --git a/src/parsers/pm_io/data.jl b/src/parsers/pm_io/data.jl index 417efabfc6..dd774e0461 100644 --- a/src/parsers/pm_io/data.jl +++ b/src/parsers/pm_io/data.jl @@ -2,7 +2,7 @@ "" -function calc_branch_t(branch::Dict{String,Any}) +function calc_branch_t(branch::Dict{String,<:Any}) tap_ratio = branch["tap"] angle_shift = branch["shift"] @@ -14,7 +14,7 @@ end "" -function calc_branch_y(branch::Dict{String,Any}) +function calc_branch_y(branch::Dict{String,<:Any}) y = LinearAlgebra.pinv(branch["br_r"] + im * branch["br_x"]) g, b = real(y), imag(y) return g, b @@ -22,7 +22,7 @@ end "" -function calc_theta_delta_bounds(data::Dict{String,Any}) +function calc_theta_delta_bounds(data::Dict{String,<:Any}) bus_count = length(data["bus"]) branches = [branch for branch in values(data["branch"])] if haskey(data, "ne_branch") @@ -61,9 +61,10 @@ function calc_theta_delta_bounds(data::Dict{String,Any}) end if haskey(data, "conductors") - amin = MultiConductorVector(angle_min) - amax = MultiConductorVector(angle_max) - return amin, amax + error("Multiconductor Not Supported in PowerSystems") + #amin = MultiConductorVector(angle_min) + #amax = MultiConductorVector(angle_max) + #return amin, amax else return angle_min[1], angle_max[1] end @@ -86,7 +87,7 @@ end "" -function _calc_max_cost_index(data::Dict{String,Any}) +function _calc_max_cost_index(data::Dict{String,<:Any}) max_index = 0 for (i,gen) in data["gen"] @@ -116,37 +117,36 @@ function _calc_max_cost_index(data::Dict{String,Any}) return max_index end +"maps component types to status parameters" +const pm_component_status = Dict( + "bus" => "bus_type", + "load" => "status", + "shunt" => "status", + "gen" => "gen_status", + "storage" => "status", + "switch" => "status", + "branch" => "br_status", + "dcline" => "br_status", +) -"" -function check_keys(data, keys) - for key in keys - if haskey(data, key) - error("attempting to overwrite value of $(key) in PowerModels data,\n$(data)") - end - end -end - - -"prints the text summary for a data file or dictionary to stdout" -function print_summary(obj::Union{String, Dict{String,Any}}; kwargs...) - summary(stdout, obj; kwargs...) -end - - -"prints the text summary for a data file to IO" -function summary(io::IO, file::String; kwargs...) - data = parse_file(file) - _summary(io, data; kwargs...) - return data -end - +"maps component types to inactive status values" +const pm_component_status_inactive = Dict( + "bus" => 4, + "load" => 0, + "shunt" => 0, + "gen" => 0, + "storage" => 0, + "switch" => 0, + "branch" => 0, + "dcline" => 0, +) -pm_component_types_order = Dict( +const _pm_component_types_order = Dict( "bus" => 1.0, "load" => 2.0, "shunt" => 3.0, "gen" => 4.0, "storage" => 5.0, - "branch" => 6.0, "dcline" => 7.0 + "switch" => 6.0, "branch" => 7.0, "dcline" => 8.0 ) -pm_component_parameter_order = Dict( +const _pm_component_parameter_order = Dict( "bus_i" => 1.0, "load_bus" => 2.0, "shunt_bus" => 3.0, "gen_bus" => 4.0, "storage_bus" => 5.0, "f_bus" => 6.0, "t_bus" => 7.0, @@ -155,6 +155,8 @@ pm_component_parameter_order = Dict( "vm" => 10.0, "va" => 11.0, "pd" => 20.0, "qd" => 21.0, "gs" => 30.0, "bs" => 31.0, + "ps" => 35.0, "qs" => 36.0, + "psw" => 37.0, "qsw" => 38.0, "pg" => 40.0, "qg" => 41.0, "vg" => 42.0, "mbase" => 43.0, "energy" => 44.0, "br_r" => 50.0, "br_x" => 51.0, "g_fr" => 52.0, "b_fr" => 53.0, @@ -180,24 +182,16 @@ pm_component_parameter_order = Dict( "model" => 90.0, "ncost" => 91.0, "cost" => 92.0, "startup" => 93.0, "shutdown" => 94.0 ) -pm_component_status_parameters = Set(["status", "gen_status", "br_status"]) - +const _pm_component_status_parameters = Set(["status", "gen_status", "br_status"]) -"prints the text summary for a data dictionary to IO" -function _summary(io::IO, data::Dict{String,Any}; kwargs...) - summary(io, data; - component_types_order = pm_component_types_order, - component_parameter_order = pm_component_parameter_order, - component_status_parameters = pm_component_status_parameters, - kwargs...) -end -component_table(data::Dict{String,Any}, component::String, args...) = component_table(data, component, args...) +#component_table(data::Dict{String,Any}, component::String, args...) = component_table(data, component, args...) +#= "recursively applies new_data to data, overwriting information" -function update_data(data::Dict{String,Any}, new_data::Dict{String,Any}) +function update_data!(data::Dict{String,<:Any}, new_data::Dict{String,<:Any}) if haskey(data, "conductors") && haskey(new_data, "conductors") if data["conductors"] != new_data["conductors"] error("update_data requires datasets with the same number of conductors") @@ -209,45 +203,46 @@ function update_data(data::Dict{String,Any}, new_data::Dict{String,Any}) end update_data!(data, new_data) end - - -"calls the replicate function with PowerModels' global keys" -function replicate(sn_data::Dict{String,Any}, count::Int; global_keys::Set{String}=Set{String}()) - pm_global_keys = Set(["time_elapsed", "baseMVA", "per_unit"]) - return im_replicate(sn_data, count, global_keys=union(global_keys, pm_global_keys)) +=# +""" +Turns in given single network data in multinetwork data with a `count` +replicate of the given network. Note that this function performs a deepcopy +of the network data. Significant multinetwork space savings can often be +achieved by building application specific methods of building multinetwork +with minimal data replication. +""" +function replicate(sn_data::Dict{String,<:Any}, count::Int; global_keys::Set{String}=Set{String}()) + pm_global_keys = Set(["baseMVA", "per_unit"]) + return im_replicate(sn_data, count, union(global_keys, pm_global_keys)) end "" -function apply_func(data::Dict{String,Any}, key::String, func) +function _apply_func!(data::Dict{String,<:Any}, key::String, func) if haskey(data, key) - if isa(data[key], MultiConductorVector) - data[key] = MultiConductorVector([func(v) for v in data[key]]) - else - data[key] = func(data[key]) - end + data[key] = func(data[key]) # multiconductor not supported in PowerSystems end end "Transforms network data into per-unit" -function make_per_unit(data::Dict{String,Any}) +function make_per_unit!(data::Dict{String,<:Any}) if !haskey(data, "per_unit") || data["per_unit"] == false data["per_unit"] = true mva_base = data["baseMVA"] - if ismultinetwork(data) + if ismultinetwork(data) for (i,nw_data) in data["nw"] - _make_per_unit(nw_data, mva_base) + _make_per_unit!(nw_data, mva_base) end else - _make_per_unit(data, mva_base) + _make_per_unit!(data, mva_base) end end end "" -function _make_per_unit(data::Dict{String,Any}, mva_base::Real) +function _make_per_unit!(data::Dict{String,<:Any}, mva_base::Real) # to be consistent with matpower's opf.flow_lim= 'I' with current magnitude # limit defined in MVA at 1 p.u. voltage ka_base = mva_base @@ -259,56 +254,64 @@ function _make_per_unit(data::Dict{String,Any}, mva_base::Real) if haskey(data, "bus") for (i, bus) in data["bus"] - apply_func(bus, "va", deg2rad) + _apply_func!(bus, "va", deg2rad) - apply_func(bus, "lam_kcl_r", rescale_dual) - apply_func(bus, "lam_kcl_i", rescale_dual) + _apply_func!(bus, "lam_kcl_r", rescale_dual) + _apply_func!(bus, "lam_kcl_i", rescale_dual) end end if haskey(data, "load") for (i, load) in data["load"] - apply_func(load, "pd", rescale) - apply_func(load, "qd", rescale) + _apply_func!(load, "pd", rescale) + _apply_func!(load, "qd", rescale) end end if haskey(data, "shunt") for (i, shunt) in data["shunt"] - apply_func(shunt, "gs", rescale) - apply_func(shunt, "bs", rescale) + _apply_func!(shunt, "gs", rescale) + _apply_func!(shunt, "bs", rescale) end end if haskey(data, "gen") for (i, gen) in data["gen"] - apply_func(gen, "pg", rescale) - apply_func(gen, "qg", rescale) + _apply_func!(gen, "pg", rescale) + _apply_func!(gen, "qg", rescale) - apply_func(gen, "pmax", rescale) - apply_func(gen, "pmin", rescale) + _apply_func!(gen, "pmax", rescale) + _apply_func!(gen, "pmin", rescale) - apply_func(gen, "qmax", rescale) - apply_func(gen, "qmin", rescale) + _apply_func!(gen, "qmax", rescale) + _apply_func!(gen, "qmin", rescale) - _rescale_cost_model(gen, mva_base) + _rescale_cost_model!(gen, mva_base) end end if haskey(data, "storage") for (i, strg) in data["storage"] - apply_func(strg, "energy", rescale) - apply_func(strg, "energy_rating", rescale) - apply_func(strg, "charge_rating", rescale) - apply_func(strg, "discharge_rating", rescale) - apply_func(strg, "thermal_rating", rescale) - apply_func(strg, "current_rating", rescale) - apply_func(strg, "qmin", rescale) - apply_func(strg, "qmax", rescale) - apply_func(strg, "standby_loss", rescale) + _apply_func!(strg, "energy", rescale) + _apply_func!(strg, "energy_rating", rescale) + _apply_func!(strg, "charge_rating", rescale) + _apply_func!(strg, "discharge_rating", rescale) + _apply_func!(strg, "thermal_rating", rescale) + _apply_func!(strg, "current_rating", rescale) + _apply_func!(strg, "qmin", rescale) + _apply_func!(strg, "qmax", rescale) + _apply_func!(strg, "standby_loss", rescale) end end + if haskey(data, "switch") + for (i, switch) in data["switch"] + _apply_func!(switch, "psw", rescale) + _apply_func!(switch, "qsw", rescale) + _apply_func!(switch, "thermal_rating", rescale) + _apply_func!(switch, "current_rating", rescale) + end + end branches = [] if haskey(data, "branch") @@ -320,44 +323,44 @@ function _make_per_unit(data::Dict{String,Any}, mva_base::Real) end for branch in branches - apply_func(branch, "rate_a", rescale) - apply_func(branch, "rate_b", rescale) - apply_func(branch, "rate_c", rescale) + _apply_func!(branch, "rate_a", rescale) + _apply_func!(branch, "rate_b", rescale) + _apply_func!(branch, "rate_c", rescale) - apply_func(branch, "c_rating_a", rescale_ampere) - apply_func(branch, "c_rating_b", rescale_ampere) - apply_func(branch, "c_rating_c", rescale_ampere) + _apply_func!(branch, "c_rating_a", rescale_ampere) + _apply_func!(branch, "c_rating_b", rescale_ampere) + _apply_func!(branch, "c_rating_c", rescale_ampere) - apply_func(branch, "shift", deg2rad) - apply_func(branch, "angmax", deg2rad) - apply_func(branch, "angmin", deg2rad) + _apply_func!(branch, "shift", deg2rad) + _apply_func!(branch, "angmax", deg2rad) + _apply_func!(branch, "angmin", deg2rad) - apply_func(branch, "pf", rescale) - apply_func(branch, "pt", rescale) - apply_func(branch, "qf", rescale) - apply_func(branch, "qt", rescale) + _apply_func!(branch, "pf", rescale) + _apply_func!(branch, "pt", rescale) + _apply_func!(branch, "qf", rescale) + _apply_func!(branch, "qt", rescale) - apply_func(branch, "mu_sm_fr", rescale_dual) - apply_func(branch, "mu_sm_to", rescale_dual) + _apply_func!(branch, "mu_sm_fr", rescale_dual) + _apply_func!(branch, "mu_sm_to", rescale_dual) end if haskey(data, "dcline") for (i, dcline) in data["dcline"] - apply_func(dcline, "loss0", rescale) - apply_func(dcline, "pf", rescale) - apply_func(dcline, "pt", rescale) - apply_func(dcline, "qf", rescale) - apply_func(dcline, "qt", rescale) - apply_func(dcline, "pmaxt", rescale) - apply_func(dcline, "pmint", rescale) - apply_func(dcline, "pmaxf", rescale) - apply_func(dcline, "pminf", rescale) - apply_func(dcline, "qmaxt", rescale) - apply_func(dcline, "qmint", rescale) - apply_func(dcline, "qmaxf", rescale) - apply_func(dcline, "qminf", rescale) + _apply_func!(dcline, "loss0", rescale) + _apply_func!(dcline, "pf", rescale) + _apply_func!(dcline, "pt", rescale) + _apply_func!(dcline, "qf", rescale) + _apply_func!(dcline, "qt", rescale) + _apply_func!(dcline, "pmaxt", rescale) + _apply_func!(dcline, "pmint", rescale) + _apply_func!(dcline, "pmaxf", rescale) + _apply_func!(dcline, "pminf", rescale) + _apply_func!(dcline, "qmaxt", rescale) + _apply_func!(dcline, "qmint", rescale) + _apply_func!(dcline, "qmaxf", rescale) + _apply_func!(dcline, "qminf", rescale) - _rescale_cost_model(dcline, mva_base) + _rescale_cost_model!(dcline, mva_base) end end @@ -365,23 +368,23 @@ end "Transforms network data into mixed-units (inverse of per-unit)" -function make_mixed_units(data::Dict{String,Any}) +function make_mixed_units!(data::Dict{String,<:Any}) if haskey(data, "per_unit") && data["per_unit"] == true data["per_unit"] = false mva_base = data["baseMVA"] if ismultinetwork(data) for (i,nw_data) in data["nw"] - _make_mixed_units(nw_data, mva_base) + _make_mixed_units!(nw_data, mva_base) end else - _make_mixed_units(data, mva_base) + _make_mixed_units!(data, mva_base) end end end "" -function _make_mixed_units(data::Dict{String,Any}, mva_base::Real) +function _make_mixed_units!(data::Dict{String,<:Any}, mva_base::Real) # to be consistent with matpower's opf.flow_lim= 'I' with current magnitude # limit defined in MVA at 1 p.u. voltage ka_base = mva_base @@ -392,53 +395,62 @@ function _make_mixed_units(data::Dict{String,Any}, mva_base::Real) if haskey(data, "bus") for (i, bus) in data["bus"] - apply_func(bus, "va", rad2deg) + _apply_func!(bus, "va", rad2deg) - apply_func(bus, "lam_kcl_r", rescale_dual) - apply_func(bus, "lam_kcl_i", rescale_dual) + _apply_func!(bus, "lam_kcl_r", rescale_dual) + _apply_func!(bus, "lam_kcl_i", rescale_dual) end end if haskey(data, "load") for (i, load) in data["load"] - apply_func(load, "pd", rescale) - apply_func(load, "qd", rescale) + _apply_func!(load, "pd", rescale) + _apply_func!(load, "qd", rescale) end end if haskey(data, "shunt") for (i, shunt) in data["shunt"] - apply_func(shunt, "gs", rescale) - apply_func(shunt, "bs", rescale) + _apply_func!(shunt, "gs", rescale) + _apply_func!(shunt, "bs", rescale) end end if haskey(data, "gen") for (i, gen) in data["gen"] - apply_func(gen, "pg", rescale) - apply_func(gen, "qg", rescale) + _apply_func!(gen, "pg", rescale) + _apply_func!(gen, "qg", rescale) - apply_func(gen, "pmax", rescale) - apply_func(gen, "pmin", rescale) + _apply_func!(gen, "pmax", rescale) + _apply_func!(gen, "pmin", rescale) - apply_func(gen, "qmax", rescale) - apply_func(gen, "qmin", rescale) + _apply_func!(gen, "qmax", rescale) + _apply_func!(gen, "qmin", rescale) - _rescale_cost_model(gen, 1.0/mva_base) + _rescale_cost_model!(gen, 1.0/mva_base) end end if haskey(data, "storage") for (i, strg) in data["storage"] - apply_func(strg, "energy", rescale) - apply_func(strg, "energy_rating", rescale) - apply_func(strg, "charge_rating", rescale) - apply_func(strg, "discharge_rating", rescale) - apply_func(strg, "thermal_rating", rescale) - apply_func(strg, "current_rating", rescale) - apply_func(strg, "qmin", rescale) - apply_func(strg, "qmax", rescale) - apply_func(strg, "standby_loss", rescale) + _apply_func!(strg, "energy", rescale) + _apply_func!(strg, "energy_rating", rescale) + _apply_func!(strg, "charge_rating", rescale) + _apply_func!(strg, "discharge_rating", rescale) + _apply_func!(strg, "thermal_rating", rescale) + _apply_func!(strg, "current_rating", rescale) + _apply_func!(strg, "qmin", rescale) + _apply_func!(strg, "qmax", rescale) + _apply_func!(strg, "standby_loss", rescale) + end + end + + if haskey(data, "switch") + for (i, switch) in data["switch"] + _apply_func!(switch, "psw", rescale) + _apply_func!(switch, "qsw", rescale) + _apply_func!(switch, "thermal_rating", rescale) + _apply_func!(switch, "current_rating", rescale) end end @@ -453,44 +465,44 @@ function _make_mixed_units(data::Dict{String,Any}, mva_base::Real) end for branch in branches - apply_func(branch, "rate_a", rescale) - apply_func(branch, "rate_b", rescale) - apply_func(branch, "rate_c", rescale) + _apply_func!(branch, "rate_a", rescale) + _apply_func!(branch, "rate_b", rescale) + _apply_func!(branch, "rate_c", rescale) - apply_func(branch, "c_rating_a", rescale_ampere) - apply_func(branch, "c_rating_b", rescale_ampere) - apply_func(branch, "c_rating_c", rescale_ampere) + _apply_func!(branch, "c_rating_a", rescale_ampere) + _apply_func!(branch, "c_rating_b", rescale_ampere) + _apply_func!(branch, "c_rating_c", rescale_ampere) - apply_func(branch, "shift", rad2deg) - apply_func(branch, "angmax", rad2deg) - apply_func(branch, "angmin", rad2deg) + _apply_func!(branch, "shift", rad2deg) + _apply_func!(branch, "angmax", rad2deg) + _apply_func!(branch, "angmin", rad2deg) - apply_func(branch, "pf", rescale) - apply_func(branch, "pt", rescale) - apply_func(branch, "qf", rescale) - apply_func(branch, "qt", rescale) + _apply_func!(branch, "pf", rescale) + _apply_func!(branch, "pt", rescale) + _apply_func!(branch, "qf", rescale) + _apply_func!(branch, "qt", rescale) - apply_func(branch, "mu_sm_fr", rescale_dual) - apply_func(branch, "mu_sm_to", rescale_dual) + _apply_func!(branch, "mu_sm_fr", rescale_dual) + _apply_func!(branch, "mu_sm_to", rescale_dual) end if haskey(data, "dcline") for (i,dcline) in data["dcline"] - apply_func(dcline, "loss0", rescale) - apply_func(dcline, "pf", rescale) - apply_func(dcline, "pt", rescale) - apply_func(dcline, "qf", rescale) - apply_func(dcline, "qt", rescale) - apply_func(dcline, "pmaxt", rescale) - apply_func(dcline, "pmint", rescale) - apply_func(dcline, "pmaxf", rescale) - apply_func(dcline, "pminf", rescale) - apply_func(dcline, "qmaxt", rescale) - apply_func(dcline, "qmint", rescale) - apply_func(dcline, "qmaxf", rescale) - apply_func(dcline, "qminf", rescale) + _apply_func!(dcline, "loss0", rescale) + _apply_func!(dcline, "pf", rescale) + _apply_func!(dcline, "pt", rescale) + _apply_func!(dcline, "qf", rescale) + _apply_func!(dcline, "qt", rescale) + _apply_func!(dcline, "pmaxt", rescale) + _apply_func!(dcline, "pmint", rescale) + _apply_func!(dcline, "pmaxf", rescale) + _apply_func!(dcline, "pminf", rescale) + _apply_func!(dcline, "qmaxt", rescale) + _apply_func!(dcline, "qmint", rescale) + _apply_func!(dcline, "qmaxf", rescale) + _apply_func!(dcline, "qminf", rescale) - _rescale_cost_model(dcline, 1.0/mva_base) + _rescale_cost_model!(dcline, 1.0/mva_base) end end @@ -498,7 +510,7 @@ end "" -function _rescale_cost_model(comp::Dict{String,Any}, scale::Real) +function _rescale_cost_model!(comp::Dict{String,<:Any}, scale::Real) if "model" in keys(comp) && "cost" in keys(comp) if comp["model"] == 1 for i in 1:2:length(comp["cost"]) @@ -516,8 +528,446 @@ function _rescale_cost_model(comp::Dict{String,Any}, scale::Real) end + +"computes the generator cost from given network data" +function calc_gen_cost(data::Dict{String,<:Any}) + @assert("per_unit" in keys(data) && data["per_unit"]) + @assert(!haskey(data, "conductors")) + + if ismultinetwork(data) + nw_costs = Dict{String,Any}() + for (i,nw_data) in data["nw"] + nw_costs[i] = _calc_gen_cost(nw_data) + end + return sum(nw_cost for (i,nw_cost) in nw_costs) + else + return _calc_gen_cost(data) + end +end + +function _calc_gen_cost(data::Dict{String,<:Any}) + cost = 0.0 + for (i,gen) in data["gen"] + if gen["gen_status"] == 1 + if haskey(gen, "model") + if gen["model"] == 1 + cost += _calc_cost_pwl(gen, "pg") + elseif gen["model"] == 2 + cost += _calc_cost_polynomial(gen, "pg") + else + @info "generator $(i) has an unknown cost model $(gen["model"])" maxlog=PS_MAX_LOG + end + else + @info "generator $(i) does not have a cost model" maxlog=PS_MAX_LOG + end + end + end + return cost +end + + +"computes the dcline cost from given network data" +function calc_dcline_cost(data::Dict{String,<:Any}) + @assert("per_unit" in keys(data) && data["per_unit"]) + @assert(!haskey(data, "conductors")) + + if ismultinetwork(data) + nw_costs = Dict{String,Any}() + for (i,nw_data) in data["nw"] + nw_costs[i] = _calc_dcline_cost(nw_data) + end + return sum(nw_cost for (i,nw_cost) in nw_costs) + else + return _calc_dcline_cost(data) + end +end + +function _calc_dcline_cost(data::Dict{String,<:Any}) + cost = 0.0 + for (i,dcline) in data["dcline"] + if dcline["br_status"] == 1 + if haskey(dcline, "model") + if dcline["model"] == 1 + cost += _calc_cost_pwl(dcline, "pf") + elseif dcline["model"] == 2 + cost += _calc_cost_polynomial(dcline, "pf") + else + @info "dcline $(i) has an unknown cost model $(dcline["model"])" maxlog=PS_MAX_LOG + end + else + @info "dcline $(i) does not have a cost model" maxlog=PS_MAX_LOG + end + end + end + return cost +end + + + +""" +compute lines in m and b from from pwl cost models data is a list of components. + +Can be run on data or ref data structures +""" +function calc_cost_pwl_lines(comp_dict::Dict) + lines = Dict() + for (i,comp) in comp_dict + lines[i] = _calc_comp_lines(comp) + end + return lines +end + + +""" +compute lines in m and b from from pwl cost models +""" +function _calc_comp_lines(component::Dict{String,<:Any}) + @assert component["model"] == 1 + points = component["cost"] + + line_data = [] + for i in 3:2:length(points) + x1 = points[i-2] + y1 = points[i-1] + x2 = points[i-0] + y2 = points[i+1] + + m = (y2 - y1)/(x2 - x1) + b = y1 - m * x1 + + push!(line_data, (slope=m, intercept=b)) + end + + for i in 2:length(line_data) + if line_data[i-1].slope > line_data[i].slope + @info "non-convex pwl function found in points $(component["cost"])\nlines: $(line_data)" maxlog=PS_MAX_LOG + end + end + + return line_data +end + + +function _calc_cost_pwl(component::Dict{String,<:Any}, setpoint_id) + comp_lines = _calc_comp_lines(component) + + setpoint = component[setpoint_id] + cost = -Inf + for line in comp_lines + cost = max(cost, line.slope*setpoint + line.intercept) + end + + return cost +end + + +function _calc_cost_polynomial(component::Dict{String,<:Any}, setpoint_id) + cost_terms_rev = reverse(component["cost"]) + + setpoint = component[setpoint_id] + + if length(cost_terms_rev) == 0 + cost = 0.0 + elseif length(cost_terms_rev) == 1 + cost = cost_terms_rev[1] + elseif length(cost_terms_rev) == 2 + cost = cost_terms_rev[1] + cost_terms_rev[2]*setpoint + else + cost_terms_rev_high = cost_terms_rev[3:end] + cost = cost_terms_rev[1] + cost_terms_rev[2]*setpoint + sum( v*setpoint^(d+1) for (d,v) in enumerate(cost_terms_rev_high) ) + end + + return cost +end + + + +"assumes a vaild ac solution is included in the data and computes the branch flow values" +function calc_branch_flow_ac(data::Dict{String,<:Any}) + @assert("per_unit" in keys(data) && data["per_unit"]) + @assert(!haskey(data, "conductors")) + + if ismultinetwork(data) + nws = Dict{String,Any}() + for (i,nw_data) in data["nw"] + nws[i] = _calc_branch_flow_ac(nw_data) + end + return Dict{String,Any}( + "nw" => nws, + "per_unit" => data["per_unit"], + "baseMVA" => data["baseMVA"] + ) + else + flows = _calc_branch_flow_ac(data) + flows["per_unit"] = data["per_unit"] + flows["baseMVA"] = data["baseMVA"] + return flows + end +end + + +"helper function for calc_branch_flow_ac" +function _calc_branch_flow_ac(data::Dict{String,<:Any}) + vm = Dict(bus["index"] => bus["vm"] for (i,bus) in data["bus"]) + va = Dict(bus["index"] => bus["va"] for (i,bus) in data["bus"]) + + flows = Dict{String,Any}() + for (i,branch) in data["branch"] + if branch["br_status"] != 0 + f_bus = branch["f_bus"] + t_bus = branch["t_bus"] + + g, b = calc_branch_y(branch) + tr, ti = calc_branch_t(branch) + g_fr = branch["g_fr"] + b_fr = branch["b_fr"] + g_to = branch["g_to"] + b_to = branch["b_to"] + + tm = branch["tap"] + + vm_fr = vm[f_bus] + vm_to = vm[t_bus] + va_fr = va[f_bus] + va_to = va[t_bus] + + p_fr = (g+g_fr)/tm^2*vm_fr^2 + (-g*tr+b*ti)/tm^2*(vm_fr*vm_to*cos(va_fr-va_to)) + (-b*tr-g*ti)/tm^2*(vm_fr*vm_to*sin(va_fr-va_to)) + q_fr = -(b+b_fr)/tm^2*vm_fr^2 - (-b*tr-g*ti)/tm^2*(vm_fr*vm_to*cos(va_fr-va_to)) + (-g*tr+b*ti)/tm^2*(vm_fr*vm_to*sin(va_fr-va_to)) + + p_to = (g+g_to)*vm_to^2 + (-g*tr-b*ti)/tm^2*(vm_to*vm_fr*cos(va_to-va_fr)) + (-b*tr+g*ti)/tm^2*(vm_to*vm_fr*sin(va_to-va_fr)) + q_to = -(b+b_to)*vm_to^2 - (-b*tr+g*ti)/tm^2*(vm_to*vm_fr*cos(va_to-va_fr)) + (-g*tr-b*ti)/tm^2*(vm_to*vm_fr*sin(va_to-va_fr)) + else + p_fr = NaN + q_fr = NaN + + p_to = NaN + q_to = NaN + end + + flows[i] = Dict( + "pf" => p_fr, + "qf" => q_fr, + "pt" => p_to, + "qt" => q_to + ) + end + + return Dict{String,Any}("branch" => flows) +end + + + +"assumes a vaild dc solution is included in the data and computes the branch flow values" +function calc_branch_flow_dc(data::Dict{String,<:Any}) + @assert("per_unit" in keys(data) && data["per_unit"]) + @assert(!haskey(data, "conductors")) + + if ismultinetwork(data) + nws = Dict{String,Any}() + for (i,nw_data) in data["nw"] + nws[i] = _calc_branch_flow_dc(nw_data) + end + return Dict{String,Any}( + "nw" => nws, + "per_unit" => data["per_unit"], + "baseMVA" => data["baseMVA"] + ) + else + flows = _calc_branch_flow_dc(data) + flows["per_unit"] = data["per_unit"] + flows["baseMVA"] = data["baseMVA"] + return flows + end +end + + +"helper function for calc_branch_flow_dc" +function _calc_branch_flow_dc(data::Dict{String,<:Any}) + vm = Dict(bus["index"] => bus["vm"] for (i,bus) in data["bus"]) + va = Dict(bus["index"] => bus["va"] for (i,bus) in data["bus"]) + + flows = Dict{String,Any}() + for (i,branch) in data["branch"] + if branch["br_status"] != 0 + f_bus = branch["f_bus"] + t_bus = branch["t_bus"] + + g, b = calc_branch_y(branch) + + p_fr = -b*(va[f_bus] - va[t_bus]) + else + p_fr = NaN + end + + flows[i] = Dict( + "pf" => p_fr, + "qf" => NaN, + "pt" => -p_fr, + "qt" => NaN + ) + end + + return Dict{String,Any}("branch" => flows) +end + + + + +"assumes a vaild solution is included in the data and computes the power balance at each bus" +function calc_power_balance(data::Dict{String,<:Any}) + @assert("per_unit" in keys(data) && data["per_unit"]) # may not be strictly required + @assert(!haskey(data, "conductors")) + + if ismultinetwork(data) + nws = Dict{String,Any}() + for (i,nw_data) in data["nw"] + nws[i] = _calc_power_balance(nw_data) + end + return Dict{String,Any}( + "nw" => nws, + "per_unit" => data["per_unit"], + "baseMVA" => data["baseMVA"] + ) + else + flows = _calc_power_balance(data) + flows["per_unit"] = data["per_unit"] + flows["baseMVA"] = data["baseMVA"] + return flows + end +end + + +"helper function for calc_power_balance" +function _calc_power_balance(data::Dict{String,<:Any}) + bus_values = Dict(bus["index"] => Dict{String,Float64}() for (i,bus) in data["bus"]) + for (i,bus) in data["bus"] + bvals = bus_values[bus["index"]] + bvals["vm"] = bus["vm"] + + bvals["pd"] = 0.0 + bvals["qd"] = 0.0 + + bvals["gs"] = 0.0 + bvals["bs"] = 0.0 + + bvals["ps"] = 0.0 + bvals["qs"] = 0.0 + + bvals["pg"] = 0.0 + bvals["qg"] = 0.0 + + bvals["p"] = 0.0 + bvals["q"] = 0.0 + + bvals["psw"] = 0.0 + bvals["qsw"] = 0.0 + + bvals["p_dc"] = 0.0 + bvals["q_dc"] = 0.0 + end + + for (i,load) in data["load"] + if load["status"] != 0 + bvals = bus_values[load["load_bus"]] + bvals["pd"] += load["pd"] + bvals["qd"] += load["qd"] + end + end + + for (i,shunt) in data["shunt"] + if shunt["status"] != 0 + bvals = bus_values[shunt["shunt_bus"]] + bvals["gs"] += shunt["gs"] + bvals["bs"] += shunt["bs"] + end + end + + for (i,storage) in data["storage"] + if storage["status"] != 0 + bvals = bus_values[storage["storage_bus"]] + bvals["ps"] += storage["ps"] + bvals["qs"] += storage["qs"] + end + end + + for (i,gen) in data["gen"] + if gen["gen_status"] != 0 + bvals = bus_values[gen["gen_bus"]] + bvals["pg"] += gen["pg"] + bvals["qg"] += gen["qg"] + end + end + + for (i,switch) in data["switch"] + if switch["status"] != 0 + bus_fr = switch["f_bus"] + bvals_fr = bus_values[bus_fr] + bvals_fr["psw"] += switch["psw"] + bvals_fr["qsw"] += switch["qsw"] + + bus_to = switch["t_bus"] + bvals_to = bus_values[bus_to] + bvals_to["psw"] -= switch["psw"] + bvals_to["qsw"] -= switch["qsw"] + end + end + + for (i,branch) in data["branch"] + if branch["br_status"] != 0 + bus_fr = branch["f_bus"] + bvals_fr = bus_values[bus_fr] + bvals_fr["p"] += branch["pf"] + bvals_fr["q"] += branch["qf"] + + bus_to = branch["t_bus"] + bvals_to = bus_values[bus_to] + bvals_to["p"] += branch["pt"] + bvals_to["q"] += branch["qt"] + end + end + + for (i,dcline) in data["dcline"] + if dcline["br_status"] != 0 + bus_fr = dcline["f_bus"] + bvals_fr = bus_values[bus_fr] + bvals_fr["p_dc"] += dcline["pf"] + bvals_fr["q_dc"] += dcline["qf"] + + bus_to = dcline["t_bus"] + bvals_to = bus_values[bus_to] + bvals_to["p_dc"] += dcline["pt"] + bvals_to["q_dc"] += dcline["qt"] + end + end + + deltas = Dict{String,Any}() + for (i,bus) in data["bus"] + if bus["bus_type"] != 4 + bvals = bus_values[bus["index"]] + p_delta = bvals["p"] + bvals["p_dc"] + bvals["psw"] - bvals["pg"] + bvals["ps"] + bvals["pd"] + bvals["gs"]*(bvals["vm"]^2) + q_delta = bvals["q"] + bvals["q_dc"] + bvals["qsw"] - bvals["qg"] + bvals["qs"] + bvals["qd"] - bvals["bs"]*(bvals["vm"]^2) + else + p_delta = NaN + q_delta = NaN + end + + deltas[i] = Dict( + "p_delta" => p_delta, + "q_delta" => q_delta, + ) + end + + return Dict{String,Any}("bus" => deltas) +end + + + + + + + "" -function check_conductors(data::Dict{String,Any}) +function check_conductors(data::Dict{String,<:Any}) if ismultinetwork(data) for (i,nw_data) in data["nw"] _check_conductors(nw_data) @@ -529,7 +979,7 @@ end "" -function _check_conductors(data::Dict{String,Any}) +function _check_conductors(data::Dict{String,<:Any}) if haskey(data, "conductors") && data["conductors"] < 1 error("conductor values must be positive integers, given $(data["conductors"])") end @@ -537,7 +987,7 @@ end "checks that voltage angle differences are within 90 deg., if not tightens" -function check_voltage_angle_differences(data::Dict{String,Any}, default_pad = 1.0472) +function correct_voltage_angle_differences!(data::Dict{String,<:Any}, default_pad = 1.0472) if ismultinetwork(data) error("check_voltage_angle_differences does not yet support multinetwork data") end @@ -592,9 +1042,9 @@ end "checks that each branch has a reasonable thermal rating-a, if not computes one" -function check_thermal_limits(data::Dict{String,Any}) +function correct_thermal_limits!(data::Dict{String,<:Any}) if ismultinetwork(data) - error("check_thermal_limits does not yet support multinetwork data") + error("correct_thermal_limits! does not yet support multinetwork data") end @assert("per_unit" in keys(data) && data["per_unit"]) @@ -610,7 +1060,7 @@ function check_thermal_limits(data::Dict{String,Any}) for branch in branches if !haskey(branch, "rate_a") if haskey(data, "conductors") - branch["rate_a"] = MultiConductorVector(0.0, data["conductors"]) + error("Multiconductor Not Supported in PowerSystems") else branch["rate_a"] = 0.0 end @@ -657,9 +1107,9 @@ end "checks that each branch has a reasonable current rating-a, if not computes one" -function check_current_limits(data::Dict{String,Any}) +function correct_current_limits!(data::Dict{String,<:Any}) if ismultinetwork(data) - error("check_current_limits does not yet support multinetwork data") + error("correct_current_limits! does not yet support multinetwork data") end @assert("per_unit" in keys(data) && data["per_unit"]) @@ -676,7 +1126,7 @@ function check_current_limits(data::Dict{String,Any}) if !haskey(branch, "c_rating_a") if haskey(data, "conductors") - branch["c_rating_a"] = MultiConductorVector(0.0, data["conductors"]) + error("Multiconductor Not Supported in PowerSystems") else branch["c_rating_a"] = 0.0 end @@ -724,9 +1174,9 @@ end "checks that all parallel branches have the same orientation" -function check_branch_directions(data::Dict{String,Any}) +function correct_branch_directions!(data::Dict{String,<:Any}) if ismultinetwork(data) - error("check_branch_directions does not yet support multinetwork data") + error("correct_branch_directions! does not yet support multinetwork data") end modified = Set{Int}() @@ -764,7 +1214,7 @@ end "checks that all branches connect two distinct buses" -function check_branch_loops(data::Dict{String,Any}) +function check_branch_loops(data::Dict{String,<:Any}) if ismultinetwork(data) error("check_branch_loops does not yet support multinetwork data") end @@ -778,12 +1228,12 @@ end "checks that all buses are unique and other components link to valid buses" -function check_connectivity(data::Dict{String,Any}) +function check_connectivity(data::Dict{String,<:Any}) if ismultinetwork(data) error("check_connectivity does not yet support multinetwork data") end - bus_ids = Set([bus["index"] for (i,bus) in data["bus"]]) + bus_ids = Set(bus["index"] for (i,bus) in data["bus"]) @assert(length(bus_ids) == length(data["bus"])) # if this is not true something very bad is going on for (i, load) in data["load"] @@ -810,6 +1260,18 @@ function check_connectivity(data::Dict{String,Any}) end end + if haskey(data, "switch") + for (i, switch) in data["switch"] + if !(switch["f_bus"] in bus_ids) + throw(DataFormatError("from bus $(branch["f_bus"]) in switch $(i) is not defined")) + end + + if !(switch["t_bus"] in bus_ids) + throw(DataFormatError("to bus $(branch["t_bus"]) in switch $(i) is not defined")) + end + end + end + for (i, branch) in data["branch"] if !(branch["f_bus"] in bus_ids) throw(DataFormatError("from bus $(branch["f_bus"]) in branch $(i) is not defined")) @@ -832,12 +1294,111 @@ function check_connectivity(data::Dict{String,Any}) end +"checks that active components are not connected to inactive buses, otherwise prints warnings" +function check_status(data::Dict{String,<:Any}) + if ismultinetwork(data) + error("check_status does not yet support multinetwork data") + end + + active_bus_ids = Set(bus["index"] for (i,bus) in data["bus"] if bus["bus_type"] != 4) + + for (i, load) in data["load"] + if load["status"] != 0 && !(load["load_bus"] in active_bus_ids) + @warn("active load $(i) is connected to inactive bus $(load["load_bus"])") + end + end + + for (i, shunt) in data["shunt"] + if shunt["status"] != 0 && !(shunt["shunt_bus"] in active_bus_ids) + @warn("active shunt $(i) is connected to inactive bus $(shunt["shunt_bus"])") + end + end + + for (i, gen) in data["gen"] + if gen["gen_status"] != 0 && !(gen["gen_bus"] in active_bus_ids) + @warn("active generator $(i) is connected to inactive bus $(gen["gen_bus"])") + end + end + + for (i, strg) in data["storage"] + if strg["status"] != 0 && !(strg["storage_bus"] in active_bus_ids) + @warn("active storage unit $(i) is connected to inactive bus $(strg["storage_bus"])") + end + end + + for (i, branch) in data["branch"] + if branch["br_status"] != 0 && !(branch["f_bus"] in active_bus_ids) + @warn("active branch $(i) is connected to inactive bus $(branch["f_bus"])") + end + + if branch["br_status"] != 0 && !(branch["t_bus"] in active_bus_ids) + @warn("active branch $(i) is connected to inactive bus $(branch["t_bus"])") + end + end + + for (i, dcline) in data["dcline"] + if dcline["br_status"] != 0 && !(dcline["f_bus"] in active_bus_ids) + @warn("active dcline $(i) is connected to inactive bus $(dcline["f_bus"])") + end + + if dcline["br_status"] != 0 && !(dcline["t_bus"] in active_bus_ids) + @warn("active dcline $(i) is connected to inactive bus $(dcline["t_bus"])") + end + end +end + + +"checks that contains at least one refrence bus" +function check_reference_bus(data::Dict{String,<:Any}) + if ismultinetwork(data) + error("check_reference_bus does not yet support multinetwork data") + end + + ref_buses = Dict{String,Any}() + for (k,v) in data["bus"] + if v["bus_type"] == 3 + ref_buses[k] = v + end + end + + if length(ref_buses) == 0 + if length(data["gen"]) > 0 + big_gen = _biggest_generator(data["gen"]) + gen_bus = big_gen["gen_bus"] + ref_bus = data["bus"]["$(gen_bus)"] + ref_bus["bus_type"] = 3 + @warn("no reference bus found, setting bus $(gen_bus) as reference based on generator $(big_gen["index"])") + else + (bus_item, state) = Base.iterate(data["bus"]) + bus_item.second["bus_type"] = 3 + @warn("no reference bus found, setting bus $(bus_item.second["index"]) as reference") + end + end +end + + +"find the largest active generator in the network" +function _biggest_generator(gens) + biggest_gen = nothing + biggest_value = -Inf + for (k,gen) in gens + pmax = maximum(gen["pmax"]) + if pmax > biggest_value + biggest_gen = gen + biggest_value = pmax + end + end + @assert(biggest_gen != nothing) + return biggest_gen +end + + """ checks that each branch has a reasonable transformer parameters this is important because setting tap == 0.0 leads to NaN computations, which are hard to debug """ -function check_transformer_parameters(data::Dict{String,Any}) +function correct_transformer_parameters!(data::Dict{String,<:Any}) if ismultinetwork(data) error("check_transformer_parameters does not yet support multinetwork data") end @@ -848,9 +1409,9 @@ function check_transformer_parameters(data::Dict{String,Any}) for (i, branch) in data["branch"] if !haskey(branch, "tap") - @info("branch found without tap value, setting a tap to 1.0") + @info "branch found without tap value, setting a tap to 1.0" maxlog=PS_MAX_LOG if haskey(data, "conductors") - branch["tap"] = MultiConductorVector{Float64}(ones(data["conductors"])) + error("Multiconductor Not Supported in PowerSystems") else branch["tap"] = 1.0 end @@ -872,7 +1433,7 @@ function check_transformer_parameters(data::Dict{String,Any}) if !haskey(branch, "shift") @info("branch found without shift value, setting a shift to 0.0") if haskey(data, "conductors") - branch["shift"] = MultiConductorVector{Float64}(zeros(data["conductors"])) + error("Multiconductor Not Supported in PowerSystems") else branch["shift"] = 0.0 end @@ -905,53 +1466,73 @@ function check_storage_parameters(data::Dict{String,Any}) if strg["discharge_rating"] < 0.0 throw(DataFormatError("storage unit $(strg["index"]) has a non-positive discharge rating $(strg["energy_rating"])")) end + if strg["standby_loss"] < 0.0 + throw(DataFormatError("storage unit $(strg["index"]) has a non-positive standby losses $(strg["standby_loss"])")) + end if strg["r"] < 0.0 throw(DataFormatError("storage unit $(strg["index"]) has a non-positive resistance $(strg["r"])")) end if strg["x"] < 0.0 throw(DataFormatError("storage unit $(strg["index"]) has a non-positive reactance $(strg["x"])")) end - if strg["standby_loss"] < 0.0 - throw(DataFormatError("storage unit $(strg["index"]) has a non-positive standby losses $(strg["standby_loss"])")) - end - if haskey(strg, "thermal_rating") && strg["thermal_rating"] < 0.0 - throw(DataFormatError("storage unit $(strg["index"]) has a non-positive thermal rating $(strg["thermal_rating"])")) + throw(DataFormatError( "storage unit $(strg["index"]) has a non-positive thermal rating $(strg["thermal_rating"])")) end if haskey(strg, "current_rating") && strg["current_rating"] < 0.0 throw(DataFormatError("storage unit $(strg["index"]) has a non-positive current rating $(strg["thermal_rating"])")) end - + if !isapprox(strg["x"], 0.0, atol=1e-6, rtol=1e-6) + throw(DataFormatError("storage unit $(strg["index"]) has a non-zero reactance $(strg["x"]), which is currently ignored")) + end if strg["charge_efficiency"] < 0.0 throw(DataFormatError("storage unit $(strg["index"]) has a non-positive charge efficiency of $(strg["charge_efficiency"])")) end if strg["charge_efficiency"] <= 0.0 || strg["charge_efficiency"] > 1.0 - @info("storage unit $(strg["index"]) charge efficiency of $(strg["charge_efficiency"]) is out of the valid range (0.0. 1.0]") + @info "storage unit $(strg["index"]) charge efficiency of $(strg["charge_efficiency"]) is out of the valid range (0.0. 1.0]" maxlog=PS_MAX_LOG end if strg["discharge_efficiency"] < 0.0 throw(DataFormatError("storage unit $(strg["index"]) has a non-positive discharge efficiency of $(strg["discharge_efficiency"])")) end if strg["discharge_efficiency"] <= 0.0 || strg["discharge_efficiency"] > 1.0 - @info("storage unit $(strg["index"]) discharge efficiency of $(strg["discharge_efficiency"]) is out of the valid range (0.0. 1.0]") + @info "storage unit $(strg["index"]) discharge efficiency of $(strg["discharge_efficiency"]) is out of the valid range (0.0. 1.0]" maxlog=PS_MAX_LOG end - if !isapprox(strg["x"], 0.0, atol=1e-6, rtol=1e-6) - @info("storage unit $(strg["index"]) has a non-zero reactance $(strg["x"]), which is currently ignored") + if strg["standby_loss"] > 0.0 && strg["energy"] <= 0.0 + @info "storage unit $(strg["index"]) has standby losses but zero initial energy. This can lead to model infeasiblity." maxlog=PS_MAX_LOG end + end +end - if strg["standby_loss"] > 0.0 && strg["energy"] <= 0.0 - @info("storage unit $(strg["index"]) has standby losses but zero initial energy. This can lead to model infeasiblity.") + +""" +checks that each switch has a reasonable parameters +""" +function check_switch_parameters(data::Dict{String,<:Any}) + if ismultinetwork(data) + error("check_switch_parameters does not yet support multinetwork data") + end + + for (i, switch) in data["switch"] + if switch["state"] <= 0.0 && (!isapprox(switch["psw"], 0.0) || !isapprox(switch["qsw"], 0.0)) + @info "switch $(switch["index"]) is open with non-zero power values $(switch["psw"]), $(switch["qsw"])" maxlog=PS_MAX_LOG + end + if haskey(switch, "thermal_rating") && switch["thermal_rating"] < 0.0 + throw(DataFormatError( "switch $(switch["index"]) has a non-positive thermal_rating $(switch["thermal_rating"])")) + end + if haskey(switch, "current_rating") && switch["current_rating"] < 0.0 + throw(DataFormatError("switch $(switch["index"]) has a non-positive current_rating $(switch["current_rating"])")) end + end end "checks bus types are consistent with generator connections, if not, fixes them" -function check_bus_types(data::Dict{String,Any}) +function correct_bus_types!(data::Dict{String,<:Any}) if ismultinetwork(data) error("check_bus_types does not yet support multinetwork data") end @@ -991,7 +1572,7 @@ end "checks that parameters for dc lines are reasonable" -function check_dcline_limits(data::Dict{String,Any}) +function correct_dcline_limits!(data::Dict{String,Any}) if ismultinetwork(data) error("check_dcline_limits does not yet support multinetwork data") end @@ -1006,7 +1587,7 @@ function check_dcline_limits(data::Dict{String,Any}) for (i, dcline) in data["dcline"] if dcline["loss0"][c] < 0.0 new_rate = 0.0 - @info("this code only supports positive loss0 values, changing the value on dcline $(dcline["index"])$(cnd_str) from $(mva_base*dcline["loss0"][c]) to $(mva_base*new_rate)") + @info "this code only supports positive loss0 values, changing the value on dcline $(dcline["index"])$(cnd_str) from $(mva_base*dcline["loss0"][c]) to $(mva_base*new_rate)" maxlog=PS_MAX_LOG if haskey(data, "conductors") dcline["loss0"][c] = new_rate else @@ -1017,7 +1598,7 @@ function check_dcline_limits(data::Dict{String,Any}) if dcline["loss0"][c] >= dcline["pmaxf"][c]*(1-dcline["loss1"][c] )+ dcline["pmaxt"][c] new_rate = 0.0 - @info("this code only supports loss0 values which are consistent with the line flow bounds, changing the value on dcline $(dcline["index"])$(cnd_str) from $(mva_base*dcline["loss0"][c]) to $(mva_base*new_rate)") + @info "this code only supports loss0 values which are consistent with the line flow bounds, changing the value on dcline $(dcline["index"])$(cnd_str) from $(mva_base*dcline["loss0"][c]) to $(mva_base*new_rate)" maxlog=PS_MAX_LOG if haskey(data, "conductors") dcline["loss0"][c] = new_rate else @@ -1028,7 +1609,7 @@ function check_dcline_limits(data::Dict{String,Any}) if dcline["loss1"][c] < 0.0 new_rate = 0.0 - @info("this code only supports positive loss1 values, changing the value on dcline $(dcline["index"])$(cnd_str) from $(dcline["loss1"][c]) to $(new_rate)") + @info "this code only supports positive loss1 values, changing the value on dcline $(dcline["index"])$(cnd_str) from $(dcline["loss1"][c]) to $(new_rate)" maxlog=PS_MAX_LOG if haskey(data, "conductors") dcline["loss1"][c] = new_rate else @@ -1039,7 +1620,7 @@ function check_dcline_limits(data::Dict{String,Any}) if dcline["loss1"][c] >= 1.0 new_rate = 0.0 - @info("this code only supports loss1 values < 1, changing the value on dcline $(dcline["index"])$(cnd_str) from $(dcline["loss1"][c]) to $(new_rate)") + @info "this code only supports loss1 values < 1, changing the value on dcline $(dcline["index"])$(cnd_str) from $(dcline["loss1"][c]) to $(new_rate)" maxlog=PS_MAX_LOG if haskey(data, "conductors") dcline["loss1"][c] = new_rate else @@ -1050,7 +1631,7 @@ function check_dcline_limits(data::Dict{String,Any}) if dcline["pmint"][c] <0.0 && dcline["loss1"][c] > 0.0 #new_rate = 0.0 - @info("the dc line model is not meant to be used bi-directionally when loss1 > 0, be careful interpreting the results as the dc line losses can now be negative. change loss1 to 0 to avoid this warning") + @info "the dc line model is not meant to be used bi-directionally when loss1 > 0, be careful interpreting the results as the dc line losses can now be negative. change loss1 to 0 to avoid this warning" maxlog=PS_MAX_LOG #dcline["loss0"] = new_rate end end @@ -1061,7 +1642,7 @@ end "throws warnings if generator and dc line voltage setpoints are not consistent with the bus voltage setpoint" -function check_voltage_setpoints(data::Dict{String,Any}) +function check_voltage_setpoints(data::Dict{String,<:Any}) if ismultinetwork(data) error("check_voltage_setpoints does not yet support multinetwork data") end @@ -1097,21 +1678,21 @@ end "throws warnings if cost functions are malformed" -function check_cost_functions(data::Dict{String,Any}) +function correct_cost_functions!(data::Dict{String,<:Any}) if ismultinetwork(data) error("check_cost_functions does not yet support multinetwork data") end modified_gen = Set{Int}() for (i,gen) in data["gen"] - if _check_cost_function(i, gen, "generator") + if _correct_cost_function!(i, gen, "generator") push!(modified_gen, gen["index"]) end end modified_dcline = Set{Int}() for (i, dcline) in data["dcline"] - if _check_cost_function(i, dcline, "dcline") + if _correct_cost_function!(i, dcline, "dcline") push!(modified_dcline, dcline["index"]) end end @@ -1121,7 +1702,7 @@ end "" -function _check_cost_function(id, comp, type_name) +function _correct_cost_function!(id, comp, type_name) #println(comp) modified = false @@ -1133,6 +1714,9 @@ function _check_cost_function(id, comp, type_name) if length(comp["cost"]) < 4 error("cost includes $(comp["ncost"]) points, but at least two points are required on $(type_name) $(id)") end + + modified = _remove_pwl_cost_duplicates!(id, comp, type_name) + for i in 3:2:length(comp["cost"]) if comp["cost"][i-2] >= comp["cost"][i] error("non-increasing x values in pwl cost model on $(type_name) $(id)") @@ -1147,13 +1731,13 @@ function _check_cost_function(id, comp, type_name) end end end - modified = _simplify_pwl_cost(id, comp, type_name) + modified |= _simplify_pwl_cost!(id, comp, type_name) elseif comp["model"] == 2 if length(comp["cost"]) != comp["ncost"] error("ncost of $(comp["ncost"]) not consistent with $(length(comp["cost"])) cost values on $(type_name) $(id)") end else - @info("Unknown cost model of type $(comp["model"]) on $(type_name) $(id)") + @info "Unknown cost model of type $(comp["model"]) on $(type_name) $(id)" maxlog=PS_MAX_LOG end end @@ -1161,8 +1745,34 @@ function _check_cost_function(id, comp, type_name) end +"checks that each point in the a pwl function is unqiue, simplifies the function if duplicates appear" +function _remove_pwl_cost_duplicates!(id, comp, type_name, tolerance = 1e-2) + @assert comp["model"] == 1 + + unique_costs = Float64[comp["cost"][1], comp["cost"][2]] + for i in 3:2:length(comp["cost"]) + x1 = unique_costs[end-1] + y1 = unique_costs[end] + x2 = comp["cost"][i+0] + y2 = comp["cost"][i+1] + if !(isapprox(x1, x2) && isapprox(y1, y2)) + push!(unique_costs, x2) + push!(unique_costs, y2) + end + end + + if length(unique_costs) < length(comp["cost"]) + @info "removing duplicate points from pwl cost on $(type_name) $(id), $(comp["cost"]) -> $(unique_costs)" maxlog=PS_MAX_LOG + comp["cost"] = unique_costs + comp["ncost"] = length(unique_costs)/2 + return true + end + return false +end + + "checks the slope of each segment in a pwl function, simplifies the function if the slope changes is below a tolerance" -function _simplify_pwl_cost(id, comp, type_name, tolerance = 1e-2) +function _simplify_pwl_cost!(id, comp, type_name, tolerance = 1e-2) @assert comp["model"] == 1 slopes = Float64[] @@ -1192,7 +1802,7 @@ function _simplify_pwl_cost(id, comp, type_name, tolerance = 1e-2) push!(smpl_cost, y2) if length(smpl_cost) < length(comp["cost"]) - @info("simplifying pwl cost on $(type_name) $(id), $(comp["cost"]) -> $(smpl_cost)") + @info "simplifying pwl cost on $(type_name) $(id), $(comp["cost"]) -> $(smpl_cost)" maxlog=PS_MAX_LOG comp["cost"] = smpl_cost comp["ncost"] = length(smpl_cost)/2 return true @@ -1202,7 +1812,7 @@ end "trims zeros from higher order cost terms" -function simplify_cost_terms(data::Dict{String,Any}) +function simplify_cost_terms!(data::Dict{String,<:Any}) if ismultinetwork(data) networks = data["nw"] else @@ -1246,7 +1856,7 @@ function simplify_cost_terms(data::Dict{String,Any}) end if length(dcline["cost"]) != ncost dcline["ncost"] = length(dcline["cost"]) - @info("removing $(ncost - dcline["ncost"]) cost terms from dcline $(i): $(dcline["cost"])") + @info "removing $(ncost - dcline["ncost"]) cost terms from dcline $(i): $(dcline["cost"])" maxlog=PS_MAX_LOG push!(modified_dcline, dcline["index"]) end end @@ -1259,7 +1869,7 @@ end "ensures all polynomial costs functions have the same number of terms" -function standardize_cost_terms(data::Dict{String,Any}; order=-1) +function standardize_cost_terms!(data::Dict{String,<:Any}; order=-1) comp_max_order = 1 if ismultinetwork(data) @@ -1317,10 +1927,10 @@ function standardize_cost_terms(data::Dict{String,Any}; order=-1) for (i, network) in networks if haskey(network, "gen") - _standardize_cost_terms(network["gen"], comp_max_order, "generator") + _standardize_cost_terms!(network["gen"], comp_max_order, "generator") end if haskey(network, "dcline") - _standardize_cost_terms(network["dcline"], comp_max_order, "dcline") + _standardize_cost_terms!(network["dcline"], comp_max_order, "dcline") end end @@ -1328,7 +1938,7 @@ end "ensures all polynomial costs functions have at exactly comp_order terms" -function _standardize_cost_terms(components::Dict{String,Any}, comp_order::Int, cost_comp_name::String) +function _standardize_cost_terms!(components::Dict{String,<:Any}, comp_order::Int, cost_comp_name::String) modified = Set{Int}() for (i, comp) in components if haskey(comp, "model") && comp["model"] == 2 && length(comp["cost"]) != comp_order @@ -1342,7 +1952,7 @@ function _standardize_cost_terms(components::Dict{String,Any}, comp_order::Int, comp["ncost"] = comp_order #println("std gen cost: $(comp["cost"])") - @info("Updated $(cost_comp_name) $(comp["index"]) cost function with order $(length(current_cost)) to a function of order $(comp_order): $(comp["cost"])") + @info "Updated $(cost_comp_name) $(comp["index"]) cost function with order $(length(current_cost)) to a function of order $(comp_order): $(comp["cost"])" maxlog=PS_MAX_LOG push!(modified, comp["index"]) end end @@ -1361,19 +1971,19 @@ Works on a PowerModels data dict, so that a it can be used without a GenericPowe Warning: this implementation has quadratic complexity, in the worst case """ -function propagate_topology_status(data::Dict{String,Any}) +function propagate_topology_status!(data::Dict{String,<:Any}) if ismultinetwork(data) for (i,nw_data) in data["nw"] - _propagate_topology_status(nw_data) + _propagate_topology_status!(nw_data) end else - _propagate_topology_status(data) + _propagate_topology_status!(data) end end "" -function _propagate_topology_status(data::Dict{String,Any}) +function _propagate_topology_status!(data::Dict{String,<:Any}) buses = Dict(bus["bus_i"] => bus for (i,bus) in data["bus"]) for (i,load) in data["load"] @@ -1438,7 +2048,7 @@ function _propagate_topology_status(data::Dict{String,Any}) t_bus = buses[branch["t_bus"]] if f_bus["bus_type"] == 4 || t_bus["bus_type"] == 4 - @info("deactivating branch $(i):($(branch["f_bus"]),$(branch["t_bus"])) due to connecting bus status") + @info "deactivating branch $(i):($(branch["f_bus"]),$(branch["t_bus"])) due to connecting bus status" maxlog=PS_MAX_LOG branch["br_status"] = 0 updated = true end @@ -1451,7 +2061,7 @@ function _propagate_topology_status(data::Dict{String,Any}) t_bus = buses[dcline["t_bus"]] if f_bus["bus_type"] == 4 || t_bus["bus_type"] == 4 - @info("deactivating dcline $(i):($(dcline["f_bus"]),$(dcline["t_bus"])) due to connecting bus status") + @info "deactivating dcline $(i):($(dcline["f_bus"]),$(dcline["t_bus"])) due to connecting bus status" maxlog=PS_MAX_LOG dcline["br_status"] = 0 updated = true end @@ -1474,7 +2084,7 @@ function _propagate_topology_status(data::Dict{String,Any}) #println("bus $(i) active shunt $(incident_active_shunt)") if incident_active_edge == 1 && length(incident_active_gen[i]) == 0 && length(incident_active_load[i]) == 0 && length(incident_active_shunt[i]) == 0 - @info("deactivating bus $(i) due to dangling bus without generation and load") + @info "deactivating bus $(i) due to dangling bus without generation and load" maxlog=PS_MAX_LOG bus["bus_type"] = 4 updated = true end @@ -1482,7 +2092,7 @@ function _propagate_topology_status(data::Dict{String,Any}) else # bus type == 4 for load in incident_active_load[i] if load["status"] != 0 - @info("deactivating load $(load["index"]) due to inactive bus $(i)") + @info "deactivating load $(load["index"]) due to inactive bus $(i)" maxlog=PS_MAX_LOG load["status"] = 0 updated = true end @@ -1490,7 +2100,7 @@ function _propagate_topology_status(data::Dict{String,Any}) for shunt in incident_active_shunt[i] if shunt["status"] != 0 - @info("deactivating shunt $(shunt["index"]) due to inactive bus $(i)") + @info "deactivating shunt $(shunt["index"]) due to inactive bus $(i)" maxlog=PS_MAX_LOG shunt["status"] = 0 updated = true end @@ -1498,7 +2108,7 @@ function _propagate_topology_status(data::Dict{String,Any}) for gen in incident_active_gen[i] if gen["gen_status"] != 0 - @info("deactivating generator $(gen["index"]) due to inactive bus $(i)") + @info "deactivating generator $(gen["index"]) due to inactive bus $(i)" maxlog=PS_MAX_LOG gen["gen_status"] = 0 updated = true end @@ -1528,7 +2138,7 @@ function _propagate_topology_status(data::Dict{String,Any}) active_gen_count = sum(cc_active_gens) if (active_load_count == 0 && active_shunt_count == 0) || active_gen_count == 0 - @info("deactivating connected component $(cc) due to isolation without generation and load") + @info "deactivating connected component $(cc) due to isolation without generation and load" maxlog=PS_MAX_LOG for i in cc buses[i]["bus_type"] = 4 end @@ -1538,7 +2148,7 @@ function _propagate_topology_status(data::Dict{String,Any}) end - @info("topology status propagation fixpoint reached in $(iteration) rounds") + @info "topology status propagation fixpoint reached in $(iteration) rounds" maxlog=PS_MAX_LOG check_reference_buses(data) end @@ -1559,23 +2169,23 @@ end "" -function _select_largest_component(data::Dict{String,Any}) - ccs = connected_components(data) - @info("found $(length(ccs)) components") +function _select_largest_component!(data::Dict{String,<:Any}) + ccs = calc_connected_components(data) + @info "found $(length(ccs)) components" maxlog=PS_MAX_LOG ccs_order = sort(collect(ccs); by=length) largest_cc = ccs_order[end] - @info("largest component has $(length(largest_cc)) buses") + @info "largest component has $(length(largest_cc)) buses" maxlog=PS_MAX_LOG for (i,bus) in data["bus"] if bus["bus_type"] != 4 && !(bus["index"] in largest_cc) bus["bus_type"] = 4 - @info("deactivating bus $(i) due to small connected component") + @info "deactivating bus $(i) due to small connected component" maxlog=PS_MAX_LOG end end - check_reference_buses(data) + correct_reference_buses!(data) end @@ -1585,20 +2195,20 @@ checks that each connected components has a reference bus, if not, adds one function check_reference_buses(data::Dict{String,Any}) if ismultinetwork(data) for (i,nw_data) in data["nw"] - _check_reference_buses(nw_data) + _correct_reference_buses!(nw_data) end else - _check_reference_buses(data) + _correct_reference_buses!(data) end end "" -function _check_reference_buses(data::Dict{String,Any}) +function _correct_reference_buses!(data::Dict{String,<:Any}) bus_lookup = Dict(bus["bus_i"] => bus for (i,bus) in data["bus"]) bus_gen = bus_gen_lookup(data["gen"], data["bus"]) - ccs = connected_components(data) + ccs = calc_connected_components(data) ccs_order = sort(collect(ccs); by=length) bus_to_cc = Dict() @@ -1618,7 +2228,7 @@ function _check_reference_buses(data::Dict{String,Any}) end for (i, cc) in enumerate(ccs_order) - check_component_refrence_bus(cc, bus_lookup, cc_gens[i]) + correct_component_refrence_bus!(cc, bus_lookup, cc_gens[i]) end end @@ -1626,7 +2236,7 @@ end """ checks that a connected component has a reference bus, if not, adds one """ -function check_component_refrence_bus(component_bus_ids, bus_lookup, component_gens) +function correct_component_refrence_bus!(component_bus_ids, bus_lookup, component_gens) refrence_buses = Set() for bus_id in component_bus_ids bus = bus_lookup[bus_id] @@ -1639,7 +2249,7 @@ function check_component_refrence_bus(component_bus_ids, bus_lookup, component_g @info("no reference bus found in connected component $(component_bus_ids)") if length(component_gens) > 0 - big_gen = biggest_generator(component_gens) + big_gen = _biggest_generator(component_gens) gen_bus = bus_lookup[big_gen["gen_bus"]] gen_bus["bus_type"] = 3 @info("setting bus $(gen_bus["index"]) as reference bus in connected component $(component_bus_ids), based on generator $(big_gen["index"])") @@ -1651,7 +2261,7 @@ end "builds a lookup list of what generators are connected to a given bus" -function bus_gen_lookup(gen_data::Dict{String,Any}, bus_data::Dict{String,Any}) +function bus_gen_lookup(gen_data::Dict{String,<:Any}, bus_data::Dict{String,<:Any}) bus_gen = Dict(bus["bus_i"] => [] for (i,bus) in bus_data) for (i,gen) in gen_data push!(bus_gen[gen["gen_bus"]], gen) @@ -1661,7 +2271,7 @@ end "builds a lookup list of what loads are connected to a given bus" -function bus_load_lookup(load_data::Dict{String,Any}, bus_data::Dict{String,Any}) +function bus_load_lookup(load_data::Dict{String,<:Any}, bus_data::Dict{String,<:Any}) bus_load = Dict(bus["bus_i"] => [] for (i,bus) in bus_data) for (i,load) in load_data push!(bus_load[load["load_bus"]], load) @@ -1671,7 +2281,7 @@ end "builds a lookup list of what shunts are connected to a given bus" -function bus_shunt_lookup(shunt_data::Dict{String,Any}, bus_data::Dict{String,Any}) +function bus_shunt_lookup(shunt_data::Dict{String,<:Any}, bus_data::Dict{String,<:Any}) bus_shunt = Dict(bus["bus_i"] => [] for (i,bus) in bus_data) for (i,shunt) in shunt_data push!(bus_shunt[shunt["shunt_bus"]], shunt) @@ -1680,34 +2290,37 @@ function bus_shunt_lookup(shunt_data::Dict{String,Any}, bus_data::Dict{String,An end +"builds a lookup list of what storage is connected to a given bus" +function bus_storage_lookup(storage_data::Dict{String,<:Any}, bus_data::Dict{String,<:Any}) + bus_storage = Dict(bus["bus_i"] => [] for (i,bus) in bus_data) + for (i,storage) in storage_data + push!(bus_storage[storage["shunt_bus"]], storage) + end + return bus_storage +end + + """ computes the connected components of the network graph returns a set of sets of bus ids, each set is a connected component """ -function connected_components(data::Dict{String,Any}) +function calc_connected_components(data::Dict{String,<:Any}; edges=["branch", "dcline"]) if ismultinetwork(data) error("connected_components does not yet support multinetwork data") end active_bus = Dict(x for x in data["bus"] if x.second["bus_type"] != 4) - #active_bus = filter((i, bus) -> bus["bus_type"] != 4, data["bus"]) active_bus_ids = Set{Int64}([bus["bus_i"] for (i,bus) in active_bus]) - #println(active_bus_ids) neighbors = Dict(i => [] for i in active_bus_ids) - for (i,branch) in data["branch"] - if branch["br_status"] != 0 && branch["f_bus"] in active_bus_ids && branch["t_bus"] in active_bus_ids - push!(neighbors[branch["f_bus"]], branch["t_bus"]) - push!(neighbors[branch["t_bus"]], branch["f_bus"]) - end - end - for (i,dcline) in data["dcline"] - if dcline["br_status"] != 0 && dcline["f_bus"] in active_bus_ids && dcline["t_bus"] in active_bus_ids - push!(neighbors[dcline["f_bus"]], dcline["t_bus"]) - push!(neighbors[dcline["t_bus"]], dcline["f_bus"]) + for line_type in edges + for line in values(get(data, line_type, Dict())) + if get(line, "br_status", 1) != 0 && line["f_bus"] in active_bus_ids && line["t_bus"] in active_bus_ids + push!(neighbors[line["f_bus"]], line["t_bus"]) + push!(neighbors[line["t_bus"]], line["f_bus"]) + end end end - #println(neighbors) component_lookup = Dict(i => Set{Int64}([i]) for i in active_bus_ids) touched = Set{Int64}() @@ -1741,69 +2354,12 @@ function _dfs(i, neighbors, component_lookup, touched) end -"Transforms single-conductor network data into multi-conductor data" -function make_multiconductor(data::Dict{String,Any}, conductors::Int) - if ismultinetwork(data) - for (i,nw_data) in data["nw"] - _make_multiconductor(nw_data, conductors) - end - else - _make_multiconductor(data, conductors) - end -end - - -"feild names that should not be multi-conductor values" -conductorless = Set(["index", "bus_i", "bus_type", "status", "gen_status", - "br_status", "gen_bus", "load_bus", "shunt_bus", "storage_bus", "f_bus", "t_bus", - "transformer", "area", "zone", "base_kv", "energy", "energy_rating", "charge_rating", - "discharge_rating", "charge_efficiency", "discharge_efficiency", "standby_loss", - "model", "ncost", "cost", "startup", "shutdown"]) - -conductor_matrix = Set(["br_r", "br_x"]) - - -"" -function _make_multiconductor(data::Dict{String,Any}, conductors::Real) - if haskey(data, "conductors") - @info("skipping network that is already multiconductor") - return - end - - data["conductors"] = conductors - - for (key, item) in data - if isa(item, Dict{String,Any}) - for (item_id, item_data) in item - if isa(item_data, Dict{String,Any}) - item_ref_data = Dict{String,Any}() - for (param, value) in item_data - if param in conductorless - item_ref_data[param] = value - else - if param in conductor_matrix - item_ref_data[param] = MultiConductorMatrix(value, conductors) - else - item_ref_data[param] = MultiConductorVector(value, conductors) - end - end - end - item[item_id] = item_ref_data - end - end - else - #root non-dict items - end - end -end - - """ Move gentype and genfuel fields to be subfields of gen """ function move_genfuel_and_gentype!(data::Dict{String,Any}) ngen = length(data["gen"]) - + toplevkeys = ("genfuel", "gentype") sublevkeys = ("fuel", "type") for i in range(1, stop=length(toplevkeys)) @@ -1817,8 +2373,8 @@ function move_genfuel_and_gentype!(data::Dict{String,Any}) for (key,val) in data[toplevkeys[i]] data["gen"][key][sublevkeys[i]] = val["col_1"] end - delete!(data, toplevkeys[i]) + delete!(data, toplevkeys[i]) end end - + end diff --git a/src/parsers/pm_io/matpower.jl b/src/parsers/pm_io/matpower.jl index b85c777b53..1f23d51c3b 100644 --- a/src/parsers/pm_io/matpower.jl +++ b/src/parsers/pm_io/matpower.jl @@ -5,24 +5,31 @@ ######################################################################### "Parses the matpwer data from either a filename or an IO object" -function parse_matpower(file::Union{IO, String}; validate=true) - mp_data = parse_matpower_file(file) - pm_data = matpower_to_powermodels(mp_data) +function parse_matpower(io::IO; validate=true)::Dict + mp_data = _parse_matpower_string(read(io, String)) + pm_data = _matpower_to_powermodels!(mp_data) if validate - check_network_data(pm_data) + correct_network_data!(pm_data) end return pm_data end +function parse_matpower(file::String; kwargs...)::Dict + mp_data = open(file) do io + parse_matpower(io; kwargs...) + end + return mp_data +end + ### Data and functions specific to Matpower format ### -mp_data_names = ["mpc.version", "mpc.baseMVA", "mpc.bus", "mpc.gen", +const _mp_data_names = ["mpc.version", "mpc.baseMVA", "mpc.bus", "mpc.gen", "mpc.branch", "mpc.dcline", "mpc.gencost", "mpc.dclinecost", - "mpc.bus_name", "mpc.storage" + "mpc.bus_name", "mpc.storage", "mpc.switch" ] -mp_bus_columns = [ +const _mp_bus_columns = [ ("bus_i", Int), ("bus_type", Int), ("pd", Float64), ("qd", Float64), @@ -36,11 +43,11 @@ mp_bus_columns = [ ("mu_vmax", Float64), ("mu_vmin", Float64) ] -mp_bus_name_columns = [ - ("bus_name", Union{String,SubString{String}}) +const _mp_bus_name_columns = [ + ("name", Union{String,SubString{String}}) ] -mp_gen_columns = [ +const _mp_gen_columns = [ ("gen_bus", Int), ("pg", Float64), ("qg", Float64), ("qmax", Float64), ("qmin", Float64), @@ -61,7 +68,7 @@ mp_gen_columns = [ ("mu_qmax", Float64), ("mu_qmin", Float64) ] -mp_branch_columns = [ +const _mp_branch_columns = [ ("f_bus", Int), ("t_bus", Int), ("br_r", Float64), ("br_x", Float64), @@ -78,7 +85,7 @@ mp_branch_columns = [ ("mu_angmin", Float64), ("mu_angmax", Float64) ] -mp_dcline_columns = [ +const _mp_dcline_columns = [ ("f_bus", Int), ("t_bus", Int), ("br_status", Int), @@ -95,8 +102,9 @@ mp_dcline_columns = [ ("mu_qmint", Float64), ("mu_qmaxt", Float64) ] -mp_storage_columns = [ +const _mp_storage_columns = [ ("storage_bus", Int), + ("ps", Float64), ("qs", Float64), ("energy", Float64), ("energy_rating", Float64), ("charge_rating", Float64), ("discharge_rating", Float64), ("charge_efficiency", Float64), ("discharge_efficiency", Float64), @@ -107,28 +115,17 @@ mp_storage_columns = [ ("status", Int) ] - -"" -function parse_matpower_file(file_string::String) - mp_data = open(file_string) do io - parse_matpower_file(io) - end - - return mp_data -end - - -"" -function parse_matpower_file(io::IO) - lines = readlines(io) - - return parse_matpower_string(lines) -end +const _mp_switch_columns = [ + ("f_bus", Int), ("t_bus", Int), + ("psw", Float64), ("qsw", Float64), ("state", Int), + ("thermal_rating", Float64), + ("status", Int) +] "" -function parse_matpower_string(data_lines::Array{String}) - matlab_data, func_name, colnames = parse_matlab_string(data_lines, extended=true) +function _parse_matpower_string(data_string::String) + matlab_data, func_name, colnames = parse_matlab_string(data_string, extended=true) case = Dict{String,Any}() @@ -141,7 +138,7 @@ function parse_matpower_string(data_lines::Array{String}) case["source_type"] = "matpower" if haskey(matlab_data, "mpc.version") - case["source_version"] = VersionNumber(matlab_data["mpc.version"]) + case["source_version"] = matlab_data["mpc.version"] else @info(string("no case version found in matpower file. The file seems to be missing \"mpc.version = ...\"")) case["source_version"] = "0.0.0+" @@ -158,8 +155,9 @@ function parse_matpower_string(data_lines::Array{String}) if haskey(matlab_data, "mpc.bus") buses = [] for bus_row in matlab_data["mpc.bus"] - bus_data = row_to_typed_dict(bus_row, mp_bus_columns) + bus_data = row_to_typed_dict(bus_row, _mp_bus_columns) bus_data["index"] = check_type(Int, bus_row[1]) + bus_data["source_id"] = ["bus", bus_data["index"]] push!(buses, bus_data) end case["bus"] = buses @@ -170,8 +168,9 @@ function parse_matpower_string(data_lines::Array{String}) if haskey(matlab_data, "mpc.gen") gens = [] for (i, gen_row) in enumerate(matlab_data["mpc.gen"]) - gen_data = row_to_typed_dict(gen_row, mp_gen_columns) + gen_data = row_to_typed_dict(gen_row, _mp_gen_columns) gen_data["index"] = i + gen_data["source_id"] = ["gen", i] push!(gens, gen_data) end case["gen"] = gens @@ -182,8 +181,9 @@ function parse_matpower_string(data_lines::Array{String}) if haskey(matlab_data, "mpc.branch") branches = [] for (i, branch_row) in enumerate(matlab_data["mpc.branch"]) - branch_data = row_to_typed_dict(branch_row, mp_branch_columns) + branch_data = row_to_typed_dict(branch_row, _mp_branch_columns) branch_data["index"] = i + branch_data["source_id"] = ["branch", i] push!(branches, branch_data) end case["branch"] = branches @@ -194,8 +194,9 @@ function parse_matpower_string(data_lines::Array{String}) if haskey(matlab_data, "mpc.dcline") dclines = [] for (i, dcline_row) in enumerate(matlab_data["mpc.dcline"]) - dcline_data = row_to_typed_dict(dcline_row, mp_dcline_columns) + dcline_data = row_to_typed_dict(dcline_row, _mp_dcline_columns) dcline_data["index"] = i + dcline_data["source_id"] = ["dcline", i] push!(dclines, dcline_data) end case["dcline"] = dclines @@ -204,19 +205,31 @@ function parse_matpower_string(data_lines::Array{String}) if haskey(matlab_data, "mpc.storage") storage = [] for (i, storage_row) in enumerate(matlab_data["mpc.storage"]) - storage_data = row_to_typed_dict(storage_row, mp_storage_columns) + storage_data = row_to_typed_dict(storage_row, _mp_storage_columns) storage_data["index"] = i + storage_data["source_id"] = ["storage", i] push!(storage, storage_data) end case["storage"] = storage end + if haskey(matlab_data, "mpc.switch") + switch = [] + for (i, switch_row) in enumerate(matlab_data["mpc.switch"]) + switch_data = row_to_typed_dict(switch_row, _mp_switch_columns) + switch_data["index"] = i + switch_data["source_id"] = ["switch", i] + push!(switch, switch_data) + end + case["switch"] = switch + end if haskey(matlab_data, "mpc.bus_name") bus_names = [] for (i, bus_name_row) in enumerate(matlab_data["mpc.bus_name"]) - bus_name_data = row_to_typed_dict(bus_name_row, mp_bus_name_columns) + bus_name_data = row_to_typed_dict(bus_name_row, _mp_bus_name_columns) bus_name_data["index"] = i + bus_name_data["source_id"] = ["bus_name", i] push!(bus_names, bus_name_data) end case["bus_name"] = bus_names @@ -229,8 +242,9 @@ function parse_matpower_string(data_lines::Array{String}) if haskey(matlab_data, "mpc.gencost") gencost = [] for (i, gencost_row) in enumerate(matlab_data["mpc.gencost"]) - gencost_data = mp_cost_data(gencost_row) + gencost_data = _mp_cost_data(gencost_row) gencost_data["index"] = i + gencost_data["source_id"] = ["gencost", i] push!(gencost, gencost_data) end case["gencost"] = gencost @@ -243,8 +257,9 @@ function parse_matpower_string(data_lines::Array{String}) if haskey(matlab_data, "mpc.dclinecost") dclinecosts = [] for (i, dclinecost_row) in enumerate(matlab_data["mpc.dclinecost"]) - dclinecost_data = mp_cost_data(dclinecost_row) + dclinecost_data = _mp_cost_data(dclinecost_row) dclinecost_data["index"] = i + dclinecost_data["source_id"] = ["dclinecost", i] push!(dclinecosts, dclinecost_data) end case["dclinecost"] = dclinecosts @@ -254,8 +269,9 @@ function parse_matpower_string(data_lines::Array{String}) end end + for k in keys(matlab_data) - if !in(k, mp_data_names) && startswith(k, "mpc.") + if !in(k, _mp_data_names) && startswith(k, "mpc.") case_name = k[5:length(k)] value = matlab_data[k] if isa(value, Array) @@ -267,6 +283,7 @@ function parse_matpower_string(data_lines::Array{String}) for (i, row) in enumerate(matlab_data[k]) row_data = row_to_dict(row, column_names) row_data["index"] = i + row_data["source_id"] = [case_name, i] push!(tbl, row_data) end case[case_name] = tbl @@ -283,7 +300,7 @@ end "" -function mp_cost_data(cost_row) +function _mp_cost_data(cost_row) ncost = cost_row[4] model = cost_row[1] if model == 1 @@ -327,8 +344,8 @@ end """ Converts a Matpower dict into a PowerModels dict """ -function matpower_to_powermodels(mp_data::Dict{String,Any}) - pm_data = deepcopy(mp_data) +function _matpower_to_powermodels!(mp_data::Dict{String,<:Any}) + pm_data = mp_data # required default values if !haskey(pm_data, "dcline") @@ -343,26 +360,29 @@ function matpower_to_powermodels(mp_data::Dict{String,Any}) if !haskey(pm_data, "storage") pm_data["storage"] = [] end + if !haskey(pm_data, "switch") + pm_data["switch"] = [] + end # translate component models - mp2pm_branch(pm_data) - mp2pm_dcline(pm_data) + _mp2pm_branch!(pm_data) + _mp2pm_dcline!(pm_data) # translate cost models - add_dcline_costs(pm_data) + _add_dcline_costs!(pm_data) # merge data tables - merge_bus_name_data(pm_data) - merge_generator_cost_data(pm_data) - merge_generic_data(pm_data) + _merge_bus_name_data!(pm_data) + _merge_generator_cost_data!(pm_data) + _merge_generic_data!(pm_data) # split loads and shunts from buses - split_loads_shunts(pm_data) + _split_loads_shunts!(pm_data) # use once available arrays_to_dicts!(pm_data) - for optional in ["dcline", "load", "shunt", "storage"] + for optional in ["dcline", "load", "shunt", "storage", "switch"] if length(pm_data[optional]) == 0 pm_data[optional] = Dict{String,Any}() end @@ -373,13 +393,13 @@ end """ - split_loads_shunts(data) + _split_loads_shunts!(data) Seperates Loads and Shunts in `data` under separate "load" and "shunt" keys in the PowerModels data format. Includes references to originating bus via "load_bus" and "shunt_bus" keys, respectively. """ -function split_loads_shunts(data::Dict{String,Any}) +function _split_loads_shunts!(data::Dict{String,Any}) data["load"] = [] data["shunt"] = [] @@ -387,20 +407,26 @@ function split_loads_shunts(data::Dict{String,Any}) shunt_num = 1 for (i,bus) in enumerate(data["bus"]) if bus["pd"] != 0.0 || bus["qd"] != 0.0 - append!(data["load"], [Dict{String,Any}("pd" => bus["pd"], - "qd" => bus["qd"], - "load_bus" => bus["bus_i"], - "status" => convert(Int8, bus["bus_type"] != 4), - "index" => load_num)]) + append!(data["load"], [Dict{String,Any}( + "pd" => bus["pd"], + "qd" => bus["qd"], + "load_bus" => bus["bus_i"], + "status" => convert(Int8, bus["bus_type"] != 4), + "index" => load_num, + "source_id" => ["bus", bus["bus_i"]] + )]) load_num += 1 end if bus["gs"] != 0.0 || bus["bs"] != 0.0 - append!(data["shunt"], [Dict{String,Any}("gs" => bus["gs"], - "bs" => bus["bs"], - "shunt_bus" => bus["bus_i"], - "status" => convert(Int8, bus["bus_type"] != 4), - "index" => shunt_num)]) + append!(data["shunt"], [Dict{String,Any}( + "gs" => bus["gs"], + "bs" => bus["bs"], + "shunt_bus" => bus["bus_i"], + "status" => convert(Int8, bus["bus_type"] != 4), + "index" => shunt_num, + "source_id" => ["bus", bus["bus_i"]] + )]) shunt_num += 1 end @@ -412,7 +438,7 @@ end "sets all branch transformer taps to 1.0, to simplify branch models" -function mp2pm_branch(data::Dict{String,Any}) +function _mp2pm_branch!(data::Dict{String,Any}) branches = [branch for branch in data["branch"]] if haskey(data, "ne_branch") append!(branches, data["ne_branch"]) @@ -447,7 +473,7 @@ end "adds pmin and pmax values at to and from buses" -function mp2pm_dcline(data::Dict{String,Any}) +function _mp2pm_dcline!(data::Dict{String,Any}) for dcline in data["dcline"] pmin = dcline["pmin"] pmax = dcline["pmax"] @@ -499,7 +525,7 @@ end "adds dcline costs, if gen costs exist" -function add_dcline_costs(data::Dict{String,Any}) +function _add_dcline_costs!(data::Dict{String,Any}) if length(data["gencost"]) > 0 && length(data["dclinecost"]) <= 0 && length(data["dcline"]) > 0 @info("added zero cost function data for dclines") model = data["gencost"][1]["model"] @@ -533,14 +559,15 @@ end "merges generator cost functions into generator data, if costs exist" -function merge_generator_cost_data(data::Dict{String,Any}) +function _merge_generator_cost_data!(data::Dict{String,Any}) if haskey(data, "gencost") for (i, gencost) in enumerate(data["gencost"]) gen = data["gen"][i] @assert(gen["index"] == gencost["index"]) delete!(gencost, "index") + delete!(gencost, "source_id") - check_keys(gen, keys(gencost)) + _check_keys(gen, keys(gencost)) merge!(gen, gencost) end delete!(data, "gencost") @@ -551,8 +578,9 @@ function merge_generator_cost_data(data::Dict{String,Any}) dcline = data["dcline"][i] @assert(dcline["index"] == dclinecost["index"]) delete!(dclinecost, "index") + delete!(dclinecost, "source_id") - check_keys(dcline, keys(dclinecost)) + _check_keys(dcline, keys(dclinecost)) merge!(dcline, dclinecost) end delete!(data, "dclinecost") @@ -561,15 +589,16 @@ end "merges bus name data into buses, if names exist" -function merge_bus_name_data(data::Dict{String,Any}) +function _merge_bus_name_data!(data::Dict{String,Any}) if haskey(data, "bus_name") # can assume same length is same as bus # this is validated during matpower parsing for (i, bus_name) in enumerate(data["bus_name"]) bus = data["bus"][i] delete!(bus_name, "index") + delete!(bus_name, "source_id") - check_keys(bus, keys(bus_name)) + _check_keys(bus, keys(bus_name)) merge!(bus, bus_name) end delete!(data, "bus_name") @@ -578,8 +607,8 @@ end "merges Matpower tables based on the table extension syntax" -function merge_generic_data(data::Dict{String,Any}) - mp_matrix_names = [name[5:length(name)] for name in mp_data_names] +function _merge_generic_data!(data::Dict{String,Any}) + mp_matrix_names = [name[5:length(name)] for name in _mp_data_names] key_to_delete = [] for (k,v) in data @@ -599,6 +628,7 @@ function merge_generic_data(data::Dict{String,Any}) merge_row = v[i] #@assert(row["index"] == merge_row["index"]) # note this does not hold for the bus table delete!(merge_row, "index") + delete!(merge_row, "source_id") for key in keys(merge_row) if haskey(row, key) error("failed to extend the matpower matrix \"$(mp_name)\" with the matrix \"$(k)\" because they both share \"$(key)\" as a column name.") @@ -626,7 +656,7 @@ function export_matpower(data::Dict{String,Any}) end " Get a default value for dict entry " -function get_default(dict, key, default=0.0) +function _get_default(dict, key, default=0.0) if haskey(dict, key) && dict[key] != NaN return dict[key] end @@ -634,17 +664,28 @@ function get_default(dict, key, default=0.0) end +"" +function _check_keys(data, keys) + for key in keys + if haskey(data, key) + error("attempting to overwrite value of $(key) in PowerModels data,\n$(data)") + end + end +end + + + "Export power network data in the matpower format" function export_matpower(io::IO, data::Dict{String,Any}) data = deepcopy(data) #convert data to mixed unit if data["per_unit"] - make_mixed_units(data) + make_mixed_units!(data) end # make all costs have the name number of items (to prepare for table export) - standardize_cost_terms(data) + standardize_cost_terms!(data) # create some useful maps and data structures buses = Dict{Int, Dict}() @@ -655,6 +696,10 @@ function export_matpower(io::IO, data::Dict{String,Any}) for (idx,gen) in data["gen"] generators[gen["index"]] = gen end + storage = Dict{Int, Dict}() + for (idx,strg) in data["storage"] + storage[strg["index"]] = strg + end branches = Dict{Int, Dict}() for (idx,branch) in data["branch"] branches[branch["index"]] = branch @@ -713,23 +758,24 @@ function export_matpower(io::IO, data::Dict{String,Any}) println(io, "% bus_i type Pd Qd Gs Bs area Vm Va baseKV zone Vmax Vmin") println(io, "mpc.bus = [") for (idx,bus) in sort(buses) - println(io, "\t", bus["index"], + println(io, + "\t", bus["index"], "\t", bus["bus_type"], - "\t", get_default(pd, bus["index"]), - "\t", get_default(qd, bus["index"]), - "\t", get_default(gs, bus["index"]), - "\t", get_default(bs, bus["index"]), - "\t", get_default(bus, "area"), - "\t", get_default(bus, "vm"), - "\t", get_default(bus, "va"), - "\t", get_default(bus, "base_kv"), - "\t", get_default(bus, "zone"), - "\t", get_default(bus, "vmax"), - "\t", get_default(bus, "vmin"), - "\t", get_default(bus, "lam_p", ""), - "\t", get_default(bus, "lam_q", ""), - "\t", get_default(bus, "mu_vmax", ""), - "\t", get_default(bus, "mu_vmin",""), + "\t", _get_default(pd, bus["index"]), + "\t", _get_default(qd, bus["index"]), + "\t", _get_default(gs, bus["index"]), + "\t", _get_default(bs, bus["index"]), + "\t", _get_default(bus, "area"), + "\t", _get_default(bus, "vm"), + "\t", _get_default(bus, "va"), + "\t", _get_default(bus, "base_kv"), + "\t", _get_default(bus, "zone"), + "\t", _get_default(bus, "vmax"), + "\t", _get_default(bus, "vmin"), + "\t", _get_default(bus, "lam_p", ""), + "\t", _get_default(bus, "lam_q", ""), + "\t", _get_default(bus, "mu_vmax", ""), + "\t", _get_default(bus, "mu_vmin",""), ) end println(io, "];") @@ -755,38 +801,73 @@ function export_matpower(io::IO, data::Dict{String,Any}) if idx != gen["index"] @info("The index of the generator does not match the matpower assigned index. Any data that uses generator indexes for reference is corrupted."); end - println(io, "\t", gen["gen_bus"], - "\t", get_default(gen, "pg"), - "\t", get_default(gen, "qg"), - "\t", get_default(gen, "qmax"), - "\t", get_default(gen, "qmin"), - "\t", get_default(gen, "vg"), - "\t", get_default(gen, "mbase"), - "\t", get_default(gen, "gen_status"), - "\t", get_default(gen, "pmax"), - "\t", get_default(gen, "pmin"), - "\t", get_default(gen, "pc1", ""), - "\t", get_default(gen, "pc2", ""), - "\t", get_default(gen, "qc1min", ""), - "\t", get_default(gen, "qc1max", ""), - "\t", get_default(gen, "qc2min", ""), - "\t", get_default(gen, "qc2max", ""), - "\t", get_default(gen, "ramp_agc", ""), - "\t", (haskey(gen, "ramp_10") ? gen["ramp_10"] : haskey(gen, "ramp_30") ? 0 : ""), - "\t", get_default(gen, "ramp_30", ""), - "\t", get_default(gen, "ramp_q", ""), - "\t", get_default(gen, "apf", ""), - "\t", get_default(gen, "mu_pmax", ""), - "\t", get_default(gen, "mu_pmin", ""), - "\t", get_default(gen, "mu_qmax", ""), - "\t", get_default(gen, "mu_qmin", ""), - ) - + println(io, + "\t", gen["gen_bus"], + "\t", _get_default(gen, "pg"), + "\t", _get_default(gen, "qg"), + "\t", _get_default(gen, "qmax"), + "\t", _get_default(gen, "qmin"), + "\t", _get_default(gen, "vg"), + "\t", _get_default(gen, "mbase"), + "\t", _get_default(gen, "gen_status"), + "\t", _get_default(gen, "pmax"), + "\t", _get_default(gen, "pmin"), + "\t", _get_default(gen, "pc1", ""), + "\t", _get_default(gen, "pc2", ""), + "\t", _get_default(gen, "qc1min", ""), + "\t", _get_default(gen, "qc1max", ""), + "\t", _get_default(gen, "qc2min", ""), + "\t", _get_default(gen, "qc2max", ""), + "\t", _get_default(gen, "ramp_agc", ""), + "\t", (haskey(gen, "ramp_10") ? gen["ramp_10"] : haskey(gen, "ramp_30") ? 0 : ""), + "\t", _get_default(gen, "ramp_30", ""), + "\t", _get_default(gen, "ramp_q", ""), + "\t", _get_default(gen, "apf", ""), + "\t", _get_default(gen, "mu_pmax", ""), + "\t", _get_default(gen, "mu_pmin", ""), + "\t", _get_default(gen, "mu_qmax", ""), + "\t", _get_default(gen, "mu_qmin", ""), + ) i = i+1 end println(io,"];") println(io) + + if length(storage) > 0 + # Print the storage data + println(io, "%% storage data") + println(io, "% storage_bus ps qs energy energy_rating charge_rating discharge_rating charge_efficiency discharge_efficiency thermal_rating qmin qmax r x standby_loss status") + println(io, "mpc.storage = [") + i = 1 + for (idx,strg) in sort(storage) + if idx != strg["index"] + @info("The index of the storage does not match the matpower assigned index. Any data that uses storage indexes for reference is corrupted."); + end + println(io, "\t", strg["storage_bus"], + "\t", _get_default(strg, "ps"), + "\t", _get_default(strg, "qs"), + "\t", _get_default(strg, "energy"), + "\t", _get_default(strg, "energy_rating"), + "\t", _get_default(strg, "charge_rating"), + "\t", _get_default(strg, "discharge_rating"), + "\t", _get_default(strg, "charge_efficiency"), + "\t", _get_default(strg, "discharge_efficiency"), + "\t", _get_default(strg, "thermal_rating"), + "\t", _get_default(strg, "qmin"), + "\t", _get_default(strg, "qmax"), + "\t", _get_default(strg, "r"), + "\t", _get_default(strg, "x"), + "\t", _get_default(strg, "standby_loss"), + "\t", _get_default(strg, "status"), + ) + i = i+1 + end + println(io,"];") + println(io) + end + + # Print the branch data println(io, "%% branch data") println(io, "% fbus tbus r x b rateA rateB rateC ratio angle status angmin angmax") @@ -797,73 +878,75 @@ function export_matpower(io::IO, data::Dict{String,Any}) @info("The index of the branch does not match the matpower assigned index. Any data that uses branch indexes for reference is corrupted."); end println(io, - "\t", get_default(branch, "f_bus"), - "\t", get_default(branch, "t_bus"), - "\t", get_default(branch, "br_r"), - "\t", get_default(branch, "br_x"), + "\t", _get_default(branch, "f_bus"), + "\t", _get_default(branch, "t_bus"), + "\t", _get_default(branch, "br_r"), + "\t", _get_default(branch, "br_x"), "\t", (branch["b_to"] + branch["b_fr"]) != NaN ? (branch["b_to"] + branch["b_fr"]) : 0, - "\t", get_default(branch, "rate_a"), - "\t", get_default(branch, "rate_b"), - "\t", get_default(branch, "rate_c"), + "\t", _get_default(branch, "rate_a"), + "\t", _get_default(branch, "rate_b"), + "\t", _get_default(branch, "rate_c"), "\t", (branch["transformer"] ? branch["tap"] : 0), "\t", (branch["transformer"] ? branch["shift"] : 0), - "\t", get_default(branch, "br_status"), - "\t", get_default(branch, "angmin"), - "\t", get_default(branch, "angmax"), - "\t", get_default(branch, "pf", ""), - "\t", get_default(branch, "qf", ""), - "\t", get_default(branch, "pt", ""), - "\t", get_default(branch, "qt", ""), - "\t", get_default(branch, "mu_sf", ""), - "\t", get_default(branch, "mu_st", ""), - "\t", get_default(branch, "mu_angmin", ""), - "\t", get_default(branch, "mu_angmax", ""), + "\t", _get_default(branch, "br_status"), + "\t", _get_default(branch, "angmin"), + "\t", _get_default(branch, "angmax"), + "\t", _get_default(branch, "pf", ""), + "\t", _get_default(branch, "qf", ""), + "\t", _get_default(branch, "pt", ""), + "\t", _get_default(branch, "qt", ""), + "\t", _get_default(branch, "mu_sf", ""), + "\t", _get_default(branch, "mu_st", ""), + "\t", _get_default(branch, "mu_angmin", ""), + "\t", _get_default(branch, "mu_angmax", ""), ) i = i+1 end println(io, "];") println(io) - # print the dcline data - println(io, "%% dcline data") - println(io, "% fbus tbus status Pf Pt Qf Qt Vf Vt Pmin Pmax QminF QmaxF QminT QmaxT loss0 loss1") - println(io, "mpc.dcline = [") - for (idx, dcline) in sort(dclines) - println(io, - "\t", get_default(dcline, "f_bus"), - "\t", get_default(dcline, "t_bus"), - "\t", get_default(dcline, "br_status"), - "\t", get_default(dcline, "pf"), - "\t", -get_default(dcline, "pt"), # opposite convention - "\t", -get_default(dcline, "qf"), # opposite convention - "\t", -get_default(dcline, "qt"), # opposite convention - "\t", get_default(dcline, "vf"), - "\t", get_default(dcline, "vt"), - "\t", (haskey(dcline, "mp_pmin") ? dcline["mp_pmin"] : min(dcline["pmaxt"], dcline["pmint"], dcline["pmaxf"], dcline["pminf"])), - "\t", (haskey(dcline, "mp_pmax") ? dcline["mp_pmax"] : max(dcline["pmaxt"], dcline["pmint"], dcline["pmaxf"], dcline["pminf"])), - "\t", get_default(dcline, "qminf"), - "\t", get_default(dcline, "qmaxf"), - "\t", get_default(dcline, "qmint"), - "\t", get_default(dcline, "qmaxt"), - "\t", get_default(dcline, "loss0"), - "\t", get_default(dcline, "loss1"), - "\t", get_default(dcline, "mu_pmin", ""), - "\t", get_default(dcline, "mu_pmax", ""), - "\t", get_default(dcline, "mu_qminf", ""), - "\t", get_default(dcline, "mu_qmaxf", ""), - "\t", get_default(dcline, "mu_qmint", ""), - "\t", get_default(dcline, "mu_qmaxt", ""), - ) + if length(dclines) > 0 + # print the dcline data + println(io, "%% dcline data") + println(io, "% fbus tbus status Pf Pt Qf Qt Vf Vt Pmin Pmax QminF QmaxF QminT QmaxT loss0 loss1") + println(io, "mpc.dcline = [") + for (idx, dcline) in sort(dclines) + println(io, + "\t", _get_default(dcline, "f_bus"), + "\t", _get_default(dcline, "t_bus"), + "\t", _get_default(dcline, "br_status"), + "\t", _get_default(dcline, "pf"), + "\t", -_get_default(dcline, "pt"), # opposite convention + "\t", -_get_default(dcline, "qf"), # opposite convention + "\t", -_get_default(dcline, "qt"), # opposite convention + "\t", _get_default(dcline, "vf"), + "\t", _get_default(dcline, "vt"), + "\t", (haskey(dcline, "mp_pmin") ? dcline["mp_pmin"] : min(dcline["pmaxt"], dcline["pmint"], dcline["pmaxf"], dcline["pminf"])), + "\t", (haskey(dcline, "mp_pmax") ? dcline["mp_pmax"] : max(dcline["pmaxt"], dcline["pmint"], dcline["pmaxf"], dcline["pminf"])), + "\t", _get_default(dcline, "qminf"), + "\t", _get_default(dcline, "qmaxf"), + "\t", _get_default(dcline, "qmint"), + "\t", _get_default(dcline, "qmaxt"), + "\t", _get_default(dcline, "loss0"), + "\t", _get_default(dcline, "loss1"), + "\t", _get_default(dcline, "mu_pmin", ""), + "\t", _get_default(dcline, "mu_pmax", ""), + "\t", _get_default(dcline, "mu_qminf", ""), + "\t", _get_default(dcline, "mu_qmaxf", ""), + "\t", _get_default(dcline, "mu_qmint", ""), + "\t", _get_default(dcline, "mu_qmaxt", ""), + ) + end + println(io, "];") + println(io) end - println(io, "];") - println(io) # Print the gen cost data - export_cost_data(io, generators, "mpc.gencost") + _export_cost_data(io, generators, "mpc.gencost") # Print the dcline cost data - export_cost_data(io, dclines, "mpc.dclinecost") + _export_cost_data(io, dclines, "mpc.dclinecost") # ne branch is not part of the matpower specs. However, it is treated as a special case by the matpower parser # for example, br_b is converted into b_to and b_fr @@ -878,18 +961,18 @@ function export_matpower(io::IO, data::Dict{String,Any}) println(io, "\t", branch["f_bus"], "\t", branch["t_bus"], - "\t", get_default(branch, "br_r"), - "\t", get_default(branch, "br_x"), + "\t", _get_default(branch, "br_r"), + "\t", _get_default(branch, "br_x"), "\t", (haskey(branch,"b_to") ? branch["b_to"] + branch["b_fr"] : 0), - "\t", get_default(branch, "rate_a"), - "\t", get_default(branch, "rate_b"), - "\t", get_default(branch, "rate_c"), + "\t", _get_default(branch, "rate_a"), + "\t", _get_default(branch, "rate_b"), + "\t", _get_default(branch, "rate_c"), "\t", (branch["transformer"] ? branch["tap"] : 0), "\t", (branch["transformer"] ? branch["shift"] : 0), - "\t", get_default(branch, "br_status"), - "\t", get_default(branch, "angmin"), - "\t", get_default(branch, "angmax"), - "\t", get_default(branch, "construction_cost"), + "\t", _get_default(branch, "br_status"), + "\t", _get_default(branch, "angmin"), + "\t", _get_default(branch, "angmax"), + "\t", _get_default(branch, "construction_cost"), ) i = i+1 end @@ -897,39 +980,55 @@ function export_matpower(io::IO, data::Dict{String,Any}) println(io) end + if all(haskey(bus, "name") for (i,bus) in buses) + # Print the bus name data + println(io, "%% bus names") + println(io, "mpc.bus_name = {") + for (idx,bus) in sort(buses) + println(io, + "\t", "'", bus["name"], "'" + ) + end + println(io, "};") + println(io) + end + # Print the extra bus data - export_extra_data(io, data, "bus", Set(["index", "gs", "bs", "zone", "bus_i", "bus_type", "qd", "vmax", "area", "vmin", "va", "vm", "base_kv", "pd", "bus_name", "lam_p", "lam_q", "mu_vmax", "mu_vmin"]); postfix="_data") + _export_extra_data(io, data, "bus", Set(["index", "source_id", "gs", "bs", "zone", "bus_i", "bus_type", "qd", "vmax", "area", "vmin", "va", "vm", "base_kv", "pd", "name", "lam_p", "lam_q", "mu_vmax", "mu_vmin"]); postfix="_data") # Print the extra generator data - export_extra_data(io, data, "gen", Set(["index", "gen_bus", "pg", "qg", "qmax", "qmin", "vg", "mbase", "gen_status", "pmax", "pmin", "pc1", "pc2", "qc1min", "qc1max", "qc2min", "qc2max", "ramp_agc", "ramp_10", "ramp_30", "ramp_q", "apf", "ncost", "model", "shutdown", "startup", "cost", "mu_pmax", "mu_pmin", "mu_qmax", "mu_qmin"]); postfix="_data") + _export_extra_data(io, data, "gen", Set(["index", "source_id", "gen_bus", "pg", "qg", "qmax", "qmin", "vg", "mbase", "gen_status", "pmax", "pmin", "pc1", "pc2", "qc1min", "qc1max", "qc2min", "qc2max", "ramp_agc", "ramp_10", "ramp_30", "ramp_q", "apf", "ncost", "model", "shutdown", "startup", "cost", "mu_pmax", "mu_pmin", "mu_qmax", "mu_qmin"]); postfix="_data") + + # Print the extra storage data + _export_extra_data(io, data, "storage", Set(["index", "source_id", "storage_bus", "ps", "qs", "energy", "energy_rating", "charge_rating", "discharge_rating", "charge_efficiency", "discharge_efficiency", "thermal_rating", "qmin", "qmax", "r", "x", "standby_loss", "status"]); postfix="_data") # Print the extra branch data - export_extra_data(io, data, "branch", Set(["index", "f_bus", "t_bus", "br_r", "br_x", "br_b", "b_to", "b_fr", "rate_a", "rate_b", "rate_c", "tap", "shift", "br_status", "angmin", "angmax", "transformer", "g_to", "g_fr", "pf", "qf", "pt", "qt", "mu_sf", "mu_st", "mu_angmin", "mu_angmax"]); postfix="_data") + _export_extra_data(io, data, "branch", Set(["index", "source_id", "f_bus", "t_bus", "br_r", "br_x", "br_b", "b_to", "b_fr", "rate_a", "rate_b", "rate_c", "tap", "shift", "br_status", "angmin", "angmax", "transformer", "g_to", "g_fr", "pf", "qf", "pt", "qt", "mu_sf", "mu_st", "mu_angmin", "mu_angmax"]); postfix="_data") # Print the extra dcline data - export_extra_data(io, data, "dcline", Set(["index", "mu_qmaxt", "mu_qmint", "mu_qmaxf", "mu_qminf", "mu_pmax", "mu_pmin", "loss0", "loss1", "qmint", "qmaxt", "pmin", "pmax", "qminf", "qmaxf", "f_bus", "t_bus", "br_status", "pf", "pt", "qf", "qt", "vf", "vt", "ncost", "model", "shutdown", "pmaxt", "startup", "pmint", "cost", "pminf", "pmaxf", "mp_pmax", "mp_pmin"]); postfix="_data") + _export_extra_data(io, data, "dcline", Set(["index", "source_id", "mu_qmaxt", "mu_qmint", "mu_qmaxf", "mu_qminf", "mu_pmax", "mu_pmin", "loss0", "loss1", "qmint", "qmaxt", "pmin", "pmax", "qminf", "qmaxf", "f_bus", "t_bus", "br_status", "pf", "pt", "qf", "qt", "vf", "vt", "ncost", "model", "shutdown", "pmaxt", "startup", "pmint", "cost", "pminf", "pmaxf", "mp_pmax", "mp_pmin"]); postfix="_data") # Print the extra ne_branch data if haskey(data, "ne_branch") - export_extra_data(io, data, "ne_branch", Set(["index", "f_bus", "t_bus", "br_r", "br_x", "br_b", "b_to", "b_fr", "rate_a", "rate_b", "rate_c", "tap", "shift", "br_status", "angmin", "angmax", "transformer", "construction_cost", "g_to", "g_fr"]); postfix="_data") + _export_extra_data(io, data, "ne_branch", Set(["index", "source_id", "f_bus", "t_bus", "br_r", "br_x", "br_b", "b_to", "b_fr", "rate_a", "rate_b", "rate_c", "tap", "shift", "br_status", "angmin", "angmax", "transformer", "construction_cost", "g_to", "g_fr"]); postfix="_data") end # print the extra load data - export_extra_data(io, data, "load", Set(["index", "load_bus", "status", "qd", "pd"]); postfix="_data") + _export_extra_data(io, data, "load", Set(["index", "source_id", "load_bus", "status", "qd", "pd"]); postfix="_data") # print the extra shunt data - export_extra_data(io, data, "shunt", Set(["index", "shunt_bus", "status", "gs", "bs"]); postfix="_data") + _export_extra_data(io, data, "shunt", Set(["index", "source_id", "shunt_bus", "status", "gs", "bs"]); postfix="_data") # print the extra component data for (key, value) in data - if key != "bus" && key != "gen" && key != "branch" && key != "load" && key != "shunt" && key != "dcline" && key != "ne_branch" && key != "version" && key != "baseMVA" && key != "per_unit" && key != "name" && key != "source_type" && key != "source_version" - export_extra_data(io, data, key) + if key != "bus" && key != "gen" && key != "branch" && key != "load" && key != "shunt" && key != "storage" && key != "dcline" && key != "ne_branch" && key != "version" && key != "baseMVA" && key != "per_unit" && key != "name" && key != "source_type" && key != "source_version" + _export_extra_data(io, data, key) end end end "Export fields of a component type" -function export_extra_data(io::IO, data::Dict{String,Any}, component, excluded_fields=Set(["index"]); postfix="") +function _export_extra_data(io::IO, data::Dict{String,<:Any}, component, excluded_fields=Set(["index", "source_id"]); postfix="") if isa(data[component], Int) || isa(data[component], Int64) || isa(data[component], Float64) println(io, "mpc.", component, " = ", data[component], ";") println(io) @@ -1003,7 +1102,7 @@ function export_extra_data(io::IO, data::Dict{String,Any}, component, excluded_f end "Export cost data" -function export_cost_data(io::IO, components::Dict{Int,Dict}, prefix::String) +function _export_cost_data(io::IO, components::Dict{Int,Dict}, prefix::String) if length(components) <= 0 return end diff --git a/src/parsers/pm_io/multiconductor.jl b/src/parsers/pm_io/multiconductor.jl deleted file mode 100644 index 68789e816d..0000000000 --- a/src/parsers/pm_io/multiconductor.jl +++ /dev/null @@ -1,217 +0,0 @@ -export MultiConductorValue, MultiConductorVector, MultiConductorMatrix, conductors - -# "a data structure for working with multiconductor datasets" -if VERSION < v"0.7.0-" - abstract type MultiConductorValue{T,N} end -else - abstract type MultiConductorValue{T,N} <: AbstractArray{T,N} end -end - - -"a data structure for working with multiconductor datasets" -mutable struct MultiConductorVector{T} <: MultiConductorValue{T,1} - values::Vector{T} -end - -MultiConductorVector(value::T, conductors::Int) where T = MultiConductorVector([value for i in 1:conductors]) -Base.map(f, a::MultiConductorVector{T}) where T = MultiConductorVector{T}(map(f, a.values)) -Base.map(f, a::MultiConductorVector{T}, b::MultiConductorVector{T}) where T = MultiConductorVector{T}(map(f, a.values, b.values)) -conductors(mcv::MultiConductorVector) = length(mcv.values) - -MultiConductorVector(value::Array{T,2}) where T = MultiConductorMatrix{T}(value) - - -"" -function Base.setindex!(mcv::MultiConductorVector{T}, v::T, i::Int) where T - mcv.values[i] = v -end - - - -"" -mutable struct MultiConductorMatrix{T} <: MultiConductorValue{T,2} - values::Matrix{T} -end - - -MultiConductorMatrix(value::T, conductors::Int) where T = MultiConductorMatrix(value*Matrix{Float64}(LinearAlgebra.I, conductors, conductors)) -Base.map(f, a::MultiConductorMatrix{T}) where T = MultiConductorMatrix{T}(map(f, a.values)) -Base.map(f, a::MultiConductorMatrix{T}, b::MultiConductorMatrix{T}) where T = MultiConductorMatrix{T}(map(f, a.values, b.values)) -conductors(mcv::MultiConductorMatrix) = size(mcv.values, 1) - -"" -function Base.setindex!(mcv::MultiConductorMatrix{T}, v::T, i::Int, j::Int) where T - mcv.values[i,j] = v -end - - - - -if VERSION < v"0.7.0-" - Base.start(mcv::MultiConductorValue) = start(mcv.values) - Base.next(mcv::MultiConductorValue, state) = next(mcv.values, state) - Base.done(mcv::MultiConductorValue, state) = done(mcv.values, state) -else - iterate(mcv::MultiConductorValue, kwargs...) = iterate(mcv.values, kwargs...) -end - -Base.length(mcv::MultiConductorValue) = length(mcv.values) -Base.size(mcv::MultiConductorValue, a...) = size(mcv.values, a...) -Base.getindex(mcv::MultiConductorValue, args...) = mcv.values[args...] - -Base.show(io::IO, mcv::MultiConductorValue) = Base.show(io, mcv.values) - -Base.broadcast(f::Any, a::Any, b::MultiConductorValue) = broadcast(f, a, b.values) -Base.broadcast(f::Any, a::MultiConductorValue, b::Any) = broadcast(f, a.values, b) -Base.broadcast(f::Any, a::MultiConductorValue, b::MultiConductorValue) = broadcast(f, a.values, b.values) - -# Broadcast implementation for Julia v0.7 -if VERSION > v"0.7.0-" - Base.BroadcastStyle(::Type{<:MultiConductorVector}) = Broadcast.ArrayStyle{MultiConductorVector}() - Base.BroadcastStyle(::Type{<:MultiConductorMatrix}) = Broadcast.ArrayStyle{MultiConductorMatrix}() - - function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{MultiConductorVector}}, ::Type{ElType}) where ElType - A = find_mcv(bc) - return MultiConductorVector(similar(Array{ElType}, axes(bc))) - end - - "`A = find_mcv(As)` returns the first MultiConductorVector among the arguments." - find_mcv(bc::Base.Broadcast.Broadcasted) = find_mcv(bc.args) - find_mcv(args::Tuple) = find_mcv(find_mcv(args[1]), Base.tail(args)) - find_mcv(x) = x - find_mcv(a::MultiConductorVector, rest) = a - find_mcv(::Any, rest) = find_mcv(rest) - - - function Base.similar(bc::Broadcast.Broadcasted{Broadcast.ArrayStyle{MultiConductorMatrix}}, ::Type{ElType}) where ElType - A = find_mcm(bc) - return MultiConductorMatrix(similar(Array{ElType}, axes(bc))) - end - - "`A = find_mcm(As)` returns the first MultiConductorMatrix among the arguments." - find_mcm(bc::Base.Broadcast.Broadcasted) = find_mcm(bc.args) - find_mcm(args::Tuple) = find_mcm(find_mcm(args[1]), Base.tail(args)) - find_mcm(x) = x - find_mcm(a::MultiConductorMatrix, rest) = a - find_mcm(::Any, rest) = find_mcm(rest) -end - - -# Vectors -Base.:+(a::MultiConductorVector) = MultiConductorVector(+(a.values)) -Base.:+(a::MultiConductorVector, b::Union{Array,Number}) = MultiConductorVector(+(a.values, b)) -Base.:+(a::Union{Array,Number}, b::MultiConductorVector) = MultiConductorVector(+(a, b.values)) -Base.:+(a::MultiConductorVector, b::MultiConductorVector) = MultiConductorVector(+(a.values, b.values)) - -Base.:-(a::MultiConductorVector) = MultiConductorVector(-(a.values)) -Base.:-(a::MultiConductorVector, b::Union{Array,Number}) = MultiConductorVector(-(a.values, b)) -Base.:-(a::Union{Array,Number}, b::MultiConductorVector) = MultiConductorVector(-(a, b.values)) -Base.:-(a::MultiConductorVector, b::MultiConductorVector) = MultiConductorVector(-(a.values, b.values)) - -Base.:*(a::Number, b::MultiConductorVector) = MultiConductorVector(*(a, b.values)) -Base.:*(a::MultiConductorVector, b::Number) = MultiConductorVector(*(a.values, b)) -Base.:*(a::Array, b::MultiConductorVector) = MultiConductorVector(Base.broadcast(*, a, b.values)) -Base.:*(a::MultiConductorVector, b::Array) = MultiConductorVector(Base.broadcast(*, a.values, b)) -Base.:*(a::MultiConductorVector, b::MultiConductorVector) = MultiConductorVector(Base.broadcast(*, a.values, b.values)) - -Base.:/(a::MultiConductorVector, b::Number) = MultiConductorVector(/(a.values, b)) -Base.:/(a::Union{Array,Number}, b::MultiConductorVector) = MultiConductorVector(Base.broadcast(/, a, b.values)) -Base.:/(a::MultiConductorVector, b::MultiConductorVector) = MultiConductorVector(Base.broadcast(/, a.values, b.values)) - -if VERSION < v"0.7.0-" - Base.:*(a::MultiConductorVector, b::RowVector) = MultiConductorMatrix(Base.broadcast(*, a.values, b)) - Base.:*(a::RowVector, b::MultiConductorVector) = MultiConductorMatrix(Base.broadcast(*, a, b.values)) -else - Base.:*(a::MultiConductorVector, b::LinearAlgebra.Adjoint) = MultiConductorMatrix(Base.broadcast(*, a.values, b)) - Base.:*(a::LinearAlgebra.Adjoint, b::MultiConductorVector) = MultiConductorMatrix(Base.broadcast(*, a, b.values)) -end - -# Matrices -Base.:+(a::MultiConductorMatrix) = MultiConductorMatrix(+(a.values)) -Base.:+(a::MultiConductorMatrix, b::Union{Array,Number}) = MultiConductorMatrix(+(a.values, b)) -Base.:+(a::Union{Array,Number}, b::MultiConductorMatrix) = MultiConductorMatrix(+(a, b.values)) -Base.:+(a::MultiConductorMatrix, b::MultiConductorMatrix) = MultiConductorMatrix(+(a.values, b.values)) - -Base.:-(a::MultiConductorMatrix) = MultiConductorMatrix(-(a.values)) -Base.:-(a::MultiConductorMatrix, b::Union{Array,Number}) = MultiConductorMatrix(-(a.values, b)) -Base.:-(a::Union{Array,Number}, b::MultiConductorMatrix) = MultiConductorMatrix(-(a, b.values)) -Base.:-(a::MultiConductorMatrix, b::MultiConductorMatrix) = MultiConductorMatrix(-(a.values, b.values)) - -Base.:*(a::MultiConductorMatrix, b::Number) = MultiConductorMatrix(*(a.values, b)) -Base.:*(a::Number, b::MultiConductorMatrix) = MultiConductorMatrix(*(a, b.values)) -Base.:*(a::MultiConductorMatrix, b::Array) = MultiConductorMatrix(*(a.values, b)) -Base.:*(a::Array, b::MultiConductorMatrix) = MultiConductorMatrix(*(a, b.values)) -Base.:*(a::MultiConductorMatrix, b::MultiConductorMatrix) = MultiConductorMatrix(*(a.values, b.values)) - -Base.:/(a::MultiConductorMatrix, b::Union{Array,Number}) = MultiConductorMatrix(/(a.values, b)) -Base.:/(a::Union{Array,Number}, b::MultiConductorMatrix) = MultiConductorMatrix(/(a, b.values)) -Base.:/(a::MultiConductorMatrix, b::MultiConductorMatrix) = MultiConductorMatrix(/(a.values, b.values)) - -Base.:*(a::MultiConductorMatrix, b::MultiConductorVector) = MultiConductorVector(*(a.values, b.values)) - -if VERSION < v"0.7.0-" - Base.:/(a::MultiConductorMatrix, b::RowVector) = MultiConductorVector(squeeze(/(a.values, b), 2)) -else - Base.:/(a::MultiConductorMatrix, b::LinearAlgebra.Adjoint) = MultiConductorVector(squeeze(/(a.values, b), 2)) -end - - -Base.:^(a::MultiConductorVector, b::Complex) = MultiConductorVector(Base.broadcast(^, a.values, b)) -Base.:^(a::MultiConductorVector, b::Integer) = MultiConductorVector(Base.broadcast(^, a.values, b)) -Base.:^(a::MultiConductorVector, b::AbstractFloat) = MultiConductorVector(Base.broadcast(^, a.values, b)) -Base.:^(a::MultiConductorMatrix, b::Complex) = MultiConductorMatrix(a.values ^ b) -Base.:^(a::MultiConductorMatrix, b::Integer) = MultiConductorMatrix(a.values ^ b) -Base.:^(a::MultiConductorMatrix, b::AbstractFloat) = MultiConductorMatrix(a.values ^ b) - - -LinearAlgebra.inv(a::MultiConductorMatrix) = MultiConductorMatrix(inv(a.values)) -LinearAlgebra.pinv(a::MultiConductorMatrix) = MultiConductorMatrix(LinearAlgebra.pinv(a.values)) - -Base.real(a::MultiConductorVector) = MultiConductorVector(real(a.values)) -Base.real(a::MultiConductorMatrix) = MultiConductorMatrix(real(a.values)) -Base.imag(a::MultiConductorVector) = MultiConductorVector(imag(a.values)) -Base.imag(a::MultiConductorMatrix) = MultiConductorMatrix(imag(a.values)) - -LinearAlgebra.transpose(a::MultiConductorVector) = a.values' -LinearAlgebra.transpose(a::MultiConductorMatrix) = MultiConductorMatrix(a.values') - -LinearAlgebra.diag(a::MultiConductorMatrix) = MultiConductorVector(diag(a.values)) -LinearAlgebra.diagm(p::Pair{<:Integer, MultiConductorVector{S}}) where S = MultiConductorMatrix(diagm(p.first => p.second.values)) - -if VERSION <= v"0.7.0-" - LinearAlgebra.diagm(a::MultiConductorVector{S}) where S = LinearAlgebra.diagm(0 => a) -end - -Base.rad2deg(a::MultiConductorVector) = MultiConductorVector(map(rad2deg, a.values)) -Base.rad2deg(a::MultiConductorMatrix) = MultiConductorMatrix(map(rad2deg, a.values)) - -Base.deg2rad(a::MultiConductorVector) = MultiConductorVector(map(deg2rad, a.values)) -Base.deg2rad(a::MultiConductorMatrix) = MultiConductorMatrix(map(deg2rad, a.values)) - - - - -JSON.lower(mcv::MultiConductorValue) = mcv.values - - -"converts a MultiConductorValue value to a string in summary" -function _value2string(mcv::MultiConductorValue, float_precision::Int) - a = join([_value2string(v, float_precision) for v in mcv.values], ", ") - return "[$(a)]" -end - - -"" -function Base.isapprox(a::MultiConductorValue, b::MultiConductorValue; kwargs...) - if length(a) == length(b) - return all( isapprox(a[i], b[i]; kwargs...) for i in 1:length(a)) - end - return false -end - - -getmcv(value::Any, conductor::Int) = value -getmcv(value::Any, conductor_i::Int, conductor_j::Int) = value -getmcv(value::MultiConductorVector, conductor::Int) = value[conductor] -getmcv(value::MultiConductorMatrix{T}, conductor::Int) where T = MultiConductorVector{T}(value[conductor]) -getmcv(value::MultiConductorMatrix, conductor_i::Int, conductor_j::Int) = value[conductor_i, conductor_j] diff --git a/src/parsers/pm_io/psse.jl b/src/parsers/pm_io/psse.jl index 0b887678dc..e76620f206 100644 --- a/src/parsers/pm_io/psse.jl +++ b/src/parsers/pm_io/psse.jl @@ -2,12 +2,12 @@ """ - init_bus!(bus, id) + _init_bus!(bus, id) Initializes a `bus` of id `id` with default values given in the PSS(R)E specification. """ -function init_bus!(bus::Dict{String,Any}, id::Int) +function _init_bus!(bus::Dict{String,Any}, id::Int) bus["bus_i"] = id bus["bus_type"] = 1 bus["area"] = 1 @@ -23,12 +23,12 @@ end """ - get_bus_value(bus_i, field, pm_data) + _get_bus_value(bus_i, field, pm_data) Returns the value of `field` of `bus_i` from the PowerModels data. Requires "bus" Dict to already be populated. """ -function get_bus_value(bus_i, field, pm_data) +function _get_bus_value(bus_i, field, pm_data) if isa(pm_data["bus"], Array) for bus in pm_data["bus"] if bus["index"] == bus_i @@ -49,11 +49,11 @@ end """ - find_max_bus_id(pm_data) + _find_max_bus_id(pm_data) Returns the maximum bus id in `pm_data` """ -function find_max_bus_id(pm_data::Dict)::Int +function _find_max_bus_id(pm_data::Dict)::Int max_id = 0 for bus in pm_data["bus"] if bus["index"] > max_id && !endswith(bus["name"], "starbus") @@ -73,44 +73,44 @@ by `["bus_i", "name", "I", "J", "K", "CKT"]` where "bus_i" and "name" are the modified names for the starbus, and "I", "J", "K" and "CKT" come from the originating transformer, in the PSS(R)E transformer specification. """ -function create_starbus_from_transformer(pm_data::Dict, transformer::Dict)::Dict +function _create_starbus_from_transformer(pm_data::Dict, transformer::Dict)::Dict starbus = Dict{String,Any}() # transformer starbus ids will be one order of magnitude larger than highest real bus id - base = convert(Int, 10 ^ ceil(log10(abs(find_max_bus_id(pm_data))))) + base = convert(Int, 10 ^ ceil(log10(abs(_find_max_bus_id(pm_data))))) starbus_id = transformer["I"] + base - init_bus!(starbus, starbus_id) + _init_bus!(starbus, starbus_id) starbus["name"] = "$(transformer["I"]) starbus" starbus["vm"] = transformer["VMSTAR"] starbus["va"] = transformer["ANSTAR"] starbus["bus_type"] = transformer["STAT"] - starbus["area"] = get_bus_value(transformer["I"], "area", pm_data) - starbus["zone"] = get_bus_value(transformer["I"], "zone", pm_data) - starbus["source_id"] = push!([starbus["bus_i"], starbus["name"]], transformer["I"], transformer["J"], transformer["K"], transformer["CKT"]) + starbus["area"] = _get_bus_value(transformer["I"], "area", pm_data) + starbus["zone"] = _get_bus_value(transformer["I"], "zone", pm_data) + starbus["source_id"] = push!(["transformer", starbus["bus_i"], starbus["name"]], transformer["I"], transformer["J"], transformer["K"], transformer["CKT"]) return starbus end "Imports remaining keys from `data_in` into `data_out`, excluding keys in `exclude`" -function import_remaining!(data_out::Dict, data_in::Dict, import_all::Bool; exclude=[]) +function _import_remaining!(data_out::Dict, data_in::Dict, import_all::Bool; exclude=[]) if import_all for (k, v) in data_in if !(k in exclude) if isa(v, Array) for (n, item) in enumerate(v) if isa(item, Dict) - import_remaining!(item, item, import_all) + _import_remaining!(item, item, import_all) if !("index" in keys(item)) item["index"] = n end end end elseif isa(v, Dict) - import_remaining!(v, v, import_all) + _import_remaining!(v, v, import_all) end data_out[lowercase(k)] = v delete!(data_in, k) @@ -121,12 +121,12 @@ end """ - psse2pm_branch!(pm_data, pti_data) + _psse2pm_branch!(pm_data, pti_data) Parses PSS(R)E-style Branch data into a PowerModels-style Dict. "source_id" is given by `["I", "J", "CKT"]` in PSS(R)E Branch specification. """ -function psse2pm_branch!(pm_data::Dict, pti_data::Dict, import_all::Bool) +function _psse2pm_branch!(pm_data::Dict, pti_data::Dict, import_all::Bool) pm_data["branch"] = [] @@ -152,10 +152,10 @@ function psse2pm_branch!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["angmax"] = 0.0 sub_data["transformer"] = false - sub_data["source_id"] = [sub_data["f_bus"], sub_data["t_bus"], pop!(branch, "CKT")] + sub_data["source_id"] = ["branch", sub_data["f_bus"], sub_data["t_bus"], pop!(branch, "CKT")] sub_data["index"] = i - import_remaining!(sub_data, branch, import_all; exclude=["B", "BI", "BJ"]) + _import_remaining!(sub_data, branch, import_all; exclude=["B", "BI", "BJ"]) if sub_data["rate_a"] == 0.0 delete!(sub_data, "rate_a") @@ -174,12 +174,12 @@ end """ - psse2pm_generator!(pm_data, pti_data) + _psse2pm_generator!(pm_data, pti_data) Parses PSS(R)E-style Generator data in a PowerModels-style Dict. "source_id" is given by `["I", "ID"]` in PSS(R)E Generator specification. """ -function psse2pm_generator!(pm_data::Dict, pti_data::Dict, import_all::Bool) +function _psse2pm_generator!(pm_data::Dict, pti_data::Dict, import_all::Bool) pm_data["gen"] = [] if haskey(pti_data, "GENERATOR") for gen in pti_data["GENERATOR"] @@ -203,10 +203,10 @@ function psse2pm_generator!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["ncost"] = 2 sub_data["cost"] = [1.0, 0.0] - sub_data["source_id"] = [sub_data["gen_bus"], pop!(gen, "ID")] + sub_data["source_id"] = ["generator", sub_data["gen_bus"], pop!(gen, "ID")] sub_data["index"] = length(pm_data["gen"]) + 1 - import_remaining!(sub_data, gen, import_all) + _import_remaining!(sub_data, gen, import_all) push!(pm_data["gen"], sub_data) end @@ -215,12 +215,12 @@ end """ - psse2pm_bus!(pm_data, pti_data) + _psse2pm_bus!(pm_data, pti_data) Parses PSS(R)E-style Bus data into a PowerModels-style Dict. "source_id" is given by ["I", "NAME"] in PSS(R)E Bus specification. """ -function psse2pm_bus!(pm_data::Dict, pti_data::Dict, import_all::Bool) +function _psse2pm_bus!(pm_data::Dict, pti_data::Dict, import_all::Bool) pm_data["bus"] = [] if haskey(pti_data, "BUS") for bus in pti_data["BUS"] @@ -234,20 +234,14 @@ function psse2pm_bus!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["base_kv"] = pop!(bus, "BASKV") sub_data["zone"] = pop!(bus, "ZONE") sub_data["name"] = pop!(bus, "NAME") + sub_data["vmax"] = pop!(bus, "NVHI") + sub_data["vmin"] = pop!(bus, "NVLO") - if haskey(bus, "NVHI") && haskey(bus, "NVLO") - sub_data["vmax"] = pop!(bus, "NVHI") - sub_data["vmin"] = pop!(bus, "NVLO") - else - @info("PTI v$(pm_data["source_version"]) does not contain vmin and vmax values, defaults of 0.9 and 1.1, respectively, assumed.") - sub_data["vmax"] = 1.1 - sub_data["vmin"] = 0.9 - end + sub_data["source_id"] = ["bus", "$(bus["I"])"] - sub_data["source_id"] = ["$(bus["I"])"] sub_data["index"] = pop!(bus, "I") - import_remaining!(sub_data, bus, import_all) + _import_remaining!(sub_data, bus, import_all) push!(pm_data["bus"], sub_data) end @@ -256,12 +250,12 @@ end """ - psse2pm_load!(pm_data, pti_data) + _psse2pm_load!(pm_data, pti_data) Parses PSS(R)E-style Load data into a PowerModels-style Dict. "source_id" is given by `["I", "ID"]` in the PSS(R)E Load specification. """ -function psse2pm_load!(pm_data::Dict, pti_data::Dict, import_all::Bool) +function _psse2pm_load!(pm_data::Dict, pti_data::Dict, import_all::Bool) pm_data["load"] = [] if haskey(pti_data, "LOAD") for load in pti_data["LOAD"] @@ -272,10 +266,10 @@ function psse2pm_load!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["qd"] = pop!(load, "QL") sub_data["status"] = pop!(load, "STATUS") - sub_data["source_id"] = [sub_data["load_bus"], pop!(load, "ID")] + sub_data["source_id"] = ["load", sub_data["load_bus"], pop!(load, "ID")] sub_data["index"] = length(pm_data["load"]) + 1 - import_remaining!(sub_data, load, import_all) + _import_remaining!(sub_data, load, import_all) push!(pm_data["load"], sub_data) end @@ -284,14 +278,14 @@ end """ - psse2pm_shunt!(pm_data, pti_data) + _psse2pm_shunt!(pm_data, pti_data) Parses PSS(R)E-style Fixed and Switched Shunt data into a PowerModels-style Dict. "source_id" is given by `["I", "ID"]` for Fixed Shunts, and `["I", "SWREM"]` for Switched Shunts, as given by the PSS(R)E Fixed and Switched Shunts specifications. """ -function psse2pm_shunt!(pm_data::Dict, pti_data::Dict, import_all::Bool) +function _psse2pm_shunt!(pm_data::Dict, pti_data::Dict, import_all::Bool) pm_data["shunt"] = [] if haskey(pti_data, "FIXED SHUNT") @@ -303,10 +297,10 @@ function psse2pm_shunt!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["bs"] = pop!(shunt, "BL") sub_data["status"] = pop!(shunt, "STATUS") - sub_data["source_id"] = [sub_data["shunt_bus"], pop!(shunt, "ID")] + sub_data["source_id"] = ["fixed shunt", sub_data["shunt_bus"], pop!(shunt, "ID")] sub_data["index"] = length(pm_data["shunt"]) + 1 - import_remaining!(sub_data, shunt, import_all) + _import_remaining!(sub_data, shunt, import_all) push!(pm_data["shunt"], sub_data) end @@ -323,10 +317,10 @@ function psse2pm_shunt!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["bs"] = pop!(shunt, "BINIT") sub_data["status"] = pop!(shunt, "STAT") - sub_data["source_id"] = [sub_data["shunt_bus"], pop!(shunt, "SWREM")] + sub_data["source_id"] = ["switched shunt", sub_data["shunt_bus"], pop!(shunt, "SWREM")] sub_data["index"] = length(pm_data["shunt"]) + 1 - import_remaining!(sub_data, shunt, import_all) + _import_remaining!(sub_data, shunt, import_all) push!(pm_data["shunt"], sub_data) end @@ -335,14 +329,14 @@ end """ - psse2pm_transformer!(pm_data, pti_data) + _psse2pm_transformer!(pm_data, pti_data) Parses PSS(R)E-style Transformer data into a PowerModels-style Dict. "source_id" is given by `["I", "J", "K", "CKT", "winding"]`, where "winding" is 0 if transformer is two-winding, and 1, 2, or 3 for three-winding, and the remaining keys are defined in the PSS(R)E Transformer specification. """ -function psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool) +function _psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool) if !haskey(pm_data, "branch") pm_data["branch"] = [] end @@ -365,8 +359,8 @@ function psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool) else br_r, br_x = transformer["R1-2"], transformer["X1-2"] end - br_r *= (transformer["NOMV1"]^2 / get_bus_value(transformer["I"], "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE1-2"]) - br_x *= (transformer["NOMV1"]^2 / get_bus_value(transformer["I"], "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE1-2"]) + br_r *= (transformer["NOMV1"]^2 / _get_bus_value(transformer["I"], "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE1-2"]) + br_x *= (transformer["NOMV1"]^2 / _get_bus_value(transformer["I"], "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE1-2"]) end sub_data["br_r"] = br_r @@ -396,7 +390,7 @@ function psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool) # Unit Transformations if transformer["CW"] != 1 # NOT "for off-nominal turns ratio in pu of winding bus base voltage" - sub_data["tap"] *= get_bus_value(transformer["J"], "base_kv", pm_data) / get_bus_value(transformer["I"], "base_kv", pm_data) + sub_data["tap"] *= _get_bus_value(transformer["J"], "base_kv", pm_data) / _get_bus_value(transformer["I"], "base_kv", pm_data) if transformer["CW"] == 3 # "for off-nominal turns ratio in pu of nominal winding voltage, NOMV1, NOMV2 and NOMV3." sub_data["tap"] *= transformer["NOMV1"] / transformer["NOMV2"] end @@ -407,11 +401,11 @@ function psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["angmin"] = 0.0 sub_data["angmax"] = 0.0 - sub_data["source_id"] = [pop!(transformer, "I"), pop!(transformer, "J"), pop!(transformer, "K"), pop!(transformer, "CKT"), 0] + sub_data["source_id"] = ["transformer", pop!(transformer, "I"), pop!(transformer, "J"), pop!(transformer, "K"), pop!(transformer, "CKT"), 0] sub_data["transformer"] = true sub_data["index"] = length(pm_data["branch"]) + 1 - import_remaining!(sub_data, transformer, import_all; exclude=["I", "J", "K", "CZ", "CW", "R1-2", "R2-3", "R3-1", + _import_remaining!(sub_data, transformer, import_all; exclude=["I", "J", "K", "CZ", "CW", "R1-2", "R2-3", "R3-1", "X1-2", "X2-3", "X3-1", "SBASE1-2", "SBASE2-3", "SBASE3-1", "MAG1", "MAG2", "STAT", "NOMV1", "NOMV2"]) @@ -420,7 +414,7 @@ function psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool) bus_id1, bus_id2, bus_id3 = transformer["I"], transformer["J"], transformer["K"] # Creates a starbus (or "dummy" bus) to which each winding of the transformer will connect - starbus = create_starbus_from_transformer(pm_data, transformer) + starbus = _create_starbus_from_transformer(pm_data, transformer) push!(pm_data["bus"], starbus) # Create 3 branches from a three winding transformer (one for each winding, which will each connect to the starbus) @@ -440,13 +434,13 @@ function psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool) # Unit Transformations if transformer["CZ"] != 1 # NOT "for resistance and reactance in pu on system MVA base and winding voltage base" - br_r12 *= (transformer["NOMV1"]^2 / get_bus_value(bus_id1, "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE1-2"]) - br_r23 *= (transformer["NOMV2"]^2 / get_bus_value(bus_id2, "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE2-3"]) - br_r31 *= (transformer["NOMV3"]^2 / get_bus_value(bus_id3, "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE3-1"]) + br_r12 *= (transformer["NOMV1"]^2 / _get_bus_value(bus_id1, "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE1-2"]) + br_r23 *= (transformer["NOMV2"]^2 / _get_bus_value(bus_id2, "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE2-3"]) + br_r31 *= (transformer["NOMV3"]^2 / _get_bus_value(bus_id3, "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE3-1"]) - br_x12 *= (transformer["NOMV1"]^2 / get_bus_value(bus_id1, "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE1-2"]) - br_x23 *= (transformer["NOMV2"]^2 / get_bus_value(bus_id2, "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE2-3"]) - br_x31 *= (transformer["NOMV3"]^2 / get_bus_value(bus_id3, "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE3-1"]) + br_x12 *= (transformer["NOMV1"]^2 / _get_bus_value(bus_id1, "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE1-2"]) + br_x23 *= (transformer["NOMV2"]^2 / _get_bus_value(bus_id2, "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE2-3"]) + br_x31 *= (transformer["NOMV3"]^2 / _get_bus_value(bus_id3, "base_kv", pm_data)^2) * (pm_data["baseMVA"] / transformer["SBASE3-1"]) end # See "Power System Stability and Control", ISBN: 0-07-035958-X, Eq. 6.72 @@ -491,7 +485,7 @@ function psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool) # Unit Transformations if transformer["CW"] != 1 # NOT "for off-nominal turns ratio in pu of winding bus base voltage" - sub_data["tap"] /= get_bus_value(bus_id, "base_kv", pm_data) + sub_data["tap"] /= _get_bus_value(bus_id, "base_kv", pm_data) if transformer["CW"] == 3 # "for off-nominal turns ratio in pu of nominal winding voltage, NOMV1, NOMV2 and NOMV3." sub_data["tap"] *= transformer["NOMV$m"] end @@ -502,11 +496,11 @@ function psse2pm_transformer!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["angmin"] = 0.0 sub_data["angmax"] = 0.0 - sub_data["source_id"] = [transformer["I"], transformer["J"], transformer["K"], transformer["CKT"], m] + sub_data["source_id"] = ["transformer", transformer["I"], transformer["J"], transformer["K"], transformer["CKT"], m] sub_data["transformer"] = true sub_data["index"] = length(pm_data["branch"]) + 1 - import_remaining!(sub_data, transformer, import_all; exclude=["I", "J", "K", "CZ", "CW", "R1-2", "R2-3", "R3-1", + _import_remaining!(sub_data, transformer, import_all; exclude=["I", "J", "K", "CZ", "CW", "R1-2", "R2-3", "R3-1", "X1-2", "X2-3", "X3-1", "SBASE1-2", "SBASE2-3", "CKT", "SBASE3-1", "MAG1", "MAG2", "STAT","NOMV1", "NOMV2", "NOMV3", "WINDV1", "WINDV2", "WINDV3", "RATA1", @@ -524,7 +518,7 @@ end """ - psse2pm_dcline!(pm_data, pti_data) + _psse2pm_dcline!(pm_data, pti_data) Parses PSS(R)E-style Two-Terminal and VSC DC Lines data into a PowerModels compatible Dict structure by first converting them to a simple DC Line Model. @@ -534,7 +528,7 @@ is given by `["IBUS1", "IBUS2", "NAME"]`, where "IBUS1" is "IBUS" of the first converter bus, and "IBUS2" is the "IBUS" of the second converter bus, in the PSS(R)E Voltage Source Converter specification. """ -function psse2pm_dcline!(pm_data::Dict, pti_data::Dict, import_all::Bool) +function _psse2pm_dcline!(pm_data::Dict, pti_data::Dict, import_all::Bool) pm_data["dcline"] = [] if haskey(pti_data, "TWO-TERMINAL DC") @@ -552,8 +546,8 @@ function psse2pm_dcline!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["pt"] = power_demand sub_data["qf"] = 0.0 sub_data["qt"] = 0.0 - sub_data["vf"] = get_bus_value(pop!(dcline, "IPR"), "vm", pm_data) - sub_data["vt"] = get_bus_value(pop!(dcline, "IPI"), "vm", pm_data) + sub_data["vf"] = _get_bus_value(pop!(dcline, "IPR"), "vm", pm_data) + sub_data["vt"] = _get_bus_value(pop!(dcline, "IPI"), "vm", pm_data) sub_data["pminf"] = 0.0 sub_data["pmaxf"] = dcline["SETVL"] > 0 ? power_demand : -power_demand @@ -586,10 +580,10 @@ function psse2pm_dcline!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["cost"] = [0.0, 0.0, 0.0] sub_data["model"] = 2 - sub_data["source_id"] = [sub_data["f_bus"], sub_data["t_bus"], pop!(dcline, "NAME")] + sub_data["source_id"] = ["two-terminal dc", sub_data["f_bus"], sub_data["t_bus"], pop!(dcline, "NAME")] sub_data["index"] = length(pm_data["dcline"]) + 1 - import_remaining!(sub_data, dcline, import_all) + _import_remaining!(sub_data, dcline, import_all) push!(pm_data["dcline"], sub_data) end @@ -640,10 +634,10 @@ function psse2pm_dcline!(pm_data::Dict, pti_data::Dict, import_all::Bool) sub_data["cost"] = [0.0, 0.0, 0.0] sub_data["model"] = 2 - sub_data["source_id"] = [sub_data["f_bus"], sub_data["t_bus"], pop!(dcline, "NAME")] + sub_data["source_id"] = ["vsc dc", sub_data["f_bus"], sub_data["t_bus"], pop!(dcline, "NAME")] sub_data["index"] = length(pm_data["dcline"]) + 1 - import_remaining!(sub_data, dcline, import_all) + _import_remaining!(sub_data, dcline, import_all) push!(pm_data["dcline"], sub_data) end @@ -651,41 +645,46 @@ function psse2pm_dcline!(pm_data::Dict, pti_data::Dict, import_all::Bool) end -function psse2pm_storage!(pm_data::Dict, pti_data::Dict, import_all::Bool) +function _psse2pm_storage!(pm_data::Dict, pti_data::Dict, import_all::Bool) pm_data["storage"] = [] end +function _psse2pm_switch!(pm_data::Dict, pti_data::Dict, import_all::Bool) + pm_data["switch"] = [] +end + """ - parse_psse(pti_data) + _pti_to_powermodels!(pti_data) Converts PSS(R)E-style data parsed from a PTI raw file, passed by `pti_data` into a format suitable for use internally in PowerModels. Imports all remaining data from the PTI file if `import_all` is true (Default: false). """ -function parse_psse(pti_data::Dict; import_all=false, validate=true)::Dict +function _pti_to_powermodels!(pti_data::Dict; import_all=false, validate=true)::Dict pm_data = Dict{String,Any}() rev = pop!(pti_data["CASE IDENTIFICATION"][1], "REV") pm_data["per_unit"] = false pm_data["source_type"] = "pti" - pm_data["source_version"] = VersionNumber("$rev") + pm_data["source_version"] = "$rev" pm_data["baseMVA"] = pop!(pti_data["CASE IDENTIFICATION"][1], "SBASE") pm_data["name"] = pop!(pti_data["CASE IDENTIFICATION"][1], "NAME") - import_remaining!(pm_data, pti_data["CASE IDENTIFICATION"][1], import_all) + _import_remaining!(pm_data, pti_data["CASE IDENTIFICATION"][1], import_all) - psse2pm_bus!(pm_data, pti_data, import_all) - psse2pm_load!(pm_data, pti_data, import_all) - psse2pm_shunt!(pm_data, pti_data, import_all) - psse2pm_generator!(pm_data, pti_data, import_all) - psse2pm_branch!(pm_data, pti_data, import_all) - psse2pm_transformer!(pm_data, pti_data, import_all) - psse2pm_dcline!(pm_data, pti_data, import_all) - psse2pm_storage!(pm_data, pti_data, import_all) + _psse2pm_bus!(pm_data, pti_data, import_all) + _psse2pm_load!(pm_data, pti_data, import_all) + _psse2pm_shunt!(pm_data, pti_data, import_all) + _psse2pm_generator!(pm_data, pti_data, import_all) + _psse2pm_branch!(pm_data, pti_data, import_all) + _psse2pm_transformer!(pm_data, pti_data, import_all) + _psse2pm_dcline!(pm_data, pti_data, import_all) + _psse2pm_storage!(pm_data, pti_data, import_all) + _psse2pm_switch!(pm_data, pti_data, import_all) - import_remaining!(pm_data, pti_data, import_all; exclude=[ + _import_remaining!(pm_data, pti_data, import_all; exclude=[ "CASE IDENTIFICATION", "BUS", "LOAD", "FIXED SHUNT", "SWITCHED SHUNT", "GENERATOR","BRANCH", "TRANSFORMER", "TWO-TERMINAL DC", "VOLTAGE SOURCE CONVERTER" @@ -705,7 +704,7 @@ function parse_psse(pti_data::Dict; import_all=false, validate=true)::Dict end if validate - check_network_data(pm_data) + correct_network_data!(pm_data) end return pm_data @@ -726,5 +725,5 @@ end function parse_psse(io::IO; kwargs...)::Dict pti_data = parse_pti(io) - return parse_psse(pti_data; kwargs...) + return _pti_to_powermodels!(pti_data; kwargs...) end diff --git a/src/parsers/pm_io/pti.jl b/src/parsers/pm_io/pti.jl index 240c717a99..83a321e003 100644 --- a/src/parsers/pm_io/pti.jl +++ b/src/parsers/pm_io/pti.jl @@ -6,12 +6,12 @@ """ - get_pti_sections() +_get_pti_sections() -Returns `Array` of the names of the sections, in the order that they -appear in a PTI file, v33+ +Internal function. Returns `Array` of the names of the sections, in the order +that they appear in a PTI file, v33 """ -function get_pti_sections()::Array +function _get_pti_sections()::Array return ["CASE IDENTIFICATION", "BUS", "LOAD", "FIXED SHUNT", "GENERATOR", "BRANCH", "TRANSFORMER", "AREA INTERCHANGE", "TWO-TERMINAL DC", "VOLTAGE SOURCE CONVERTER", "IMPEDANCE CORRECTION", "MULTI-TERMINAL DC", "MULTI-SECTION LINE", "ZONE", "INTER-AREA TRANSFER", "OWNER", @@ -19,13 +19,15 @@ function get_pti_sections()::Array end + + """ - get_pti_dtypes(field_name) + _get_pti_dtypes(field_name) -Returns `OrderedDict` of data types for PTI file section given by `field_name`, -as enumerated by PSS/E Program Operation Manual +Internal function. Returns array of data types for PTI file section given by +`field_name`, as enumerated by PSS/E Program Operation Manual. """ -function get_pti_dtypes(field_name::AbstractString)::Array +function _get_pti_dtypes(field_name::AbstractString)::Array transaction_dtypes = [("IC", Int64), ("SBASE", Float64), ("REV", Int64), ("XFRRAT", Float64), ("NXFRAT", Float64), ("BASFRQ", Float64)] @@ -39,7 +41,7 @@ function get_pti_dtypes(field_name::AbstractString)::Array ("IQ", Float64), ("YP", Float64), ("YQ", Float64), ("OWNER", Int64), ("SCALE", Int64), ("INTRPT", Int64)] - fixded_shunt_dtypes = [("I", Int64), ("ID", String), ("STATUS", Int64), ("GL", Float64), + fixed_shunt_dtypes = [("I", Int64), ("ID", String), ("STATUS", Int64), ("GL", Float64), ("BL", Float64)] generator_dtypes = [("I", Int64), ("ID", String), ("PG", Float64), ("QG", Float64), @@ -57,58 +59,48 @@ function get_pti_dtypes(field_name::AbstractString)::Array ("O2", Int64), ("F2", Float64), ("O3", Int64), ("F3", Float64), ("O4", Int64), ("F4", Float64)] - transformer_3_dtypes = [("I", Int64), ("J", Int64), ("K", Int64), ("CKT", String), - ("CW", Int64), ("CZ", Int64), ("CM", Int64), ("MAG1", Float64), - ("MAG2", Float64), ("NMETR", Int64), ("NAME", String), - ("STAT", Int64), - ("O1", Int64), ("F1", Float64), - ("O2", Int64), ("F2", Float64), - ("O3", Int64), ("F3", Float64), - ("O4", Int64), ("F4", Float64), - ("VECGRP", String), - - ("R1-2", Float64), ("X1-2", Float64), ("SBASE1-2", Float64), - ("R2-3", Float64), ("X2-3", Float64), ("SBASE2-3", Float64), - ("R3-1", Float64), ("X3-1", Float64), ("SBASE3-1", Float64), - ("VMSTAR", Float64), ("ANSTAR", Float64), - - ("WINDV1", Float64), ("NOMV1", Float64), ("ANG1", Float64), - ("RATA1", Float64), ("RATB1", Float64), ("RATC1", Float64), - ("COD1", Int64), ("CONT1", Int64), ("RMA1", Float64), ("RMI1", Float64), - ("VMA1", Float64), ("VMI1", Float64), ("NTP1", Float64), ("TAB1", Int64), - ("CR1", Float64), ("CX1", Float64), ("CNXA1", Float64), - - ("WINDV2", Float64), ("NOMV2", Float64), ("ANG2", Float64), - ("RATA2", Float64), ("RATB2", Float64), ("RATC2", Float64), - ("COD2", Int64), ("CONT2", Int64), ("RMA2", Float64), ("RMI2", Float64), - ("VMA2", Float64), ("VMI2", Float64), ("NTP2", Float64), ("TAB2", Int64), - ("CR2", Float64), ("CX2", Float64), ("CNXA2", Float64), - - ("WINDV3", Float64), ("NOMV3", Float64), ("ANG3", Float64), - ("RATA3", Float64), ("RATB3", Float64), ("RATC3", Float64), - ("COD3", Int64), ("CONT3", Int64), ("RMA3", Float64), ("RMI3", Float64), - ("VMA3", Float64), ("VMI3", Float64), ("NTP3", Float64), ("TAB3", Int64), - ("CR3", Float64), ("CX3", Float64), ("CNXA3", Float64)] - - transformer_2_dtypes = [("I", Int64), ("J", Int64), ("K", Int64), ("CKT", String), - ("CW", Int64), ("CZ", Int64), ("CM", Int64), ("MAG1", Float64), - ("MAG2", Float64), ("NMETR", Int64), ("NAME", String), - ("STAT", Int64), - ("O1", Int64), ("F1", Float64), - ("O2", Int64), ("F2", Float64), - ("O3", Int64), ("F3", Float64), - ("O4", Int64), ("F4", Float64), - ("VECGRP", String), - - ("R1-2", Float64), ("X1-2", Float64), ("SBASE1-2", Float64), - - ("WINDV1", Float64), ("NOMV1", Float64), ("ANG1", Float64), - ("RATA1", Float64), ("RATB1", Float64), ("RATC1", Float64), - ("COD1", Int64), ("CONT1", Int64), ("RMA1", Float64), ("RMI1", Float64), - ("VMA1", Float64), ("VMI1", Float64), ("NTP1", Float64), ("TAB1", Int64), - ("CR1", Float64), ("CX1", Float64), ("CNXA1", Float64), - - ("WINDV2", Float64), ("NOMV2", Float64)] + transformer_dtypes = [("I", Int64), ("J", Int64), ("K", Int64), ("CKT", String), + ("CW", Int64), ("CZ", Int64), ("CM", Int64), ("MAG1", Float64), + ("MAG2", Float64), ("NMETR", Int64), ("NAME", String), + ("STAT", Int64), + ("O1", Int64), ("F1", Float64), + ("O2", Int64), ("F2", Float64), + ("O3", Int64), ("F3", Float64), + ("O4", Int64), ("F4", Float64), + ("VECGRP", String)] + +transformer_3_1_dtypes = [("R1-2", Float64), ("X1-2", Float64), ("SBASE1-2", Float64), + ("R2-3", Float64), ("X2-3", Float64), ("SBASE2-3", Float64), + ("R3-1", Float64), ("X3-1", Float64), ("SBASE3-1", Float64), + ("VMSTAR", Float64), ("ANSTAR", Float64)] + +transformer_3_2_dtypes = [("WINDV1", Float64), ("NOMV1", Float64), ("ANG1", Float64), + ("RATA1", Float64), ("RATB1", Float64), ("RATC1", Float64), + ("COD1", Int64), ("CONT1", Int64), ("RMA1", Float64), ("RMI1", Float64), + ("VMA1", Float64), ("VMI1", Float64), ("NTP1", Float64), ("TAB1", Int64), + ("CR1", Float64), ("CX1", Float64), ("CNXA1", Float64)] + +transformer_3_3_dtypes = [("WINDV2", Float64), ("NOMV2", Float64), ("ANG2", Float64), + ("RATA2", Float64), ("RATB2", Float64), ("RATC2", Float64), + ("COD2", Int64), ("CONT2", Int64), ("RMA2", Float64), ("RMI2", Float64), + ("VMA2", Float64), ("VMI2", Float64), ("NTP2", Float64), ("TAB2", Int64), + ("CR2", Float64), ("CX2", Float64), ("CNXA2", Float64)] + +transformer_3_4_dtypes = [("WINDV3", Float64), ("NOMV3", Float64), ("ANG3", Float64), + ("RATA3", Float64), ("RATB3", Float64), ("RATC3", Float64), + ("COD3", Int64), ("CONT3", Int64), ("RMA3", Float64), ("RMI3", Float64), + ("VMA3", Float64), ("VMI3", Float64), ("NTP3", Float64), ("TAB3", Int64), + ("CR3", Float64), ("CX3", Float64), ("CNXA3", Float64)] + +transformer_2_1_dtypes = [("R1-2", Float64), ("X1-2", Float64), ("SBASE1-2", Float64)] + +transformer_2_2_dtypes = [("WINDV1", Float64), ("NOMV1", Float64), ("ANG1", Float64), + ("RATA1", Float64), ("RATB1", Float64), ("RATC1", Float64), + ("COD1", Int64), ("CONT1", Int64), ("RMA1", Float64), ("RMI1", Float64), + ("VMA1", Float64), ("VMI1", Float64), ("NTP1", Float64), ("TAB1", Int64), + ("CR1", Float64), ("CX1", Float64), ("CNXA1", Float64)] + +transformer_2_3_dtypes = [("WINDV2", Float64), ("NOMV2", Float64)] area_interchange_dtypes = [("I", Int64), ("ISW", Int64), ("PDES", Float64), ("PTOL", Float64), @@ -208,13 +200,19 @@ function get_pti_dtypes(field_name::AbstractString)::Array ("X3", Float64), ("E1", Float64), ("SE1", Float64), ("E2", Float64), ("SE2", Float64), ("IA1", Float64), ("IA2", Float64), ("XAMULT", Float64)] - dtypes = Dict{String,Array}("BUS" => bus_dtypes, + dtypes = Dict{String,Array}("BUS" => bus_dtypes, "LOAD" => load_dtypes, - "FIXED SHUNT" => fixded_shunt_dtypes, + "FIXED SHUNT" => fixed_shunt_dtypes, "GENERATOR" => generator_dtypes, "BRANCH" => branch_dtypes, - "TRANSFORMER TWO WINDING" => transformer_2_dtypes, - "TRANSFORMER THREE WINDING" => transformer_3_dtypes, + "TRANSFORMER" => transformer_dtypes, + "TRANSFORMER TWO-WINDING LINE 1" => transformer_2_1_dtypes, + "TRANSFORMER TWO-WINDING LINE 2" => transformer_2_2_dtypes, + "TRANSFORMER TWO-WINDING LINE 3" => transformer_2_3_dtypes, + "TRANSFORMER THREE-WINDING LINE 1" => transformer_3_1_dtypes, + "TRANSFORMER THREE-WINDING LINE 2" => transformer_3_2_dtypes, + "TRANSFORMER THREE-WINDING LINE 3" => transformer_3_3_dtypes, + "TRANSFORMER THREE-WINDING LINE 4" => transformer_3_4_dtypes, "AREA INTERCHANGE" => area_interchange_dtypes, "TWO-TERMINAL DC" => two_terminal_line_dtypes, "VOLTAGE SOURCE CONVERTER" => vsc_line_dtypes, @@ -239,38 +237,238 @@ end """ - parse_line_element!(data, elements, section) + _get_component_property(section, ret, search_field, search_value) + +Internal function. Finds a component in `section` where `search_field` == +`search_value` and returns `ret` from that component. +""" +function _get_component_property(section, ret, search_field, search_value) + for component in section + if component[search_field] == search_value + return component[ret] + end + end + Memento.warn(_LOGGER, "cannot find $search_field = $search_value, no return for $ret") +end + + +""" + _get_pti_default(section, field, data, component) + +Internal function. Returns a default value in `section` for `field` in +`component` from `data`. +""" +function _get_pti_default(section::AbstractString, field::AbstractString, data::Dict, component::Dict; sub_field=nothing) + case_identification = Dict{String,Any}("IC" => 0, "SBASE" => 100.0, "REV" => 33, + "XFRRAT" => 0, "NXFRAT" => 0, "BASFRQ" => 60) + + bus = Dict{String,Any}("BASKV" => 0.0, "IDE" => 1, "AREA" => 1, "ZONE" => 1, + "OWNER" => 1, "VM" => 1.0, "VA" => 0.0, "NVHI" => 1.1, + "NVLO" => 0.9, "EVHI" => 1.1, "EVLO" => 0.9, + "NAME" => " ") + + load = Dict{String,Any}("ID" => 1, "STATUS" => 1, "PL" => 0.0, "QL" => 0.0, + "IP" => 0.0, "IQ" => 0.0, "YP" => 0.0, "YQ" => 0.0, + "SCALE" => 1, "INTRPT" => 0, + "AREA" => Expr(:call, :_get_component_property, data["BUS"], "AREA", "I", get(component, "I", 0)), + "ZONE" => Expr(:call, :_get_component_property, data["BUS"], "ZONE", "I", get(component, "I", 0)), + "OWNER" => Expr(:call, :_get_component_property, data["BUS"], "OWNER", "I", get(component, "I", 0))) + + fixed_shunt = Dict{String,Any}("ID" => 1, "STATUS" => 1, "GL" => 0.0, "BL" => 0.0) + + generator = Dict{String,Any}("ID" => 1, "PG" => 0.0, "QG" => 0.0, "QT" => 9999.0, + "QB" => -9999.0, "VS" => 1.0, "IREG" => 0, + "MBASE" => data["CASE IDENTIFICATION"][1]["SBASE"], + "ZR" => 0.0, "ZX" => 1.0, "RT" => 0.0, "XT" => 0.0, + "GTAP" => 1.0, "STAT" => 1, "RMPCT" => 100.0, + "PT" => 9999.0, "PB" => -9999.0, + "O1" => Expr(:call, :_get_component_property, data["BUS"], "OWNER", "I", get(component, "I", 0)), + "O2" => 0, "O3" => 0, "O4" => 0, "F1" => 1.0,"F2" => 1.0, "F3" => 1.0, + "F4" => 1.0, "WMOD" => 0, "WPF" => 1.0) + + branch = Dict{String,Any}("CKT" => 1, "B" => 0.0, "RATEA" => 0.0, "RATEB" => 0.0, + "RATEC" => 0.0, "GI" => 0.0, "BI" => 0.0, "GJ" => 0.0, + "BJ" => 0.0, "ST" => 1, "MET" => 1, "LEN" => 0.0, + "O1" => Expr(:call, :_get_component_property, data["BUS"], "OWNER", "I", get(component, "I", 0)), + "O2" => 0, "O3" => 0, "O4" => 0, "F1" => 1.0, + "F2" => 1.0, "F3" => 1.0, "F4" => 1.0) + + transformer = Dict{String,Any}("K" => 0, "CKT" => 1, "CW" => 1, "CZ" => 1, "CM" => 1, + "MAG1" => 0.0, "MAG2" => 0.0, "NMETR" => 2, + "NAME" => " ", "STAT" => 1, + "O1" => Expr(:call, :_get_component_property, data["BUS"], "OWNER", "I", get(component, "I", 0)), + "O2" => 0, "O3" => 0, "O4" => 0, + "F1" => 1.0, "F2" => 1.0, "F3" => 1.0, "F4" => 1.0, + "VECGRP" => " ", "R1-2" => 0.0, + "SBASE1-2" => data["CASE IDENTIFICATION"][1]["SBASE"], "R2-3" => 0.0, + "SBASE2-3" => data["CASE IDENTIFICATION"][1]["SBASE"], "R3-1" => 0.0, + "SBASE3-1" => data["CASE IDENTIFICATION"][1]["SBASE"], "VMSTAR" => 1.0, + "ANSTAR" => 0.0, + "WINDV1" => get(component, "CW", 1) == 2 ? Expr(:call, :_get_component_property, data["BUS"], "BASKV", "I", get(component, "I", 0)) : 1.0, + "NOMV1" => 0.0, "ANG1" => 0.0, "RATA1" => 0.0, + "RATB1" => 0.0, "RATC1" => 0.0, "COD1" => 0, + "CONT1" => 0, "RMA1" => 1.1, "RMI1" => 0.9, + "VMA1" => 1.1, "VMI1" => 0.9, "NTP1" => 33, + "TAB1" => 0, "CR1" => 0.0, "CX1" => 0.0, "CNXA1" => 0.0, + "WINDV2" => get(component, "CW", 1) == 2 ? Expr(:call, :_get_component_property, data["BUS"], "BASKV", "I", get(component, "I", 0)) : 1.0, + "NOMV2" => 0.0, "ANG2" => 0.0, "RATA2" => 0.0, + "RATB2" => 0.0, "RATC2" => 0.0, "COD2" => 0, + "CONT2" => 0, "RMA2" => 1.1, "RMI2" => 0.9, + "VMA2" => 1.1, "VMI2" => 0.9, "NTP2" => 33, + "TAB2" => 0, "CR2" => 0.0, "CX2" => 0.0, + "CNXA2" => 0.0, + "WINDV3" => get(component, "CW", 1) == 2 ? Expr(:call, :_get_component_property, data["BUS"], "BASKV", "I", get(component, "I", 0)) : 1.0, + "NOMV3" => 0.0, "ANG3" => 0.0, "RATA3" => 0.0, + "RATB3" => 0.0, "RATC3" => 0.0, "COD3" => 0, + "CONT3" => 0, "RMA3" => 1.1, "RMI3" => 0.9, + "VMA3" => 1.1, "VMI3" => 0.9, "NTP3" => 33, + "TAB3" => 0, "CR3" => 0.0, "CX3" => 0.0, + "CNXA3" => 0.0) + + area_interchange = Dict{String,Any}("ISW" => 0, "PDES" => 0.0, "PTOL" => 10.0, + "ARNAME" => " ") + + two_terminal_dc = Dict{String,Any}("MDC" => 0, "VCMOD" => 0.0, "RCOMP" => 0.0, + "DELTI" => 0.0, "METER" => "I", "DCVMIN" => 0.0, + "CCCITMX" => 20, "CCCACC" => 1.0, "TRR" => 1.0, + "TAPR" => 1.0, "TMXR" => 1.5, "TMNR" => 0.51, + "STPR" => 0.00625, "ICR" => 0, "IFR" => 0, + "ITR" => 0, "IDR" => "1", "XCAPR" => 0.0, + "TRI" => 1.0, + "TAPI" => 1.0, "TMXI" => 1.5, "TMNI" => 0.51, + "STPI" => 0.00625, "ICI" => 0, "IFI" => 0, + "ITI" => 0, "IDI" => "1", "XCAPI" => 0.0) + + vsc_dc = Dict{String,Any}("MDC" => 1, + "O1" => Expr(:call, :_get_component_property, data["BUS"], "OWNER", "I", get(get(component, "CONVERTER BUSES", [Dict()])[1], "IBUS", 0)), + "O2" => 0, "O3" => 0, "O4" => 0, + "F1" => 1.0, "F2" => 1.0, "F3" => 1.0, "F4" => 1.0, + "CONVERTER BUSES" => Dict{String,Any}("MODE" => 1, "ACSET" => 1.0, "ALOSS" => 1.0, "BLOSS" => 0.0, "MINLOSS" => 0.0, + "SMAX" => 0.0, "IMAX" => 0.0, "PWF" => 1.0, "MAXQ" => 9999.0, "MINQ" => -9999.0, + "REMOT" => 0, "RMPCT" => 100.0)) + + impedance_correction = Dict{String,Any}("T1" => 0.0, "T2" => 0.0, "T3" => 0.0, + "T4" => 0.0, "T5" => 0.0, "T6" => 0.0, + "T7" => 0.0, "T8" => 0.0, "T9" => 0.0, + "T10" => 0.0, "T11" => 0.0, "F1" => 0.0, + "F2" => 0.0, "F3" => 0.0, "F4" => 0.0, + "F5" => 0.0, "F6" => 0.0, "F7" => 0.0, + "F8" => 0.0, "F9" => 0.0, "F10" => 0.0, + "F11" => 0.0) + + multi_term_dc = Dict{String,Any}("MDC" => 0, "VCMOD" => 0.0, "VCONVN" => 0, + "CONV" => Dict{String,Any}("TR" => 1.0, "TAP" => 1.0, "TPMX" => 1.5, "TPMN" => 0.51, "TSTP" => 0.00625, + "DCPF" => 1, "MARG" => 0.0, "CNVCOD" => 1), + "DCBS" => Dict{String,Any}("IB" => 0.0, "AREA" => 1, "ZONE" => 1, "DCNAME" => " ", "IDC2" => 0, + "RGRND" => 0.0, "OWNER" => 1), + "DCLN" => Dict{String,Any}("DCCKT" => 1, "MET" => 1, "LDC" => 0.0)) + + multi_section = Dict{String,Any}("ID" => "&1", "MET" => 1) + + zone = Dict{String,Any}("ZONAME" => " ") + + interarea = Dict{String,Any}("TRID" => 1, "PTRAN" => 0.0) + + owner = Dict{String,Any}("OWNAME" => " ") + + facts = Dict{String,Any}("J" => 0, "MODE" => 1, "PDES" => 0.0, "QDES" => 0.0, + "VSET" => 1.0, "SHMX" => 9999.0, "TRMX" => 9999.0, + "VTMN" => 0.9, "VTMX" => 1.1, "VSMX" => 1.0, + "IMX" => 0.0, "LINX" => 0.05, "RMPCT" => 100.0, + "OWNER" => 1, "SET1" => 0.0, "SET2" => 0.0, + "VSREF" => 0, "REMOT" => 0, "MNAME" => "") + + switched_shunt = Dict{String,Any}("MODSW" => 1, "ADJM" => 0, "STAT" => 1, + "VSWHI" => 1.0, "VSWLO" => 1.0, "SWREM" => 0, + "RMPCT" => 100.0, "RMIDNT" => "", "BINIT" => 0.0, + "N1" => 0.0, "N2" => 0.0, "N3" => 0.0, + "N4" => 0.0, "N5" => 0.0, "N6" => 0.0, + "N7" => 0.0, "N8" => 0.0, "B1" => 0.0, + "B2" => 0.0, "B3" => 0.0, "B4" => 0.0, + "B5" => 0.0, "B6" => 0.0, "B7" => 0.0, + "B8" => 0.0) + + gne_device = Dict{String,Any}("NTERM" => 1, "NREAL" => 0, "NINTG" => 0, + "NCHAR" => 0, "STATUS" => 1, + "OWNER" => Expr(:call, :_get_component_property, data["BUS"], "OWNER", "I", get(component, "BUS1", 0)), + "NMETR" => get(component, "NTERM", 1), "REAL" => 0, + "INTG" => nothing, + "CHAR" => "1") + + induction_machine = Dict{String,Any}("ID" => 1, "STAT" => 1, "SCODE" => 1, "DCODE" => 2, + "AREA" => Expr(:call, :_get_component_property, data["BUS"], "AREA", "I", get(component, "I", 0)), + "ZONE" => Expr(:call, :_get_component_property, data["BUS"], "ZONE", "I", get(component, "I", 0)), + "OWNER" => Expr(:call, :_get_component_property, data["BUS"], "OWNER", "I", get(component, "I", 0)), + "TCODE" => 1, "BCODE" => 1, + "MBASE" => data["CASE IDENTIFICATION"][1]["SBASE"], "RATEKV" => 0.0, + "PCODE" => 1, "H" => 1.0, "A" => 1.0, + "B" => 1.0, "D" => 1.0, "E" => 1.0, + "RA" => 0.0, "XA" => 0.0, "XM" => 2.5, + "R1" => 999.0, "X1" => 999.0, "R2" => 999.0, + "X2" => 999.0, "X3" => 0.0, "E1" => 1.0, + "SE1" => 0.0, "E2" => 1.2, "SE2" => 0.0, + "IA1" => 0.0, "IA2" => 0.0, "XAMULT" => 1) + + defaults = Dict{String,Dict}("BUS" => bus, + "LOAD" => load, + "FIXED SHUNT" => fixed_shunt, + "GENERATOR" => generator, + "BRANCH" => branch, + "TRANSFORMER" => transformer, + "AREA INTERCHANGE" => area_interchange, + "TWO-TERMINAL DC" => two_terminal_dc, + "VOLTAGE SOURCE CONVERTER" => vsc_dc, + "IMPEDANCE CORRECTION" => impedance_correction, + "MULTI-TERMINAL DC" => multi_term_dc, + "MULTI-SECTION LINE" => multi_section, + "ZONE" => zone, + "INTER-AREA TRANSFER" => interarea, + "OWNER" => owner, + "FACTS CONTROL DEVICE" => facts, + "SWITCHED SHUNT" => switched_shunt, + "CASE IDENTIFICATION" => case_identification, + "GNE DEVICE" => gne_device, + "INDUCTION MACHINE" => induction_machine) + + if sub_field != nothing + return eval(defaults[section][field][sub_field]) + else + return eval(defaults[section][field]) + end +end -Parses a single "line" of data elements from a PTI file, as given by `elements` -which is an array of the line, typically split at `,`. Elements are parsed into -data types given by `section` and saved into `data::Dict` """ -function parse_line_element!(data::Dict, elements::Array, section::AbstractString) - missing = [] - for (field, dtype) in get_pti_dtypes(section) +_parse_line_element!(data, elements, section) + +Internal function. Parses a single "line" of data elements from a PTI file, as +given by `elements` which is an array of the line, typically split at `,`. +Elements are parsed into data types given by `section` and saved into `data::Dict`. +""" +function _parse_line_element!(data::Dict, elements::Array, section::AbstractString) + missing_fields = [] + for (field, dtype) in _get_pti_dtypes(section) try element = popfirst!(elements) catch message if isa(message, ArgumentError) @debug "Have run out of elements in $section at $field" - push!(missing, field) + push!(missing_fields, field) continue end end - if startswith(element, "'") && endswith(element, "'") + if startswith(strip(element), "'") && endswith(strip(element), "'") dtype = String - element = chop(reverse(chop(reverse(element)))) + element = chop(reverse(chop(reverse(strip(element))))) end try - if dtype != String + if dtype != String && element != "" data[field] = parse(dtype, element) else data[field] = element end - catch message if isa(message, Meta.ParseError) data[field] = element @@ -280,8 +478,11 @@ function parse_line_element!(data::Dict, elements::Array, section::AbstractStrin end end - if length(missing) > 0 - missing_str = join(missing, ", ") + if length(missing_fields) > 0 + for field in missing_fields + data[field] = "" + end + missing_str = join(missing_fields, ", ") if !(section == "SWITCHED SHUNT" && startswith(missing_str, "N")) && !(section == "MULTI-SECTION LINE" && startswith(missing_str, "DUM")) && !(section == "IMPEDANCE CORRECTION" && startswith(missing_str, "T")) @@ -294,10 +495,11 @@ end """ add_section_data!(pti_data, section_data, section) -Adds `section_data::Dict`, which contains all parsed elements of a PTI file -section given by `section`, into the parent `pti_data::Dict` +Internal function. Adds `section_data::Dict`, which contains all parsed +elements of a PTI file section given by `section`, into the parent +`pti_data::Dict` """ -function add_section_data!(pti_data::Dict, section_data::Dict, section::AbstractString) +function _add_section_data!(pti_data::Dict, section_data::Dict, section::AbstractString) try pti_data[section] = append!(pti_data[section], [deepcopy(section_data)]) catch message @@ -311,45 +513,42 @@ end """ - get_line_elements(line) + _get_line_elements(line) -Uses regular expressions to extract all separate data elements from a line of -a PTI file and populate them into an `Array{String}`. Comments, typically -indicated at the end of a line with a `'/'` character, are also extracted -separately, and `Array{Array{String}, String}` is returned. +Internal function. Uses regular expressions to extract all separate data +elements from a line of a PTI file and populate them into an `Array{String}`. +Comments, typically indicated at the end of a line with a `'/'` character, +are also extracted separately, and `Array{Array{String}, String}` is returned. """ -function get_line_elements(line::AbstractString)::Array - match_string = r"(-*\d*\.*\d+[eE]*[+-]*\d*)|(\'.{12}\')|(\'[^\']*?\')|(\"[^\"]*?\")|(\w+)|\,(\s+)?\,|(\/.*)" - matches = collect((m.match for m = eachmatch(match_string, line, overlap=false))) - #matches = matchall(match_string, line) - - @debug "$line" - @debug "$matches" - - elements = [] - comment = "" - for item in matches - if startswith(item, ',') && endswith(item, ',') && length(item) == 2 - elements = append!(elements, [""]) - elseif startswith(item, '/') - comment = item - else - elements = append!(elements, [item]) - end +function _get_line_elements(line::AbstractString)::Array + if length(collect(eachmatch(r"'", line))) % 2 == 1 + throw(DataFormatError("There are an uneven number of single-quotes in \"{line}\", the line cannot be parsed.")) end + comment_split = r"(?!\B[\'][^\']*)[\/](?![^\']*[\']\B)" + line_comment = split(line, comment_split, limit=2) + line = strip(line_comment[1]) + comment = length(line_comment) > 1 ? strip(line_comment[2]) : "" + + split_string = r",(?=(?:[^']*'[^']*')*[^']*$)" + elements = [strip(element) for element in split(line, split_string)] + + @debug("$line") + @debug("$comment") + @debug("$elements") + return [elements, comment] end """ - parse_pti_data(data_string, sections) + _parse_pti_data(data_string, sections) -Parse a PTI raw file into a `Dict`, given the `data_string` of the file and a -list of the `sections` in the PTI file (typically given by default by -`get_pti_sections()`. +Internal function. Parse a PTI raw file into a `Dict`, given the +`data_string` of the file and a list of the `sections` in the PTI +file (typically given by default by `get_pti_sections()`. """ -function parse_pti_data(data_io::IO, sections::Array) +function _parse_pti_data(data_io::IO, sections::Array) data_lines = readlines(data_io) skip_lines = 0 skip_sublines = 0 @@ -363,7 +562,7 @@ function parse_pti_data(data_io::IO, sections::Array) for (line_number, line) in enumerate(data_lines) @debug "$line_number: $line" - (elements, comment) = get_line_elements(line) + (elements, comment) = _get_line_elements(line) if length(elements) != 0 && elements[1] == "Q" && line_number > 3 break @@ -373,31 +572,17 @@ function parse_pti_data(data_io::IO, sections::Array) section = popfirst!(sections) end - match_string = r"\s*END OF ([\w\s-]+) DATA(?:, BEGIN ([\w\s-]+) DATA)?" - @debug "$comment" - matches = match(match_string, comment) - - if !isa(matches, Nothing) - guess_section = matches.captures[1] - else - guess_section = "" + if length(elements) > 1 + @info("At line $line_number, new section started with '0', but additional non-comment data is present. Pattern '^\\s*0\\s*[/]*.*' is reserved for section start/end.") + elseif length(comment) > 0 + @info("At line $line_number, unexpected section: expected: $section, comment specified: $(section)") end - if guess_section == section && length(sections) > 0 + if !isempty(sections) section = popfirst!(sections) - continue - else - if length(elements) > 1 - @info("At line $line_number, new section started with '0', but additional non-comment data is present. Pattern '^\\s*0\\s*[/]*.*' is reserved for section start/end.") - elseif length(comment) > 0 - @info("At line $line_number, unexpected section: expected: $section, comment specified: $(guess_section)") - end - if !isempty(sections) - section = popfirst!(sections) - end - - continue end + + continue else if line_number == 4 section = popfirst!(sections) @@ -413,7 +598,7 @@ function parse_pti_data(data_io::IO, sections::Array) if !(section in ["CASE IDENTIFICATION","TRANSFORMER","VOLTAGE SOURCE CONVERTER","MULTI-TERMINAL DC","TWO-TERMINAL DC","GNE DEVICE"]) section_data = Dict{String,Any}() try - parse_line_element!(section_data, elements, section) + _parse_line_element!(section_data, elements, section) catch message throw(@error("Parsing failed at line $line_number: $(sprint(showerror, message))")) end @@ -421,18 +606,13 @@ function parse_pti_data(data_io::IO, sections::Array) elseif section == "CASE IDENTIFICATION" if line_number == 1 try - parse_line_element!(section_data, elements, section) + _parse_line_element!(section_data, elements, section) catch message throw(@error("Parsing failed at line $line_number: $(sprint(showerror, message))")) end - try - if section_data["REV"] < 33 - @info("Version $(section_data["REV"]) of PTI format is unsupported, parser may not function correctly.") - end - catch message - if isa(message, KeyError) - @error("This file is unrecognized and cannot be parsed") - end + + if section_data["REV"] != "" && section_data["REV"] < 33 + @info("Version $(section_data["REV"]) of PTI format is unsupported, parser may not function correctly.") end else section_data["Comment_Line_$(line_number - 1)"] = line @@ -444,29 +624,40 @@ function parse_pti_data(data_io::IO, sections::Array) elseif section == "TRANSFORMER" section_data = Dict{String,Any}() - if length(get_line_elements(data_lines[line_number + 1])[1]) == 3 && parse(Int64, get_line_elements(line)[1][3]) == 0 # two winding transformer - temp_section = "TRANSFORMER TWO WINDING" - (elements, comment) = get_line_elements(join(data_lines[line_number:line_number + 3], ',')) + if parse(Int64, _get_line_elements(line)[1][3]) == 0 # two winding transformer + winding = "TWO-WINDING" skip_lines = 3 - elseif length(get_line_elements(data_lines[line_number + 1])[1]) == 11 && parse(Int64, get_line_elements(line)[1][3]) != 0 # three winding transformer - temp_section = "TRANSFORMER THREE WINDING" - (elements, comment) = get_line_elements(join(data_lines[line_number:line_number + 4], ',')) + elseif parse(Int64, _get_line_elements(line)[1][3]) != 0 # three winding transformer + winding = "THREE-WINDING" skip_lines = 4 else @error("Cannot detect type of Transformer") end try - parse_line_element!(section_data, elements, temp_section) + for transformer_line in 0:4 + if transformer_line == 0 + temp_section = section + else + temp_section = join([section, winding, "LINE", transformer_line], " ") + end + + if winding == "TWO-WINDING" && transformer_line == 4 + break + else + elements = _get_line_elements(data_lines[line_number + transformer_line])[1] + _parse_line_element!(section_data, elements, temp_section) + end + end catch message throw(@error("Parsing failed at line $line_number: $(sprint(showerror, message))")) end elseif section == "VOLTAGE SOURCE CONVERTER" - if length(get_line_elements(line)[1]) == 11 + if length(_get_line_elements(line)[1]) == 11 section_data = Dict{String,Any}() try - parse_line_element!(section_data, elements, section) + _parse_line_element!(section_data, elements, section) catch message throw(@error("Parsing failed at line $line_number: $(sprint(showerror, message))")) end @@ -476,12 +667,16 @@ function parse_pti_data(data_io::IO, sections::Array) elseif skip_sublines > 0 skip_sublines -= 1 - (elements, comment) = get_line_elements(line) + (elements, comment) = _get_line_elements(line) subsection_data = Dict{String,Any}() - for (field, dtype) in get_pti_dtypes("$section SUBLINES") + for (field, dtype) in _get_pti_dtypes("$section SUBLINES") element = popfirst!(elements) - subsection_data[field] = parse(dtype, element) + if element != "" + subsection_data[field] = parse(dtype, element) + else + subsection_data[field] = "" + end end try @@ -498,13 +693,13 @@ function parse_pti_data(data_io::IO, sections::Array) elseif section == "TWO-TERMINAL DC" section_data = Dict{String,Any}() - if length(get_line_elements(line)[1]) == 12 - (elements, comment) = get_line_elements(join(data_lines[line_number:line_number + 2], ',')) + if length(_get_line_elements(line)[1]) == 12 + (elements, comment) = _get_line_elements(join(data_lines[line_number:line_number + 2], ',')) skip_lines = 2 end try - parse_line_element!(section_data, elements, section) + _parse_line_element!(section_data, elements, section) catch message throw(@error("Parsing failed at line $line_number: $(sprint(showerror, message))")) end @@ -513,7 +708,7 @@ function parse_pti_data(data_io::IO, sections::Array) if skip_sublines == 0 section_data = Dict{String,Any}() try - parse_line_element!(section_data, elements, section) + _parse_line_element!(section_data, elements, section) catch message throw(@error("Parsing failed at line $line_number: $(sprint(showerror, message))")) end @@ -538,17 +733,14 @@ function parse_pti_data(data_io::IO, sections::Array) subsection_data = Dict{String,Any}() - for (field, dtype) in get_pti_dtypes("$section $subsection") - element = popfirst!(elements) - if startswith(element, "'") && endswith(element, "'") - subsection_data[field] = element[2:end-1] - else - subsection_data[field] = parse(dtype, element) - end + try + _parse_line_element!(subsection_data, elements, "$section $subsection") + catch message + throw(error("Parsing failed at line $line_number: $(sprint(showerror, message))")) end try - section_data["$(subsection[2:end])"] = append!(section_data["$(subsection[2:end])"], [deepcopy(subsection_data)]) + section_data["$(subsection[2:end])"] = push!(section_data["$(subsection[2:end])"], deepcopy(subsection_data)) if skip_sublines > 0 && subsection != "NDCLN" continue end @@ -588,9 +780,11 @@ function parse_pti_data(data_io::IO, sections::Array) if subsection != "" @debug "appending data" end - add_section_data!(pti_data, section_data, section) + _add_section_data!(pti_data, section_data, section) end + _populate_defaults!(pti_data) + return pti_data end @@ -617,8 +811,64 @@ Reads PTI data in `io::IO`, returning a `Dict` of the data parsed into the proper types. """ function parse_pti(io::IO)::Dict - pti_data = parse_pti_data(io, get_pti_sections()) - pti_data["CASE IDENTIFICATION"][1]["NAME"] = match(r"^\$", lowercase(io.name)).captures[1] + pti_data = _parse_pti_data(io, _get_pti_sections()) + try + pti_data["CASE IDENTIFICATION"][1]["NAME"] = match(r"^\$", lowercase(io.name)).captures[1] + catch + throw(error( "This file is unrecognized and cannot be parsed")) + end return pti_data end + + +""" + _populate_defaults!(pti_data) + +Internal function. Populates empty fields with PSS(R)E PTI v33 default values +""" +function _populate_defaults!(data::Dict) + for section in _get_pti_sections() + if haskey(data, section) + section_components = [] + for component in data[section] + new_component = deepcopy(component) + for (field, field_value) in component + if isa(field_value, Array) + new_array = [] + for sub_component in field_value + new_sub_component = deepcopy(sub_component) + for (sub_field, sub_field_value) in sub_component + if sub_field_value == "" + try + new_sub_component[sub_field] = _get_pti_default(section, field, data, sub_component; sub_field=sub_field) + catch msg + if isa(msg, KeyError) + @warn( "'$sub_field' in '$field' in '$section' has no default value") + else + rethrow(msg) + end + end + end + end + push!(new_array, new_sub_component) + end + new_component[field] = new_array + elseif field_value == "" && !(field in ["Comment_Line_1", "Comment_Line_2"]) && !startswith(field, "DUM") + try + new_component[field] = _get_pti_default(section, field, data, component) + catch msg + if isa(msg, KeyError) + @warn("'$field' in '$section' has no default value") + else + rethrow(msg) + end + end + end + end + push!(section_components, new_component) + end + data[section] = section_components + end + end +end diff --git a/src/parsers/power_system_table_data.jl b/src/parsers/power_system_table_data.jl new file mode 100644 index 0000000000..846e8d4e06 --- /dev/null +++ b/src/parsers/power_system_table_data.jl @@ -0,0 +1,906 @@ + +const POWER_SYSTEM_DESCRIPTOR_FILE = joinpath(dirname(pathof(PowerSystems)), + "descriptors", "power_system_inputs.json") + +struct PowerSystemTableData + basepower::Float64 + branch::Union{DataFrames.DataFrame, Nothing} + bus::DataFrames.DataFrame + dcline::Union{DataFrames.DataFrame, Nothing} + gen::Union{DataFrames.DataFrame, Nothing} + load::Union{DataFrames.DataFrame, Nothing} + services::Union{DataFrames.DataFrame, Nothing} + category_to_df::Dict{InputCategory, DataFrames.DataFrame} + timeseries_metadata_file::Union{String, Nothing} + directory::String + user_descriptors::Dict + descriptors::Dict + generator_mapping::Dict{NamedTuple, DataType} +end + +function PowerSystemTableData( + data::Dict{String, Any}, + directory::String, + user_descriptors::Union{String, Dict}, + descriptors::Union{String, Dict}, + generator_mapping::Union{String, Dict}; + timeseries_metadata_file = joinpath(directory, + "timeseries_pointers") + ) + category_to_df = Dict{InputCategory, DataFrames.DataFrame}() + categories = [ + ("branch", BRANCH::InputCategory), + ("bus", BUS::InputCategory), + ("dc_branch", DC_BRANCH::InputCategory), + ("gen", GENERATOR::InputCategory), + ("load", LOAD::InputCategory), + ("reserves", RESERVES::InputCategory), + ] + + if !haskey(data, "bus") + throw(DataFormatError("key 'bus' not found in input data")) + end + + if !haskey(data, "basepower") + @warn "key 'basepower' not found in input data; using default=$(DEFAULT_BASE_MVA)" + end + basepower = get(data, "basepower", DEFAULT_BASE_MVA) + + dfs = Vector() + for (label, category) in categories + val = get(data, label, nothing) + if isnothing(val) + @warn "key '$label' not found in input data, set to nothing" + else + category_to_df[category] = val + end + + push!(dfs, val) + end + + if !isfile(timeseries_metadata_file) + if isfile(string(timeseries_metadata_file,".json")) + timeseries_metadata_file = string(timeseries_metadata_file,".json") + elseif isfile(string(timeseries_metadata_file,".csv")) + timeseries_metadata_file = string(timeseries_metadata_file,".csv") + else + timeseries_metadata_file = nothing + end + end + + if user_descriptors isa AbstractString + user_descriptors = _read_config_file(user_descriptors) + end + + if descriptors isa AbstractString + descriptors = _read_config_file(descriptors) + end + + if generator_mapping isa AbstractString + generator_mapping = get_generator_mapping(generator_mapping) + end + + return PowerSystemTableData(basepower, dfs..., category_to_df, timeseries_metadata_file, + directory, user_descriptors, descriptors, generator_mapping) +end + +""" + PowerSystemTableData(directory::AbstractString, + basepower::Float64, + user_descriptor_file::AbstractString; + descriptor_file=POWER_SYSTEM_DESCRIPTOR_FILE) + +Reads in all the data stored in csv files +The general format for data is + folder: + gen.csv + branch.csv + bus.csv + .. + load.csv + +# Arguments +- `directory::AbstractString`: directory containing CSV files +- `basepower::Float64`: base power for System +- `user_descriptor_file::AbstractString`: customized input descriptor file +- `descriptor_file=POWER_SYSTEM_DESCRIPTOR_FILE`: PowerSystems descriptor file +- `generator_mapping_file=GENERATOR_MAPPING_FILE`: generator mapping configuration file +""" +function PowerSystemTableData( + directory::AbstractString, + basepower::Float64, + user_descriptor_file::AbstractString; + descriptor_file=POWER_SYSTEM_DESCRIPTOR_FILE, + generator_mapping_file=GENERATOR_MAPPING_FILE, + timeseries_metadata_file = joinpath(directory, + "timeseries_pointers") + ) + files = readdir(directory) + REGEX_DEVICE_TYPE = r"(.*?)\.csv" + REGEX_IS_FOLDER = r"^[A-Za-z]+$" + data = Dict{String,Any}() + + if length(files) == 0 + error("No files in the folder") + else + data["basepower"] = basepower + end + + encountered_files = 0 + for d_file in files + try + if match(REGEX_IS_FOLDER, d_file) != nothing + @info "Parsing csv files in $d_file ..." + d_file_data = Dict{String,Any}() + for file in readdir(joinpath(directory,d_file)) + if match(REGEX_DEVICE_TYPE, file) != nothing + @info "Parsing csv data in $file ..." + encountered_files += 1 + fpath = joinpath(directory,d_file,file) + raw_data = CSV.File(fpath) |> DataFrames.DataFrame + d_file_data[split(file,r"[.]")[1]] = raw_data + end + end + + if length(d_file_data) > 0 + data[d_file] = d_file_data + @info "Successfully parsed $d_file" + end + + elseif match(REGEX_DEVICE_TYPE, d_file) != nothing + @info "Parsing csv data in $d_file ..." + encountered_files += 1 + fpath = joinpath(directory,d_file) + raw_data = CSV.File(fpath)|> DataFrames.DataFrame + data[split(d_file,r"[.]")[1]] = raw_data + @info "Successfully parsed $d_file" + end + catch ex + @error "Error occurred while parsing $d_file" exception=ex + throw(ex) + end + end + if encountered_files == 0 + error("No csv files or folders in $directory") + end + + return PowerSystemTableData(data, directory, user_descriptor_file, descriptor_file, + generator_mapping_file, + timeseries_metadata_file = timeseries_metadata_file) +end + +""" +Return the custom name stored in the user descriptor file. + +Throws DataFormatError if a required value is not found in the file. +""" +function get_user_field(data::PowerSystemTableData, category::InputCategory, + field::AbstractString) + if !haskey(data.user_descriptors, category) + throw(DataFormatError("Invalid category=$category")) + end + + try + for item in data.user_descriptors[category] + if item["name"] == field + return Symbol(item["custom_name"]) + end + end + catch(err) + if err == KeyError + msg = "Failed to find category=$category field=$field in input descriptors $err" + throw(DataFormatError(msg)) + else + throw(err) + end + end + + msg = "Failed to find category=$category field=$field in input descriptors" + throw(DataFormatError(msg)) +end + +"""Return a vector of user-defined fields for the category.""" +function get_user_fields(data::PowerSystemTableData, category::InputCategory) + if !haskey(data.user_descriptors, category) + throw(DataFormatError("Invalid category=$category")) + end + + return [x["name"] for x in data.user_descriptors[category]] +end + +"""Return the dataframe for the category.""" +function get_dataframe(data::PowerSystemTableData, category::InputCategory) + @assert haskey(data.category_to_df, category) + return data.category_to_df[category] +end + +""" + iterate_rows(data::PowerSystemTableData, category; na_to_nothing=true) + +Return a NamedTuple of parameters from the descriptor file for each row of a dataframe, +making type conversions as necessary. + +Refer to the PowerSystems descriptor file for field names that will be created. +""" +function iterate_rows(data::PowerSystemTableData, category; na_to_nothing=true) + df = data.category_to_df[category] + field_infos = _get_field_infos(data, category, names(df)) + + Channel() do channel + for row in eachrow(df) + obj = _read_data_row(data, row, field_infos; na_to_nothing=na_to_nothing) + put!(channel, obj) + end + end +end + +""" + System(data::PowerSystemTableData) + +Construct a System from PowerSystemTableData data. + +# Arguments +- `forecast_resolution::Union{DateTime, Nothing}=nothing`: only store forecasts that match + this resolution. + +Throws DataFormatError if forecasts with multiple resolutions are detected. +- A component-label pair is not unique within a forecast array. +- A forecast has a different resolution than others. +- A forecast has a different horizon than others. + +""" +function System(data::PowerSystemTableData; forecast_resolution=nothing) + sys = System(data.basepower) + + bus_csv_parser!(sys, data) + loadzone_csv_parser!(sys, data) + load_csv_parser!(sys, data) + + # Services and forecasts must be last. + parsers = ( + (data.branch, branch_csv_parser!), + (data.dcline, dc_branch_csv_parser!), + (data.gen, gen_csv_parser!), + (data.services, services_csv_parser!), + ) + + for (val, parser) in parsers + if !isnothing(val) + parser(sys, data) + end + end + + if !isnothing(data.timeseries_metadata_file) + add_forecasts!(sys, data.timeseries_metadata_file; resolution=forecast_resolution) + end + + check!(sys) + return sys +end + +""" + bus_csv_parser!(sys::System, bus_raw::DataFrames.DataFrame) + +Add buses to the System from the raw data. + +""" +function bus_csv_parser!(sys::System, data::PowerSystemTableData) + for bus in iterate_rows(data, BUS::InputCategory) + bus_type = get_enum_value(BusType, bus.bus_type) + number = bus.bus_id + voltage_limits = (min=0.95, max=1.05) + ps_bus = Bus( + number, + bus.name, + bus_type, + bus.angle, + bus.voltage, + voltage_limits, + bus.base_voltage, + ) + + add_component!(sys, ps_bus) + end +end + +""" + branch_csv_parser!(sys::System, data::PowerSystemTableData) + +Add branches to the System from the raw data. + +""" +function branch_csv_parser!(sys::System, data::PowerSystemTableData) + available = true + + for branch in iterate_rows(data, BRANCH::InputCategory) + bus_from = get_bus(sys, branch.connection_points_from) + bus_to = get_bus(sys, branch.connection_points_to) + connection_points = Arc(bus_from, bus_to) + pf = get(branch, :pf, 0.0) + qf = get(branch, :qf, 0.0) + + #TODO: noop math...Phase-Shifting Transformer angle + alpha = (branch.primary_shunt / 2) - (branch.primary_shunt / 2) + + branch_type = get_branch_type(branch.tap, alpha) + + if branch_type == Line + b = branch.primary_shunt / 2 + anglelimits = (min=-π/2, max=π/2) #TODO: add field in CSV + value = Line( + name = branch.name, + available = available, + activepower_flow = pf, + reactivepower_flow = qf, + arc = connection_points, + r = branch.r, + x = branch.x, + b = (from=b, to=b), + rate = branch.rate, + anglelimits = anglelimits + ) + elseif branch_type == Transformer2W + value = Transformer2W( + name = branch.name, + available = available, + activepower_flow = pf, + reactivepower_flow = qf, + arc = connection_points, + r = branch.r, + x = branch.x, + primaryshunt = branch.primary_shunt, + rate = branch.rate, + ) + elseif branch_type == TapTransformer + value = TapTransformer( + name = branch.name, + available = available, + activepower_flow = pf, + reactivepower_flow = qf, + arc = connection_points, + r = branch.r, + x = branch.x, + primaryshunt = branch.primary_shunt, + tap = branch.tap, + rate = branch.rate, + ) + elseif branch_type == PhaseShiftingTransformer + # TODO create PhaseShiftingTransformer + error("Unsupported branch type $branch_type") + else + error("Unsupported branch type $branch_type") + end + + add_component!(sys, value) + end +end + +""" + dc_branch_csv_parser!(sys::System, data::PowerSystemTableData) + +Add DC branches to the System from raw data. + +""" +function dc_branch_csv_parser!(sys::System, data::PowerSystemTableData) + for dc_branch in iterate_rows(data, DC_BRANCH::InputCategory) + available = true + bus_from = get_bus(sys, dc_branch.connection_points_from) + bus_to = get_bus(sys, dc_branch.connection_points_to) + connection_points = Arc(bus_from, bus_to) + pf = get(dc_branch, :pf, 0.0) + + if dc_branch.control_mode == "Power" + mw_load = dc_branch.mw_load + + #TODO: is there a better way to calculate these?, + activepowerlimits_from = (min=-1 * mw_load, max=mw_load) + activepowerlimits_to = (min=-1 * mw_load, max=mw_load) + reactivepowerlimits_from = (min=0.0, max=0.0) + reactivepowerlimits_to = (min=0.0, max=0.0) + loss = (l0=0.0, l1=dc_branch.loss) #TODO: Can we infer this from the other data?, + + value = HVDCLine( + name = dc_branch.name, + available = available, + activepower_flow = pf, + arc = connection_points, + activepowerlimits_from = activepowerlimits_from, + activepowerlimits_to = activepowerlimits_to, + reactivepowerlimits_from = reactivepowerlimits_from, + reactivepowerlimits_to = reactivepowerlimits_to, + loss = loss + ) + else + rectifier_taplimits = (min=dc_branch.rectifier_tap_limits_min, + max=dc_branch.rectifier_tap_limits_max) + rectifier_xrc = dc_branch.rectifier_xrc #TODO: What is this?, + rectifier_firingangle = dc_branch.rectifier_firingangle + inverter_taplimits = (min=dc_branch.inverter_tap_limits_min, + max=dc_branch.inverter_tap_limits_max) + inverter_xrc = dc_branch.inverter_xrc #TODO: What is this? + inverter_firingangle = (min=dc_branch.inverter_firing_angle_min, + max=dc_branch.inverter_firing_angle_max) + value = VSCDCLine( + name = dc_branch.name, + available=true, + activepower_flow = pf, + arc = connection_points, + rectifier_taplimits = rectifier_taplimits, + rectifier_xrc = rectifier_xrc, + rectifier_firingangle = rectifier_firingangle, + inverter_taplimits = inverter_taplimits, + inverter_xrc = inverter_xrc, + inverter_firingangle = inverter_firingangle, + ) + end + + add_component!(sys, value) + end +end + +""" + gen_csv_parser!(sys::System, data::PowerSystemTableData) + +Add generators to the System from the raw data. + +""" +function gen_csv_parser!(sys::System, data::PowerSystemTableData) + output_percent_fields = Vector{Symbol}() + heat_rate_fields = Vector{Symbol}() + fields = get_user_fields(data, GENERATOR::InputCategory) + for field in fields + if occursin("output_percent", field) + push!(output_percent_fields, Symbol(field)) + elseif occursin("heat_rate_avg", field) + push!(heat_rate_fields, Symbol(field)) + end + end + + @assert length(output_percent_fields) > 0 + cost_colnames = zip(heat_rate_fields, output_percent_fields) + + for gen in iterate_rows(data, GENERATOR::InputCategory) + bus = get_bus(sys, gen.bus_id) + if isnothing(bus) + throw(DataFormatError("could not find $(gen.bus_id)")) + end + + generator = make_generator(data, gen, cost_colnames, bus) + if !isnothing(generator) + add_component!(sys, generator) + end + end +end + +""" + load_csv_parser!(sys::System, data::PowerSystemTableData) + +Add loads to the System from the raw data. + +""" +function load_csv_parser!(sys::System, data::PowerSystemTableData) + for ps_bus in get_components(Bus, sys) + max_active_power = 0.0 + max_reactive_power = 0.0 + active_power = 0.0 + reactive_power = 0.0 + found = false + for bus in iterate_rows(data, BUS::InputCategory) + if bus.bus_id == ps_bus.number + max_active_power = bus.max_active_power + max_reactive_power = bus.max_reactive_power + active_power = get(bus, :active_power, max_active_power) + reactive_power = get(bus, :reactive_power, max_reactive_power) + found = true + break + end + end + + if !found + throw(DataFormatError("Did not find bus index in Load data $(ps_bus.name)")) + end + + + load = PowerLoad( + name = ps_bus.name, + available = true, + bus = ps_bus, + model = ConstantPower::LoadModel, + activepower = active_power, + reactivepower = reactive_power, + maxactivepower = max_active_power, + maxreactivepower = max_reactive_power) + add_component!(sys, load) + end +end + +""" + loadzone_csv_parser!(sys::System, data::PowerSystemTableData) + +Add branches to the System from the raw data. + +""" +function loadzone_csv_parser!(sys::System, data::PowerSystemTableData) + area_column = get_user_field(data, BUS::InputCategory, "area") + if !in(area_column, names(data.bus)) + @warn "Missing Data : no 'area' information for buses, cannot create loads based " + "on areas" + return + end + + values = unique(data.bus[!, area_column]) + lbs = zip(values, [sum(data.bus[!, area_column] .== a) for a in values]) + for (zone, count) in lbs + bus_numbers = Set{Int}() + active_powers = Vector{Float64}() + reactive_powers = Vector{Float64}() + for bus in iterate_rows(data, BUS::InputCategory) + if bus.area == zone + bus_number = bus.bus_id + push!(bus_numbers, bus_number) + + active_power = bus.max_active_power + push!(active_powers, active_power) + + reactive_power = bus.max_reactive_power + push!(reactive_powers, reactive_power) + end + end + + buses = get_buses(sys, bus_numbers) + name = string(zone) + load_zones = LoadZones(zone, name, buses, sum(active_powers), sum(reactive_powers)) + add_component!(sys, load_zones) + end +end + +""" + services_csv_parser!(sys::System, data::PowerSystemTableData) + +Add services to the System from the raw data. + +""" +function services_csv_parser!(sys::System, data::PowerSystemTableData) + bus_id_column = get_user_field(data, BUS::InputCategory, "bus_id") + bus_area_column = get_user_field(data, BUS::InputCategory, "area") + + # Shortcut for data that looks like "(val1,val2,val3)" + make_array(x) = split(strip(x, ['(', ')']), ",") + + for reserve in iterate_rows(data, RESERVES::InputCategory) + device_categories = make_array(reserve.eligible_device_categories) + device_subcategories = make_array(reserve.eligible_device_subcategories) + regions = make_array(reserve.eligible_regions) + contributing_devices = Vector{Device}() + + for gen in iterate_rows(data, GENERATOR::InputCategory) + bus_ids = data.bus[!, bus_id_column] + area = string(data.bus[bus_ids .== gen.bus_id, bus_area_column][1]) + if gen.category in device_subcategories && area in regions + for dev_category in device_categories + component_type = _get_component_type_from_category(dev_category) + components = get_components_by_name(component_type, sys, gen.name) + if length(components) == 0 + # There multiple categories, so we might not find a match in some. + continue + elseif length(components) == 1 + component = components[1] + else + msg = "Found duplicate names type=$component_type name=$name" + throw(DataFormatError(msg)) + end + + push!(contributing_devices, component) + end + end + end + + if length(contributing_devices) == 0 + throw(DataFormatError( + "did not find contributing devices for service $(reserve.name)" + )) + end + + service = ProportionalReserve(reserve.name, + contributing_devices, + reserve.timeframe) + add_component!(sys, service) + end +end + +"""Creates a generator of any type.""" +function make_generator(data::PowerSystemTableData, gen, cost_colnames, bus) + generator = nothing + gen_type = get_generator_type(gen.fuel, gen.unit_type, data.generator_mapping) + + if gen_type == ThermalStandard + generator = make_thermal_generator(data, gen, cost_colnames, bus) + elseif gen_type == HydroDispatch + generator = make_hydro_generator(data, gen, bus) + elseif gen_type <: RenewableGen + generator = make_renewable_generator(gen_type, data, gen, bus) + elseif gen_type == GenericBattery + generator = make_storage(data, gen, bus) + else + @error "Skipping unsupported generator" gen_type + end + + return generator +end + +function make_thermal_generator(data::PowerSystemTableData, gen, cost_colnames, bus) + fuel_cost = gen.fuel_price / 1000 + + var_cost = [(getfield(gen, hr), getfield(gen, mw)) for (hr, mw) in cost_colnames] + var_cost = [(c[1], c[2]) for c in var_cost if !in(nothing, c)] + if length(unique(var_cost)) > 1 + var_cost[2:end] = [(var_cost[i][1] * (var_cost[i][2] - var_cost[i-1][2]) * fuel_cost * data.basepower, + var_cost[i][2]) .* gen.active_power_limits_max + for i in 2:length(var_cost)] + var_cost[1] = (var_cost[1][1] * var_cost[1][2] * fuel_cost * data.basepower, var_cost[1][2]) .* + gen.active_power_limits_max + + fixed = min(0.0, var_cost[1][1] - (var_cost[2][1] / (var_cost[2][2] - var_cost[1][2]) * var_cost[1][2])) + var_cost[1] = (var_cost[1][1] - fixed, var_cost[1][2]) + for i in 2:length(var_cost) + var_cost[i] = (var_cost[i - 1][1] + var_cost[i][1], var_cost[i][2]) + end + else + var_cost = [(0.0, var_cost[1][2]), (1.0, var_cost[1][2])] + fixed = 0.0 + end + + available = true + rating = sqrt(gen.active_power_limits_max^2 + gen.reactive_power_limits_max^2) + active_power_limits = (min=gen.active_power_limits_min, + max=gen.active_power_limits_max) + reactive_power_limits = (min=gen.reactive_power_limits_min, + max=gen.reactive_power_limits_max) + tech = TechThermal( + rating = rating, + primemover = convert(PrimeMovers, gen.unit_type), + fuel = convert(ThermalFuels, gen.fuel), + activepowerlimits = active_power_limits, + reactivepowerlimits = reactive_power_limits, + ramplimits = (up=gen.ramp_limits, down=gen.ramp_limits), + timelimits = (up=gen.min_up_time, down=gen.min_down_time), + ) + + capacity = gen.active_power_limits_max + startup_cost = gen.startup_heat_cold_cost * fuel_cost * 1000 + shutdown_cost = 0.0 + op_cost = ThreePartCost( + var_cost, + fixed, + startup_cost, + shutdown_cost + ) + + return ThermalStandard(gen.name, + available, + bus, + gen.active_power, + gen.reactive_power, + tech, + op_cost) +end + +function make_hydro_generator(data::PowerSystemTableData, gen, bus) + available = true + + rating = calculate_rating(gen.active_power_limits_max, gen.reactive_power_limits_max) + active_power_limits = (min=gen.active_power_limits_min, + max=gen.active_power_limits_max) + reactive_power_limits = (min=gen.reactive_power_limits_min, + max=gen.reactive_power_limits_max) + tech = TechHydro( + rating, + convert(PrimeMovers, gen.unit_type), + active_power_limits, + reactive_power_limits, + (up=gen.ramp_limits, down=gen.ramp_limits), + (up=gen.min_down_time, down=gen.min_down_time), + ) + + curtailcost = 0.0 + return HydroDispatch(name = gen.name, + available = available, + bus = bus, + activepower = gen.active_power, + reactivepower = gen.reactive_power, + tech = tech, + op_cost = TwoPartCost(curtailcost, 0.0)) +end + +function make_renewable_generator(gen_type, data::PowerSystemTableData, gen, bus) + generator = nothing + available = true + rating = gen.active_power_limits_max + + tech = TechRenewable(rating, + convert(PrimeMovers, gen.unit_type), + (min=gen.reactive_power_limits_min, + max=gen.reactive_power_limits_max), + 1.0) + if gen_type == RenewableDispatch + generator = RenewableDispatch( + gen.name, + available, + bus, + gen.active_power, + gen.reactive_power, + tech, + TwoPartCost(0.0, 0.0), + ) + elseif gen_type == RenewableFix + generator = RenewableFix( + gen.name, + available, + bus, + gen.active_power, + gen.reactive_power, + tech) + else + error("Unsupported type $gen_type") + end + + return generator +end + +function make_storage(data::PowerSystemTableData, gen, bus) + available = true + energy = 0.0 + capacity = (min=gen.active_power_limits_min, + max=gen.active_power_limits_max) + rating = gen.active_power_limits_max + input_active_power_limits = (min=0.0, max=gen.active_power_limits_max) + output_active_power_limits = (min=0.0, max=gen.active_power_limits_max) + efficiency = (in=0.9, out=0.9) + reactive_power_limits = (min=0.0, max=0.0) + + battery=GenericBattery( + name = gen.name, + available = available, + bus = bus, + primemover = convert(PrimeMovers, gen.unit_type), + energy = energy, + capacity = capacity, + rating = rating, + activepower = gen.active_power, + inputactivepowerlimits = input_active_power_limits, + outputactivepowerlimits = output_active_power_limits, + efficiency = efficiency, + reactivepower = gen.reactive_power, + reactivepowerlimits = reactive_power_limits + ) + + return battery +end + + +const CATEGORY_STR_TO_COMPONENT = Dict{String, DataType}( + "Bus" => Bus, + "Generator" => Generator, + "Reserve" => Service, + "LoadZone" => LoadZones, + "ElectricLoad" => ElectricLoad, +) + +function _get_component_type_from_category(category::AbstractString) + component_type = get(CATEGORY_STR_TO_COMPONENT, category, nothing) + if isnothing(component_type) + throw(DataFormatError("unsupported category=$category")) + end + + return component_type +end + +function _read_config_file(file_path::String) + return open(file_path) do io + data = YAML.load(io) + # Replace keys with enums. + config_data = Dict{InputCategory, Vector}() + for (key, val) in data + config_data[get_enum_value(InputCategory, key)] = val + end + return config_data + end +end + +"""Stores user-customized information for required dataframe columns.""" +struct _FieldInfo + name::String + custom_name::Symbol + needs_per_unit_conversion::Bool + unit_conversion::Union{NamedTuple{(:From,:To),Tuple{String,String}},Nothing} + # TODO unit, value ranges and options +end + +function _get_field_infos(data::PowerSystemTableData, category::InputCategory, df_names) + if !haskey(data.user_descriptors, category) + throw(DataFormatError("Invalid category=$category")) + end + + if !haskey(data.descriptors, category) + throw(DataFormatError("Invalid category=$category")) + end + + # Cache whether PowerSystems uses a column's values as system-per-unit. + # The user's descriptors indicate that the raw data is already system-per-unit or not. + per_unit = Dict{String, Bool}() + unit = Dict{String,Union{String,Nothing}}() + for descriptor in data.descriptors[category] + per_unit[descriptor["name"]] = get(descriptor, "system_per_unit", false) + unit[descriptor["name"]] = get(descriptor, "unit", nothing) + end + + fields = Vector{_FieldInfo}() + try + for item in data.user_descriptors[category] + custom_name = Symbol(item["custom_name"]) + name = item["name"] + if custom_name in df_names + if !per_unit[name] && get(item, "system_per_unit", false) + throw(DataFormatError("$name cannot be defined as system_per_unit")) + end + + needs_pu_conversion = per_unit[name] && + haskey(item, "system_per_unit") && + !item["system_per_unit"] + + custom_unit = get(item, "unit", nothing) + if !isnothing(unit[name]) && !isnothing(custom_unit) && custom_unit != unit[name] + unit_conversion = (From=custom_unit, To=unit[name]) + else + unit_conversion = nothing + end + + push!(fields, _FieldInfo(name, custom_name, needs_pu_conversion, unit_conversion)) + else + # TODO: This should probably be a fatal error. However, the parsing code + # doesn't use all the descriptor fields, so skip for now. + @warn "User-defined column name $custom_name is not in dataframe." + end + end + return fields + catch(err) + if err == KeyError + msg = "Failed to find category=$category field=$field in input descriptors $err" + throw(DataFormatError(msg)) + else + throw(err) + end + end + + msg = "Failed to find category=$category field=$field in input descriptors" + throw(DataFormatError(msg)) +end + +"""Reads values from dataframe row and performs necessary conversions.""" +function _read_data_row(data::PowerSystemTableData, row, field_infos; na_to_nothing=true) + fields = Vector{String}() + vals = Vector() + for field_info in field_infos + value = row[field_info.custom_name] + if na_to_nothing && value == "NA" + value = nothing + end + + if field_info.needs_per_unit_conversion + @debug "convert to system_per_unit" field_info.custom_name + value /= data.basepower + end + + # TODO: need special handling for units + if !isnothing(field_info.unit_conversion) + @debug "convert units" field_info.custom_name + value = convert_units!(value, field_info.unit_conversion) + end + # TODO: validate ranges and option lists + + push!(fields, field_info.name) + push!(vals, value) + end + + return NamedTuple{Tuple(Symbol.(fields))}(vals) +end diff --git a/src/parsers/standardfiles_parser.jl b/src/parsers/standardfiles_parser.jl index 7bb4b2e354..20df579b1c 100644 --- a/src/parsers/standardfiles_parser.jl +++ b/src/parsers/standardfiles_parser.jl @@ -1,9 +1,8 @@ -# TODO: assert a naming convention -- ?? """ -Read in power-system parameters from a Matpower, PTI, or JSON file and do some -data checks. +Create a System by parsing power-system parameters from a Matpower, PTI, or JSON file and do +some data checks. """ -function parsestandardfiles(file::String; kwargs...) +function parse_standard_files(file::String; kwargs...)::System # function `parse_file` is in pm_io/common.jl data = parse_file(file) @@ -13,23 +12,10 @@ function parsestandardfiles(file::String; kwargs...) @error "There are no buses in this file" end - # in pm2ps_parser.jl - data = pm2ps_dict(data; kwargs...) - - return data - -end - -function parsestandardfiles(file::String, ts_folder::String; kwargs...) - - # TODO: assert a naming convention - data = parsestandardfiles(file; kwargs...) - - ts_data = read_data_files(ts_folder; kwargs...) + # in pm2ps_parser.jl + sys = pm2ps_dict(data; kwargs...) - # assign_ts_data is in forecast_parser.jl - data = assign_ts_data(data, ts_data) + return sys - return data end diff --git a/src/utils/IO/base_checks.jl b/src/utils/IO/base_checks.jl index 3e7994e048..e9568cf809 100644 --- a/src/utils/IO/base_checks.jl +++ b/src/utils/IO/base_checks.jl @@ -21,7 +21,7 @@ function getresolution(ts::TimeSeries.TimeArray) res = [] for timediff in timediffs - if mod(timediff,Dates.Millisecond(Dates.Day(1))) == Dates.Millisecond(0) + if mod(timediff,Dates.Millisecond(Dates.Day(1))) == Dates.Millisecond(0) push!(res,Dates.Day(timediff/Dates.Millisecond(Dates.Day(1)))) elseif mod(timediff,Dates.Millisecond(Dates.Hour(1))) == Dates.Millisecond(0) push!(res,Dates.Hour(timediff/Dates.Millisecond(Dates.Hour(1)))) @@ -35,8 +35,9 @@ function getresolution(ts::TimeSeries.TimeArray) end if length(res) > 1 - throw(DataFormatError("timeseries has non-uniform resolution: this is currently " \ - "not supported")) + throw(DataFormatError( + "timeseries has non-uniform resolution: this is currently not supported") + ) end return res[1] @@ -52,3 +53,24 @@ function check_ascending_order(array::Array{Int}, name::AbstractString) return end + +"""Checks if a PowerSystemDevice has a field or subfield name.""" +function isafield(component::Component, field::Symbol) + + function _wrap(t,d=[]) + fn = fieldnames(typeof(t)) + for n in fn + push!(d,n) + f = getfield(t,n) + if length(fieldnames(typeof(f))) > 0 + _wrap(f,d) + end + end + return d + end + + allfields = _wrap(component) + return field in allfields +end + + diff --git a/src/utils/IO/branchdata_checks.jl b/src/utils/IO/branchdata_checks.jl index f719e38926..30946dc9f0 100644 --- a/src/utils/IO/branchdata_checks.jl +++ b/src/utils/IO/branchdata_checks.jl @@ -1,46 +1,53 @@ -function check_branches!(branches::Array{<:Branch,1}) - checkanglelimits!(branches) - check_ascending_order([b.connectionpoints.from.number for b in branches], "Branch") +function validate_struct(sys::System, ps_struct::Union{MonitoredLine, Line}) + is_valid = true + if !check_endpoint_voltages(ps_struct) + is_valid = false + else + check_angle_limits!(ps_struct) + if !calculate_thermal_limits!(ps_struct, sys.basepower) + is_valid = false + end + end + return is_valid end -# function check_angle_limits(anglelimits::(max::Float64, min::Float64)) -function checkanglelimits!(branches::Array{<:Branch,1}) - for (ix,l) in enumerate(branches) - if isa(l,Union{MonitoredLine,Line}) - orderedlimits(l.anglelimits, "Angles") - - hist = (false,l.anglelimits) - new_max = 1.57 - new_min = -1.57 - - if (l.anglelimits.max/1.57 > 3) | (-1*l.anglelimits.min/1.57 > 3) - - @warn "The angle limits provided is larger than 3π/2 radians.\n PowerSystems inferred the data provided in degrees and will transform it to radians" maxlog=PS_MAX_LOG - - (l.anglelimits.max/1.57 >= 0.99) ? new_max = min(l.anglelimits.max*(π/180),new_max) : new_max = min(l.anglelimits.max,new_max) - - (-1*l.anglelimits.min/1.57 > 0.99) ? new_min = max(l.anglelimits.min*(π/180),new_min) : new_min = max(l.anglelimits.min,new_min) - +function check_angle_limits!(line) + max_limit = pi/2 + min_limit = -pi/2 - hist = (true,(min = new_min, max = new_max)) + orderedlimits(line.anglelimits, "Angles") - else + if (line.anglelimits.max / max_limit > 3) || + (-1 * line.anglelimits.min / max_limit > 3) + @warn "The angle limits provided is larger than 3π/2 radians.\n " * + "PowerSystems inferred the data provided in degrees and will transform it to radians" maxlog=PS_MAX_LOG - (l.anglelimits.max >= 1.57 && l.anglelimits.min <= -1.57) ? hist = (true,(min = -1.57,max = 1.57)) : true - (l.anglelimits.max >= 1.57 && l.anglelimits.min >= -1.57) ? hist =(true, (min = l.anglelimits.min,max = 1.57)) : true - (l.anglelimits.max <= 1.57 && l.anglelimits.min <= -1.57) ? hist = (true,(min = -1.57,max = l.anglelimits.max)) : true - (l.anglelimits.max == 0.0 && l.anglelimits.min == 0.0) ? hist = (true,(min = -1.57,max = 1.57)) : true - - end + if line.anglelimits.max / max_limit >= 0.99 + line.anglelimits = (min=line.anglelimits.min, + max=min(line.anglelimits.max * (π / 180), max_limit)) + else + line.anglelimits = (min=line.anglelimits.min, + max=min(line.anglelimits.max, max_limit)) + end - if hist[1] + if (-1 * line.anglelimits.min / max_limit > 0.99) + line.anglelimits = (min=max(line.anglelimits.min * (π / 180), min_limit), + max=line.anglelimits.max) + else + line.anglelimits = (min=max(line.anglelimits.min, min_limit), + max=line.anglelimits.max) + end + else - branches[ix] = Line(deepcopy(l.name),deepcopy(l.available), - deepcopy(l.connectionpoints),deepcopy(l.r), - deepcopy(l.x),deepcopy(l.b),deepcopy(l.rate), - hist[2]) - end + if line.anglelimits.max >= max_limit && line.anglelimits.min <= min_limit + line.anglelimits = (min = min_limit,max = max_limit) + elseif line.anglelimits.max >= max_limit && line.anglelimits.min >= min_limit + line.anglelimits = (min=line.anglelimits.min, max=max_limit) + elseif line.anglelimits.max <= max_limit && line.anglelimits.min <= min_limit + line.anglelimits = (min=min_limit, max=line.anglelimits.max) + elseif line.anglelimits.max == 0.0 && line.anglelimits.min == 0.0 + line.anglelimits = (min = min_limit,max = max_limit) end end end @@ -50,18 +57,18 @@ function linerate_calculation(l::Line) g = l.r / (l.r^2 + l.x^2) b = -l.x / (l.r^2 + l.x^2) y_mag = sqrt(g^2 + b^2) - fr_vmax = l.connectionpoints.from.voltagelimits.max - to_vmax = l.connectionpoints.to.voltagelimits.max + fr_vmax = l.arc.from.voltagelimits.max + to_vmax = l.arc.to.voltagelimits.max if isa(fr_vmax,Nothing) || isa(to_vmax,Nothing) fr_vmax = 1.0 to_vmax = 0.9 - diff_angle = abs(l.connectionpoints.from.angle -l.connectionpoints.to.angle) - new_rate = y_mag*fr_vmax*to_vmax*cos(theta_max) + diff_angle = abs(l.arc.from.angle - l.arc.to.angle) + new_rate = y_mag * fr_vmax * to_vmax * cos(theta_max) else m_vmax = max(fr_vmax, to_vmax) - c_max = sqrt(fr_vmax^2 + to_vmax^2 - 2*fr_vmax*to_vmax*cos(theta_max)) + c_max = sqrt(fr_vmax^2 + to_vmax^2 - 2 * fr_vmax*to_vmax*cos(theta_max)) new_rate = y_mag*m_vmax*c_max end @@ -70,33 +77,82 @@ function linerate_calculation(l::Line) end -function calculatethermallimits!(branches::Array{<:Branch,1},basemva::Float64) - for (ix,l) in enumerate(branches) - - if isa(l,Line) - - flag = false - - #This is the same check as implemented in PowerModels - if l.rate <= 0.0 - (flag, rate) = (true,linerate_calculation(l)) - elseif l.rate > linerate_calculation(l) - (flag, rate) = (true,linerate_calculation(l)) - else - rating= l.rate - end - - if (l.rate/basemva) > 20 - @warn "Data for line rating is 20 times larger than the base MVA for the system\n. Power Systems inferred the Data Provided is in MVA and will transform it using a base of $("basemva")" maxlog=PS_MAX_LOG - (flag, rate) = (true,l.rate/basemva) - end - - if flag - branches[ix] = Line(deepcopy(l.name),deepcopy(l.available), - deepcopy(l.connectionpoints),deepcopy(l.r), - deepcopy(l.x),deepcopy(l.b), - rate,deepcopy(l.anglelimits)) - end +function calculate_thermal_limits!(branch, basemva::Float64) + is_valid = true + if get_rate(branch) < 0.0 + @error "PowerSystems does not support negative line rates" + is_valid = false + + elseif get_rate(branch) == 0.0 + @warn "Data for line rating is not provided, PowerSystems will infer a rate from line parameters" maxlog=PS_MAX_LOG + if get_anglelimits(branch) == get_anglelimits(Line(nothing)) + branch.rate = min(calculate_sil(branch, basemva), linerate_calculation(branch)) / basemva + else + branch.rate = linerate_calculation(branch)/basemva end + + elseif get_rate(branch) > linerate_calculation(branch) + mult = get_rate(branch) / linerate_calculation(branch) + if mult > 50 + @warn "Data for line rating is $(mult) times larger than the base MVA for the system" maxlog=PS_MAX_LOG + end + end + + check_SIL(branch, basemva) + + return is_valid +end + +const SIL_STANDARDS = Dict( #from https://neos-guide.org/sites/default/files/line_flow_approximation.pdf + 69.0 => (min=12.0, max=13.0), + 138.0 => (min=47.0, max=52.0), + 230.0 => (min=134.0, max=145.0), + 345.0 => (min=325.0, max=425.0), + 500.0 => (min=850.0, max=1075.0), + 765.0 => (min=2200.0, max=2300.0)) + + +# calculation from https://neos-guide.org/sites/default/files/line_flow_approximation.pdf +function calculate_sil(line, basemva::Float64) + arc = get_arc(line) + vrated = (get_to(arc) |> get_basevoltage) + + zbase = vrated^2 / basemva + l = get_x(line) / (2 * pi * 60) * zbase + r = get_r(line) * zbase + c = sum(get_b(line)) / (2 * pi * 60 * zbase) + zc = sqrt((r + im * 2 * pi * 60 * l) / (im * 2 * pi * 60 * c)) + sil = vrated^2 / abs(zc) + return sil + +end + +function check_SIL(line, basemva::Float64) + + arc = get_arc(line) + vrated = (get_to(arc) |> get_basevoltage) + + SIL_levels = collect(keys(SIL_STANDARDS)) + rate = get_rate(line) + closestV = findmin(abs.(SIL_levels.-vrated)) + closestSIL = SIL_STANDARDS[SIL_levels[closestV[2]]] + + #Consisten with Ned Mohan Electric Power Systems: A First Course page 70 + if !(rate >= 3*closestSIL.max / vrated) + # rate outside of expected SIL range + sil = calculate_sil(line, basemva) + mult = sil/closestSIL.max + @warn "Rate provided for $(line) is $(rate*vrated), $(mult) times larger the expected SIL $(sil) in the range of $(closestSIL)." maxlog=PS_MAX_LOG + + end +end + +function check_endpoint_voltages(line) + is_valid = true + arc = get_arc(line) + if get_from(arc) |> get_basevoltage != get_to(arc) |> get_basevoltage + is_valid = false + @error "Voltage endpoints of $(line) are different, cannot create Line" end + return is_valid end diff --git a/src/utils/IO/system_checks.jl b/src/utils/IO/system_checks.jl index fc112d9f1f..63dbec6ca0 100644 --- a/src/utils/IO/system_checks.jl +++ b/src/utils/IO/system_checks.jl @@ -1,50 +1,24 @@ ### Utility Functions needed for the construction of the Power System, mostly used for consistency checking #### -## Time Series Length ## - -function timeseriescheckload(loads::Array{T}) where {T<:ElectricLoad} - t = length(loads[1].scalingfactor) - for l in loads - if t == length(l.scalingfactor) - continue - else - @error "Inconsistent load scaling factor time series length" - end - end - return t -end - -function timeserieschecksources(generators::Array{T}, expected) where {T<:Generator} - for g in generators - actual = length(g.scalingfactor) - if expected == actual - continue - else - @error "Inconsistent generation scaling factor time series length for $(g.name)" expected actual - end - end -end - ## Check that all the buses have a type defintion ## -function buscheck(buses::Array{Bus}) +function buscheck(buses) for b in buses - if b.bustype == nothing + if isnothing(b.bustype) @warn "Bus/Nodes data does not contain information to build an a network" end end - - check_ascending_order([x.number for x in buses], "Bus") end ## Slack Bus Definition ## -function slackbuscheck(buses::Array{Bus}) +function slack_bus_check(buses) slack = -9 for b in buses - if b.bustype == "SF" + if b.bustype == REF::BusType slack = b.number + break end end if slack == -9 @@ -52,29 +26,12 @@ function slackbuscheck(buses::Array{Bus}) end end -### PV Bus Check ### - -function pvbuscheck(buses::Array{Bus}, generators::Array{T}) where {T<:Generator} - pv_list = -1*ones(Int64, length(generators)) - for (ix,g) in enumerate(generators) - g.bus.bustype == "PV" ? pv_list[ix] = g.bus.number : continue - end - - for b in buses - if b.bustype == "PV" - b.number in pv_list ? continue : 0 #@warn "The bus ", b.number, " is declared as PV without a generator connected to it" - else - continue - end - end -end - # TODO: Check for islanded Buses # check for minimum timediff -function minimumtimestep(loads::Array{T})where {T<:ElectricLoad} - if length(loads[1].scalingfactor) > 1 - timeseries = loads[1].scalingfactor +function minimumtimestep(forecasts::Array{T})where {T<:Forecast} + if length(forecasts[1].data) > 1 + timeseries = forecasts[1].data n = length(timeseries)-1 ts = [] for i in 1:n @@ -86,102 +43,3 @@ function minimumtimestep(loads::Array{T})where {T<:ElectricLoad} return ts end end - -# convert generator ramp rates to a consistent denominator -function convertramp(ramplimits::Union{NamedTuple{(:up, :down),Tuple{Float64,Float64}},Nothing}, ts::Dates.TimePeriod) - if isa(ramplimits,NamedTuple) - hr = typeof(ts)(Dates.Dates.Minute(1)) - scaling = hr/ts - up = ramplimits.up/scaling - down = ramplimits.down/scaling - R = (up = up, down = down) - return R - else - return ramplimits - end -end - - -# check for valid ramp limits -function checkramp(generators::Array{T}, ts::Dates.TimePeriod) where {T<:Generator} - for (ix,g) in enumerate(generators) - if isa(g,ThermalDispatch) - R = convertramp(g.tech.ramplimits,ts) - generators[ix] = ThermalDispatch(deepcopy(g.name),deepcopy(g.available),deepcopy(g.bus), - TechThermal(deepcopy(g.tech.activepower),deepcopy(g.tech.activepowerlimits), - deepcopy(g.tech.reactivepower),deepcopy(g.tech.reactivepowerlimits), - R,deepcopy(g.tech.timelimits)), - deepcopy(g.econ) - ) - if isa(g.tech.ramplimits, NamedTuple) - if g.tech.ramplimits.up >= (g.tech.activepowerlimits.max - g.tech.activepowerlimits.min) - @warn "The generator $(g.name) has a nonbinding ramp up limit." - end - if g.tech.ramplimits.down >= (g.tech.activepowerlimits.max - g.tech.activepowerlimits.min) - @warn "The generator $(g.name) has a nonbinding ramp down limit." - end - else - @info "Ramp defined as nothing for $(g.name)" - end - elseif isa(g,ThermalGenSeason) - R = convertramp(g.tech.ramplimits,ts) - generators[ix] = ThermalGenSeason(deepcopy(g.name),deepcopy(g.available),deepcopy(g.bus), - TechThermal(deepcopy(g.tech.activepower),deepcopy(g.tech.activepowerlimits), - deepcopy(g.tech.reactivepower),deepcopy(g.tech.reactivepowerlimits), - R,deepcopy(g.tech.timelimits)), - deepcopy(g.econ), - deepcopy(g.scalingfactor) - ) - if isa(g.tech.ramplimits, NamedTuple) - if g.tech.ramplimits.up >= (g.tech.activepowerlimits.max - g.tech.activepowerlimits.min) - @warn "The generator $(g.name) has a nonbinding ramp up limit." - end - if g.tech.ramplimits.down >= (g.tech.activepowerlimits.max - g.tech.activepowerlimits.min) - @warn "The generator $(g.name) has a nonbinding ramp down limit." - end - else - @info "Ramp defined as nothing for $(g.name)" - end - elseif isa(g,HydroCurtailment) - R = convertramp(g.tech.ramplimits,ts) - generators[ix] = HydroCurtailment(deepcopy(g.name),deepcopy(g.available),deepcopy(g.bus), - TechHydro(deepcopy(g.tech.installedcapacity),deepcopy(g.tech.activepower),deepcopy(g.tech.activepowerlimits), - deepcopy(g.tech.reactivepower),deepcopy(g.tech.reactivepowerlimits), - R,deepcopy(g.tech.timelimits)), - deepcopy(g.econ.curtailpenalty), - deepcopy(g.scalingfactor) - ) - if isa(g.tech.ramplimits, NamedTuple) - if g.tech.ramplimits.up >= (g.tech.activepowerlimits.max - g.tech.activepowerlimits.min) - @info "The generator $(g.name) has a nonbinding ramp up limit." maxlog=PS_MAX_LOG - end - if g.tech.ramplimits.down >= (g.tech.activepowerlimits.max - g.tech.activepowerlimits.min) - @info "The generator $(g.name) has a nonbinding ramp down limit." maxlog=PS_MAX_LOG - end - else - @info "Ramp defined as nothing for $(g.name)" - end - elseif isa(g,HydroStorage) - R = convertramp(g.tech.ramplimits,ts) - generators[ix] = HydroStorage(deepcopy(g.name),deepcopy(g.available),deepcopy(g.bus), - TechHydro(deepcopy(g.tech.installedcapacity),deepcopy(g.tech.activepower),deepcopy(g.tech.activepowerlimits), - deepcopy(g.tech.reactivepower),deepcopy(g.tech.reactivepowerlimits), - R,deepcopy(g.tech.timelimits)), - deepcopy(g.econ), - deepcopy(g.storagecapacity), - deepcopy(g.scalingfactor) - ) - if isa(g.tech.ramplimits, NamedTuple) - if g.tech.ramplimits.up >= (g.tech.activepowerlimits.max - g.tech.activepowerlimits.min) - @info "The generator $(g.name) has a nonbinding ramp up limit." maxlog=PS_MAX_LOG - end - if g.tech.ramplimits.down >= (g.tech.activepowerlimits.max - g.tech.activepowerlimits.min) - @info "The generator $(g.name) has a nonbinding ramp down limit." maxlog=PS_MAX_LOG - end - else - @info "Ramp defined as nothing for $(g.name)" - end - end - end - return generators -end diff --git a/src/utils/lodf_calculations.jl b/src/utils/lodf_calculations.jl deleted file mode 100644 index 7b2932d487..0000000000 --- a/src/utils/lodf_calculations.jl +++ /dev/null @@ -1,16 +0,0 @@ -function buildlodf(branches::Array{T}, nodes::Array{Bus}, dist_slack::Array{Float64}=[0.1] ) where {T<:Branch} - linecount = length(branches) - ptdf , a = PowerSystems.buildptdf(branches,nodes,dist_slack) - ptdf = ptdf.data; a = a.data; - H = gemm('N','N',ptdf,a) - ptdf_denominator = H; - for iline = 1:linecount - if (1.0 - ptdf_denominator[iline,iline] ) < 1.0E-06 - ptdf_denominator[iline,iline] = 0.0; - end - end - (Dem, dipiv, dinfo) = getrf!(Matrix{Float64}(LinearAlgebra.I,linecount,linecount) - Array(LinearAlgebra.Diagonal(ptdf_denominator))) - LODF = gemm('N','N',H,getri!(Dem,dipiv)) - LODF = LODF - Array(LinearAlgebra.Diagonal(LODF)) - Matrix{Float64}(LinearAlgebra.I,linecount,linecount) - return LODF -end \ No newline at end of file diff --git a/src/utils/logging.jl b/src/utils/logging.jl deleted file mode 100644 index 5d15638c02..0000000000 --- a/src/utils/logging.jl +++ /dev/null @@ -1,276 +0,0 @@ - -import Logging - -export configure_logging -export open_file_logger -export MultiLogger -export LogEvent -export LogEventTracker -export report_log_summary -export get_log_events - - -""" - configure_logging([console, console_stream, console_level, - file, filename, file_level, file_mode, - tracker, set_global]) - -Creates console and file loggers per caller specification and returns a MultiLogger. - -**Note:** If logging to a file users must call Base.close() on the returned MultiLogger to -ensure that all events get flushed. - -# Arguments -- `console::Bool=true`: create console logger -- `console_stream::IOStream=stderr`: stream for console logger -- `console_level::Logging.LogLevel=Logging.Error`: level for console messages -- `file::Bool=true`: create file logger -- `filename::String=log.txt`: log file -- `file_level::Logging.LogLevel=Logging.Info`: level for file messages -- `file_mode::String=w+`: mode used when opening log file -- `tracker::Union{LogEventTracker, Nothing}=LogEventTracker()`: optionally track log events -- `set_global::Bool=true`: set the created logger as the global logger - -# Example -```julia -logger = configure_logging(filename="mylog.txt") -``` -""" -function configure_logging(; - console=true, - console_stream=stderr, - console_level=Logging.Error, - file=true, - filename="log.txt", - file_level=Logging.Info, - file_mode="w+", - tracker=LogEventTracker(), - set_global=true - )::MultiLogger - if !console && !file - error("At least one of console or file must be true") - end - - loggers = Array{Logging.AbstractLogger, 1}() - if console - console_logger = Logging.ConsoleLogger(console_stream, console_level) - push!(loggers, console_logger) - end - - if file - io = open(filename, file_mode) - try - file_logger = Logging.SimpleLogger(io, file_level) - push!(loggers, file_logger) - catch - close(io) - rethrow() - end - end - - logger = MultiLogger(loggers, tracker) - if set_global - Logging.global_logger(logger) - end - - return logger -end - -""" - open_file_logger(func, filename[, level, mode]) - -Opens a file logger using Logging.SimpleLogger. - -# Example -```julia -open_file_logger("log.txt", Logging.Info) do logger - global_logger(logger) - @info "hello world" -end -``` -""" -function open_file_logger(func::Function, filename::String, level=Logging.Info, mode="w+") - stream = open(filename, mode) - try - logger = Logging.SimpleLogger(stream, level) - func(logger) - finally - close(stream) - end -end - -"""Contains information describing a log event.""" -mutable struct LogEvent - file::String - line::Int - id::Symbol - message::String - level::Logging.LogLevel - count::Int - suppressed::Int -end - -LogEvent(file, line, id, message, level) = LogEvent(file, line, id, message, level, 1, 0) - -struct LogEventTracker - events::Dict{Logging.LogLevel, Dict{Symbol, LogEvent}} - - # Defining an inner constructor to prohibit creation of a default constructor that - # takes a parameter of type Any. The outer constructor below causes an overwrite of - # that method, which results in a warning message from Julia. - LogEventTracker(events::Dict{Logging.LogLevel, Dict{Symbol, LogEvent}}) = new(events) -end - -""" - LogEventTracker(Tuple{Logging.LogLevel}) - -Tracks counts of all log events by level. - -# Examples -```julia -LogEventTracker() -LogEventTracker((Logging.Info, Logging.Warn, Logging.Error)) -``` -""" -function LogEventTracker(levels=(Logging.Info, Logging.Warn, Logging.Error)) - return LogEventTracker(Dict(l => Dict{Symbol, LogEvent}() for l in levels)) -end - -"""Returns a summary of log event counts by level.""" -function report_log_summary(tracker::LogEventTracker)::String - text = "\nLog message summary:\n" - # Order by criticality. - for level in sort!(collect(keys(tracker.events)), rev=true) - num_events = length(tracker.events[level]) - text *= "\n$num_events $level events:\n" - for event in sort!(collect(get_log_events(tracker, level)), by=x->x.count, rev=true) - text *= " count=$(event.count) at $(event.file):$(event.line)\n" - text *= " example message=\"$(event.message)\"\n" - if event.suppressed > 0 - text *= " suppressed=$(event.suppressed)\n" - end - end - end - - return text -end - -"""Returns an iterable of log events for a level.""" -function get_log_events(tracker::LogEventTracker, level::Logging.LogLevel) - if !_is_level_valid(tracker, level) - return [] - end - - return values(tracker.events[level]) -end - -"""Increments the count of a log event.""" -function increment_count(tracker::LogEventTracker, event::LogEvent, suppressed::Bool) - if _is_level_valid(tracker, event.level) - if haskey(tracker.events[event.level], event.id) - tracker.events[event.level][event.id].count += 1 - if suppressed - tracker.events[event.level][event.id].suppressed += 1 - end - else - tracker.events[event.level][event.id] = event - end - end -end - -function _is_level_valid(tracker::LogEventTracker, level::Logging.LogLevel) - return level in keys(tracker.events) -end - -""" - MultiLogger(Array{AbstractLogger}, Union{LogEventTracker, Nothing}) - -Redirects log events to multiple loggers. The primary use case is to allow logging to -both a file and the console. Secondarily, it can track the counts of all log messages. - -# Example -```julia -MultiLogger([ConsoleLogger(stderr), SimpleLogger(stream)], LogEventTracker()) -``` -""" -mutable struct MultiLogger <: Logging.AbstractLogger - loggers::Array{Logging.AbstractLogger} - tracker::Union{LogEventTracker, Nothing} -end - -""" -Creates a MultiLogger with no event tracking. - -# Example -```julia -MultiLogger([ConsoleLogger(stderr), SimpleLogger(stream)]) -``` -""" -function MultiLogger(loggers::Array{T}) where T <: Logging.AbstractLogger - return MultiLogger(loggers, nothing) -end - -Logging.shouldlog(logger::MultiLogger, level, _module, group, id) = true - -function Logging.min_enabled_level(logger::MultiLogger) - return minimum([Logging.min_enabled_level(x) for x in logger.loggers]) -end - -Logging.catch_exceptions(logger::MultiLogger) = false - -function Logging.handle_message(logger::MultiLogger, - level, - message, - _module, - group, - id, - file, - line; - maxlog=nothing, - kwargs...) - suppressed = false - for logger_ in logger.loggers - if level >= Logging.min_enabled_level(logger_) - if Logging.shouldlog(logger_, level, _module, group, id) - Logging.handle_message(logger_, level, message, _module, group, id, file, - line; maxlog=maxlog, kwargs...) - else - suppressed = true - end - end - end - - if logger.tracker != nothing - event = LogEvent(file, line, id, message, level) - increment_count(logger.tracker, event, suppressed) - end - - return -end - -"""Returns a summary of log event counts by level.""" -function report_log_summary(logger::MultiLogger)::String - if logger.tracker == nothing - error("log event tracking is not enabled") - end - - return report_log_summary(logger.tracker) -end - -"""Flush any file streams.""" -function Base.flush(logger::MultiLogger) - for logger_ in logger.loggers - if isa(logger_, Logging.SimpleLogger) - flush(logger_.stream) - end - end -end - -"""Ensures that any file streams are flushed and closed.""" -function Base.close(logger::MultiLogger) - for logger_ in logger.loggers - if isa(logger_, Logging.SimpleLogger) - close(logger_.stream) - end - end -end diff --git a/src/utils/network_calculations/common.jl b/src/utils/network_calculations/common.jl new file mode 100644 index 0000000000..7a4197b532 --- /dev/null +++ b/src/utils/network_calculations/common.jl @@ -0,0 +1,229 @@ + +abstract type PowerNetworkMatrix{T} <: AbstractArray{T,2} end + + +# The container code for PowerNetworkMatrix is based in JuMP's Container in order to +# remove the limitations of AxisArrays and the doubts about long term maintenance +# https://github.com/JuliaOpt/JuMP.jl/blob/master/src/Containers/DenseAxisArray.jl +# JuMP'sCopyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. + +function _make_ax_ref(ax::Vector) + ref = Dict{eltype(ax),Int}() + for (ix,el) in enumerate(ax) + if haskey(ref, el) + @error("Repeated index element $el. Index sets must have unique elements.") + end + ref[el] = ix + end + return ref +end + +# AbstractArray interface +Base.isempty(A::PowerNetworkMatrix) = isempty(A.data) +Base.size(A::PowerNetworkMatrix) = size(A.data) +Base.LinearIndices(A::PowerNetworkMatrix) = error("PowerSystems.$(typeof(A)) does not support this operation.") +Base.axes(A::PowerNetworkMatrix) = A.axes +Base.CartesianIndices(A::PowerNetworkMatrix) = error("PowerSystems.$(typeof(A)) does not support this operation.") + + +############ +# Indexing # +############ + +Base.eachindex(A::PowerNetworkMatrix) = CartesianIndices(size(A.data)) + +function lookup_index(i, lookup::Dict) + return isa(i, Colon) ? Colon() : lookup[i] +end + +function lookup_index(i::Component, lookup::Dict) + return isa(i, Colon) ? Colon() : lookup[Base.to_index(i)] +end + +# Lisp-y tuple recursion trick to handle indexing in a nice type- +# stable way. The idea here is that `_to_index_tuple(idx, lookup)` +# performs a lookup on the first element of `idx` and `lookup`, +# then recurses using the remaining elements of both tuples. +# The compiler knows the lengths and types of each tuple, so +# all of the types are inferable. +function _to_index_tuple(idx::Tuple, lookup::Tuple) + tuple(lookup_index(first(idx), first(lookup)), + _to_index_tuple(Base.tail(idx), Base.tail(lookup))...) +end + +# Handle the base case when we have more indices than lookups: +function _to_index_tuple(idx::NTuple{N}, ::NTuple{0}) where {N} + ntuple(k -> begin + i = idx[k] + (i == 1) ? 1 : error("invalid index $i") + end, Val(N)) +end + +# Handle the base case when we have fewer indices than lookups: +_to_index_tuple(idx::NTuple{0}, lookup::Tuple) = () + +# Resolve ambiguity with the above two base cases +_to_index_tuple(idx::NTuple{0}, lookup::NTuple{0}) = () +to_index(A::PowerNetworkMatrix, idx...) = _to_index_tuple(idx, A.lookup) + +# Doing `Colon() in idx` is relatively slow because it involves +# a non-unrolled loop through the `idx` tuple which may be of +# varying element type. Another lisp-y recursion trick fixes that +has_colon(idx::Tuple{}) = false +has_colon(idx::Tuple) = isa(first(idx), Colon) || has_colon(Base.tail(idx)) + +# TODO: better error (or just handle correctly) when user tries to index with a range like a:b +# The only kind of slicing we support is dropping a dimension with colons +function Base.getindex(A::PowerNetworkMatrix, idx...) + #= + if has_colon(idx) + PTDF(A.data[to_index(A,idx...)...], (ax for (i,ax) in enumerate(A.axes) if idx[i] == Colon())...) + else + return A.data[to_index(A,idx...)...] + end + =# + return A.data[to_index(A,idx...)...] +end +Base.getindex(A::PowerNetworkMatrix, idx::CartesianIndex) = A.data[idx] +Base.setindex!(A::PowerNetworkMatrix, v, idx...) = A.data[to_index(A,idx...)...] = v +Base.setindex!(A::PowerNetworkMatrix, v, idx::CartesianIndex) = A.data[idx] = v + +Base.IndexStyle(::Type{PowerNetworkMatrix}) = IndexAnyCartesian() + + +######## +# Keys # +######## + +struct PowerNetworkMatrixKey{T<:Tuple} + I::T +end +Base.getindex(k::PowerNetworkMatrixKey, args...) = getindex(k.I, args...) + +struct PowerNetworkMatrixKeys{T<:Tuple} + product_iter::Base.Iterators.ProductIterator{T} +end +Base.length(iter::PowerNetworkMatrixKeys) = length(iter.product_iter) +function Base.eltype(iter::PowerNetworkMatrixKeys) + return PowerNetworkMatrixKey{eltype(iter.product_iter)} +end +function Base.iterate(iter::PowerNetworkMatrixKeys) + next = iterate(iter.product_iter) + return next == nothing ? nothing : (PowerNetworkMatrixKey(next[1]), next[2]) +end +function Base.iterate(iter::PowerNetworkMatrixKeys, state) + next = iterate(iter.product_iter, state) + return next == nothing ? nothing : (PowerNetworkMatrixKey(next[1]), next[2]) +end +function Base.keys(a::PowerNetworkMatrix) + return PowerNetworkMatrixKeys(Base.Iterators.product(a.axes...)) +end +Base.getindex(a::PowerNetworkMatrix, k::PowerNetworkMatrixKey) = a[k.I...] + +######## +# Show # +######## + +# Adapted printing from JuMP's implementation of the Julia's show.jl +# used in PowerNetworkMatrixs + +# Copyright (c) 2009-2016: Jeff Bezanson, Stefan Karpinski, Viral B. Shah, +# and other contributors: +# +# https://github.com/JuliaLang/julia/contributors +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +function Base.summary(io::IO, A::PowerNetworkMatrix) + _summary(io, A) + for (k,ax) in enumerate(A.axes) + print(io, " Dimension $k, ") + show(IOContext(io, :limit=>true), ax) + println(io) + end + print(io, "And data, a ", size(A.data)) +end +_summary(io::IO, A::PowerNetworkMatrix) = println(io, "PowerNetworkMatrix") + +function Base.summary(io::IOContext{Base.GenericIOBuffer{Array{UInt8,1}}}, ::PowerNetworkMatrix) + println(io, "PowerNetworkMatrix") +end + +function Base.summary(A::PowerNetworkMatrix) + io = IOBuffer() + Base.summary(io, A) + String(take!(io)) +end + +if isdefined(Base, :print_array) # 0.7 and later + Base.print_array(io::IO, X::PowerNetworkMatrix) = Base.print_matrix(io, X.data) +end + +# n-dimensional arrays +function Base.show_nd(io::IO, a::PowerNetworkMatrix, print_matrix::Function, label_slices::Bool) + limit::Bool = get(io, :limit, false) + if isempty(a) + return + end + tailinds = Base.tail(Base.tail(axes(a.data))) + nd = ndims(a)-2 + for I in CartesianIndices(tailinds) + idxs = I.I + if limit + for i = 1:nd + ii = idxs[i] + ind = tailinds[i] + if length(ind) > 10 + if ii == ind[4] && all(d->idxs[d]==first(tailinds[d]),1:i-1) + for j=i+1:nd + szj = size(a.data,j+2) + indj = tailinds[j] + if szj>10 && first(indj)+2 < idxs[j] <= last(indj)-3 + @goto skip + end + end + #println(io, idxs) + print(io, "...\n\n") + @goto skip + end + if ind[3] < ii <= ind[end-3] + @goto skip + end + end + end + end + if label_slices + print(io, "[:, :, ") + for i = 1:(nd-1); show(io, a.axes[i+2][idxs[i]]); print(io,", "); end + show(io, a.axes[end][idxs[end]]) + println(io, "] =") + end + slice = view(a.data, axes(a.data,1), axes(a.data,2), + idxs...) + Base.print_matrix(io, slice) + print(io, idxs == map(last,tailinds) ? "" : "\n\n") + @label skip + end +end + +function Base.show(io::IO, array::PowerNetworkMatrix) + summary(io, array) + isempty(array) && return + println(io, ":") + Base.print_array(io, array) +end + +Base.to_index(b::Bus) = get_number(b) +Base.to_index(b::T) where T <: ACBranch = get_name(b) +Base.to_index(ix::Component...) = to_index.(ix) diff --git a/src/utils/network_calculations/lodf_calculations.jl b/src/utils/network_calculations/lodf_calculations.jl new file mode 100644 index 0000000000..c1497a9c41 --- /dev/null +++ b/src/utils/network_calculations/lodf_calculations.jl @@ -0,0 +1,43 @@ + +struct LODF{Ax,L<:NTuple{2,Dict}} <: PowerNetworkMatrix{Float64} + data::Array{Float64,2} + axes::Ax + lookup::L +end + +function _buildlodf(branches, nodes, dist_slack::Array{Float64}=[0.1] ) + linecount = length(branches) + ptdf , a = PowerSystems._buildptdf(branches,nodes,dist_slack) + H = gemm('N','N',ptdf,a) + ptdf_denominator = H; + for iline = 1:linecount + if (1.0 - ptdf_denominator[iline,iline] ) < 1.0E-06 + ptdf_denominator[iline,iline] = 0.0; + end + end + (Dem, dipiv, dinfo) = getrf!(Matrix{Float64}(LinearAlgebra.I,linecount,linecount) - Array(LinearAlgebra.Diagonal(ptdf_denominator))) + lodf = gemm('N','N',H,getri!(Dem,dipiv)) + lodf = lodf - Array(LinearAlgebra.Diagonal(lodf)) - Matrix{Float64}(LinearAlgebra.I,linecount,linecount) + return lodf +end + +function LODF(branches, nodes, dist_slack::Array{Float64}=[0.1]) + + #Get axis names + line_ax = [branch.name for branch in branches] + lodf = _buildlodf(branches, nodes, dist_slack) + + axes = (line_ax, line_ax) + look_up = (_make_ax_ref(line_ax),_make_ax_ref(line_ax)) + + return LODF(lodf, axes, look_up) + +end + +function LODF(sys::System, dist_slack::Array{Float64}=[0.1]) + branches = get_components(ACBranch, sys) + nodes = get_components(Bus, sys) + + return LODF(branches, nodes, dist_slack) + +end diff --git a/src/utils/ptdf_calculations.jl b/src/utils/network_calculations/ptdf_calculations.jl similarity index 51% rename from src/utils/ptdf_calculations.jl rename to src/utils/network_calculations/ptdf_calculations.jl index 9f492dfe08..db72d421d4 100644 --- a/src/utils/ptdf_calculations.jl +++ b/src/utils/network_calculations/ptdf_calculations.jl @@ -1,15 +1,18 @@ -function buildptdf(branches::Array{T}, nodes::Array{Bus}, dist_slack::Array{Float64}=[0.1] ) where {T<:Branch} + +struct PTDF{Ax,L<:NTuple{2,Dict}} <: PowerNetworkMatrix{Float64} + data::Array{Float64,2} + axes::Ax + lookup::L +end + +function _buildptdf(branches, nodes, dist_slack::Array{Float64}=[0.1]) buscount = length(nodes) linecount = length(branches) - line_axis = [branch.name for branch in branches] - num_bus = Dict{Int,Int}() + num_bus = Dict{Int32,Int32}() for (ix,b) in enumerate(nodes) - if b.number < -1 - @error "buses must be numbered consecutively in the bus/node matrix" # TODO: raise error here? - end - num_bus[b.number] = ix + num_bus[get_number(b)] = ix end A = zeros(Float64,buscount,linecount); @@ -20,33 +23,35 @@ function buildptdf(branches::Array{T}, nodes::Array{Bus}, dist_slack::Array{Floa for (ix,b) in enumerate(branches) - isa(b,DCLine) ? continue : true + if isa(b,DCBranch) + @warn("PTDF construction ignores DC-Lines") + continue + end - A[num_bus[b.connectionpoints.from.number], ix] = 1; + A[num_bus[get_arc(b) |> get_from |> get_number], ix] = 1; - A[num_bus[b.connectionpoints.to.number], ix] = -1; + A[num_bus[get_arc(b) |> get_to |> get_number], ix] = -1; if isa(b,Transformer2W) - inv_X[ix,ix] = 1/b.x; + inv_X[ix,ix] = 1/get_x(b); elseif isa(b,TapTransformer) - inv_X[ix,ix] = 1/(b.x*b.tap); + inv_X[ix,ix] = 1/(get_x(b)*get_tap(b)); elseif isa(b, Line) - inv_X[ix,ix] = 1/b.x; + inv_X[ix,ix] = 1/get_x(b); elseif isa(b,PhaseShiftingTransformer) - y = 1 / (b.r + b.x * 1im) - y_a = y / (b.tap * exp(b.α * 1im * (π / 180))) + y = 1 / (get_r(b) + get_x(b) * 1im) + y_a = y / (get_tap(b) * exp(get_α(b) * 1im * (π / 180))) inv_X[ix,ix] = 1/imag(y_a) end + end - slacks = [num_bus[n.number] for n in nodes if n.bustype == "SF"] - slack_position = slacks[1] - bus_axis = [bus.name for bus in nodes] + slacks = [num_bus[get_number(n)] for n in nodes if get_bustype(n) == REF::BusType] + slack_position = slacks[1] B = gemm('N','T', gemm('N','N',A[setdiff(1:end, slack_position),1:end] ,inv_X), A[setdiff(1:end, slack_position),1:end]) - if dist_slack[1] == 0.1 && length(dist_slack) ==1 (B, bipiv, binfo) = getrf!(B) S_ = gemm('N','N', gemm('N','T', inv_X, A[setdiff(1:end, slack_position), :]), getri!(B, bipiv) ) @@ -62,13 +67,32 @@ function buildptdf(branches::Array{T}, nodes::Array{Bus}, dist_slack::Array{Floa S = S - gemm('N','N',gemm('N','N',S,slack_array),ones(1,buscount)) elseif length(slack_position) == 0 - @warn "Slack bus not identified in the Bus/Nodes list, can't build PTDF" + @warn("Slack bus not identified in the Bus/Nodes list, can't build PTDF") S = Array{Float64,2}(undef,linecount,buscount) end - S_ax = AxisArrays.AxisArray(S, AxisArrays.Axis{:branches}(line_axis), AxisArrays.Axis{:buses}(bus_axis)) - A_ax = AxisArrays.AxisArray(A, AxisArrays.Axis{:buses}(bus_axis), AxisArrays.Axis{:lines}(line_axis)) + return S, A + +end + +function PTDF(branches, nodes, dist_slack::Array{Float64}=[0.1]) + + #Get axis names + line_ax = [get_name(branch) for branch in branches] + bus_ax = [get_number(bus) for bus in nodes] + S, A = _buildptdf(branches, nodes, dist_slack) + + axes = (line_ax, bus_ax) + look_up = (_make_ax_ref(line_ax),_make_ax_ref(bus_ax)) + + return PTDF(S, axes, look_up) + +end + +function PTDF(sys::System, dist_slack::Array{Float64}=[0.1]) + branches = get_components(ACBranch, sys) + nodes = get_components(Bus, sys) - return S_ax , A_ax + return PTDF(branches, nodes, dist_slack) end diff --git a/src/utils/network_calculations/ybus_calculations.jl b/src/utils/network_calculations/ybus_calculations.jl new file mode 100644 index 0000000000..503ee3d0f3 --- /dev/null +++ b/src/utils/network_calculations/ybus_calculations.jl @@ -0,0 +1,171 @@ +struct Ybus{Ax,L<:NTuple{2,Dict}} <: PowerNetworkMatrix{ComplexF64} + data::SparseArrays.SparseMatrixCSC{ComplexF64,Int64} + axes::Ax + lookup::L +end + +function _ybus!(ybus::SparseArrays.SparseMatrixCSC{ComplexF64,Int64}, + b::Line, + num_bus::Dict{Int64,Int64}) + + + arc = get_arc(b) + bus_from_no = num_bus[arc.from.number] + bus_to_no = num_bus[arc.to.number] + + Y_l = (1 / (get_r(b) + get_x(b) * 1im)) + Y11 = Y_l + (1im * get_b(b).from) + + ybus[bus_from_no, bus_from_no] += Y11 + Y12 = -Y_l + ybus[bus_from_no, bus_to_no] += Y12 + #Y21 = Y12 + ybus[bus_to_no, bus_from_no] += Y12 + Y22 = Y_l + (1im * get_b(b).to) + ybus[bus_to_no, bus_to_no] += Y22 + + return + +end + +function _ybus!(ybus::SparseArrays.SparseMatrixCSC{ComplexF64,Int64}, + b::Transformer2W, + num_bus::Dict{Int64,Int64}) + + arc = get_arc(b) + bus_from_no = num_bus[arc.from.number] + bus_to_no = num_bus[arc.to.number] + + Y_t = 1 / (get_r(b) + get_x(b) * 1im) + Y11 = Y_t + b = get_primaryshunt(b) + + ybus[bus_from_no, bus_from_no] += Y11 + ybus[bus_from_no, bus_to_no] += -Y_t + ybus[bus_to_no, bus_from_no] += -Y_t + ybus[bus_to_no, bus_to_no] += Y_t + (1im * b) + + return + +end + +function _ybus!(ybus::SparseArrays.SparseMatrixCSC{ComplexF64,Int64}, + b::TapTransformer, + num_bus::Dict{Int64,Int64}) + + arc = get_arc(b) + bus_from_no = num_bus[arc.from.number] + bus_to_no = num_bus[arc.to.number] + + Y_t = 1 / (get_r(b) + get_x(b) * 1im) + c = 1 / get_tap(b) + b = get_primaryshunt(b) + + Y11 = (Y_t * c^2) + ybus[bus_from_no, bus_from_no] += Y11 + Y12 = (-Y_t*c) + ybus[bus_from_no, bus_to_no] += Y12 + #Y21 = Y12 + ybus[bus_to_no, bus_from_no] += Y12 + Y22 = Y_t + ybus[bus_to_no, bus_to_no] += Y22 + (1im * b) + + return + +end + +function _ybus!(ybus::SparseArrays.SparseMatrixCSC{ComplexF64,Int64}, + b::PhaseShiftingTransformer, + num_bus::Dict{Int64,Int64}) + + arc = get_arc(b) + bus_from_no = num_bus[arc.from.number] + bus_to_no = num_bus[arc.to.number] + + Y_t = 1 / (get_r(b) + get_x(b) * 1im) + tap = (get_tap(b) * exp(get_α(b) * 1im)) + c_tap = (get_tap(b) * exp(-1*get_α(b) * 1im)) + b = get_primaryshunt(b) + + Y11 = (Y_t/abs(tap)^2) + ybus[bus_from_no, bus_from_no] += Y11 + Y12 = (-Y_t/c_tap) + ybus[bus_from_no, bus_to_no] += Y12 + Y21 = (-Y_t/tap) + ybus[bus_to_no, bus_from_no] += Y21 + Y22 = Y_t + ybus[bus_to_no, bus_to_no] += Y22 + (1im * b) + + return + +end + +function _ybus!(ybus::SparseArrays.SparseMatrixCSC{ComplexF64,Int64}, + fa::FixedAdmittance, + num_bus::Dict{Int64,Int64}) + + bus = get_bus(fa) + bus_no = num_bus[get_number(bus)] + + ybus[bus_no, bus_no] += fa.Y + + return + +end + +function _buildybus(branches, nodes, fixed_admittances) + + buscount = length(nodes) + num_bus = Dict{Int64,Int64}() + + for (ix,b) in enumerate(nodes) + num_bus[get_number(b)] = ix + end + + ybus = SparseArrays.spzeros(ComplexF64, buscount, buscount) + + for (ix,b) in enumerate(branches) + + if get_name(b) == "init" + throw(DataFormatError("The data in Branch is invalid")) + end + + _ybus!(ybus, b, num_bus) + + end + + for fa in fixed_admittances + _ybus!(ybus, fa, num_bus) + end + + return ybus + +end + +function Ybus(branches, nodes) + + #Get axis names + bus_ax = [get_number(bus) for bus in nodes] + axes = (bus_ax, bus_ax) + look_up = (_make_ax_ref(bus_ax), _make_ax_ref(bus_ax)) + + ybus = _buildybus(branches, nodes, Vector{FixedAdmittance}()) + + return Ybus(ybus, axes, look_up) + +end + +function Ybus(sys::System) + branches = get_components(ACBranch, sys) + nodes = sort(collect(get_components(Bus, sys)), by = x -> x.number) + fixed_admittances = get_components(FixedAdmittance, sys) + + # Get axis names + bus_ax = [get_number(bus) for bus in nodes] + axes = (bus_ax, bus_ax) + look_up = (_make_ax_ref(bus_ax), _make_ax_ref(bus_ax)) + + ybus = _buildybus(branches, nodes, fixed_admittances) + + return Ybus(ybus, axes, look_up) +end diff --git a/src/utils/power_flow/make_pf.jl b/src/utils/power_flow/make_pf.jl new file mode 100644 index 0000000000..6d46b6c82d --- /dev/null +++ b/src/utils/power_flow/make_pf.jl @@ -0,0 +1,134 @@ +#TODO: Apply actions according to load type +function _get_load_data(sys::System, b::Bus) + activepower = 0.0 + reactivepower = 0.0 + for l in get_components(ElectricLoad, sys) + if !isa(l,FixedAdmittance) && (l.bus == b) + activepower += get_activepower(l) + reactivepower += get_reactivepower(l) + end + end + return activepower, reactivepower +end + +""" + make_pf(sys) + +Create the objects needed to solve an powerflow case using NLsolve.jl solvers. Returns +an anonymous function with the powerflow equations, initial conditions and a dict to link the +solutions to the original system. Only supports systems with a single generator per bus and +currently doesn't support distributed slack buses and doesn't enforce reactive power limits. + +## Example +```julia +pf!, x0 = make_pf(sys) +res = NLsolve.nlsolve(pf!, x0) +``` + +# Arguments + * `sys`::System : a PowerSystems.jl system + +""" +function make_pf(system) + buses = sort(collect(get_components(Bus, system)), by = x -> get_number(x)) + N_BUS = length(buses) + + # assumes the ordering in YBus is the same as in the buses. + Yb = Ybus(system) + x0 = zeros(N_BUS * 2) + + # Use vectors to cache data for closure + # These should be read only + P_GEN_BUS = fill(0.0, N_BUS) + Q_GEN_BUS = fill(0.0, N_BUS) + P_LOAD_BUS = fill(0.0, N_BUS) + Q_LOAD_BUS = fill(0.0, N_BUS) + + BUSES = (b for b in enumerate(buses)) + + state_variable_count = 1 + + for (ix, b) in BUSES + bus_number = get_number(b)::Int + bus_angle = get_angle(b)::Float64 + bus_voltage = get_voltage(b)::Float64 + generator = nothing + for gen in get_components(Generator, system) + if gen.bus == b + !isnothing(generator) && throw(DataFormatError("There is more than one generator connected to Bus $b.name")) + generator = gen + end + end + P_GEN_BUS[ix] = isnothing(generator) ? 0.0 : get_activepower(generator) + Q_GEN_BUS[ix] = isnothing(generator) ? 0.0 : get_reactivepower(generator) + P_LOAD_BUS[ix], Q_LOAD_BUS[ix] = _get_load_data(system, b) + + if b.bustype == REF::BusType + x0[state_variable_count] = P_GEN_BUS[ix] + x0[state_variable_count + 1] = Q_GEN_BUS[ix] + state_variable_count += 2 + elseif b.bustype == PV::BusType + x0[state_variable_count] = Q_GEN_BUS[ix] + x0[state_variable_count + 1] = bus_angle + state_variable_count += 2 + elseif b.bustype == PQ::BusType + x0[state_variable_count] = bus_voltage + x0[state_variable_count + 1] = bus_angle + state_variable_count += 2 + end + end + + @assert state_variable_count - 1 == N_BUS * 2 + + function pf!(F, X) + + P_net = Vector{Float64}(undef, N_BUS) + Q_net = Vector{Float64}(undef, N_BUS) + Vm = Vector{Float64}(undef, N_BUS) + θ = Vector{Float64}(undef, N_BUS) + + for (ix, b) in BUSES + bus_voltage = get_voltage(b) + bus_angle = get_angle(b) + if b.bustype == REF::BusType + # When bustype == REFERENCE Bus, state variables are Active and Reactive Power Generated + P_net[ix] = X[2 * ix - 1] - P_LOAD_BUS[ix] + Q_net[ix] = X[2 * ix] - Q_LOAD_BUS[ix] + Vm[ix] = bus_voltage + θ[ix] = bus_angle + elseif b.bustype == PV::BusType + # When bustype == PV Bus, state variables are Reactive Power Generated and Voltage Angle + P_net[ix] = P_GEN_BUS[ix] - P_LOAD_BUS[ix] + Q_net[ix] = X[2 * ix - 1] - Q_LOAD_BUS[ix] + Vm[ix] = bus_voltage + θ[ix] = X[2 * ix] + elseif b.bustype == PQ::BusType + # When bustype == PQ Bus, state variables are Voltage Magnitude and Voltage Angle + P_net[ix] = P_GEN_BUS[ix] - P_LOAD_BUS[ix] + Q_net[ix] = Q_GEN_BUS[ix] - Q_LOAD_BUS[ix] + Vm[ix] = X[2 * ix - 1] + θ[ix] = X[2 * ix] + end + end + + # F is active and reactive power balance equations at all buses + state_count = 1 + for (ix_f, bf) in BUSES + S = -P_net[ix_f] + -Q_net[ix_f]im + V_f = Vm[ix_f] * ( cos(θ[ix_f]) + sin(θ[ix_f])im ) + for (ix_t, bt) in BUSES + iszero(Yb[ix_f, ix_t]::ComplexF64) && continue + V_t = Vm[ix_t] * ( cos(θ[ix_t]) + sin(θ[ix_t])im ) + S += V_f * conj(V_t) * conj(Yb[ix_f, ix_t]::ComplexF64) + end + F[state_count] = real(S) + F[state_count + 1] = imag(S) + state_count += 2 + end + + return F + + end + + return ( pf!, x0 ) +end diff --git a/src/utils/power_flow/power_flow.jl b/src/utils/power_flow/power_flow.jl new file mode 100644 index 0000000000..016f0ba00b --- /dev/null +++ b/src/utils/power_flow/power_flow.jl @@ -0,0 +1,148 @@ +""" + flow_val(b::TapTransformer) + +Calculates the From - To comp[lex power flow (Flow injected at the bus) of branch of type +TapTransformer + +""" + function flow_val(b::TapTransformer) + Y_t = 1 / (PowerSystems.get_r(b) + PowerSystems.get_x(b) * 1im) + c = 1 / PowerSystems.get_tap(b) + arc = PowerSystems.get_arc(b) + V_from = arc.from.voltage*(cos(arc.from.angle)+sin(arc.from.angle)*1im) + V_to = arc.to.voltage*(cos(arc.to.angle)+sin(arc.to.angle)*1im) + I = (V_from * Y_t * c^2) - (V_to * Y_t * c) + flow = V_from*conj(I) + return flow +end + +""" + flow_val(b::TapTransformer) + +Calculates the From - To complex power flow (Flow injected at the bus) of branch of type +Line + +""" +function flow_val(b::Line) + Y_t = (1 / (PowerSystems.get_r(b) + PowerSystems.get_x(b) * 1im)) + arc = PowerSystems.get_arc(b) + V_from = arc.from.voltage*(cos(arc.from.angle)+sin(arc.from.angle)*1im) + V_to = arc.to.voltage*(cos(arc.to.angle)+sin(arc.to.angle)*1im) + I = V_from*(Y_t + (1im * PowerSystems.get_b(b).from)) - V_to*Y_t + flow = V_from*conj(I) + return flow +end + +""" + flow_val(b::TapTransformer) + +Calculates the From - To complex power flow (Flow injected at the bus) of branch of type +Transformer2W + +""" +function flow_val(b::Transformer2W) + Y_t = 1 / (PowerSystems.get_r(b) + PowerSystems.get_x(b) * 1im) + arc = PowerSystems.get_arc(b) + V_from = arc.from.voltage*(cos(arc.from.angle)+sin(arc.from.angle)*1im) + V_to = arc.to.voltage*(cos(arc.to.angle)+sin(arc.to.angle)*1im) + I = V_from*(Y_t + (1im * PowerSystems.get_primaryshunt(b))) - V_to*Y_t + flow = V_from*conj(I) + return flow +end + + +function flow_val(b::PhaseShiftingTransformer) + error("Systems with PhaseShiftingTransformer not supported yet") + return +end + +function _update_branch_flow!(sys::System) + for b in get_components(ACBranch, sys) + S_flow = flow_val(b) + b.activepower_flow = real(S_flow) + b.reactivepower_flow = imag(S_flow) + end +end + +function _write_pf_sol!(sys::System, nl_result) + result = nl_result.zero + buses = enumerate(sort(collect(get_components(Bus, sys)), by = x -> get_number(x))) + + for (ix, bus) in buses + if bus.bustype == PowerSystems.REF + P_gen = result[2 * ix - 1] + Q_gen = result[2 * ix] + injection_components = get_components(Generator, sys) + devices = [d for d in injection_components if d.bus == bus] + generator = devices[1] + generator.activepower = P_gen + generator.reactivepower = Q_gen + elseif bus.bustype == PowerSystems.PQ + Q_gen = result[2 * ix - 1] + θ = result[2 * ix] + injection_components = get_components(Generator, sys) + devices = [d for d in injection_components if d.bus == bus] + if length(devices) == 1 + generator = devices[1] + generator.reactivepower = Q_gen + end + bus.angle = θ + elseif bus.bustype == PowerSystems.PV + Vm = result[2 * ix - 1] + θ = result[2 * ix] + bus.voltage = Vm + bus.angle = θ + end + end + + _update_branch_flow!(sys) + + return +end + +""" + solve_powerflow!(sys, solve_function, args...) + +Solves a the power flow into the system and writes the solution into the relevant structs. +Updates generators active and reactive power setpoints and branches active and reactive +power flows (calculated in the From - To direction) (see +[flow_val](@ref)) + +Requires loading NLsolve.jl to run. Internally it uses the make_pf (see +[make_pf](@ref)) to create the problem and solve it. As a result it doesn't enforce +reactivepower limits. + +Supports passing NLsolve kwargs in the args. By default shows the solver trace. + +Arguments available for `nlsolve`: + +* `method` : See NLSolve.jl documentation for available solvers +* `xtol`: norm difference in `x` between two successive iterates under which + convergence is declared. Default: `0.0`. +* `ftol`: infinite norm of residuals under which convergence is declared. + Default: `1e-8`. +* `iterations`: maximum number of iterations. Default: `1_000`. +* `store_trace`: should a trace of the optimization algorithm's state be + stored? Default: `false`. +* `show_trace`: should a trace of the optimization algorithm's state be shown + on `STDOUT`? Default: `false`. +* `extended_trace`: should additifonal algorithm internals be added to the state + trace? Default: `false`. + + +## Examples +```julia +using NLsolve +solve_powerflow!(sys, nlsolve) +# Passing NLsolve arguments +solve_powerflow!(sys, nlsolve, method = :Newton) + +``` + +""" +function solve_powerflow!(sys, nlsolve; args...) + pf!, x0 = PowerSystems.make_pf(sys) + res = nlsolve(pf!, x0; args...) + PowerSystems._write_pf_sol!(sys, res) + return +end diff --git a/src/utils/print.jl b/src/utils/print.jl index e451869517..d3652ca5fb 100644 --- a/src/utils/print.jl +++ b/src/utils/print.jl @@ -1,95 +1,49 @@ # "smart" summary and REPL printing -# Generic print for power system components (a new type that includes generation -# technology types). The long version simply prints every field of the -# device. Tailored versions can be made for specific devices, if/when -# desired. JJS 11/15/18 -function printPST(pst::Component, short = false, - io::IO = stdout) - if short - pst_summary = Base.summary(pst) - # objects of type TechnicalParams do not have a 'name' field - if in(:name, fieldnames(typeof(pst))) - print(io, "$pst_summary(name=\"$(pst.name)\")") - else - print(io, "$pst_summary") - end - else - print(io, Base.summary(pst),":") - fnames = fieldnames(typeof(pst)) - for i = 1:nfields(fnames) - thefield = getfield(pst, i) - # avoid warning about printing nothing - if thefield==nothing - print(io, "\n ", fnames[i], ": nothing") - else - print(io, "\n ", fnames[i], ": ", thefield) - end - end - end +function Base.summary(sys::System) + return "System (base power $(sys.basepower))" end -# overload Base.show with printPST function defined above -# single-line format -Base.show(io::IO, pst::Component) = printPST(pst, true, io) -# Multi-line format for plaintext (e.g. from repl); can specify for HTML and -# others too -Base.show(io::IO, ::MIME"text/plain", pst::Component) = - printPST(pst, false, io) -# provide limited additional information to summary of GenClasses -function summaryGenClasses(gens::GenClasses) - ntherm = gens.thermal isa Nothing ? 0 : length(gens.thermal) - nrenew = gens.renewable isa Nothing ? 0 : length(gens.renewable) - nhydro = gens.hydro isa Nothing ? 0 : length(gens.hydro) - return "GenClasses(T:$ntherm,R:$nrenew,H:$nhydro)" +function Base.show(io::IO, sys::System) + println(io, "basepower=$(sys.basepower)") + show(io, sys.data) end -Base.summary(gens::GenClasses) = summaryGenClasses(gens::GenClasses) +function Base.show(io::IO, ::MIME"text/plain", sys::System) + println(io, "System") + println(io, "======") + println(io, "Base Power: $(sys.basepower)\n") + show(io, MIME"text/plain"(), sys.data) +end -# improve output for System +function Base.show(io::IO, ::MIME"text/html", sys::System) + println(io, "

System

") + println(io, "

Base Power: $(sys.basepower)

") + show(io, MIME"text/html"(), sys.data) +end -# overload Base.summary for System to be useful, e.g., in output of -# varinfo(); also used now in printPowerSystem, JJS 11/26/18 -function summaryPowerSystem(system::System) - busstr = string("buses:", length(system.buses)) - genstr = Base.summary(system.generators) - loadstr = string("loads:", length(system.loads)) - branchstr = system.branches isa Nothing ? "nothing" : - string("branches:", length(system.branches)) - # is storage an array? not sure yet, JJS 11/28/18 - storstr = system.storage isa Nothing ? "nothing" : - string("storage:", length(system.storage)) - # include some output for basepower or time_periods? currently not doing so - output = string("System($busstr,$genstr,$loadstr,$branchstr,$storstr)") - return output +function Base.summary(tech::TechnicalParams) + return "$(typeof(tech))" end -Base.summary(system::System) = summaryPowerSystem(system::System) -# print function for System type -function printPowerSystem(system::System, short = false, - io::IO = stdout) - if short - print(io, Base.summary(system)) - else - print(io, "System:") - fnames = fieldnames(typeof(system)) - for i = 1:nfields(fnames) - fname = fnames[i] - thefield = getfield(system, i) - if thefield==nothing - print(io, "\n ", fname, ": nothing") - elseif fname==:generators - # print out long version for generators - print(io, "\n ", fname, ": \n ") - printPST(system.generators, false, io) - print(io, "\n (end generators)") - else - print(io, "\n ", fname, ": ", thefield) - end +function Base.summary(arc::Arc) + return "$(get_name(get_from(arc))) -> $(get_name(get_to(arc))): ($(typeof(arc)))" +end + +function Base.show(io::IO, ::MIME"text/plain", data::PowerSystemTableData) + println(io, "$(typeof(data)):") + println(io, " directory: $(data.directory)") + if !isnothing(data.timeseries_metadata_file) + println(io, " timeseries_metadata_file: $(data.timeseries_metadata_file)") + end + println(io, " basepower: $(data.basepower)") + for field in ("branch", "bus", "dcline", "gen", "load", "services") + print(io, " $field: ") + val = getfield(data, Symbol(field)) + if isnothing(val) + println(io, "no data") + else + println(io, "$(summary(val))") end end end -Base.show(io::IO, system::System) = printPowerSystem(system, true, io) -Base.show(io::IO, ::MIME"text/plain", system::System) = - printPowerSystem(system, false, io) - diff --git a/src/utils/utils.jl b/src/utils/utils.jl deleted file mode 100644 index 69cc04c99b..0000000000 --- a/src/utils/utils.jl +++ /dev/null @@ -1,50 +0,0 @@ -import InteractiveUtils: subtypes - - -"""Returns an array of all concrete subtypes of T.""" -function get_all_concrete_subtypes(::Type{T}) where T - return _get_all_concrete_subtypes(T) -end - -function _get_all_concrete_subtypes(::Type{T}, sub_types=[]) where T - for sub_type in subtypes(T) - push!(sub_types, sub_type) - if isabstracttype(sub_type) - _get_all_concrete_subtypes(sub_type, sub_types) - end - end - - return sub_types -end - -"""Returns an array of concrete types that are direct subtypes of T.""" -function get_concrete_subtypes(::Type{T}) where T - return [x for x in subtypes(T) if isconcretetype(x)] -end - -"""Returns an array of abstract types that are direct subtypes of T.""" -function get_abstract_subtypes(::Type{T}) where T - return [x for x in subtypes(T) if isabstracttype(x)] -end - -"""Returns an array of all super types of T.""" -function supertypes(::Type{T}, types=[]) where T - super = supertype(T) - push!(types, super) - if super == Any - return types - end - - supertypes(super, types) -end - -"""Converts a DataType to a Symbol, stripping off the module name(s).""" -function type_to_symbol(data_type::DataType) - text = string(data_type) - index = findlast(".", text) - if !isnothing(index) - text = text[index.start + 1:end] - end - - return Symbol(text) -end diff --git a/src/utils/ybus_calculations.jl b/src/utils/ybus_calculations.jl deleted file mode 100644 index b5afab74aa..0000000000 --- a/src/utils/ybus_calculations.jl +++ /dev/null @@ -1,135 +0,0 @@ - -function ybus!(Ybus::SparseArrays.SparseMatrixCSC{Complex{Float64},Int64}, b::Line) - - Y_l = (1 / (b.r + b.x * 1im)) - - Y11 = Y_l + (1im * b.b.from); - Ybus[b.connectionpoints.from.number, - b.connectionpoints.from.number] += Y11; - - Y12 = -Y_l; - Ybus[b.connectionpoints.from.number, - b.connectionpoints.to.number] += Y12; - #Y21 = Y12 - Ybus[b.connectionpoints.to.number, - b.connectionpoints.from.number] += Y12; - - Y22 = Y_l + (1im * b.b.to); - Ybus[b.connectionpoints.to.number, - b.connectionpoints.to.number] += Y22; - -end - -function ybus!(Ybus::SparseArrays.SparseMatrixCSC{Complex{Float64},Int64}, b::Transformer2W) - - Y_t = 1 / (b.r + b.x * 1im) - - Y11 = Y_t - Ybus[b.connectionpoints.from.number, - b.connectionpoints.from.number] += Y11; - Ybus[b.connectionpoints.from.number, - b.connectionpoints.to.number] += -Y_t; - Ybus[b.connectionpoints.to.number, - b.connectionpoints.from.number] += -Y_t; - Ybus[b.connectionpoints.to.number, - b.connectionpoints.to.number] += Y_t + (1im * b.primaryshunt); - -end - -function ybus!(Ybus::SparseArrays.SparseMatrixCSC{Complex{Float64},Int64}, b::TapTransformer) - - Y_t = 1 / (b.r + b.x * 1im) - c = 1 / b.tap - - Y11 = (Y_t * c^2); - Ybus[b.connectionpoints.from.number, - b.connectionpoints.from.number] += Y11; - Y12 = (-Y_t*c) ; - Ybus[b.connectionpoints.from.number, - b.connectionpoints.to.number] += Y12; - #Y21 = Y12 - Ybus[b.connectionpoints.to.number, - b.connectionpoints.from.number] += Y12; - Y22 = Y_t; - Ybus[b.connectionpoints.to.number, - b.connectionpoints.to.number] += Y22 + (1im * b.primaryshunt); - -end - -# TODO: Add testing for Ybus of a system with a PS Transformer -function ybus!(Ybus::SparseArrays.SparseMatrixCSC{Complex{Float64},Int64}, b::PhaseShiftingTransformer) - - Y_t = 1 / (b.r + b.x * 1im) - tap = (b.tap * exp(b.α * 1im)) - c_tap = (b.tap * exp(-1*b.α * 1im)) - - Y11 = (Y_t/abs(tap)^2); - Ybus[b.connectionpoints.from.number, - b.connectionpoints.from.number] += Y11; - Y12 = (-Y_t/c_tap); - Ybus[b.connectionpoints.from.number, - b.connectionpoints.to.number] += Y12; - Y21 = (-Y_t/tap); - Ybus[b.connectionpoints.to.number, - b.connectionpoints.from.number] += Y21; - Y22 = Y_t; - Ybus[b.connectionpoints.to.number, - b.connectionpoints.to.number] += Y22 + (1im * b.primaryshunt); - -end - -#= -function ybus!(Ybus::SparseArrays.SparseMatrixCSC{Complex{Float64},Int64}, b::Transformer3W) - - @warn "Data contains a 3W transformer" - - Y11 = (1 / (b.line.r + b.line.x * 1im) + (1im * b.line.b) / 2); - Ybus[b.line.connectionpoints.from.number, - b.line.connectionpoints.from.number] += Y11; - Y12 = (-1 ./ (b.line.r + b.line.x * 1im)); - Ybus[b.line.connectionpoints.from.number, - b.line.connectionpoints.to.number] += Y12; - #Y21 = Y12 - Ybus[b.line.connectionpoints.to.number, - b.line.onnectionpoints[1].number] += Y12; - #Y22 = Y11; - Ybus[b.line.connectionpoints.to.number, - b.line.connectionpoints.to.number] += Y11; - - y = 1 / (b.transformer.r + b.transformer.x * 1im) - y_a = y / (b.transformer.tap * exp(b.transformer.α * 1im * (π / 180))) - c = 1 / b.transformer.tap - - Y11 = (y_a + y * c * (c - 1) + (b.transformer.zb)); - Ybus[b.transformer.connectionpoints.from.number, - b.transformer.connectionpoints.from.number] += Y11; - Y12 = (-y_a) ; - Ybus[b.transformer.connectionpoints.from.number, - b.transformer.connectionpoints.to.number] += Y12; - #Y21 = Y12 - Ybus[b.transformer.connectionpoints.to.number, - b.transformer.connectionpoints.from.number] += Y12; - Y22 = (y_a + y * (1 - c)) ; - Ybus[b.transformer.connectionpoints.to.number, - b.transformer.connectionpoints.to.number] += Y22; - -end -=# - -function build_ybus(buscount::Int64, branches::Array{T}) where {T <: Branch} - - Ybus = SparseArrays.spzeros(Complex{Float64}, buscount, buscount) - - for b in branches - - if b.name == "init" - @error "The data in Branch is incomplete" # TODO: raise error here? - end - - ybus!(Ybus, b) - - end - - return Ybus - -end diff --git a/src/validation/branch.jl b/src/validation/branch.jl deleted file mode 100644 index 6b590450f4..0000000000 --- a/src/validation/branch.jl +++ /dev/null @@ -1,8 +0,0 @@ - -"""Validates the contents of a Branch.""" -function validate(branch::Branch)::Bool - is_valid = true - - @debug "Branch validation" branch.name is_valid - return is_valid -end diff --git a/src/validation/bus.jl b/src/validation/bus.jl deleted file mode 100644 index cf0a73d982..0000000000 --- a/src/validation/bus.jl +++ /dev/null @@ -1,8 +0,0 @@ - -"""Validates the contents of a Bus.""" -function validate(bus::Bus)::Bool - is_valid = true - - @debug "Bus validation" bus.name bus.number is_valid - return is_valid -end diff --git a/src/validation/generator.jl b/src/validation/generator.jl deleted file mode 100644 index 5c8a6b042d..0000000000 --- a/src/validation/generator.jl +++ /dev/null @@ -1,25 +0,0 @@ - -"""Validates the contents of generators.""" -function validate(gen::GenClasses)::Bool - is_valid = true - - for field in fieldnames(typeof(gen)) - generators = getfield(gen, field) - if !isnothing(generators) - if !validate_devices(generators) - is_valid = false - end - end - end - - @debug "GenClasses validation" is_valid - return is_valid -end - -"""Validates the contents of a Generator.""" -function validate(generator::Generator)::Bool - is_valid = true - - @debug "Generator validation" generator.name is_valid - return is_valid -end diff --git a/src/validation/load.jl b/src/validation/load.jl deleted file mode 100644 index 224ed4ec2c..0000000000 --- a/src/validation/load.jl +++ /dev/null @@ -1,9 +0,0 @@ - - -"""Validates the contents of an ElectricLoad.""" -function validate(load::ElectricLoad)::Bool - is_valid = true - - @debug "Load validation" load.name is_valid - return is_valid -end diff --git a/src/validation/powersystem.jl b/src/validation/powersystem.jl deleted file mode 100644 index df882b20f8..0000000000 --- a/src/validation/powersystem.jl +++ /dev/null @@ -1,48 +0,0 @@ - -include("branch.jl") -include("bus.jl") -include("generator.jl") -include("load.jl") -include("storage.jl") - - -"""Validates an array of devices.""" -function validate_devices(devices::Array{<: Device, 1})::Bool - is_valid = true - - for device in devices - if !validate(device) - is_valid = false - end - end - - return is_valid -end - -"""Validates the contents of a System.""" -function validate(sys::System)::Bool - is_valid = true - - for field in (:buses, :loads) - objs = getfield(sys, field) - if !validate_devices(objs) - is_valid = false - end - end - - if !validate(sys.generators) - is_valid = false - end - - for field in (:branches, :storage) - objs = getfield(sys, field) - if !isnothing(objs) - if !validate_devices(objs) - is_valid = false - end - end - end - - @debug "System validation" is_valid - return is_valid -end diff --git a/src/validation/storage.jl b/src/validation/storage.jl deleted file mode 100644 index b021d07543..0000000000 --- a/src/validation/storage.jl +++ /dev/null @@ -1,8 +0,0 @@ - -"""Validates the contents of a Storage.""" -function validate(storage::Storage)::Bool - is_valid = true - - @debug "Storage validation" storage.name is_valid - return is_valid -end diff --git a/test/busnumberchecks.jl b/test/busnumberchecks.jl deleted file mode 100644 index 996e0a4b80..0000000000 --- a/test/busnumberchecks.jl +++ /dev/null @@ -1,21 +0,0 @@ - -#note: may have to remove the 'data' folder from this directory and run 'build PowerSystems' -base_dir = dirname(dirname(pathof(PowerSystems))) - -ps_dict = PowerSystems.parsestandardfiles(joinpath(MATPOWER_DIR, "case5_re.m")) - -buses, generators, storage, branches, loads, loadZones, shunts, services = - PowerSystems.ps_dict2ps_struct(ps_dict); -sys = PowerSystems.System(buses, generators, loads, branches, storage, - ps_dict["baseMVA"]); - -@testset "Check bus index" begin - @test sort([b.number for b in sys.buses]) == [1, 2, 3, 4, 5] - @test sort(collect(Set([b.connectionpoints.from.number for b in sys.branches]))) == - [1, 2, 3, 4] - @test sort(collect(Set([b.connectionpoints.to.number for b in sys.branches]))) == - [2, 3, 4, 5] - - # TODO: add test for loadzones testing MAPPING_BUSNUMBER2INDEX - -end diff --git a/test/cdmparse.jl b/test/cdmparse.jl deleted file mode 100644 index 8edb42ad22..0000000000 --- a/test/cdmparse.jl +++ /dev/null @@ -1,95 +0,0 @@ -@testset "PowerSystems dict parsing" begin - @info "parsing data from $RTS_GMLC_DIR into ps_dict" - data_dict = PowerSystems.read_csv_data(RTS_GMLC_DIR, 100.0) - @test haskey(data_dict, "timeseries_pointers") -end - -@testset "CDM parsing" begin - cdm_dict = nothing - @info "parsing data from $RTS_GMLC_DIR into ps_dict" - cdm_dict = PowerSystems.csv2ps_dict(RTS_GMLC_DIR, 100.0) - @test cdm_dict isa Dict && haskey(cdm_dict, "loadzone") - - @info "assigning time series data for DA" - cdm_dict = PowerSystems.assign_ts_data(cdm_dict, cdm_dict["forecast"]["DA"]) - @test length(cdm_dict["gen"]["Renewable"]["PV"]["102_PV_1"]["scalingfactor"]) == 24 - - @info "making DA System" - sys_rts_da = System(cdm_dict) - @test sys_rts_da isa System - - @info "assigning time series data for RT" - cdm_dict = PowerSystems.assign_ts_data(cdm_dict, cdm_dict["forecast"]["RT"]) - @test length(cdm_dict["gen"]["Renewable"]["PV"]["102_PV_1"]["scalingfactor"]) == 288 - - @info "making RT System" - sys_rts_rt = System(cdm_dict) - @test sys_rts_rt isa System - - # Verify functionality of the concrete version of System. - # TODO: Refactor once the ConcreteSystem implementation is finalized. - sys = ConcreteSystem(sys_rts_da) - @test length(sys_rts_rt.branches) == length(collect(get_mixed_components(Branch, sys))) - @test length(sys_rts_rt.loads) == length(collect(get_mixed_components(ElectricLoad, sys))) - @test length(sys_rts_rt.storage) == length(collect(get_mixed_components(Storage, sys))) - @test length(sys_rts_rt.generators.thermal) == length(collect(get_mixed_components(ThermalGen, sys))) - @test length(sys_rts_rt.generators.renewable) == length(collect(get_mixed_components(RenewableGen, sys))) - @test length(sys_rts_rt.generators.hydro) == length(collect(get_mixed_components(HydroGen, sys))) - @test length(get_components(Bus, sys)) > 0 - @test length(get_components(ThermalDispatch, sys)) > 0 - for x in (true, false) - show_component_counts(sys, devnull; show_hierarchy=x) - end -end - -@testset "CDM parsing invalid directory" begin - baddir = joinpath(RTS_GMLC_DIR, "../../test") - @info "testing bad directory" - @test_throws ErrorException PowerSystems.csv2ps_dict(baddir, 100.0) -end - -@testset "consistency between CDM and standardfiles" begin - mp_dict = parsestandardfiles(joinpath(MATPOWER_DIR, "RTS_GMLC.m")) - pm_dict = parse_file(joinpath(MATPOWER_DIR, "RTS_GMLC.m")) - pmmp_dict = PowerSystems.pm2ps_dict(pm_dict) - mpmmpsys = System(pmmp_dict) - - mpsys = System(mp_dict) - - cdm_dict = PowerSystems.csv2ps_dict(RTS_GMLC_DIR, 100.0) - cdmsys = System(cdm_dict) - - @test cdmsys.generators.thermal[1].tech.activepowerlimits == mpsys.generators.thermal[1].tech.activepowerlimits - @test cdmsys.generators.thermal[1].tech.reactivepowerlimits == mpsys.generators.thermal[1].tech.reactivepowerlimits - @test_skip cdmsys.generators.thermal[1].tech.ramplimits == mpsys.generators.thermal[1].tech.ramplimits - - @test cdmsys.generators.thermal[1].econ.capacity == mpsys.generators.thermal[1].econ.capacity - @test_skip cdmsys.generators.thermal[1].econ.variablecost == mpsys.generators.thermal[1].econ.variablecost - - - @test cdmsys.generators.hydro[1].tech.activepowerlimits == mpsys.generators.hydro[1].tech.activepowerlimits - @test cdmsys.generators.hydro[1].tech.reactivepowerlimits == mpsys.generators.hydro[1].tech.reactivepowerlimits - @test cdmsys.generators.hydro[1].tech.installedcapacity == mpsys.generators.hydro[1].tech.installedcapacity - @test_skip cdmsys.generators.hydro[1].tech.ramplimits == mpsys.generators.hydro[1].tech.ramplimits # this gets adjusted in the pm2ps_dict - - @test cdmsys.generators.hydro[1].econ == mpsys.generators.hydro[1].econ - - @test cdmsys.generators.renewable[1].tech == mpsys.generators.renewable[1].tech - - @test cdmsys.generators.renewable[1].econ == mpsys.generators.renewable[1].econ - - @test cdmsys.branches[1].rate == - [b for b in mpsys.branches if - (b.connectionpoints.from.name == uppercase(cdmsys.branches[1].connectionpoints.from.name)) - & (b.connectionpoints.to.name == uppercase(cdmsys.branches[1].connectionpoints.to.name))][1].rate - - @test cdmsys.branches[6].rate == - [b for b in mpsys.branches if - (b.connectionpoints.from.name == uppercase(cdmsys.branches[6].connectionpoints.from.name)) - & (b.connectionpoints.to.name == uppercase(cdmsys.branches[6].connectionpoints.to.name))][1].rate - - @test cdmsys.branches[120].rate == [b for b in mpsys.branches if - (b.connectionpoints.from.name == uppercase(cdmsys.branches[120].connectionpoints.from.name)) - & (b.connectionpoints.to.name == uppercase(cdmsys.branches[120].connectionpoints.to.name))][1].rate - -end diff --git a/test/common.jl b/test/common.jl new file mode 100644 index 0000000000..8686383de4 --- /dev/null +++ b/test/common.jl @@ -0,0 +1,63 @@ + +const DESCRIPTORS = joinpath(RTS_GMLC_DIR, "user_descriptors.yaml") + +function create_rts_system(forecast_resolution=Dates.Hour(1)) + data = PowerSystemTableData(RTS_GMLC_DIR, 100.0, DESCRIPTORS) + return System(data; forecast_resolution=forecast_resolution) +end + +"""Allows comparison of structs that were created from different parsers which causes them +to have different UUIDs.""" +function compare_values_without_uuids(x::T, y::T)::Bool where T <: PowerSystemType + match = true + + for (fieldname, fieldtype) in zip(fieldnames(T), fieldtypes(T)) + if fieldname == :internal + continue + end + + val1 = getfield(x, fieldname) + val2 = getfield(y, fieldname) + + # Recurse if this is a PowerSystemType. + if val1 isa PowerSystemType + if !compare_values_without_uuids(val1, val2) + match = false + end + continue + end + + if val1 != val2 + @error "values do not match" fieldname repr(val1) repr(val2) + match = false + end + end + + return match +end + +"""Return the first component of type component_type that matches the name of other.""" +function get_component_by_name(sys::System, component_type, other::Component) + for component in get_components(component_type, sys) + if get_name(component) == get_name(other) + return component + end + end + + error("Did not find component $component") +end + +"""Return the Branch in the system that matches another by case-insensitive arc +names.""" +function get_branch(sys::System, other::Branch) + for branch in get_components(Branch, sys) + if lowercase(other.arc.from.name) == lowercase(branch.arc.from.name) && + lowercase(other.arc.to.name) == lowercase(branch.arc.to.name) + return branch + end + end + + error("Did not find branch with buses $(other.arc.from.name) ", + "$(other.arc.to.name)") +end + diff --git a/test/constructors.jl b/test/constructors.jl deleted file mode 100644 index 07eb9fe23f..0000000000 --- a/test/constructors.jl +++ /dev/null @@ -1,77 +0,0 @@ -@testset "Bus Constructors" begin - tBus = Bus() - tLoadZones = LoadZones() -end - -@testset "Generation Constructors" begin - tEconThermal = EconThermal() - @test tEconThermal isa PowerSystems.Component - tTechThermal = TechThermal() - @test tTechThermal isa PowerSystems.Component - tThermalGen = ThermalDispatch() - @test tThermalGen isa PowerSystems.Component - tThermalGenSeason = ThermalGenSeason() - @test tThermalGenSeason isa PowerSystems.Component - tTechHydro = TechHydro() - @test tTechHydro isa PowerSystems.Component - tEconHydro = EconHydro() - @test tEconHydro isa PowerSystems.Component - tHydroFix = HydroFix() - @test tHydroFix isa PowerSystems.Component - tHydroCurtailment = HydroCurtailment() - @test tHydroCurtailment isa PowerSystems.Component - tHydroStorage = HydroStorage() - @test tHydroStorage isa PowerSystems.Component - tTechRenewable = TechRenewable() - @test tTechRenewable isa PowerSystems.Component - tEconRenewable = EconRenewable() - @test tEconRenewable isa PowerSystems.Component - tRenewableFix = RenewableFix() - @test tRenewableFix isa PowerSystems.Component - tRenewableFullDispatch = RenewableFullDispatch() - @test tRenewableFullDispatch isa PowerSystems.Component - tRenewableCurtailment = RenewableCurtailment() - @test tRenewableCurtailment isa PowerSystems.Component -end - -@testset "Storage Constructors" begin - tStorage = GenericBattery() - @test tStorage isa PowerSystems.Component -end - -@testset "Load Constructors" begin - tPowerLoad = PowerLoad() - @test tPowerLoad isa PowerSystems.Component - tPowerLoadPF = PowerLoadPF() - @test tPowerLoadPF isa PowerSystems.Component - tPowerLoad = PowerLoad("init", true, Bus(), 0.0, 0.0) - @test tPowerLoad isa PowerSystems.Component - tPowerLoadPF = PowerLoadPF("init", true, Bus(), 0.0, 1.0) - @test tPowerLoadPF isa PowerSystems.Component - tLoad = InterruptibleLoad() - @test tLoad isa PowerSystems.Component -end - -@testset "Branch Constructors" begin - tLine = Line() - @test tLine isa PowerSystems.Component - tMonitoredLine = MonitoredLine() - @test tMonitoredLine isa PowerSystems.Component - tHVDCLine = HVDCLine() - @test tHVDCLine isa PowerSystems.Component - tVSCDCLine = VSCDCLine() - @test tVSCDCLine isa PowerSystems.Component - tTransformer2W = Transformer2W() - @test tTransformer2W isa PowerSystems.Component - tTapTransformer = TapTransformer() - @test tTapTransformer isa PowerSystems.Component - tPhaseShiftingTransformer = PhaseShiftingTransformer() - @test tPhaseShiftingTransformer isa PowerSystems.Component -end - -@testset "Product Constructors" begin - tProportionalReserve = ProportionalReserve() - @test tProportionalReserve isa PowerSystems.Service - tStaticReserve = StaticReserve() - @test tStaticReserve isa PowerSystems.Service -end diff --git a/test/data_14bus_pu.jl b/test/data_14bus_pu.jl new file mode 100644 index 0000000000..061175405f --- /dev/null +++ b/test/data_14bus_pu.jl @@ -0,0 +1,209 @@ +using TimeSeries +using Dates + + +dates = collect(DateTime("1/1/2024 0:00:00", "d/m/y H:M:S"):Hour(1):DateTime("1/1/2024 23:00:00", "d/m/y H:M:S")) + +nodes14= [ + Bus(1 , "Bus 1" , "REF" ,0.0, 1.06 , (min=0.94, max=1.06), 69), + Bus(2 , "Bus 2" , "PV" , -0.08691739674931762, 1.045 , (min=0.94, max=1.06), 69), + Bus(3 , "Bus 3" , "PV" , -0.22200588085367873, 1.01 , (min=0.94, max=1.06), 69), + Bus(4 , "Bus 4" , "PQ" , -0.18029251173101424, 1.019 , (min=0.94, max=1.06), 69), + Bus(5 , "Bus 5" , "PQ" , -0.15323990832510212, 1.02 , (min=0.94, max=1.06), 69), + Bus(6 , "Bus 6" , "PV" , -0.24818581963359368, 1.07 , (min=0.94, max=1.06), 13.8), + Bus(7 , "Bus 7" , "PQ" , -0.23335052099164186, 1.062 , (min=0.94, max=1.06), 13.8), + Bus(8 , "Bus 8" , "PV" , -0.2331759880664424, 1.09 , (min=0.94, max=1.06), 18), + Bus(9 , "Bus 9" , "PQ" , -0.2607521902479528, 1.056 , (min=0.94, max=1.06), 13.8), + Bus(10, "Bus 10" , "PQ" , -0.26354471705114374, 1.051 , (min=0.94, max=1.06), 13.8), + Bus(11, "Bus 11" , "PQ" , -0.2581341963699613, 1.057 , (min=0.94, max=1.06), 13.8), + Bus(12, "Bus 12" , "PQ" , -0.2630211182755455, 1.055 , (min=0.94, max=1.06), 13.8), + Bus(13, "Bus 13" , "PQ" , -0.2645919146023404, 1.05 , (min=0.94, max=1.06), 13.8), + Bus(14, "Bus 14" , "PQ" , -0.27995081201989047, 1.036 , (min=0.94, max=1.06), 13.8) + ] +branches14_dc = [ + Line("Line1", true, 0.0, 0.0, Arc(from=nodes14[1],to=nodes14[2]), 0.01938, 0.05917, (from=0.0264, to=0.0264), 18.046, 1.04), + Line("Line2", true, 0.0, 0.0, Arc(from=nodes14[1],to=nodes14[5]), 0.05403, 0.22304, (from=0.0246, to=0.0246), 4.896, 1.04), + HVDCLine("DCLine3", true, 0.0, Arc(from=nodes14[2],to=nodes14[3]), (min = -600.0, max = 600), (min = -600.0, max = 600), (min = -600.0, max = 600), (min = -600.0, max = 600), (l0 = 0.01, l1 = 0.001)), + HVDCLine("DCLine4", true, 0.0, Arc(from=nodes14[2],to=nodes14[4]), (min = -600.0, max = 600), (min = -600.0, max = 600), (min = -600.0, max = 600), (min = -600.0, max = 600), (l0 = 0.01, l1 = 0.001)), + #Line("Line3", true, 0.0, 0.0, Arc(from=nodes14[2],to=nodes14[3]), 0.04699, 0.19797, (from=0.0219, to=0.0219), 5.522, 1.04), + #Line("Line4", true, 0.0, 0.0, Arc(from=nodes14[2],to=nodes14[4]), 0.05811, 0.17632, (from=0.017, to=0.017), 6.052, 1.04), + Line("Line5", true, 0.0, 0.0, Arc(from=nodes14[2],to=nodes14[5]), 0.05695, 0.17388, (from=0.0173, to=0.0173), 6.140, 1.04), + Line("Line6", true, 0.0, 0.0, Arc(from=nodes14[3],to=nodes14[4]), 0.06701, 0.17103, (from=0.0064, to=0.0064), 6.116, 1.04), + Line("Line7", true, 0.0, 0.0, Arc(from=nodes14[4],to=nodes14[5]), 0.01335, 0.04211, (from=0.0, to=0.0), 25.434, 1.04), + TapTransformer("Trans3", true, 0.0, 0.0, Arc(from=nodes14[4],to=nodes14[7]), 0.0 , 0.20912, 0.0, 0.978, 20.0), + TapTransformer("Trans1", true, 0.0, 0.0, Arc(from=nodes14[4],to=nodes14[9]), 0.0 , 0.55618, 0.0, 0.969, 20.0), + TapTransformer("Trans2", true, 0.0, 0.0, Arc(from=nodes14[5],to=nodes14[6]), 0.0 , 0.25202, 0.0, 0.932, 20.0), + Line("Line8", true, 0.0, 0.0, Arc(from=nodes14[6],to=nodes14[11]), 0.09498, 0.19890, (from=0.0, to=0.0), 5.373, 1.04), + Line("Line9", true, 0.0, 0.0, Arc(from=nodes14[6],to=nodes14[12]), 0.12291, 0.25581, (from=0.0, to=0.0), 2.020, 1.04), + Line("Line10", true, 0.0, 0.0, Arc(from=nodes14[6],to=nodes14[13]), 0.06615, 0.13027, (from=0.0, to=0.0), 4.458, 1.04), + Transformer2W("Trans4", true, 0.0, 0.0, Arc(from=nodes14[7],to=nodes14[8]), 0.0 , 0.17615, 0.0, 20.0), + Line("Line16", true, 0.0, 0.0, Arc(from=nodes14[7],to=nodes14[9]), 0.0, 0.11001, (from=0.0, to=0.0), 12.444, 1.04), + Line("Line11", true, 0.0, 0.0, Arc(from=nodes14[9],to=nodes14[10]), 0.03181, 0.08450, (from=0.0, to=0.0), 5.097, 1.04), + Line("Line12", true, 0.0, 0.0, Arc(from=nodes14[9],to=nodes14[14]), 0.12711, 0.27038, (from=0.0, to=0.0), 3.959, 1.04), + Line("Line13", true, 0.0, 0.0, Arc(from=nodes14[10],to=nodes14[11]), 0.08205, 0.19207, (from=0.0, to=0.0), 7.690, 1.04), + Line("Line14", true, 0.0, 0.0, Arc(from=nodes14[12],to=nodes14[13]), 0.22092, 0.19988, (from=0.0, to=0.0), 6.378, 1.04), + Line("Line15", true, 0.0, 0.0, Arc(from=nodes14[13],to=nodes14[14]), 0.17093, 0.34802, (from=0.0, to=0.0), 10.213, 1.04) + ] + +branches14 = [ + Line("Line1", true, 0.0, 0.0, Arc(from=nodes14[1],to=nodes14[2]), 0.01938, 0.05917, (from=0.0264, to=0.0264), 18.046, 1.04), + Line("Line2", true, 0.0, 0.0, Arc(from=nodes14[1],to=nodes14[5]), 0.05403, 0.22304, (from=0.0246, to=0.0246), 4.896, 1.04), + Line("Line3", true, 0.0, 0.0, Arc(from=nodes14[2],to=nodes14[3]), 0.04699, 0.19797, (from=0.0219, to=0.0219), 5.522, 1.04), + Line("Line4", true, 0.0, 0.0, Arc(from=nodes14[2],to=nodes14[4]), 0.05811, 0.17632, (from=0.017, to=0.017), 6.052, 1.04), + Line("Line5", true, 0.0, 0.0, Arc(from=nodes14[2],to=nodes14[5]), 0.05695, 0.17388, (from=0.0173, to=0.0173), 6.140, 1.04), + Line("Line6", true, 0.0, 0.0, Arc(from=nodes14[3],to=nodes14[4]), 0.06701, 0.17103, (from=0.0064, to=0.0064), 6.116, 1.04), + Line("Line7", true, 0.0, 0.0, Arc(from=nodes14[4],to=nodes14[5]), 0.01335, 0.04211, (from=0.0, to=0.0), 25.434, 1.04), + TapTransformer("Trans3", true, 0.0, 0.0, Arc(from=nodes14[4],to=nodes14[7]), 0.0 , 0.20912, 0.0, 0.978, 20.0), + TapTransformer("Trans1", true, 0.0, 0.0, Arc(from=nodes14[4],to=nodes14[9]), 0.0 , 0.55618, 0.0, 0.969, 20.0), + TapTransformer("Trans2", true, 0.0, 0.0, Arc(from=nodes14[5],to=nodes14[6]), 0.0 , 0.25202, 0.0, 0.932, 20.0), + Line("Line8", true, 0.0, 0.0, Arc(from=nodes14[6],to=nodes14[11]), 0.09498, 0.19890, (from=0.0, to=0.0), 5.373, 1.04), + Line("Line9", true, 0.0, 0.0, Arc(from=nodes14[6],to=nodes14[12]), 0.12291, 0.25581, (from=0.0, to=0.0), 2.020, 1.04), + Line("Line10", true, 0.0, 0.0, Arc(from=nodes14[6],to=nodes14[13]), 0.06615, 0.13027, (from=0.0, to=0.0), 4.458, 1.04), + Transformer2W("Trans4", true, 0.0, 0.0, Arc(from=nodes14[7],to=nodes14[8]), 0.0 , 0.17615, 0.0, 20.0), + Line("Line16", true, 0.0, 0.0, Arc(from=nodes14[7],to=nodes14[9]), 0.0, 0.11001, (from=0.0, to=0.0), 12.444, 1.04), + Line("Line11", true, 0.0, 0.0, Arc(from=nodes14[9],to=nodes14[10]), 0.03181, 0.08450, (from=0.0, to=0.0), 5.097, 1.04), + Line("Line12", true, 0.0, 0.0, Arc(from=nodes14[9],to=nodes14[14]), 0.12711, 0.27038, (from=0.0, to=0.0), 3.959, 1.04), + Line("Line13", true, 0.0, 0.0, Arc(from=nodes14[10],to=nodes14[11]), 0.08205, 0.19207, (from=0.0, to=0.0), 7.690, 1.04), + Line("Line14", true, 0.0, 0.0, Arc(from=nodes14[12],to=nodes14[13]), 0.22092, 0.19988, (from=0.0, to=0.0), 6.378, 1.04), + Line("Line15", true, 0.0, 0.0, Arc(from=nodes14[13],to=nodes14[14]), 0.17093, 0.34802, (from=0.0, to=0.0), 10.213, 1.04) + ] + +thermal_generators14 = [ThermalStandard("Bus1", true, nodes14[1], 2.0, -0.169, + TechThermal(2.324, PowerSystems.ST, PowerSystems.COAL, (min=0.0, max=3.332), (min=0.0, max=0.1), nothing, nothing), + ThreePartCost((430.292599, 2000.0), 0.0, 0.0, 0.0) + ), + ThermalStandard("Bus2", true, nodes14[2], 0.40, 0.42, + TechThermal(1.4, PowerSystems.ST, PowerSystems.COAL, (min=0.0, max=1.40), (min=-0.4, max=0.5), nothing, nothing), + ThreePartCost((2500.0, 2000.0), 0.0, 0.0, 0.0) + ), + ThermalStandard("Bus3", true, nodes14[3], 0.0, 0.23, + TechThermal(1.0, PowerSystems.ST, PowerSystems.COAL, (min=0.0, max=1.0), (min=0.0, max=0.4), nothing, nothing), + ThreePartCost((100.0, 4000.0), 0.0, 0.0, 0.0) + ), + ThermalStandard("Bus6", true, nodes14[6], 0.0, 0.12, + TechThermal(1.0, PowerSystems.ST, PowerSystems.COAL, (min=0.0, max=1.0), (min=-0.06, max=0.24), nothing, nothing), + ThreePartCost((100.0, 4000.0), 0.0, 0.0, 0.0) + ), + ThermalStandard("Bus8", true, nodes14[8], 0.0, 0.174, + TechThermal(1.0, PowerSystems.ST, PowerSystems.COAL, (min=0.0, max=1.0), (min=-0.06, max=0.24), nothing, nothing), + ThreePartCost((100.0, 4000.0), 0.0, 0.0, 0.0) + ) + ]; + + +loadz1_ts = [ 0.792729978 + 0.723201574 + 0.710952098 + 0.677672816 + 0.668249175 + 0.67166919 + 0.687608809 + 0.711821241 + 0.756320618 + 0.7984057 + 0.827836527 + 0.840362459 + 0.84511032 + 0.834592803 + 0.822949221 + 0.816941743 + 0.824079963 + 0.905735139 + 0.989967048 + 1 + 0.991227765 + 0.960842114 + 0.921465115 + 0.837001437 ] + +loadz2_ts = [ 0.831093782 + 0.689863228 + 0.666058513 + 0.627033103 + 0.624901388 + 0.62858924 + 0.650734211 + 0.683424321 + 0.750876413 + 0.828347191 + 0.884248576 + 0.888523615 + 0.87752169 + 0.847534405 + 0.8227661 + 0.803809323 + 0.813282799 + 0.907575962 + 0.98679848 + 1 + 0.990489904 + 0.952520972 + 0.906611479 + 0.824307054] + +loadz3_ts = [ 0.871297342 + 0.670489749 + 0.642812243 + 0.630092987 + 0.652991383 + 0.671971681 + 0.716278493 + 0.770885833 + 0.810075243 + 0.85562361 + 0.892440566 + 0.910660449 + 0.922135467 + 0.898416969 + 0.879816542 + 0.896390855 + 0.978598576 + 0.96523761 + 1 + 0.969626503 + 0.901212601 + 0.81894251 + 0.771004923 + 0.717847996] + +loads14 = [PowerLoad("Bus2", true, nodes14[2], PowerSystems.ConstantPower, 0.217, 0.127, 0.217, 0.127), + PowerLoad("Bus3", true, nodes14[3], PowerSystems.ConstantPower, 0.942, 0.19, 0.942, 0.19), + PowerLoad("Bus4", true, nodes14[4], PowerSystems.ConstantPower, 0.478, -0.039, 0.478, -0.039), + PowerLoad("Bus5", true, nodes14[5], PowerSystems.ConstantPower, 0.076, 0.016, 0.076, 0.016), + PowerLoad("Bus6", true, nodes14[6], PowerSystems.ConstantPower, 0.112, 0.075, 0.112, 0.075), + PowerLoad("Bus9", true, nodes14[9], PowerSystems.ConstantPower, 0.295, 0.166, 0.295, 0.166), + PowerLoad("Bus10", true, nodes14[10], PowerSystems.ConstantPower, 0.09, 0.058, 0.09, 0.058), + PowerLoad("Bus11", true, nodes14[11], PowerSystems.ConstantPower, 0.035, 0.018, 0.035, 0.018), + PowerLoad("Bus12", true, nodes14[12], PowerSystems.ConstantPower, 0.061, 0.016, 0.061, 0.016), + PowerLoad("Bus13", true, nodes14[13], PowerSystems.ConstantPower, 0.135, 0.058, 0.135, 0.058), + PowerLoad("Bus14", true, nodes14[14], PowerSystems.ConstantPower, 0.149, 0.050, 0.149, 0.050)] + +forecast_DA14 = [Deterministic(loads14[1], "scalingfactor", TimeArray(dates, loadz1_ts)), + Deterministic(loads14[2], "scalingfactor", TimeArray(dates, loadz1_ts)), + Deterministic(loads14[3], "scalingfactor", TimeArray(dates, loadz3_ts)), + Deterministic(loads14[4], "scalingfactor", TimeArray(dates, loadz1_ts)), + Deterministic(loads14[5], "scalingfactor", TimeArray(dates, loadz2_ts)), + Deterministic(loads14[6], "scalingfactor", TimeArray(dates, loadz3_ts)), + Deterministic(loads14[7], "scalingfactor", TimeArray(dates, loadz2_ts)), + Deterministic(loads14[8], "scalingfactor", TimeArray(dates, loadz2_ts)), + Deterministic(loads14[9], "scalingfactor", TimeArray(dates, loadz2_ts)), + Deterministic(loads14[10], "scalingfactor", TimeArray(dates, loadz2_ts)), + Deterministic(loads14[11], "scalingfactor", TimeArray(dates, loadz2_ts)) +]; + +forecasts14 = Dict{Symbol,Vector{<:Forecast}}(:DA=>forecast_DA14); + +battery14 = [GenericBattery(name = "Bat", +primemover = PowerSystems.BA, +available = true, +bus = nodes14[1], +energy = 5.0, +capacity = (min = 5.0, max = 100.0), +rating = 70, +activepower = 10.0, +inputactivepowerlimits = (min = 0.0, max = 50.0), +outputactivepowerlimits = (min = 0.0, max = 50.0), +reactivepower = 0.0, +reactivepowerlimits = (min = -50.0, max = 50.0), +efficiency = (in = 0.80, out = 0.90) +)] diff --git a/test/data_5bus_pu.jl b/test/data_5bus_pu.jl new file mode 100644 index 0000000000..2c87966c30 --- /dev/null +++ b/test/data_5bus_pu.jl @@ -0,0 +1,242 @@ +using TimeSeries +using Dates +using Random + +DayAhead = collect(DateTime("1/1/2024 0:00:00", "d/m/y H:M:S"):Hour(1):DateTime("1/1/2024 23:00:00", "d/m/y H:M:S")) +#Dispatch_11am = collect(DateTime("1/1/2024 0:11:00", "d/m/y H:M:S"):Minute(15):DateTime("1/1/2024 12::00", "d/m/y H:M:S")) + +nodes5 = [Bus(1,"nodeA", "PV", 0, 1.0, (min = 0.9, max=1.05), 230), + Bus(2,"nodeB", "PQ", 0, 1.0, (min = 0.9, max=1.05), 230), + Bus(3,"nodeC", "PV", 0, 1.0, (min = 0.9, max=1.05), 230), + Bus(4,"nodeD", "REF", 0, 1.0, (min = 0.9, max=1.05), 230), + Bus(5,"nodeE", "PV", 0, 1.0, (min = 0.9, max=1.05), 230), + ]; + +branches5_dc = [Line("1", true, 0.0, 0.0, Arc(from=nodes5[1],to=nodes5[2]), 0.00281, 0.0281, (from=0.00356, to=0.00356), 2.0, (min = -0.7, max = 0.7)), + HVDCLine("DCL2", true, 0.0, Arc(from=nodes5[1],to=nodes5[4]), (min=-3000.0, max=3000.0), (min=-3000, max=3000), (min=-3000.0, max=3000.0), (min=-3000.0, max=3000.0), (l0=0.0, l1=0.01)), + Line("3", true, 0.0, 0.0, Arc(from=nodes5[1],to=nodes5[5]), 0.00064, 0.0064, (from=0.01563, to=0.01563), 18.8120, (min = -0.7, max = 0.7)), + Line("4", true, 0.0, 0.0, Arc(from=nodes5[2],to=nodes5[3]), 0.00108, 0.0108, (from=0.00926, to=0.00926), 11.1480, (min = -0.7, max = 0.7)), + Line("5", true, 0.0, 0.0, Arc(from=nodes5[3],to=nodes5[4]), 0.00297, 0.0297, (from=0.00337, to=0.00337), 40.530, (min = -0.7, max = 0.7)), + Line("6", true, 0.0, 0.0, Arc(from=nodes5[4],to=nodes5[5]), 0.00297, 0.0297, (from=0.00337, to=00.00337), 2.00, (min = -0.7, max = 0.7)) +]; + +branches5 = [Line("1", true, 0.0, 0.0, Arc(from=nodes5[1],to=nodes5[2]), 0.00281, 0.0281, (from=0.00356, to=0.00356), 2.0, (min = -0.7, max = 0.7)), + Line("2", true, 0.0, 0.0, Arc(from=nodes5[1],to=nodes5[4]), 0.00304, 0.0304, (from=0.00329, to=0.00329), 2.0, (min = -0.7, max = 0.7)), + Line("3", true, 0.0, 0.0, Arc(from=nodes5[1],to=nodes5[5]), 0.00064, 0.0064, (from=0.01563, to=0.01563), 18.8120, (min = -0.7, max = 0.7)), + Line("4", true, 0.0, 0.0, Arc(from=nodes5[2],to=nodes5[3]), 0.00108, 0.0108, (from=0.00926, to=0.00926), 11.1480, (min = -0.7, max = 0.7)), + Line("5", true, 0.0, 0.0, Arc(from=nodes5[3],to=nodes5[4]), 0.00297, 0.0297, (from=0.00337, to=0.00337), 40.530, (min = -0.7, max = 0.7)), + Line("6", true, 0.0, 0.0, Arc(from=nodes5[4],to=nodes5[5]), 0.00297, 0.0297, (from=0.00337, to=00.00337), 2.00, (min = -0.7, max = 0.7)) +]; + +branches5_ml = [MonitoredLine("1", true, 0.0, 0.0, Arc(from=nodes5[1],to=nodes5[2]), 0.00281, 0.0281, (from=0.00356, to=0.00356), (from_to=1.0, to_from=1.0), 2.0, (min = -0.7, max = 0.7)), + Line("2", true, 0.0, 0.0, Arc(from=nodes5[1],to=nodes5[4]), 0.00304, 0.0304, (from=0.00329, to=0.00329), 2.0, (min = -0.7, max = 0.7)), + Line("3", true, 0.0, 0.0, Arc(from=nodes5[1],to=nodes5[5]), 0.00064, 0.0064, (from=0.01563, to=0.01563), 18.8120, (min = -0.7, max = 0.7)), + Line("4", true, 0.0, 0.0, Arc(from=nodes5[2],to=nodes5[3]), 0.00108, 0.0108, (from=0.00926, to=0.00926), 11.1480, (min = -0.7, max = 0.7)), + Line("5", true, 0.0, 0.0, Arc(from=nodes5[3],to=nodes5[4]), 0.00297, 0.0297, (from=0.00337, to=0.00337), 40.530, (min = -0.7, max = 0.7)), + Line("6", true, 0.0, 0.0, Arc(from=nodes5[4],to=nodes5[5]), 0.00297, 0.0297, (from=0.00337, to=00.00337), 2.00, (min = -0.7, max = 0.7)) + ]; + +solar_ts_DA = [0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0.351105684 + 0.632536266 + 0.99463925 + 1 + 0.944237283 + 0.396681234 + 0.366511428 + 0.155125829 + 0.040872694 + 0 + 0 + 0 + 0 + 0 + 0] + +wind_ts_DA = [0.985205412 + 0.991791369 + 0.997654144 + 1 + 0.998663733 + 0.995497149 + 0.992414567 + 0.98252418 + 0.957203427 + 0.927650911 + 0.907181989 + 0.889095913 + 0.848186718 + 0.766813846 + 0.654052531 + 0.525336131 + 0.396098004 + 0.281771509 + 0.197790004 + 0.153241012 + 0.131355854 + 0.113688144 + 0.099302656 + 0.069569628] + +thermal_generators5 = [ThermalStandard("Alta", true, nodes5[1], 0.40, 0.010, + TechThermal(0.5, PowerSystems.ST, PowerSystems.COAL, (min=0.0, max=0.40), (min = -0.30, max = 0.30), nothing, nothing), + ThreePartCost((0.0, 1400.0), 0.0, 4.0, 2.0) + ), + ThermalStandard("Park City", true, nodes5[1], 1.70, 0.20, + TechThermal(2.2125, PowerSystems.ST, PowerSystems.COAL, (min=0.0, max=1.70), (min =-1.275, max=1.275), (up=0.02, down=0.02), (up=2.0, down=1.0)), + ThreePartCost((0.0, 1500.0), 0.0, 1.5, 0.75) + ), + ThermalStandard("Solitude", true, nodes5[3], 5.2, 1.00, + TechThermal(5.20, PowerSystems.ST, PowerSystems.COAL, (min=0.0, max=5.20), (min =-3.90, max=3.90), (up=0.012, down=0.012), (up=3.0, down=2.0)), + ThreePartCost((0.0, 3000.0), 0.0, 3.0, 1.5) + ), + ThermalStandard("Sundance", true, nodes5[4], 2.0, 0.40, + TechThermal(2.5, PowerSystems.ST, PowerSystems.COAL, (min=0.0, max=2.0), (min =-1.5, max=1.5), (up=0.015, down=0.015), (up=2.0, down=1.0)), + ThreePartCost((0.0, 4000.0), 0.0, 4.0, 2.0) + ), + ThermalStandard("Brighton", true, nodes5[5], 6.0, 1.50, + TechThermal(7.5, PowerSystems.ST, PowerSystems.COAL, (min=0.0, max=6.0), (min =-4.50, max=4.50), (up=0.015, down=0.015), (up=5.0, down=3.0)), + ThreePartCost((0.0, 1000.0), 0.0, 1.5, 0.75) + )]; + +renewable_generators5 = [RenewableDispatch("WindBusA", true, nodes5[5], 0.0, 0.0, PowerSystems.WT, 1.200, TwoPartCost(22.0, 0.0)), + RenewableDispatch("WindBusB", true, nodes5[4], 0.0, 0.0, PowerSystems.WT, 1.200, TwoPartCost(22.0, 0.0)), + RenewableDispatch("WindBusC", true, nodes5[3], 0.0, 0.0, TechRenewable(1.20, PowerSystems.WT, (min = -0.800, max = 0.800), 1.0), TwoPartCost(22.0, 0.0))]; + + + +hydro_generators5 = [ + HydroFix("HydroFix", true, nodes5[2], 0.0, 0.0, + TechHydro(0.600, PowerSystems.HY, (min = 0.0, max = 60.0), (min = 0.0, max = 60.0), nothing, nothing) + ), + HydroDispatch("HydroDispatch", true, nodes5[3], 0.0, 0.0, + TechHydro(0.600, PowerSystems.HY, (min = 0.0, max = 60.0), (min = 0.0, max = 60.0), (up = 10.0, down = 10.0), nothing), + TwoPartCost(15.0, 0.0)) + ]; + +battery5 = [GenericBattery(name = "Bat", + primemover = PowerSystems.BA, + available = true, + bus = nodes5[1], + energy = 5.0, + capacity = (min = 5.0, max = 100.0), + rating = 70, + activepower = 10.0, + inputactivepowerlimits = (min = 0.0, max = 50.0), + outputactivepowerlimits = (min = 0.0, max = 50.0), + reactivepower = 0.0, + reactivepowerlimits = (min = -50.0, max = 50.0), + efficiency = (in = 0.80, out = 0.90), + )]; + +loadbus2_ts_DA = [ 0.792729978 + 0.723201574 + 0.710952098 + 0.677672816 + 0.668249175 + 0.67166919 + 0.687608809 + 0.711821241 + 0.756320618 + 0.7984057 + 0.827836527 + 0.840362459 + 0.84511032 + 0.834592803 + 0.822949221 + 0.816941743 + 0.824079963 + 0.905735139 + 0.989967048 + 1 + 0.991227765 + 0.960842114 + 0.921465115 + 0.837001437 ] + +loadbus3_ts_DA = [ 0.831093782 + 0.689863228 + 0.666058513 + 0.627033103 + 0.624901388 + 0.62858924 + 0.650734211 + 0.683424321 + 0.750876413 + 0.828347191 + 0.884248576 + 0.888523615 + 0.87752169 + 0.847534405 + 0.8227661 + 0.803809323 + 0.813282799 + 0.907575962 + 0.98679848 + 1 + 0.990489904 + 0.952520972 + 0.906611479 + 0.824307054] + +loadbus4_ts_DA = [ 0.871297342 + 0.670489749 + 0.642812243 + 0.630092987 + 0.652991383 + 0.671971681 + 0.716278493 + 0.770885833 + 0.810075243 + 0.85562361 + 0.892440566 + 0.910660449 + 0.922135467 + 0.898416969 + 0.879816542 + 0.896390855 + 0.978598576 + 0.96523761 + 1 + 0.969626503 + 0.901212601 + 0.81894251 + 0.771004923 + 0.717847996] + +loads5 = [ PowerLoad("Bus2", true, nodes5[2], PowerSystems.ConstantPower, 3.0, 0.9861, 3.0, 0.9861), + PowerLoad("Bus3", true, nodes5[3], PowerSystems.ConstantPower, 3.0, 0.9861, 3.0, 0.9861), + PowerLoad("Bus4", true, nodes5[4], PowerSystems.ConstantPower, 4.0, 1.3147, 4.0, 1.3147), + ]; + +interruptible = [InterruptibleLoad("IloadBus4", true, nodes5[4], PowerSystems.ConstantPower, 0.10, 0.0, 0.10, 0.0, TwoPartCost(150.0, 2400.0))] +Iload_forecast = [Deterministic(interruptible[1], "scalingfactor", TimeArray(DayAhead, loadbus4_ts_DA)), + Deterministic(interruptible[1], "scalingfactor", TimeArray(DayAhead+Day(1), loadbus4_ts_DA + 0.1*rand(24))),] + +reserve5 = StaticReserve("test_reserve", thermal_generators5, 0.6, maximum([gen.tech.activepowerlimits[:max] for gen in thermal_generators5])) + +load_forecast_DA = [Deterministic(loads5[1], "scalingfactor", TimeArray(DayAhead, loadbus2_ts_DA)), + Deterministic(loads5[2], "scalingfactor", TimeArray(DayAhead, loadbus3_ts_DA)), + Deterministic(loads5[3], "scalingfactor", TimeArray(DayAhead, loadbus4_ts_DA)), + Deterministic(loads5[1], "scalingfactor", TimeArray(DayAhead+Day(1), rand(24)*0.1 + loadbus2_ts_DA)), + Deterministic(loads5[2], "scalingfactor", TimeArray(DayAhead+Day(1), rand(24)*0.1 + loadbus3_ts_DA)), + Deterministic(loads5[3], "scalingfactor", TimeArray(DayAhead+Day(1), rand(24)*0.1 + loadbus4_ts_DA))] + +ren_forecast_DA = [Deterministic(renewable_generators5[1], "scalingfactor", TimeSeries.TimeArray(DayAhead,solar_ts_DA)), + Deterministic(renewable_generators5[2], "scalingfactor", TimeSeries.TimeArray(DayAhead,wind_ts_DA)), + Deterministic(renewable_generators5[3], "scalingfactor", TimeSeries.TimeArray(DayAhead,wind_ts_DA)), + Deterministic(renewable_generators5[1], "scalingfactor", TimeSeries.TimeArray(DayAhead + Day(1), rand(24)*0.1 + solar_ts_DA)), + Deterministic(renewable_generators5[2], "scalingfactor", TimeSeries.TimeArray(DayAhead + Day(1), rand(24)*0.1 + wind_ts_DA)), + Deterministic(renewable_generators5[3], "scalingfactor", TimeSeries.TimeArray(DayAhead + Day(1), rand(24)*0.1 + wind_ts_DA)) + ]; + +hydro_forecast_DA = [Deterministic(hydro_generators5[1], "scalingfactor", TimeSeries.TimeArray(DayAhead,wind_ts_DA)), + Deterministic(hydro_generators5[2], "scalingfactor", TimeSeries.TimeArray(DayAhead,wind_ts_DA))] diff --git a/test/parse_matpower.jl b/test/parse_matpower.jl deleted file mode 100644 index c4544ec93c..0000000000 --- a/test/parse_matpower.jl +++ /dev/null @@ -1,47 +0,0 @@ -# TODO: Reviewers: Is this a correct list of keys to verify? -POWER_MODELS_KEYS = [ - "baseMVA", - "branch", - "bus", - "dcline", - "gen", - "load", - "name", - "per_unit", - "shunt", - "source_type", - "source_version", - "storage", -] - -@testset "Parse Matpower data files" begin - files = [x for x in readdir(joinpath(MATPOWER_DIR)) if splitext(x)[2] == ".m"] - if length(files) == 0 - @error "No test files in the folder" - end - - for f in files - @info "Parsing $f..." - path = joinpath(MATPOWER_DIR, f) - - pm_dict = PowerSystems.parse_file(path) - for key in POWER_MODELS_KEYS - @test haskey(pm_dict, key) - end - @info "Successfully parsed $path to PowerModels dict" - - PowerSystems.make_mixed_units(pm_dict) - @info "Successfully converted $path to mixed_units" - - ps_dict = PowerSystems.pm2ps_dict(pm_dict) - @info "Successfully parsed $path to PowerSystems dict" - - Buses, Generators, Storages, Branches, Loads, LoadZones, Shunts, Services = - PowerSystems.ps_dict2ps_struct(ps_dict) - @info "Successfully parsed $path to PowerSystems devices" - - sys_test = System(Buses, Generators, Loads, Branches, Storages, - float(ps_dict["baseMVA"])) - @info "Successfully parsed $path to System struct" - end -end diff --git a/test/parse_psse.jl b/test/parse_psse.jl deleted file mode 100644 index 04ec3415b4..0000000000 --- a/test/parse_psse.jl +++ /dev/null @@ -1,27 +0,0 @@ -@time @testset "PSSE Parsing " begin - files = readdir(PSSE_RAW_DIR) - if length(files) == 0 - error("No test files in the folder") - end - - for f in files[1:1] - @info "Parsing $f ..." - pm_dict = PowerSystems.parse_file(joinpath(PSSE_RAW_DIR, f)) - @info "Successfully parsed $f to PowerModels dict" - PowerSystems.make_mixed_units(pm_dict) - @info "Successfully converted $f to mixed_units" - ps_dict = PowerSystems.pm2ps_dict(pm_dict) - @info "Successfully parsed $f to PowerSystems dict" - Buses, Generators, Storage, Branches, Loads, LoadZones, Shunts = - PowerSystems.ps_dict2ps_struct(ps_dict) - @info "Successfully parsed $f to PowerSystems devices" - sys_test = System(Buses, Generators, Loads, Branches, Storage, - float(ps_dict["baseMVA"])) # TODO: Add DClines, Shunts - @info "Successfully parsed $f to System struct" - end - - # Test bad input - pm_dict = PowerSystems.parse_file(joinpath(PSSE_RAW_DIR, files[1])) - pm_dict["bus"] = Dict{String, Any}() - @test_throws PowerSystems.DataFormatError PowerSystems.pm2ps_dict(pm_dict) -end diff --git a/test/powersystemconstructors.jl b/test/powersystemconstructors.jl deleted file mode 100644 index 0f57cfbb21..0000000000 --- a/test/powersystemconstructors.jl +++ /dev/null @@ -1,62 +0,0 @@ -import TimeSeries - -include("../data/data_5bus.jl") -include("../data/data_14bus.jl") - - -@testset "Test System constructors" begin - tPowerSystem = System() - - sys5 = System(nodes5, generators5, loads5_DA, nothing, nothing, 1000.0) - sys5 = System(nodes5, generators5, loads5_DA, branches5, nothing, 1000.0) - - battery5=[GenericBattery(name="Bat", - status=true, - bus=nodes5[2], - activepower=10.0, - energy=5.0, - capacity=(min=0.0, max=0.0), - inputactivepowerlimits=(min=0.0, max=50.0), - outputactivepowerlimits=(min=0.0, max=50.0), - efficiency=(in=0.90, out=0.80), - )] - - sys5b = System(nodes5, generators5, loads5_DA, nothing, battery5, 1000.0) - - generators_hg5 = [ - HydroFix("HydroFix", true, nodes5[2], - TechHydro(60.0, 15.0, (min=0.0, max=60.0), nothing, nothing, nothing, - nothing), - TimeSeries.TimeArray(DayAhead, solar_ts_DA) - ), - HydroCurtailment("HydroCurtailment", true, nodes5[3], - TechHydro(60.0, 10.0, (min=0.0, max=60.0), nothing, nothing, - (up=10.0, down=10.0), nothing), - 1000.0, TimeSeries.TimeArray(DayAhead, wind_ts_DA)) - ] - - sys5bh = System(nodes5, append!(generators5, generators_hg5), loads5_DA, branches5, - battery5, 1000.0) - - #Test Data for 14 Bus - - sys14 = System(nodes14, generators14, loads14, nothing, nothing, 1000.0) - sys14 = System(nodes14, generators14, loads14, branches14, nothing, 1000.0) - - battery14 = [GenericBattery(name="Bat", - status=true, - bus=nodes14[2], - activepower=10.0, - energy=5.0, - capacity=(min=0.0, max=0.0), - inputactivepowerlimits=(min=0.0, max=50.0), - outputactivepowerlimits=(min=0.0, max=50.0), - efficiency=(in=0.90, out=0.80), - )] - - sys14b = System(nodes14, generators14, loads14, nothing, battery14, 1000.0) - sys14b = System(nodes14, generators14, loads14, branches14, battery14, 1000.0) - - ps_dict = PowerSystems.parsestandardfiles(joinpath(MATPOWER_DIR, "case5_re.m")) - sys = PowerSystems.System(ps_dict) -end diff --git a/test/printing.jl b/test/printing.jl deleted file mode 100644 index 43f5fbcd21..0000000000 --- a/test/printing.jl +++ /dev/null @@ -1,61 +0,0 @@ - -base_dir = string(dirname(dirname(@__FILE__))) -@info joinpath(base_dir,"data/data_5bus.jl") -include(joinpath(base_dir,"data/data_5bus.jl")) - - -# test printing of System type -sys5 = System(nodes5, generators5, loads5_DA, branches5, nothing, 100.0); -# short output -@test repr(sys5) == "System(buses:5,GenClasses(T:5,R:2,H:0),loads:4,branches:6,nothing)" -# long output -io = IOBuffer() -show(io, "text/plain", sys5) -@test String(take!(io)) == - "System:\n buses: Bus[Bus(name=\"nodeA\"), Bus(name=\"nodeB\"), Bus(name=\"nodeC\"), Bus(name=\"nodeD\"), Bus(name=\"nodeE\")]\n generators: \n GenClasses(T:5,R:2,H:0):\n thermal: ThermalDispatch[ThermalDispatch(name=\"Alta\"), ThermalDispatch(name=\"Park City\"), ThermalDispatch(name=\"Solitude\"), ThermalDispatch(name=\"Sundance\"), ThermalDispatch(name=\"Brighton\")]\n renewable: RenewableGen[RenewableFix(name=\"SolarBusC\"), RenewableCurtailment(name=\"WindBusA\")]\n hydro: nothing\n (end generators)\n loads: ElectricLoad[PowerLoad(name=\"Bus2\"), PowerLoad(name=\"Bus3\"), PowerLoad(name=\"Bus4\"), InterruptibleLoad(name=\"IloadBus4\")]\n branches: Line[Line(name=\"1\"), Line(name=\"2\"), Line(name=\"3\"), Line(name=\"4\"), Line(name=\"5\"), Line(name=\"6\")]\n storage: nothing\n basepower: 100.0\n time_periods: 24" - - -# test printing of a few component types - -# bus -@test repr(sys5.buses[1]) == "Bus(name=\"nodeA\")" -show(io, "text/plain", sys5.buses[1]) -@test String(take!(io)) == - "Bus:\n number: 1\n name: nodeA\n bustype: PV\n angle: 0.0\n voltage: 1.0\n voltagelimits: (min = 0.9, max = 1.05)\n basevoltage: 230.0" - -# generators -@test repr(sys5.generators) == "GenClasses(T:5,R:2,H:0)" -show(io, "text/plain", sys5.generators) -@test String(take!(io)) == - "GenClasses(T:5,R:2,H:0):\n thermal: ThermalDispatch[ThermalDispatch(name=\"Alta\"), ThermalDispatch(name=\"Park City\"), ThermalDispatch(name=\"Solitude\"), ThermalDispatch(name=\"Sundance\"), ThermalDispatch(name=\"Brighton\")]\n renewable: RenewableGen[RenewableFix(name=\"SolarBusC\"), RenewableCurtailment(name=\"WindBusA\")]\n hydro: nothing" - -# Generator -@test repr(sys5.generators.thermal[1]) == "ThermalDispatch(name=\"Alta\")" -show(io, "text/plain", sys5.generators.thermal[1]) -@test String(take!(io)) == - "ThermalDispatch:\n name: Alta\n available: true\n bus: Bus(name=\"nodeA\")\n tech: TechThermal\n econ: EconThermal" - -# generator technology -@test repr(sys5.generators.thermal[1].tech) == "TechThermal" -show(io, "text/plain", sys5.generators.thermal[1].tech) -@test String(take!(io)) == - "TechThermal:\n activepower: 40.0\n activepowerlimits: (min = 0.0, max = 40.0)\n reactivepower: 10.0\n reactivepowerlimits: (min = -30.0, max = 30.0)\n ramplimits: nothing\n timelimits: nothing" - -# load -@test repr(sys5.loads[1]) == "PowerLoad(name=\"Bus2\")" -show(io, "text/plain", sys5.branches[1]) -@test String(take!(io)) == - "Line:\n name: 1\n available: true\n connectionpoints: (from = Bus(name=\"nodeA\"), to = Bus(name=\"nodeB\"))\n r: 0.00281\n x: 0.0281\n b: (from = 0.00356, to = 0.00356)\n rate: 38.038742043967325\n anglelimits: (min = -0.7853981633974483, max = 0.7853981633974483)" - -# scalingfactor (a TimeArray) -@test_skip repr(sys5.loads[1].scalingfactor) == - "24×1 TimeArray{Float64,1,DateTime,Array{Float64,1}} 2024-01-01T00:00:00 to 2024-01-01T23:00:00\n" -show(io, "text/plain", sys5.loads[1].scalingfactor) -@test_skip String(take!(io)) == - "24×1 TimeArray{Float64,1,DateTime,Array{Float64,1}} 2024-01-01T00:00:00 to 2024-01-01T23:00:00\n│ │ A │\n├─────────────────────┼────────┤\n│ 2024-01-01T00:00:00 │ 0.7927 │\n│ 2024-01-01T01:00:00 │ 0.7232 │\n│ 2024-01-01T02:00:00 │ 0.711 │\n│ 2024-01-01T03:00:00 │ 0.6777 │\n│ 2024-01-01T04:00:00 │ 0.6682 │\n│ 2024-01-01T05:00:00 │ 0.6717 │\n│ 2024-01-01T06:00:00 │ 0.6876 │\n│ 2024-01-01T07:00:00 │ 0.7118 │\n│ 2024-01-01T08:00:00 │ 0.7563 │\n ⋮\n│ 2024-01-01T16:00:00 │ 0.8241 │\n│ 2024-01-01T17:00:00 │ 0.9057 │\n│ 2024-01-01T18:00:00 │ 0.99 │\n│ 2024-01-01T19:00:00 │ 1.0 │\n│ 2024-01-01T20:00:00 │ 0.9912 │\n│ 2024-01-01T21:00:00 │ 0.9608 │\n│ 2024-01-01T22:00:00 │ 0.9215 │\n│ 2024-01-01T23:00:00 │ 0.837 │" - -# line -@test repr(sys5.branches[1]) == "Line(name=\"1\")" -show(io, "text/plain", sys5.branches[1]) -@test_skip String(take!(io)) == - "Line:\n name: 1\n available: true\n connectionpoints: (from = Bus(name=\"nodeA\"), to = Bus(name=\"nodeB\"))\n r: 0.00281\n x: 0.0281\n b: (from = 0.00356, to = 0.00356)\n rate: 38.038742043967325\n anglelimits: (min = -0.7853981633974483, max = 0.7853981633974483)" diff --git a/test/readforecastdata.jl b/test/readforecastdata.jl deleted file mode 100644 index 32af09c4c8..0000000000 --- a/test/readforecastdata.jl +++ /dev/null @@ -1,46 +0,0 @@ -@testset "Forecast data" begin - ps_dict = PowerSystems.parsestandardfiles(joinpath(MATPOWER_DIR, "case5_re.m")) - da_time_series = PowerSystems.read_data_files(joinpath(FORECASTS_DIR, "5bus_ts"); - REGEX_FILE=r"da_(.*?)\.csv") - rt_time_series = PowerSystems.read_data_files(joinpath(FORECASTS_DIR, "5bus_ts"); - REGEX_FILE=r"rt_(.*?)\.csv") - ps_dict = PowerSystems.assign_ts_data(ps_dict,rt_time_series) - - buses, generators, storage, branches, loads, loadZones, shunts, services = - PowerSystems.ps_dict2ps_struct(ps_dict) - sys_5 = PowerSystems.System(buses, generators, loads, branches, storage, - ps_dict["baseMVA"]) - - forecast_gen = PowerSystems.make_forecast_dict(da_time_series["gen"], - Dates.Day(1), 24, generators) - forecast_load = PowerSystems.make_forecast_dict(da_time_series, Dates.Day(1), 24, loads) - forecast_gen = PowerSystems.make_forecast_dict(da_time_series["gen"], Dates.Day(1), 24, - generators) - forecast_load = PowerSystems.make_forecast_dict(da_time_series, Dates.Day(1), 24, loads) - forecast_gen = PowerSystems.make_forecast_dict(da_time_series["gen"], Dates.Day(1), 24, - generators); - forecast_load = PowerSystems.make_forecast_dict(da_time_series, Dates.Day(1), 24, - loads); - - - ps_dict = PowerSystems.parsestandardfiles(joinpath(MATPOWER_DIR, "RTS_GMLC.m")) - da_time_series = PowerSystems.read_data_files(joinpath(FORECASTS_DIR, - "RTS_GMLC_forecasts"); - REGEX_FILE=r"DAY_AHEAD(.*?)\.csv") - rt_time_series = PowerSystems.read_data_files(joinpath(FORECASTS_DIR, - "RTS_GMLC_forecasts"); - REGEX_FILE=r"REAL_TIME(.*?)\.csv") - ps_dict = PowerSystems.assign_ts_data(ps_dict,rt_time_series) - - buses, generators, storage, branches, loads, loadZones, shunts, services = - PowerSystems.ps_dict2ps_struct(ps_dict) - sys_5 = PowerSystems.System(buses, generators, loads, branches, storage, - ps_dict["baseMVA"]) - - forecast_gen = PowerSystems.make_forecast_dict(da_time_series["gen"], - Dates.Day(1), 24, generators); - forecast_load = PowerSystems.make_forecast_dict(rt_time_series, Dates.Day(1), - 288, loads, loadZones) - - forecast_struct = PowerSystems.make_forecast_array(forecast_load) -end diff --git a/test/readnetworkdata.jl b/test/readnetworkdata.jl deleted file mode 100644 index 7fa33b66a2..0000000000 --- a/test/readnetworkdata.jl +++ /dev/null @@ -1,13 +0,0 @@ - -base_dir = string(dirname(dirname(@__FILE__))) -@info joinpath(base_dir,"data/data_5bus.jl") -@test try include(joinpath(base_dir,"data/data_5bus.jl")); true finally end -@info joinpath(base_dir,"data/data_5bus_uc.jl") -@test try include(joinpath(base_dir,"data/data_5bus_uc.jl")); true finally end -@info joinpath(base_dir,"data/data_5bus_dc.jl") -@test try include(joinpath(base_dir,"data/data_5bus_dc.jl")); true finally end - -@info joinpath(base_dir,"data/data_14bus.jl") -@test try include(joinpath(base_dir,"data/data_14bus.jl")); true finally end - - diff --git a/test/runtests.jl b/test/runtests.jl index 631e6cd38c..01660edfc0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,7 +2,12 @@ using Test using Logging using Dates +import InfrastructureSystems +import InfrastructureSystems: Deterministic, Probabilistic, ScenarioBased, Forecast +const IS = InfrastructureSystems using PowerSystems +import PowerSystems: PowerSystemTableData +const PSY = PowerSystems BASE_DIR = abspath(joinpath(dirname(Base.find_package("PowerSystems")), "..")) @@ -22,6 +27,8 @@ LOG_LEVELS = Dict( "Error" => Logging.Error, ) +include("common.jl") + """ Copied @includetests from https://github.com/ssfrr/TestSetExtensions.jl. @@ -48,7 +55,10 @@ macro includetests(testarg...) rootfile = @__FILE__ if length(tests) == 0 tests = readdir(dirname(rootfile)) - tests = filter(f->endswith(f, ".jl") && f != basename(rootfile), tests) + tests = filter(f -> startswith(f, "test_") && + endswith(f, ".jl") && + f != basename(rootfile), + tests) else tests = map(f->string(f, ".jl"), tests) end @@ -76,10 +86,10 @@ function run_tests() console_logger = ConsoleLogger(stderr, console_level) file_level = get_logging_level("PS_LOG_LEVEL", "Info") - open_file_logger(LOG_FILE, file_level) do file_logger + IS.open_file_logger(LOG_FILE, file_level) do file_logger levels = (Logging.Info, Logging.Warn, Logging.Error) - multi_logger = MultiLogger([console_logger, file_logger], - LogEventTracker(levels)) + multi_logger = IS.MultiLogger([console_logger, file_logger], + IS.LogEventTracker(levels)) global_logger(multi_logger) # Testing Topological components of the schema @@ -90,7 +100,7 @@ function run_tests() # TODO: once all known error logs are fixed, add this test: #@test length(get_log_events(multi_logger.tracker, Logging.Error)) == 0 - @info report_log_summary(multi_logger) + @info IS.report_log_summary(multi_logger) end end diff --git a/test/branchchecks_testing.jl b/test/test_branchchecks_testing.jl similarity index 52% rename from test/branchchecks_testing.jl rename to test/test_branchchecks_testing.jl index 2ceea93d72..6da7002970 100644 --- a/test/branchchecks_testing.jl +++ b/test/test_branchchecks_testing.jl @@ -14,41 +14,40 @@ end @testset "Angle limits" begin nodes5 = [ - Bus(1, "nodeA", "PV", 0, 1.0, (min = 0.9, max=1.05), 230), - Bus(2, "nodeB", "PQ", 0, 1.0, (min = 0.9, max=1.05), 230), - Bus(3, "nodeC", "PV", 0, 1.0, (min = 0.9, max=1.05), 230), - Bus(4, "nodeD", "SF", 0, 1.0, (min = 0.9, max=1.05), 230), - Bus(5, "nodeE", "PV", 0, 1.0, (min = 0.9, max=1.05), 230), + Bus(1, "nodeA", PowerSystems.PV::BusType, 0, 1.0, (min = 0.9, max=1.05), 230), + Bus(2, "nodeB", PowerSystems.PQ::BusType, 0, 1.0, (min = 0.9, max=1.05), 230), + Bus(3, "nodeC", PowerSystems.PV::BusType, 0, 1.0, (min = 0.9, max=1.05), 230), + Bus(4, "nodeD", PowerSystems.REF::BusType, 0, 1.0, (min = 0.9, max=1.05), 230), + Bus(5, "nodeE", PowerSystems.PV::BusType, 0, 1.0, (min = 0.9, max=1.05), 230), ] branches_test = [ - Line("1", true, (from=nodes5[1], to=nodes5[2]), 0.00281, 0.0281, + Line("1", true, 0.0, 0.0, Arc(from=nodes5[1], to=nodes5[2]), 0.00281, 0.0281, (from=0.00356, to=0.00356), 400.0, (min=-360.0, max=360.0)), - Line("2", true, (from=nodes5[1], to=nodes5[4]), 0.00304, 0.0304, + Line("2", true, 0.0, 0.0, Arc(from=nodes5[1], to=nodes5[4]), 0.00304, 0.0304, (from=0.00329, to=0.00329), 3960.0, (min=-360.0, max=75.0)), - Line("3", true, (from=nodes5[1], to=nodes5[5]), 0.00064, 0.0064, + Line("3", true, 0.0, 0.0, Arc(from=nodes5[1], to=nodes5[5]), 0.00064, 0.0064, (from=0.01563, to=0.01563), 18812.0, (min=-75.0, max=360.0)), - Line("4", true, (from=nodes5[2], to=nodes5[3]), 0.00108, 0.0108, + Line("4", true, 0.0, 0.0, Arc(from=nodes5[2], to=nodes5[3]), 0.00108, 0.0108, (from=0.00926, to=0.00926), 11148.0, (min=0.0, max=0.0)), - Line("5", true, (from=nodes5[3], to=nodes5[4]), 0.00297, 0.0297, + Line("5", true, 0.0, 0.0, Arc(from=nodes5[3], to=nodes5[4]), 0.00297, 0.0297, (from=0.00337, to=0.00337), 4053.0, (min=-1.2, max=60.0)), - Line("6", true, (from=nodes5[4], to=nodes5[5]), 0.00297, 0.0297, + Line("6", true, 0.0, 0.0, Arc(from=nodes5[4], to=nodes5[5]), 0.00297, 0.0297, (from=0.00337, to=00.00337), 240.0, (min=-1.17, max=1.17)) ] - PowerSystems.checkanglelimits!(branches_test) - @test branches_test[1].anglelimits == (min=-1.57, max=1.57) - @test branches_test[2].anglelimits == (min=-1.57, max=75.0 * (π / 180)) - @test branches_test[3].anglelimits == (min=-75.0 * (π / 180), max=1.57) - @test branches_test[4].anglelimits == (min=-1.57, max=1.57) + foreach(x -> PowerSystems.check_angle_limits!(x), branches_test) + + @test branches_test[1].anglelimits == (min=-pi/2, max=pi/2) + @test branches_test[2].anglelimits == (min=-pi/2, max=75.0 * (π / 180)) + @test branches_test[3].anglelimits == (min=-75.0 * (π / 180), max=pi/2) + @test branches_test[4].anglelimits == (min=-pi/2, max=pi/2) @test branches_test[5].anglelimits == (min=-1.2, max=60.0 * (π / 180)) @test branches_test[6].anglelimits == (min=-1.17, max=1.17) - bad_angle_limits = [ - Line("1", true, (from=nodes5[1], to=nodes5[2]), 0.00281, 0.0281, + bad_angle_limits = Line("1", true, 0.0, 0.0, Arc(from=nodes5[1], to=nodes5[2]), 0.00281, 0.0281, (from=0.00356, to=0.00356), 400.0, (min=360.0, max=-360.0)) - ] @test_throws(PowerSystems.DataFormatError, - PowerSystems.checkanglelimits!(bad_angle_limits)) + PowerSystems.check_angle_limits!(bad_angle_limits)) end diff --git a/test/test_busnumberchecks.jl b/test/test_busnumberchecks.jl new file mode 100644 index 0000000000..45e3041139 --- /dev/null +++ b/test/test_busnumberchecks.jl @@ -0,0 +1,15 @@ + +base_dir = dirname(dirname(pathof(PowerSystems))) + +sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "case5_re.m")) + +@testset "Check bus index" begin + @test sort([b.number for b in collect(get_components(Bus, sys))]) == [1, 2, 3, 4, 10] + @test sort(collect(Set([b.arc.from.number for + b in collect(get_components(Branch,sys))]))) == [1, 2, 3, 4] + @test sort(collect(Set([b.arc.to.number for + b in collect(get_components(Branch,sys))]))) == [2, 3, 4, 10] + + # TODO: add test for loadzones testing MAPPING_BUSNUMBER2INDEX + +end diff --git a/test/test_constructors.jl b/test/test_constructors.jl new file mode 100644 index 0000000000..280743c35b --- /dev/null +++ b/test/test_constructors.jl @@ -0,0 +1,100 @@ +@testset "Bus Constructors" begin + tBus = Bus(nothing) + tLoadZones = LoadZones(nothing) + + bus = Bus(1, "test", PowerSystems.SLACK::BusType, 0.0, 0.0, (min=0.0, max=0.0), nothing) + @test PowerSystems.get_bustype(bus) == PowerSystems.REF::BusType + + @test_throws(PowerSystems.DataFormatError, + Bus(1, "test", PowerSystems.ISOLATED::BusType, 0.0, 0.0, + (min=0.0, max=0.0), nothing)) +end + +@testset "Generation Constructors" begin + tThreePartCost = ThreePartCost(nothing) + @test tThreePartCost isa PowerSystemType + tTwoPartCost = TwoPartCost(nothing) + @test tTwoPartCost isa PowerSystemType + tTechThermal = TechThermal(nothing) + @test tTechThermal isa PowerSystemType + tThermalGen = ThermalStandard(nothing) + @test tThermalGen isa PowerSystems.Component + tTechHydro = TechHydro(nothing) + @test tTechHydro isa PowerSystemType + tHydroFix = HydroFix(nothing) + @test tHydroFix isa PowerSystems.Component + tHydroDispatch = HydroDispatch(nothing) + @test tHydroDispatch isa PowerSystems.Component + tHydroStorage = HydroStorage(nothing) + @test tHydroStorage isa PowerSystems.Component + tTechRenewable = TechRenewable(nothing) + @test tTechRenewable isa PowerSystemType + tRenewableFix = RenewableFix(nothing) + @test tRenewableFix isa PowerSystems.Component + tRenewableDispatch = RenewableDispatch(nothing) + @test tRenewableDispatch isa PowerSystems.Component + tRenewableDispatch = RenewableDispatch(nothing) + @test tRenewableDispatch isa PowerSystems.Component +end + +@testset "Storage Constructors" begin + tStorage = GenericBattery(nothing) + @test tStorage isa PowerSystems.Component +end + +@testset "Load Constructors" begin + tPowerLoad = PowerLoad(nothing) + @test tPowerLoad isa PowerSystems.Component + tPowerLoadPF = PowerLoadPF(nothing) + @test tPowerLoadPF isa PowerSystems.Component + tPowerLoad = PowerLoad("init", true, Bus(nothing), nothing, 0.0, 0.0, 0.0, 0.0) + @test tPowerLoad isa PowerSystems.Component + tPowerLoadPF = PowerLoadPF("init", true, Bus(nothing), nothing, 0.0, 0.0, 1.0) + @test tPowerLoadPF isa PowerSystems.Component + tLoad = InterruptibleLoad(nothing) + @test tLoad isa PowerSystems.Component +end + +@testset "Branch Constructors" begin + tLine = Line(nothing) + @test tLine isa PowerSystems.Component + tMonitoredLine = MonitoredLine(nothing) + @test tMonitoredLine isa PowerSystems.Component + tHVDCLine = HVDCLine(nothing) + @test tHVDCLine isa PowerSystems.Component + tVSCDCLine = VSCDCLine(nothing) + @test tVSCDCLine isa PowerSystems.Component + tTransformer2W = Transformer2W(nothing) + @test tTransformer2W isa PowerSystems.Component + tTapTransformer = TapTransformer(nothing) + @test tTapTransformer isa PowerSystems.Component + tPhaseShiftingTransformer = PhaseShiftingTransformer(nothing) + @test tPhaseShiftingTransformer isa PowerSystems.Component +end + +@testset "Service Constructors" begin + #tProportionalReserve = ProportionalReserve(nothing) + #@test tProportionalReserve isa PowerSystems.Service + tStaticReserve = StaticReserve(nothing) + @test tStaticReserve isa PowerSystems.Service +end + +@testset "Forecast Constructors" begin + tg = RenewableFix(nothing) + forecast_data = PowerSystems.TimeSeries.TimeArray([DateTime("01-01-01"), DateTime("01-01-01")+Hour(1)], [1.0, 1.0]) + #Deterministic Tests + tDeterministicForecast = PSY.Deterministic(tg,"scalingfactor", Hour(1),DateTime("01-01-01"),24) + @test tDeterministicForecast isa PowerSystems.Forecast + tDeterministicForecast = PSY.Deterministic(tg,"scalingfactor", forecast_data) + @test tDeterministicForecast isa PowerSystems.Forecast + #Probabilistic Tests + tProbabilisticForecast = PSY.Probabilistic(tg,"scalingfactor", Hour(1), DateTime("01-01-01"),[0.5, 0.5], 24) + @test tProbabilisticForecast isa PowerSystems.Forecast + tProbabilisticForecast = PSY.Probabilistic(tg,"scalingfactor", [1.0], forecast_data) + @test tProbabilisticForecast isa PowerSystems.Forecast + #Scenario Tests + tScenarioForecast = PSY.ScenarioBased(tg, "scalingfactor", Hour(1), DateTime("01-01-01"), 2, 24) + @test tScenarioForecast isa PowerSystems.Forecast + tScenarioForecast = PSY.ScenarioBased(tg,"scalingfactor",forecast_data) + @test tScenarioForecast isa PowerSystems.Forecast +end diff --git a/test/data.jl b/test/test_data.jl similarity index 100% rename from test/data.jl rename to test/test_data.jl diff --git a/test/test_generate_structs.jl b/test/test_generate_structs.jl new file mode 100644 index 0000000000..6674ff75b3 --- /dev/null +++ b/test/test_generate_structs.jl @@ -0,0 +1,25 @@ +@testset "Test generated structs" begin + descriptor_file = joinpath(@__DIR__, "..", "src", "descriptors", + "power_system_structs.json") + existing_dir = joinpath(@__DIR__, "..", "src", "models", "generated") + output_dir = "tmp-test-generated-structs" + if isdir(output_dir) + rm(output_dir; recursive=true) + end + mkdir(output_dir) + + IS.generate_structs(descriptor_file, output_dir; print_results=false) + + matched = true + try + run(`diff $output_dir $existing_dir`) + catch(err) + @error "Generated structs do not match the descriptor file." + matched = false + finally + rm(output_dir; recursive=true) + end + + @test matched +end + diff --git a/test/test_internal.jl b/test/test_internal.jl new file mode 100644 index 0000000000..2d484f8c6d --- /dev/null +++ b/test/test_internal.jl @@ -0,0 +1,49 @@ +import UUIDs + +"""Recursively validates that the object and fields have UUIDs.""" +function validate_uuids(obj::T) where T + if !(obj isa PowerSystemType) + return true + end + + result = true + if !(IS.get_uuid(obj) isa Base.UUID) + result = false + @error "object does not have a UUID" obj + end + + for fieldname in fieldnames(T) + val = getfield(obj, fieldname) + if !validate_uuids(val) + result = false + end + end + + return result +end + +function validate_uuids(obj::T) where T <: AbstractArray + result = true + for elem in obj + if !validate_uuids(elem) + result = false + end + end + + return result +end + +function validate_uuids(obj::T) where T <: AbstractDict + result = true + for elem in values(obj) + if !validate_uuids(elem) + result = false + end + end + return result +end + +@testset "Test internal values" begin + sys_rts = create_rts_system() + @test validate_uuids(sys) +end diff --git a/test/test_logging.jl b/test/test_logging.jl deleted file mode 100644 index 70c41e25d0..0000000000 --- a/test/test_logging.jl +++ /dev/null @@ -1,131 +0,0 @@ - -import PowerSystems: MultiLogger, InvalidParameter, configure_logging, increment_count - -TEST_MSG = "test log message" - -@testset "Test LogEventTracker" begin - levels = (Logging.Info, Logging.Warn, Logging.Error) - tracker = LogEventTracker(levels) - - events = ( - LogEvent("file", 14, :id, TEST_MSG, Logging.Debug), - LogEvent("file", 14, :id, TEST_MSG, Logging.Info), - LogEvent("file", 14, :id, TEST_MSG, Logging.Warn), - LogEvent("file", 14, :id, TEST_MSG, Logging.Error), - ) - - for i in range(1, length=2) - for event in events - increment_count(tracker, event, false) - end - end - - @test length(get_log_events(tracker, Logging.Debug)) == 0 - for level in levels - test_events = collect(get_log_events(tracker, level)) - @test length(test_events) == 1 - @test test_events[1].count == 2 - end - - text = report_log_summary(tracker) - @test !occursin("Debug", text) - @test !occursin("suppressed", text) - for level in ("Error", "Warn", "Info") - @test occursin("1 $level event", text) - end - - # Test again with suppression. - increment_count(tracker, events[2], true) - text = report_log_summary(tracker) - @test occursin("suppressed=1", text) -end - -@testset "Test MultiLogger with no event tracking" begin - logger = MultiLogger([ConsoleLogger(devnull, Logging.Info), - SimpleLogger(devnull, Logging.Debug)]) - with_logger(logger) do - @info TEST_MSG - end - - @test_throws ErrorException report_log_summary(logger) -end - -@testset "Test MultiLogger with event tracking" begin - levels = (Logging.Debug, Logging.Info, Logging.Warn, Logging.Error) - logger = MultiLogger([ConsoleLogger(devnull, Logging.Info), - SimpleLogger(devnull, Logging.Debug)], - LogEventTracker(levels)) - - with_logger(logger) do - for i in range(1, length=2) - @debug TEST_MSG - @info TEST_MSG - @warn TEST_MSG - @error TEST_MSG maxlog=1 - end - end - - events = collect(get_log_events(logger.tracker, Logging.Error)) - @test length(events) == 1 - events[1].suppressed == 1 - - text = report_log_summary(logger) - for level in levels - @test occursin("1 $level event", text) - end -end - -@testset "Test configure_logging" begin - # Verify logging to a file. - logfile = "testlog.txt" - logger = configure_logging(; file=true, filename=logfile, file_level=Logging.Info, - set_global=false) - with_logger(logger) do - @info TEST_MSG - end - - close(logger) - - @test isfile(logfile) - open(logfile) do io - lines = readlines(io) - @test length(lines) == 2 # two lines per message - @test occursin(TEST_MSG, lines[1]) - end - rm(logfile) - - # Verify logging with no file. - logger = configure_logging(; console=true, file=false, - console_stream=devnull, - filename=logfile, file_level=Logging.Info, - set_global=false) - with_logger(logger) do - @error TEST_MSG - end - - events = collect(get_log_events(logger.tracker, Logging.Error)) - @test length(events) == 1 - close(logger) - - @test !isfile(logfile) - - # Verify disabling of tracker. - logger = configure_logging(; console=true, file=false, - console_stream=devnull, - filename=logfile, file_level=Logging.Info, - set_global=false, tracker=nothing) - with_logger(logger) do - @error TEST_MSG - @test isnothing(logger.tracker) - end - - # Verify setting of global logger - orig_logger = global_logger() - logger = configure_logging(; console=true, file=false, - console_stream=devnull, - filename=logfile, file_level=Logging.Info, - set_global=true, tracker=nothing) - @error TEST_MSG - @test orig_logger != global_logger() - global_logger(orig_logger) -end diff --git a/test/network_matrices.jl b/test/test_network_matrices.jl similarity index 98% rename from test/network_matrices.jl rename to test/test_network_matrices.jl index f2784a8295..c8f895091f 100644 --- a/test/network_matrices.jl +++ b/test/test_network_matrices.jl @@ -1,11 +1,15 @@ using SparseArrays -include("../data/data_5bus.jl") -include("../data/data_14bus.jl") +using Test +include("../data/data_5bus_pu.jl") +include("../data/data_14bus_pu.jl") # The 5-bus case from PowerModels data is modified to include 2 phase shifters pm_dict = PowerSystems.parse_file(joinpath(MATPOWER_DIR, "case5.m")); -ps_dict = PowerSystems.pm2ps_dict(pm_dict); -Buses_ps, Generators_ps, Storages_ps, Branches_ps, Loads_ps, Shunts_ps, Services_ps = PowerSystems.ps_dict2ps_struct(ps_dict); +sys = PowerSystems.pm2ps_dict(pm_dict); +RTS = create_rts_system(); +# mixed up ids for data_5bus_pu +Br5NS_ids = [2,3,5,1,4,6] +Bu5NS_ids = [1,3,4,5,2] #PTDFs obtained from Matpower S5_slackB4 = [0.1939 -0.4759 -0.3490 0 0.1595; @@ -159,6 +163,8 @@ SRTS_GMLC = [ -2.8435579e-02 -2.7568386e-02 -5.5683888e-02 -2.5101872e-02 -2.2723873e-02 -1.9404357e-02 3.0561834e-02 9.0914720e-03 -2.3082049e-02 -1.6810577e-02 -2.5757010e-02 -1.1893921e-02 0.0000000e+00 -5.4000028e-02 -1.1101393e-01 -9.3674745e-02 -1.2117562e-01 -1.3398468e-01 -6.1354558e-02 -3.3250048e-02 -1.4587881e-01 -1.3616889e-01 -1.7792568e-02 -9.0052529e-02 1.1748184e-01 1.1844298e-01 8.7229511e-02 1.2115534e-01 1.2383328e-01 1.2752382e-01 1.2689268e-01 1.2689268e-01 1.2337649e-01 1.3040888e-01 1.2807752e-01 1.4496048e-01 1.4648826e-01 1.1310173e-01 7.4168001e-02 9.2064302e-02 6.6560211e-02 6.8485409e-02 1.3063723e-01 1.6417890e-01 7.0273092e-02 6.8813694e-02 1.8262682e-01 7.9116263e-02 5.3778758e-01 5.3837378e-01 5.1933680e-01 5.4002804e-01 5.4166130e-01 5.4391215e-01 5.4352722e-01 5.4352722e-01 5.4138271e-01 5.4567174e-01 5.4053779e-01 5.5825876e-01 5.5842746e-01 5.2194514e-01 4.8190543e-01 4.9582689e-01 4.5970852e-01 4.4062817e-01 5.3643100e-01 5.7173892e-01 4.5516089e-01 4.5694839e-01 5.9115827e-01 4.9608604e-01 -3.8651469e-01 ] +RTS_branchnames = ["A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "A11", "AB1", "A12-1", "A13-2", "A14", "A15", "A16", "A17", "A18", "A19", "A20", "A21", "A22", "AB2", "A23", "A24", "A25-1", "A25-2", "A26", "A27", "A28", "A29", "A30", "A31-1", "A31-2", "A32-1", "A32-2", "A33-1", "A33-2", "A34", "AB3", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9", "B10", "B11", "B12-1", "B13-2", "B14", "B15", "B16", "B17", "B18", "B19", "B20", "B21", "B22", "B23", "B24", "B25-1", "B25-2", "B26", "B27", "B28", "B29", "B30", "B31-1", "B31-2", "B32-1", "B32-2", "B33-1", "B33-2", "B34", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "C10", "C11", "C12-1", "C13-2", "C14", "C15", "C16", "C17", "C18", "C19", "C20", "C21", "C22", "C23", "C24", "C25-1", "C25-2", "C26", "C27", "C28", "C29", "C30", "C31-1", "C31-2", "C32-1", "C32-2", "C33-1", "C33-2", "C34", "CA-1", "CB-1", "C35"] + Lodf_5 = [-1.0000 0.3448 0.3071 -1.0000 -1.0000 -0.3071; 0.5429 -1.0000 0.6929 0.5429 0.5429 -0.6929; 0.4571 0.6552 -1.0000 0.4571 0.4571 1.0000; @@ -263,7 +269,7 @@ Ybus14_matpower[9,14] = -1.42400548701993 + 3.0290504569306im Ybus14_matpower[13,14]= -1.13699415780633 + 2.31496347510535im Ybus14_matpower[14,14]= 2.56099964482626 - 5.34401393203596im; -Ybus5_phaseshifter = Matrix{Complex{Float64}}(undef,5,5) +Ybus5_phaseshifter = Matrix{Complex{Float64}}(undef,5,5) Ybus5_phaseshifter[1,1]= 22.2506856885351 - 222.484376885351im Ybus5_phaseshifter[2,1]= -3.52348402099997 + 35.2348402099996im Ybus5_phaseshifter[4,1]= -3.2569046378322 + 32.569046378322im @@ -282,31 +288,49 @@ Ybus5_phaseshifter[1,5]= -15.470297029703 + 154.70297029703im Ybus5_phaseshifter[4,5]= -3.33366670000333 + 33.3366670000333im Ybus5_phaseshifter[5,5]= 18.8039637297063 - 188.020637297063im; -@time @testset "PTDF matrices" begin - P5, A5 = PowerSystems.buildptdf(branches5, nodes5); +@testset "PTDF matrices" begin + P5 = PowerSystems.PTDF(branches5, nodes5); @test maximum(P5.data - S5_slackB4) <= 1e-3 + @test P5[branches5[1],nodes5[1]] == 0.1939166051164976 - P14, A14 = PowerSystems.buildptdf(branches14, nodes14); + P14 = PowerSystems.PTDF(branches14, nodes14); @test maximum(P14.data - S14_slackB1) <= 1e-3 + P5NS = PTDF([branches5[b] for b in Br5NS_ids], [nodes5[b] for b in Bu5NS_ids]); + for br in Br5NS_ids, b in Bu5NS_ids + @test getindex(P5NS,string(br),b) - S5_slackB4[br,b] <= 1e-3 + end + + PRTS = PTDF(RTS); + bnums = sort([PowerSystems.get_number(b) for b in get_components(Bus,RTS)]) + for (ibr,br) in enumerate(RTS_branchnames), (ib,b) in enumerate(bnums) + @test getindex(PRTS,br,b) - SRTS_GMLC[ibr,ib] <= 1e-3 + end end -@time @testset "LODF matrices" begin - L5 = PowerSystems.buildlodf(branches5,nodes5) - @test maximum(L5 - Lodf_5) <= 1e-3 +@testset "LODF matrices" begin + L5 = PowerSystems.LODF(branches5,nodes5) + @test maximum(L5.data - Lodf_5) <= 1e-3 + @test L5[branches5[1],branches5[2]] == 0.3447946513849091 + + L14 = PowerSystems.LODF(branches14,nodes14) + @test maximum(L14.data - Lodf_14) <= 1e-3 - L14 = PowerSystems.buildlodf(branches14,nodes14) - @test maximum(L14 - Lodf_14) <= 1e-3 + L5NS = PowerSystems.LODF(sys) + @test getindex(L5NS,"5","4") - 0.0003413469090 <= 1e-4 + + L5NS = LODF([branches5[b] for b in Br5NS_ids], [nodes5[b] for b in Bu5NS_ids]); + for brf in Br5NS_ids, brt in Br5NS_ids + @test getindex(L5NS,string(brf),string(brt)) - Lodf_5[brf,brt] <= 1e-3 + end - #PRTS = PowerSystems.buildptdf(branches_gmlc, nodes_gmlc) - #@test maximum(PTRS - SRTS_GMLC) <= 1e-6 end +@testset "Ybus Matrix" begin -@time @testset "Ybus Matrix" begin - Ybus5 = PowerSystems.build_ybus(length(nodes5), branches5) + Ybus5 = Ybus(branches5, nodes5) - I, J, V = findnz(Ybus5) + I, J, V = findnz(Ybus5.data) indices = collect(zip(I,J)) for i in indices @@ -314,21 +338,22 @@ end end - Ybus14 = PowerSystems.build_ybus(length(nodes14), branches14); - I, J, V = findnz(Ybus14) + Ybus14 = Ybus(branches14, nodes14); + I, J, V = findnz(Ybus14.data) indices = collect(zip(I,J)) for i in indices @test isapprox(Ybus14[i[1], i[2]], Ybus14_matpower[i[1], i[2]], atol=1e-2) end + Y5NS = Ybus(sys) + @test isapprox(getindex(Y5NS, 10, 4), -3.3336667 + 33.336667im, atol= 1e-4) - Ybus5_ps = PowerSystems.build_ybus(length(Buses_ps), Branches_ps) - I, J, V = findnz(Ybus5_ps) - indices = collect(zip(I,J)) + #Y5NS = Ybus([branches5[b] for b in Br5NS_ids], [nodes5[b] for b in Bu5NS_ids]); + #for buf in Bu5NS_ids, but in Bu5NS_ids + # @test isapprox(getindex(Y5NS, buf, but), Ybus5_matpower[buf,but], atol=1e-3) + #end - for i in indices - @test isapprox(Ybus5_phaseshifter[i[1], i[2]], Ybus5_ps[i[1], i[2]], atol=1e-2) - end + @test Ybus5[nodes5[1],nodes5[2]] == (-3.5234840209999647 + 35.234840209999646im) end diff --git a/test/test_parse_matpower.jl b/test/test_parse_matpower.jl new file mode 100644 index 0000000000..e9542778f9 --- /dev/null +++ b/test/test_parse_matpower.jl @@ -0,0 +1,75 @@ +# TODO: Reviewers: Is this a correct list of keys to verify? +POWER_MODELS_KEYS = [ + "baseMVA", + "branch", + "bus", + "dcline", + "gen", + "load", + "name", + "per_unit", + "shunt", + "source_type", + "source_version", + "storage", +] + +badfiles = Dict("case30.m" => PSY.InvalidValue) + +@testset "Parse Matpower data files" begin + files = [x for x in readdir(joinpath(MATPOWER_DIR)) if splitext(x)[2] == ".m"] + if length(files) == 0 + @error "No test files in the folder" + end + + for f in files + @info "Parsing $f..." + path = joinpath(MATPOWER_DIR, f) + + pm_dict = PowerSystems.parse_file(path) + for key in POWER_MODELS_KEYS + @test haskey(pm_dict, key) + end + @info "Successfully parsed $path to PowerModels dict" + + if f in keys(badfiles) + @test_logs((:error, r"cannot create Line"), match_mode=:any, + @test_throws(badfiles[f], + PowerSystems.pm2ps_dict(pm_dict) + ) + ) + else + sys = PowerSystems.pm2ps_dict(pm_dict) + @info "Successfully parsed $path to System struct" + end + end +end + +@testset "Parse PowerModel Matpower data files" begin + files = [x for x in readdir(joinpath(DATA_DIR, "pm_data","matpower")) if splitext(x)[2] == ".m"] + if length(files) == 0 + @error "No test files in the folder" + end + + for f in files + @info "Parsing $f..." + path = joinpath(joinpath(DATA_DIR, "pm_data","matpower"), f) + + pm_dict = PowerSystems.parse_file(path) + for key in POWER_MODELS_KEYS + @test haskey(pm_dict, key) + end + @info "Successfully parsed $path to PowerModels dict" + + if f in keys(badfiles) + @test_logs((:error, r"cannot create Line"), match_mode=:any, + @test_throws(badfiles[f], + PowerSystems.pm2ps_dict(pm_dict) + ) + ) + else + sys = PowerSystems.pm2ps_dict(pm_dict) + @info "Successfully parsed $path to System struct" + end + end +end diff --git a/test/test_parse_psse.jl b/test/test_parse_psse.jl new file mode 100644 index 0000000000..31c994be31 --- /dev/null +++ b/test/test_parse_psse.jl @@ -0,0 +1,36 @@ +@testset "PSSE Parsing " begin + files = readdir(PSSE_RAW_DIR) + if length(files) == 0 + error("No test files in the folder") + end + + for f in files[1:1] + @info "Parsing $f ..." + pm_dict = PowerSystems.parse_file(joinpath(PSSE_RAW_DIR, f)) + @info "Successfully parsed $f to PowerModels dict" + sys = PowerSystems.pm2ps_dict(pm_dict) + @info "Successfully parsed $f to System struct" + end + + # Test bad input + pm_dict = PowerSystems.parse_file(joinpath(PSSE_RAW_DIR, files[1])) + pm_dict["bus"] = Dict{String, Any}() + @test_throws PowerSystems.DataFormatError PowerSystems.pm2ps_dict(pm_dict) +end + +@testset "PSSE PowerModel Parsing " begin + PM_PSSE_PATH = joinpath(DATA_DIR, "pm_data","pti") + files = readdir(PM_PSSE_PATH) + if length(files) == 0 + error("No test files in the folder") + end + + for f in files[1:1] + @info "Parsing $f ..." + pm_dict = PowerSystems.parse_file(joinpath(PM_PSSE_PATH, f)) + @info "Successfully parsed $f to PowerModels dict" + + sys = PowerSystems.pm2ps_dict(pm_dict) + @info "Successfully parsed $f to System struct" + end +end diff --git a/test/test_power_system_table_data.jl b/test/test_power_system_table_data.jl new file mode 100644 index 0000000000..641982b345 --- /dev/null +++ b/test/test_power_system_table_data.jl @@ -0,0 +1,104 @@ + +import PowerSystems: LazyDictFromIterator + +@testset "PowerSystemTableData parsing" begin + resolutions = ( + (resolution=Dates.Minute(5), len=288), + (resolution=Dates.Minute(60), len=24), + ) + + for (resolution, len) in resolutions + sys = create_rts_system(resolution) + for forecast in iterate_forecasts(sys) + @test length(forecast) == len + end + end +end + +@testset "PowerSystemTableData parsing invalid directory" begin + baddir = abspath(joinpath(RTS_GMLC_DIR, "../../test")) + @test_throws ErrorException PowerSystemTableData(baddir, 100.0, DESCRIPTORS) +end + +@testset "consistency between PowerSystemTableData and standardfiles" begin + mpsys = parse_standard_files(joinpath(MATPOWER_DIR, "RTS_GMLC.m")) + cdmsys = create_rts_system() + + mp_iter = get_components(HydroGen, mpsys) + mp_generators = LazyDictFromIterator(String, HydroGen, mp_iter, get_name) + for cdmgen in get_components(HydroGen, cdmsys) + mpgen = get(mp_generators, uppercase(get_name(cdmgen))) + if isnothing(mpgen) + error("did not find $cdmgen") + end + @test cdmgen.available == mpgen.available + @test lowercase(cdmgen.bus.name) == lowercase(mpgen.bus.name) + tech_dat = (structname = :tech, + fields= (:rating, :activepowerlimits, :reactivepowerlimits, :ramplimits)) + gen_dat = (structname = nothing, + fields= (:activepower, :reactivepower)) + function check_fields(chk_dat) + for field in chk_dat.fields + n = get(chk_dat, :structname, nothing) + (cdmd, mpd) = isnothing(n) ? (cdmgen, mpgen) : (getfield(cdmgen,n), getfield(mpgen,n)) + cdmgen_val = getfield(cdmd, field) + mpgen_val = getfield(mpd, field) + if isnothing(cdmgen_val) || isnothing(mpgen_val) + @warn "Skip value with nothing" repr(cdmgen_val) repr(mpgen_val) + continue + end + @test cdmgen_val == mpgen_val + end + end + check_fields(gen_dat) + check_fields(tech_dat) + end + + mp_iter = get_components(ThermalGen, mpsys) + mp_generators = LazyDictFromIterator(String, ThermalGen, mp_iter, get_name) + for cdmgen in get_components(ThermalGen, cdmsys) + mpgen = get(mp_generators, uppercase(get_name(cdmgen))) + @test cdmgen.available == mpgen.available + @test lowercase(cdmgen.bus.name) == lowercase(mpgen.bus.name) + for field in (:activepowerlimits, :reactivepowerlimits, :ramplimits) + cdmgen_val = getfield(cdmgen.tech, field) + mpgen_val = getfield(mpgen.tech, field) + if isnothing(cdmgen_val) || isnothing(mpgen_val) + @warn "Skip value with nothing" repr(cdmgen_val) repr(mpgen_val) + continue + end + @test cdmgen_val == mpgen_val + end + + if length(mpgen.op_cost.variable) == 4 + @test [isapprox(cdmgen.op_cost.variable[i][1], + mpgen.op_cost.variable[i][1], atol= .1) + for i in 1:4] == [true, true, true, true] + #@test compare_values_without_uuids(cdmgen.op_cost, mpgen.op_cost) + end + end + + mp_iter = get_components(RenewableGen, mpsys) + mp_generators = LazyDictFromIterator(String, RenewableGen, mp_iter, get_name) + for cdmgen in get_components(RenewableGen, cdmsys) + mpgen = get(mp_generators, uppercase(get_name(cdmgen))) + # TODO + #@test cdmgen.available == mpgen.available + @test lowercase(cdmgen.bus.name) == lowercase(mpgen.bus.name) + for field in (:rating, :reactivepowerlimits, :powerfactor) + cdmgen_val = getfield(cdmgen.tech, field) + mpgen_val = getfield(mpgen.tech, field) + if isnothing(cdmgen_val) || isnothing(mpgen_val) + @warn "Skip value with nothing" repr(cdmgen_val) repr(mpgen_val) + continue + end + @test cdmgen_val == mpgen_val + end + #@test compare_values_without_uuids(cdmgen.op_cost, mpgen.op_cost) + end + + cdm_branches = collect(get_components(Branch,cdmsys)) + @test cdm_branches[2].rate == get_branch(mpsys, cdm_branches[2]).rate + @test cdm_branches[6].rate == get_branch(mpsys, cdm_branches[6]).rate + @test cdm_branches[120].rate == get_branch(mpsys, cdm_branches[120]).rate +end diff --git a/test/test_powerflow.jl b/test/test_powerflow.jl new file mode 100644 index 0000000000..c70c9814fa --- /dev/null +++ b/test/test_powerflow.jl @@ -0,0 +1,46 @@ +result = [2.32551, +-0.155293, +0.469214, +-0.0870457, +0.271364, +-0.222398, +1.01423, +-0.179009, +1.01724, +-0.152972, +0.216039, +-0.251637, +1.05034, +-0.231289, +0.245388, +-0.231289, +1.03371, +-0.258872, +1.03256, +-0.262519, +1.04748, +-0.259143, +1.0535, +-0.266484, +1.04711, +-0.267177, +1.02131, +-0.280381] + +include(joinpath(BASE_DIR,"data/data_14bus_pu.jl")) +c_sys14 = System(nodes14, thermal_generators14, loads14, branches14, nothing, 100.0, nothing, nothing, nothing); + +include(joinpath(BASE_DIR,"data/data_5bus_pu.jl")) +c_sys5_re = System(nodes5, vcat(thermal_generators5, renewable_generators5), loads5, + nothing, nothing, 100.0, nothing, nothing, nothing) + + +@testset begin + using NLsolve + pf!, x0 = make_pf(c_sys14); + res = NLsolve.nlsolve(pf!, x0) + @test res.zero ≈ result rtol=1e-3 + + solve_powerflow!(c_sys14, nlsolve, method = :newton) + @test_throws PowerSystems.DataFormatError solve_powerflow!(c_sys5_re, nlsolve) +end diff --git a/test/test_powersystemconstructors.jl b/test/test_powersystemconstructors.jl new file mode 100644 index 0000000000..4a63d67c8a --- /dev/null +++ b/test/test_powersystemconstructors.jl @@ -0,0 +1,58 @@ + +include(joinpath(DATA_DIR,"data_5bus_pu.jl")) +include(joinpath(DATA_DIR,"data_14bus_pu.jl")) + +checksys = false + +@testset "Test System constructors from .jl files" begin + tPowerSystem = System(nothing) + + for i in nodes5 + nodes5[i].angle = deg2rad(nodes5[i].angle) + end + + sys5 = System(nodes5, thermal_generators5, loads5, nothing, nothing, 100.0, nothing, + nothing, nothing; runchecks = checksys) + + sys5b = System(nodes5, thermal_generators5, loads5, nothing, battery5, 100.0, nothing, + nothing, nothing; runchecks = checksys) + + # GitHub issue #234 - fix forecasts5 in data file, use new format + #_sys5b = PowerSystems._System(nodes5, thermal_generators5, loads5, nothing, battery5, + # 100.0, forecasts5, nothing, nothing) + #sys5b = System(_sys5b) + + sys5bh = System(nodes5, vcat(thermal_generators5, hydro_generators5), loads5, branches5, + battery5, 100.0, nothing, nothing, nothing; runchecks = checksys) + + sys5bh = System(; buses=nodes5, + generators=vcat(thermal_generators5, hydro_generators5), + loads=loads5, + branches=branches5, + storage=battery5, + basepower=100.0, + forecasts=nothing, + services=nothing, + annex=nothing, + runchecks = checksys) + + # Test Data for 14 Bus + + # GitHub issue #234 - fix forecasts5 in data file, use new format + #_sys14 = PowerSystems._System(nodes14, thermal_generators14, loads14, nothing, nothing, + # 100.0, Dict{Symbol,Vector{<:Forecast}}(),nothing,nothing) + #sys14 = System(_sys14) + + for i in nodes14 + nodes14[i].angle = deg2rad(nodes14[i].angle) + end + + sys14b = PowerSystems.System(nodes14, thermal_generators14, loads14, nothing, + battery14, 100.0, nothing, nothing, nothing; runchecks = checksys) + sys14b = PowerSystems.System(nodes14, thermal_generators14, loads14, branches14, + battery14, 100.0, nothing, nothing, nothing; runchecks = checksys) +end + +@testset "Test System constructor from Matpower" begin + sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "case5_re.m")) +end diff --git a/test/test_printing.jl b/test/test_printing.jl new file mode 100644 index 0000000000..13a5173b34 --- /dev/null +++ b/test/test_printing.jl @@ -0,0 +1,58 @@ + +function are_type_and_fields_in_output(obj::T) where T <: Component + match = true + normal = repr(obj) + io = IOBuffer() + show(io, "text/plain", obj) + custom = String(take!(io)) + fields = fieldnames(T) + + # Type must always be present. name should be also, if the type defines it. + #for text in (normal, custom) + for text in (custom,) + if !occursin(string(T), text) + @error "type name is not in output" string(T) text + match = false + end + if :name in fields + if !occursin(obj.name, text) + @error "name is not in output" name text + match = false + end + end + end + + for field in fields + if isnothing(getfield(obj, field)) || field == :internal + continue + end + + if !occursin(string(getfield(obj, field)), custom) + @error "field's value is not in custom output" field custom + match = false + end + end + + return match +end + +sys = create_rts_system() +@test are_type_and_fields_in_output(iterate(get_components(Bus, sys))[1]) +@test are_type_and_fields_in_output(iterate(get_components(Generator, sys))[1]) +@test are_type_and_fields_in_output(iterate(get_components(ThermalGen, sys))[1]) +@test are_type_and_fields_in_output(iterate(get_components(Branch, sys))[1]) +@test are_type_and_fields_in_output(iterate(get_components(ElectricLoad, sys))[1]) + +# Just make sure nothing blows up. +for component in iterate_components(sys) + print(devnull, component) + print(devnull, MIME"text/plain") + @test !isempty(summary(component)) +end +for forecast in iterate_forecasts(sys) + show(devnull, forecast) + show(devnull, MIME"text/plain") + @test !isempty(summary(forecast)) +end + +@test !isempty(summary(sys)) diff --git a/test/test_readforecastdata.jl b/test/test_readforecastdata.jl new file mode 100644 index 0000000000..b4f47a5082 --- /dev/null +++ b/test/test_readforecastdata.jl @@ -0,0 +1,202 @@ +import DataFrames +import Dates +import TimeSeries + +function verify_forecasts(sys::System, num_initial_times, num_forecasts, horizon) + initial_times = get_forecast_initial_times(sys) + if length(initial_times) != num_initial_times + @error "count of initial_times doesn't match" num_initial_times initial_times + return false + end + + total_forecasts = 0 + for it in initial_times + forecasts = get_forecasts(Forecast, sys, it) + for forecast in forecasts + if IS.get_horizon(forecast) != horizon + @error "horizon doesn't match" IS.get_horizon(forecast) horizon + return false + end + end + total_forecasts += length(forecasts) + end + + if num_forecasts != total_forecasts + @error "num_forecasts doesn't match" num_forecasts total_forecasts + return false + end + + return true +end + +@testset "Test read_timeseries_metadata" begin + forecasts = IS.read_timeseries_metadata(joinpath(RTS_GMLC_DIR, + "timeseries_pointers.json")) + @test length(forecasts) == 282 + + for forecast in forecasts + @test isfile(forecast.data_file) + end +end + +@testset "Test forecast normalization" begin + component_name = "122_HYDRO_1" + timeseries_file = joinpath(DATA_DIR, "forecasts", "RTS_GMLC_forecasts", "gen", "Hydro", + "DAY_AHEAD_hydro.csv") + timeseries = IS.read_timeseries(timeseries_file)[Symbol(component_name)] + max_value = maximum(TimeSeries.values(timeseries)) + + metadata = IS.TimeseriesFileMetadata( + "DAY_AHEAD", + "Generator", + "122_HYDRO_1", + "PMax MW", + 1.0, + timeseries_file, + [], + "Deterministic", + ) + + # Test code path where no normalization occurs. + sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "RTS_GMLC.m")) + add_forecasts!(sys, [metadata]) + verify_forecasts(sys, 1, 1, 24) + forecast = collect(PSY.iterate_forecasts(sys))[1] + @test TimeSeries.values(forecast.data) == TimeSeries.values(timeseries) + + # Test code path where timeseries is normalized by dividing by the max value. + metadata.scaling_factor = "Max" + sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "RTS_GMLC.m")) + add_forecasts!(sys, [metadata]) + verify_forecasts(sys, 1, 1, 24) + forecast = collect(PSY.iterate_forecasts(sys))[1] + @test TimeSeries.values(forecast.data) == TimeSeries.values(timeseries ./ max_value) + + # Test code path where timeseries is normalized by dividing by a custom value. + sf = 95.0 + metadata.scaling_factor = sf + sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "RTS_GMLC.m")) + add_forecasts!(sys, [metadata]) + verify_forecasts(sys, 1, 1, 24) + forecast = collect(PSY.iterate_forecasts(sys))[1] + @test TimeSeries.values(forecast.data) == TimeSeries.values(timeseries ./ sf) +end + +@testset "Test single forecast addition" begin + component_name = "122_HYDRO_1" + timeseries_file = joinpath(DATA_DIR, "forecasts", "RTS_GMLC_forecasts", "gen", "Hydro", + "DAY_AHEAD_hydro.csv") + timeseries = IS.read_timeseries(timeseries_file)[Symbol(component_name)] + + # Test with a filename. + sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "RTS_GMLC.m")) + component = get_component(HydroDispatch, sys, component_name) + add_forecast!(sys, timeseries_file, component, "PMax MW", 1.0) + verify_forecasts(sys, 1, 1, 24) + forecast = collect(PSY.iterate_forecasts(sys))[1] + @test TimeSeries.values(forecast.data) == TimeSeries.values(timeseries) + @test IS.get_timeseries(forecast) == timeseries + + # Test with TimeSeries.TimeArray. + sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "RTS_GMLC.m")) + component = get_component(HydroDispatch, sys, component_name) + add_forecast!(sys, timeseries, component, "PMax MW", 1.0) + verify_forecasts(sys, 1, 1, 24) + forecast = collect(PSY.iterate_forecasts(sys))[1] + @test TimeSeries.values(forecast.data) == TimeSeries.values(timeseries) + @test IS.get_timeseries(forecast) == timeseries + + # Test with DataFrames.DataFrame. + sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "RTS_GMLC.m")) + component = get_component(HydroDispatch, sys, component_name) + df = DataFrames.DataFrame(timeseries) + add_forecast!(sys, df, component, "PMax MW", 1.0) + verify_forecasts(sys, 1, 1, 24) + forecast = collect(PSY.iterate_forecasts(sys))[1] +end + +@testset "Forecast data matpower" begin + sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "case5_re.m")) + forecasts_metadata = joinpath(FORECASTS_DIR, "5bus_ts", "timeseries_pointers_da.json") + add_forecasts!(sys, forecasts_metadata) + @test verify_forecasts(sys, 1, 5, 24) + + + # Add the same files. + # This will fail because the component-label pairs will be duplicated. + @test_throws IS.DataFormatError add_forecasts!(sys, forecasts_metadata) + + forecasts_metadata = joinpath(FORECASTS_DIR, "5bus_ts", "timeseries_pointers_rt.json") + + ## This will fail because the resolutions are different. + @test_throws IS.DataFormatError add_forecasts!(sys, forecasts_metadata) + + ## TODO: need a dataset with same resolution but different horizon. + + sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "case5_re.m")) + add_forecasts!(sys, forecasts_metadata) + @test verify_forecasts(sys, 1, 5, 288) +end + +@testset "Test forecast splitting" begin + component_name = "122_HYDRO_1" + timeseries_file = joinpath(DATA_DIR, "forecasts", "RTS_GMLC_forecasts", "gen", "Hydro", + "DAY_AHEAD_hydro.csv") + timeseries = IS.read_timeseries(timeseries_file)[Symbol(component_name)] + + # Test with a filename. + sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "RTS_GMLC.m")) + component = get_component(HydroDispatch, sys, component_name) + add_forecast!(sys, timeseries_file, component, "PMax MW", 1.0) + verify_forecasts(sys, 1, 1, 24) + forecast = collect(PSY.iterate_forecasts(sys))[1] + @test TimeSeries.values(forecast.data) == TimeSeries.values(timeseries) + @test IS.get_timeseries(forecast) == timeseries + @test IS.get_resolution(forecast) == Dates.Hour(1) + + interval = Dates.Hour(1) + horizon = 12 + forecasts = IS.make_forecasts(forecast, interval, horizon) + @test length(forecasts) == 13 + compare_initial_time = IS.get_initial_time(forecast) + for forecast_ in forecasts + @test IS.get_horizon(forecast_) == horizon + @test IS.get_initial_time(forecast_) == compare_initial_time + compare_initial_time += interval + end + + # Interval is smaller than resolution. + @test_throws(IS.ArgumentError, + IS.make_forecasts(forecast, Dates.Minute(1), horizon)) + # Interval is not multiple of resolution. + @test_throws(IS.ArgumentError, + IS.make_forecasts(forecast, Dates.Minute(13), horizon)) + # Horizon is larger than forecast horizon. + @test_throws(IS.ArgumentError, + IS.make_forecasts(forecast, interval, 25)) + + # making a series of forecasts from a list of forecasts + forecasts = get_forecasts(IS.Deterministic, sys, IS.get_initial_time(forecast)) + forecasts_ = IS.make_forecasts(forecasts, Hour(1), 2) + @test length(forecasts_) == 23 + + # removal of all forecasts + clear_forecasts!(sys) + forecasts = get_forecasts(IS.Deterministic, sys, IS.get_initial_time(forecast)) + @test length(forecasts) == 0 + + # adding series of forecasts + add_forecasts!(sys, forecasts_) + ts = get_forecast_initial_times(sys) + @test length(ts) == 23 + + # replace a long forecast with a series of forecasts + clear_forecasts!(sys) + add_forecast!(sys, forecast) + forecasts = get_forecasts(IS.Deterministic, sys, IS.get_initial_time(forecast)) + split_forecasts!(sys, forecasts, Hour(6), 12) + ts = get_forecast_initial_times(sys) + @test length(ts) == 3 + + # TODO: need to cover serialization. +end diff --git a/test/test_readnetworkdata.jl b/test/test_readnetworkdata.jl new file mode 100644 index 0000000000..db68f7a78b --- /dev/null +++ b/test/test_readnetworkdata.jl @@ -0,0 +1,7 @@ + +base_dir = string(dirname(@__FILE__)) + +@testset "read_data" begin + include(joinpath(base_dir,"data_5bus_pu.jl")); + include(joinpath(base_dir,"data_14bus_pu.jl")) +end diff --git a/test/test_serialization.jl b/test/test_serialization.jl new file mode 100644 index 0000000000..b23c38de13 --- /dev/null +++ b/test/test_serialization.jl @@ -0,0 +1,65 @@ +import JSON2 + +function validate_serialization(sys::System) + path, io = mktemp() + @info "Serializing to $path" + + try + to_json(io, sys) + catch + close(io) + rm(path) + rethrow() + end + close(io) + + try + sys2 = System(path) + return IS.compare_values(sys, sys2) + finally + @debug "delete temp file" path + rm(path) + end +end + +@testset "Test JSON serialization of CDM data" begin + sys = create_rts_system() + @test validate_serialization(sys) + text = JSON2.write(sys) + @test length(text) > 0 +end + +@testset "Test JSON serialization of matpower data" begin + sys = PowerSystems.parse_standard_files(joinpath(MATPOWER_DIR, "case5_re.m")) + + # Add a Probabilistic forecast to get coverage serializing it. + bus = Bus(nothing) + bus.name = "Bus1234" + add_component!(sys, bus) + tg = RenewableFix(nothing) + tg.bus = bus + tProbabilisticForecast = PSY.Probabilistic(tg, "scalingfactor", Hour(1), + DateTime("01-01-01"), [0.5, 0.5], 24) + add_component!(sys, tg) + add_forecast!(sys, tProbabilisticForecast) + + @test validate_serialization(sys) +end + +@testset "Test JSON serialization of ACTIVSg2000 data" begin + sys = PowerSystems.parse_standard_files(joinpath(DATA_DIR, "ACTIVSg2000", + "ACTIVSg2000.m")) + validate_serialization(sys) +end + +@testset "Test serialization utility functions" begin + text = "SomeType{ParameterType1, ParameterType2}" + type_str, parameters = IS.separate_type_and_parameter_types(text) + @test type_str == "SomeType" + @test parameters == ["ParameterType1", "ParameterType2"] + + text = "SomeType" + type_str, parameters = IS.separate_type_and_parameter_types(text) + @test type_str == "SomeType" + @test parameters == [] +end diff --git a/test/test_system.jl b/test/test_system.jl new file mode 100644 index 0000000000..f0244d3249 --- /dev/null +++ b/test/test_system.jl @@ -0,0 +1,166 @@ + +@testset "Test functionality of System" begin + sys = create_rts_system() + summary(devnull, sys) + + generators = collect(get_components(ThermalStandard, sys)) + generator = get_component(ThermalStandard, sys, get_name(generators[1])) + @test IS.get_uuid(generator) == IS.get_uuid(generators[1]) + @test_throws(IS.ArgumentError, add_component!(sys, generator)) + + generators2 = get_components_by_name(ThermalGen, sys, get_name(generators[1])) + @test length(generators2) == 1 + @test IS.get_uuid(generators2[1]) == IS.get_uuid(generators[1]) + + @test isnothing(get_component(ThermalStandard, sys, "not-a-name")) + @test isempty(get_components_by_name(ThermalGen, sys, "not-a-name")) + @test_throws(IS.ArgumentError, + get_component(ThermalGen, sys, "not-a-name")) + @test_throws(IS.ArgumentError, + get_components_by_name(ThermalStandard, sys, "not-a-name")) + + # Test get_bus* functionality. + bus_numbers = Vector{Int}() + for bus in get_components(Bus, sys) + push!(bus_numbers, bus.number) + if length(bus_numbers) >= 2 + break + end + end + + bus = PowerSystems.get_bus(sys, bus_numbers[1]) + @test bus.number == bus_numbers[1] + + buses = PowerSystems.get_buses(sys, Set(bus_numbers)) + sort!(bus_numbers) + sort!(buses, by=x -> x.number) + @test length(bus_numbers) == length(buses) + for (bus_number, bus) in zip(bus_numbers, buses) + @test bus_number == bus.number + end + + initial_times = get_forecast_initial_times(sys) + @assert length(initial_times) == 1 + initial_time = initial_times[1] + + # Get forecasts with a label and without. + components = get_components(HydroDispatch, sys) + forecasts = get_forecasts(Forecast, sys, initial_time, components, "PMax MW") + @test length(forecasts) > 0 + + forecasts = collect(get_forecasts(Forecast, sys, initial_time, components)) + count = length(forecasts) + @test count > 0 + + # Verify that the two accessor functions return the same results. + all_components = get_components(Component, sys) + all_forecasts1 = get_forecasts(Forecast, sys, initial_time, all_components) + all_forecasts2 = get_forecasts(Forecast, sys, initial_time) + @test length(all_forecasts1) == length(all_forecasts2) + + # Get specific forecasts. They should not match. + specific_forecasts = get_forecasts(PSY.Deterministic{Bus}, sys, initial_time) + @test length(specific_forecasts) < length(all_forecasts1) + + @test get_forecasts_horizon(sys) == 24 + @test get_forecasts_initial_time(sys) == Dates.DateTime("2020-01-01T00:00:00") + @test get_forecasts_interval(sys) == Dates.Hour(1) # TODO + @test get_forecasts_resolution(sys) == Dates.Hour(1) # TODO + + for forecast in collect(get_forecasts(Forecast, sys, initial_time)) + remove_forecast!(sys, forecast) + end + + @test length(get_forecasts(Forecast, sys, initial_time)) == 0 + + # ArgumentError is thrown if the type is concrete and there is no forecast for a + # component. + @test_throws(IS.ArgumentError, + get_forecasts(Forecast, sys, initial_time, components)) + + # But not if the type is abstract. + new_forecasts = get_forecasts(Forecast, sys, initial_time, + get_components(HydroGen, sys)) + @test length(new_forecasts) == 0 + + add_forecasts!(sys, forecasts) + + forecasts = get_forecasts(Forecast, sys, initial_time, components) + @assert length(forecasts) == count + + @test_throws(IS.ArgumentError, + get_forecasts(PSY.Deterministic{Bus}, sys, initial_time, components)) + + #Get forecast by type + res = get_component_forecasts(RenewableDispatch, sys, initial_time) + for i in res + @test isa(i,PSY.Deterministic{RenewableDispatch}) + end + + @test_throws(IS.ArgumentError, + get_component_forecasts(RenewableGen, sys, initial_time)) + + f = forecasts[1] + forecast = PSY.Deterministic(Bus(nothing), f.label, f.resolution, f.initial_time, f.data) + @test_throws(IS.ArgumentError, add_forecasts!(sys, [forecast])) + + component = deepcopy(f.component) + component.internal = IS.InfrastructureSystemsInternal() + forecast = PSY.Deterministic(component, f.label, f.resolution, f.initial_time, f.data) + @test_throws(IS.ArgumentError, add_forecasts!(sys, [forecast])) +end + +@testset "Test System iterators" begin + sys = create_rts_system() + + i = 0 + for component in iterate_components(sys) + i += 1 + end + + components = get_components(Component, sys) + @test i == length(components) + + i = 0 + for forecast in iterate_forecasts(sys) + i += 1 + end + + j = 0 + initial_times = get_forecast_initial_times(sys) + for initial_time in initial_times + j += length(get_forecasts(Forecast, sys, initial_time)) + end + + @test i == j +end + +@testset "Test remove_component" begin + sys = create_rts_system() + generators = get_components(ThermalStandard, sys) + initial_length = length(generators) + @assert initial_length > 0 + gen = collect(generators)[1] + + remove_component!(sys, gen) + + @test isnothing(get_component(typeof(gen), sys, get_name(gen))) + generators = get_components(typeof(gen), sys) + @test length(generators) == initial_length - 1 + + @test_throws(IS.ArgumentError, remove_component!(sys, gen)) + + add_component!(sys, gen) + remove_component!(typeof(gen), sys, get_name(gen)) + @test isnothing(get_component(typeof(gen), sys, get_name(gen))) + + @assert length(get_components(typeof(gen), sys)) > 0 + remove_components!(typeof(gen), sys) + @test_throws(IS.ArgumentError, remove_components!(typeof(gen), sys)) +end + +@testset "Test missing Arc bus" begin + sys = System(100) + line = Line(nothing) + @test_throws(IS.ArgumentError, add_component!(sys, line)) +end diff --git a/test/test_validation.jl b/test/test_validation.jl new file mode 100644 index 0000000000..e55b9503fd --- /dev/null +++ b/test/test_validation.jl @@ -0,0 +1,125 @@ +import YAML + +const WRONG_FORMAT_CONFIG_FILE = joinpath(dirname(pathof(PowerSystems)), + "descriptors", "config.yml") +include(joinpath(DATA_DIR,"data_5bus_pu.jl")) + +@testset "Test reading in config data" begin + data = IS.read_validation_descriptor(PSY.POWER_SYSTEM_STRUCT_DESCRIPTOR_FILE) + @test data isa Vector + @test !isempty(data) + function find_struct() + for item in data + if item["struct_name"] == "TechThermal" + return true + end + end + return false + end + @test find_struct() + @test_throws(ErrorException, IS.read_validation_descriptor("badfile.toml")) +end + +@testset "Test adding custom validation YAML file to System" begin + sys_no_config = System(nodes5, thermal_generators5, loads5, nothing, nothing, + 100.0, nothing, nothing, nothing; runchecks=true) + @test !isempty(sys_no_config.data.validation_descriptors) + + sys_no_runchecks = System(nodes5, thermal_generators5, loads5, nothing, nothing, + 100.0, nothing, nothing, nothing; runchecks=false) + @test isempty(sys_no_runchecks.data.validation_descriptors) +end + +@testset "Test extracting struct info from validation_descriptor vector" begin + data = [Dict("fields"=>Dict{Any,Any}[ + Dict("name"=>"curtailpenalty","valid_range"=>Dict{Any,Any}("max"=>nothing,"min"=>0.0)), + Dict("name"=>"variablecost","valid_range"=>Dict{Any,Any}("max"=>nothing,"min"=>0.0)), + Dict("name"=>"internal")], + "struct_name"=>"EconHydro"), + Dict("fields"=>Dict{Any,Any}[ + Dict("name"=>"curtailpenalty","valid_range"=>Dict{Any,Any}("max"=>nothing,"min"=>0.0)), + Dict("name"=>"variablecost","valid_range"=>Dict{Any,Any}("max"=>nothing,"min"=>0.0)), + Dict("name"=>"internal")], + "struct_name"=>"EconLoad")] + struct_name = "EconHydro" + descriptor = IS.get_config_descriptor(data, struct_name) + @test descriptor isa Dict{String,Any} + @test haskey(descriptor, "struct_name") + @test haskey(descriptor, "fields") + @test descriptor["struct_name"] == struct_name + +end + +@testset "Test extracting field info from struct descriptor dictionary" begin + config = Dict{Any,Any}("fields"=>Dict{Any,Any}[ + Dict("name"=>"name","data_type"=>"String"), + Dict("name"=>"available","data_type"=>"Bool"), + Dict("name"=>"bus","data_type"=>"Bus"), + Dict("name"=>"tech","data_type"=>"Union{Nothing, TechThermal}"), + Dict("name"=>"econ","data_type"=>"Union{Nothing, EconThermal}"), + Dict("name"=>"internal","data_type"=>"IS.InfrastructureSystemsInternal")], + "struct_name"=>"ThermalStandard") + field_name = "econ" + field_descriptor = IS.get_field_descriptor(config, field_name) + @test field_descriptor isa Dict{Any, Any} + @test haskey(field_descriptor, "name") + @test field_descriptor["name"] == field_name +end + +@testset "Test retrieving validation action" begin + warn_descriptor = Dict{Any,Any}("name"=>"ramplimits", + "valid_range"=>Dict{Any,Any}("max"=>5,"min"=>0), + "validation_action"=>"warn") + error_descriptor = Dict{Any,Any}("name"=>"ramplimits", + "valid_range"=>Dict{Any,Any}("max"=>5,"min"=>0), + "validation_action"=>"error") + typo_descriptor = Dict{Any,Any}("name"=>"ramplimits", + "valid_range"=>Dict{Any,Any}("max"=>5,"min"=>0), + "validation_action"=>"asdfasdfsd") + @test IS.get_validation_action(warn_descriptor) == IS.validation_warning + @test IS.get_validation_action(error_descriptor) == IS.validation_error + @test_throws(ErrorException, IS.get_validation_action(typo_descriptor)) +end + +@testset "Test field validation" begin + #test recursive call of validate_fields and a regular valid range + bad_therm_gen_rating = deepcopy(thermal_generators5) + bad_therm_gen_rating[1].tech.rating = -10 + @test_logs((:error, r"Invalid range"), + @test_throws(PSY.InvalidRange, + System(nodes5, bad_therm_gen_rating, loads5, nothing, nothing, + 100.0, nothing, nothing, nothing; runchecks=true) + ) + ) + + #test custom range (activepower and activepowerlimits) + bad_therm_gen_act_power = deepcopy(thermal_generators5) + bad_therm_gen_act_power[1].activepower = 10 + @test_logs (:warn, r"Invalid range") System(nodes5, bad_therm_gen_act_power, loads5, + nothing, nothing, 100.0, nothing, nothing, nothing; runchecks=true) + + #test validating named tuple + bad_therm_gen_ramp_lim = deepcopy(thermal_generators5) + bad_therm_gen_ramp_lim[1].tech.ramplimits = (up = -10, down = -3) + @test_logs((:error, r"Invalid range"), match_mode=:any, + @test_throws(PSY.InvalidRange, + System(nodes5, bad_therm_gen_ramp_lim, loads5, nothing, nothing, 100.0, + nothing, nothing, nothing; runchecks=true) + ) + ) +end + +@testset "Test field validation" begin + sys = System(nodes5, thermal_generators5, loads5, nothing, nothing, + 100.0, nothing, nothing, nothing; runchecks=true) + + add_component!(sys,Bus(11,"11",PSY.PQ,1,1,(min=.9,max=1.1),123)) + B = get_components(Bus,sys) |> collect + a = Arc(B[1],B[6]) + badline = Line("badline",true,0.01,0.01,a,0.002,0.014,(from = 0.015, to = 0.015),5.0,(min = -1, max = 1)) + @test_logs((:error, r"cannot create Line"), match_mode=:any, + @test_throws(PSY.InvalidValue, + add_component!(sys, badline) + ) + ) +end