From 56a724e7ecd0586b95ecfa3eb95498527c1e921f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Vinot?= Date: Wed, 29 Nov 2023 16:17:07 +0100 Subject: [PATCH 01/51] Apply conda forge update proposal --- conda/environment.yml | 5 +++-- conda/meta.yaml | 9 +++++++-- poetry.lock | 4 ++-- pyproject.toml | 3 ++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/conda/environment.yml b/conda/environment.yml index 1285b8e3..10c01870 100644 --- a/conda/environment.yml +++ b/conda/environment.yml @@ -11,7 +11,8 @@ dependencies: - regex >=2022.1.18 - pint >=0.21.0 - requests >=2.28.1 - - typing-extensions >=4.6.2 + - typing_extensions >=4.6.2 - rich >=13.5.1 - - matplotlib >=3.7.2 + - pyproj >=3.3.0 + - matplotlib-base >=3.7.2 - networkx >=3.0.0 diff --git a/conda/meta.yaml b/conda/meta.yaml index 3ce83ce7..a53a8f7a 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -33,9 +33,10 @@ requirements: - regex >=2022.1.18 - pint >=0.21.0 - requests >=2.28.1 - - typing-extensions >=4.6.2 + - typing_extensions >=4.6.2 - rich >=13.5.1 - - matplotlib >=3.7.2 + - pyproj >=3.3.0 + - matplotlib-base >=3.7.2 - networkx >=3.0.0 test: @@ -45,6 +46,10 @@ test: - roseau.load_flow.io - roseau.load_flow.models - roseau.load_flow.utils + commands: + - pip check + requires: + - pip about: home: https://github.com/RoseauTechnologies/Roseau_Load_Flow/ diff --git a/poetry.lock b/poetry.lock index 2d058cdc..54a9df86 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -2236,4 +2236,4 @@ plot = ["matplotlib"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "ed57186be9cfa88d048a30d9d37120cd0ecbbebd74197bc207235f1cb88b05ab" +content-hash = "5e888e2d0ed76b9b934cbae9793a66fafd87d9d72d4976c4cf8241f12a2d82d2" diff --git a/pyproject.toml b/pyproject.toml index 9004aaad..18cf5dbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ requests = ">=2.28.1" pint = ">=0.21.0" typing-extensions = ">=4.6.2" rich = ">=13.5.1" +pyproj = "^3.3.0" # Optional dependencies matplotlib = { version = ">=3.7.2", optional = true } @@ -141,7 +142,7 @@ directory = "htmlcov" # Pytest [tool.pytest.ini_options] -addopts = "--color=yes -vv -n=0" +addopts = "--color=yes -vv -n=0 --import-mode=importlib" testpaths = ["roseau/load_flow/"] filterwarnings = [ 'ignore:.*utcfromtimestamp:DeprecationWarning:dateutil.*', # dateutil is imported by pandas, not us From 495648dd7bab963c720b23ddd82edd0398e77076 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Fri, 1 Dec 2023 17:09:37 +0100 Subject: [PATCH 02/51] Update to Python 3.10 (#151) Set the minimum supported Python version at 3.10 and applies the ruff fixes --- .github/workflows/ci.yml | 2 +- .github/workflows/conda.yml | 2 +- .pre-commit-config.yaml | 3 +- .vscode/extensions.json | 5 - .vscode/settings.json | 2 +- README.md | 2 +- doc/Changelog.md | 4 + doc/index.md | 2 +- poetry.lock | 80 +---------- pyproject.toml | 7 +- roseau/load_flow/_wrapper.py | 20 +-- roseau/load_flow/converters.py | 2 +- roseau/load_flow/models/branches.py | 6 +- roseau/load_flow/models/buses.py | 30 ++-- roseau/load_flow/models/core.py | 10 +- roseau/load_flow/models/grounds.py | 4 +- roseau/load_flow/models/lines/lines.py | 24 ++-- roseau/load_flow/models/lines/parameters.py | 56 ++++---- .../models/loads/flexible_parameters.py | 136 +++++++++--------- roseau/load_flow/models/loads/loads.py | 20 +-- roseau/load_flow/models/potential_refs.py | 6 +- roseau/load_flow/models/sources.py | 6 +- .../models/transformers/parameters.py | 56 ++++---- .../models/transformers/transformers.py | 20 +-- roseau/load_flow/network.py | 40 +++--- roseau/load_flow/solvers.py | 3 +- roseau/load_flow/typing.py | 29 ++-- roseau/load_flow/units.py | 7 +- roseau/load_flow/utils/mixins.py | 2 +- 29 files changed, 250 insertions(+), 336 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a6e9cdc..962c0c4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/conda.yml b/.github/workflows/conda.yml index 7fcd9e14..c840d7fe 100644 --- a/.github/workflows/conda.yml +++ b/.github/workflows/conda.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9"] + python-version: ["3.12"] steps: - uses: actions/checkout@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7a8257c..684b603c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - - id: check-builtin-literals - id: check-json exclude: ^.vscode/ - id: check-merge-conflict @@ -30,7 +29,7 @@ repos: - id: blacken-docs entry: bash -c "blacken-docs -l 90 $(find doc/ -name '*.md')" args: [-l 90] - additional_dependencies: [black==23.10.1] # keep in sync with black above + additional_dependencies: [black==23.11.0] - repo: https://github.com/pre-commit/mirrors-prettier rev: v3.1.0 hooks: diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 07f32b7a..51acae9d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,12 +2,7 @@ "recommendations": [ "charliermarsh.ruff", "esbenp.prettier-vscode", - "ms-python.black-formatter", "ms-python.python", "ms-python.vscode-pylance", ], - "unwantedRecommendations": [ - "ms-python.flake8", // We use ruff - "ms-python.isort" // We use ruff - ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 5c4f517a..da5cade4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,7 +14,7 @@ }, "python.testing.pytestEnabled": true, "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter", + "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports.ruff": "explicit", diff --git a/README.md b/README.md index b878d1d3..b8a9b591 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Documentation](https://github.com/RoseauTechnologies/Roseau_Load_Flow/actions/workflows/doc.yml/badge.svg)](https://github.com/RoseauTechnologies/Roseau_Load_Flow/actions/workflows/doc.yml) [![pre-commit](https://github.com/RoseauTechnologies/Roseau_Load_Flow/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/RoseauTechnologies/Roseau_Load_Flow/actions/workflows/pre-commit.yml) -_Roseau Load Flow_ is a highly capable three-phase load flow solver. This project is compatible with Python 3.9 and +_Roseau Load Flow_ is a highly capable three-phase load flow solver. This project is compatible with Python 3.10 and above. Please take a look at our documentation to see how to install and use `roseau-load-flow`. diff --git a/doc/Changelog.md b/doc/Changelog.md index 6eea5d84..b6dfec49 100644 --- a/doc/Changelog.md +++ b/doc/Changelog.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- {gh-pr}`151` Require Python 3.10 or newer. + ## Version 0.6.0 - {gh-pr}`149` {gh-issue}`145` Add custom pint wrapper for better handling of pint arrays. diff --git a/doc/index.md b/doc/index.md index 49e1c4c7..23e9db54 100644 --- a/doc/index.md +++ b/doc/index.md @@ -17,7 +17,7 @@ More details are given in the [Catalogues page](catalogues-networks). ## Installation -`roseau-load-flow` is the python interface to the solver. It is compatible with Python version 3.9 +`roseau-load-flow` is the python interface to the solver. It is compatible with Python version 3.10 and newer and can be installed with: ```{toctree} diff --git a/poetry.lock b/poetry.lock index 54a9df86..c040570c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -519,7 +519,6 @@ certifi = "*" click = ">=8.0,<9.0" click-plugins = ">=1.0" cligj = ">=0.5" -importlib-metadata = {version = "*", markers = "python_version < \"3.10\""} setuptools = "*" six = "*" @@ -665,43 +664,6 @@ files = [ {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] -[[package]] -name = "importlib-metadata" -version = "6.8.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, - {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] - -[[package]] -name = "importlib-resources" -version = "6.1.1" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_resources-6.1.1-py3-none-any.whl", hash = "sha256:e8bf90d8213b486f428c9c39714b920041cb02c184686a3dee24905aaa8105d6"}, - {file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff", "zipp (>=3.17)"] - [[package]] name = "iniconfig" version = "2.0.0" @@ -908,16 +870,6 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -991,7 +943,6 @@ files = [ contourpy = ">=1.0.1" cycler = ">=0.10" fonttools = ">=4.22.0" -importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} kiwisolver = ">=1.3.1" numpy = ">=1.21,<2" packaging = ">=20.0" @@ -1549,7 +1500,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1557,15 +1507,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1582,7 +1525,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1590,7 +1532,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1900,7 +1841,6 @@ babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} docutils = ">=0.18.1,<0.21" imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" Pygments = ">=2.14" @@ -2040,7 +1980,6 @@ files = [ [package.dependencies] docutils = ">=0.8,<0.18.dev0 || >=0.20.dev0" -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} pybtex = ">=0.24" pybtex-docutils = ">=1.0.0" Sphinx = ">=3.5" @@ -2214,26 +2153,11 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] -[[package]] -name = "zipp" -version = "3.17.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] - [extras] graph = ["networkx"] plot = ["matplotlib"] [metadata] lock-version = "2.0" -python-versions = "^3.9" -content-hash = "5e888e2d0ed76b9b934cbae9793a66fafd87d9d72d4976c4cf8241f12a2d82d2" +python-versions = "^3.10" +content-hash = "d216daa4b15d67093d163277ed61afe6d156d4a9e7b7edff0d978248c4711b93" diff --git a/pyproject.toml b/pyproject.toml index 18cf5dbb..de1f8dd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,6 @@ packages = [ ] classifiers = [ "Development Status :: 3 - Alpha", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -38,7 +37,7 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry.dependencies] -python = "^3.9" +python = "^3.10" numpy = [ { version = ">=1.21.5", python = "<3.12" }, { version = ">=1.26.0", python = ">=3.12,<3.13" }, @@ -89,11 +88,11 @@ sphinxcontrib-bibtex = "^2.5.0" [tool.ruff] line-length = 120 -target-version = "py39" +target-version = "py310" show-fixes = true namespace-packages = ["roseau"] extend-include = ["*.ipynb"] -select = ["E", "F", "C90", "W", "B", "UP", "I", "RUF100", "TID", "SIM", "PT", "PIE", "N", "C4", "NPY"] +select = ["E", "F", "C90", "W", "B", "UP", "I", "RUF100", "TID", "SIM", "PT", "PIE", "N", "C4", "NPY", "T10"] unfixable = ["B"] ignore = ["E501", "B024", "N818"] diff --git a/roseau/load_flow/_wrapper.py b/roseau/load_flow/_wrapper.py index 3f0153f7..003cdd7e 100644 --- a/roseau/load_flow/_wrapper.py +++ b/roseau/load_flow/_wrapper.py @@ -1,8 +1,8 @@ import functools -from collections.abc import Iterable, MutableSequence +from collections.abc import Callable, Iterable, MutableSequence from inspect import Parameter, Signature, signature from itertools import zip_longest -from typing import Any, Callable, Optional, TypeVar, Union +from typing import Any, TypeVar from pint import Quantity, Unit from pint.registry import UnitRegistry @@ -12,7 +12,7 @@ FuncT = TypeVar("FuncT", bound=Callable) -def _parse_wrap_args(args: Iterable[Optional[Union[str, Unit]]]) -> Callable: +def _parse_wrap_args(args: Iterable[str | Unit | None]) -> Callable: """Create a converter function for the wrapper""" # _to_units_container args_as_uc = [to_units_container(arg) for arg in args] @@ -63,8 +63,8 @@ def _apply_defaults(sig: Signature, args: tuple[Any], kwargs: dict[str, Any]) -> def wraps( ureg: UnitRegistry, - ret: Optional[Union[str, Unit, Iterable[Optional[Union[str, Unit]]]]], - args: Optional[Union[str, Unit, Iterable[Optional[Union[str, Unit]]]]], + ret: str | Unit | Iterable[str | Unit | None] | None, + args: str | Unit | Iterable[str | Unit | None] | None, ) -> Callable[[FuncT], FuncT]: """Wraps a function to become pint-aware. @@ -94,23 +94,23 @@ def wraps( if the number of given arguments does not match the number of function parameters. if any of the provided arguments is not a unit a string or Quantity """ - if not isinstance(args, (list, tuple)): + if not isinstance(args, list | tuple): args = (args,) for arg in args: - if arg is not None and not isinstance(arg, (ureg.Unit, str)): + if arg is not None and not isinstance(arg, ureg.Unit | str): raise TypeError(f"wraps arguments must by of type str or Unit, not {type(arg)} ({arg})") converter = _parse_wrap_args(args) - is_ret_container = isinstance(ret, (list, tuple)) + is_ret_container = isinstance(ret, list | tuple) if is_ret_container: for arg in ret: - if arg is not None and not isinstance(arg, (ureg.Unit, str)): + if arg is not None and not isinstance(arg, ureg.Unit | str): raise TypeError(f"wraps 'ret' argument must by of type str or Unit, not {type(arg)} ({arg})") ret = ret.__class__([to_units_container(arg, ureg) for arg in ret]) else: - if ret is not None and not isinstance(ret, (ureg.Unit, str)): + if ret is not None and not isinstance(ret, ureg.Unit | str): raise TypeError(f"wraps 'ret' argument must by of type str or Unit, not {type(ret)} ({ret})") ret = to_units_container(ret, ureg) diff --git a/roseau/load_flow/converters.py b/roseau/load_flow/converters.py index 1ebeafe3..12ce353b 100644 --- a/roseau/load_flow/converters.py +++ b/roseau/load_flow/converters.py @@ -173,4 +173,4 @@ def calculate_voltage_phases(phases: str) -> list[str]: if len(phases) == 2: return [phases] else: - return [p1 + p2 for p1, p2 in zip(phases, np.roll(list(phases), -1))] + return [p1 + p2 for p1, p2 in zip(phases, np.roll(list(phases), -1), strict=True)] diff --git a/roseau/load_flow/models/branches.py b/roseau/load_flow/models/branches.py index ea5fb888..55d0effd 100644 --- a/roseau/load_flow/models/branches.py +++ b/roseau/load_flow/models/branches.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Optional, Union +from typing import Any import numpy as np from shapely import LineString, Point @@ -33,7 +33,7 @@ def __init__( *, phases1: str, phases2: str, - geometry: Optional[Union[Point, LineString]] = None, + geometry: Point | LineString | None = None, **kwargs: Any, ) -> None: """AbstractBranch constructor. @@ -66,7 +66,7 @@ def __init__( self.bus2 = bus2 self.geometry = geometry self._connect(bus1, bus2) - self._res_currents: Optional[tuple[ComplexArray, ComplexArray]] = None + self._res_currents: tuple[ComplexArray, ComplexArray] | None = None def __repr__(self) -> str: s = f"{type(self).__name__}(id={self.id!r}, phases1={self.phases1!r}, phases2={self.phases2!r}" diff --git a/roseau/load_flow/models/buses.py b/roseau/load_flow/models/buses.py index b84b256b..a4e6d2f5 100644 --- a/roseau/load_flow/models/buses.py +++ b/roseau/load_flow/models/buses.py @@ -1,6 +1,6 @@ import logging from collections.abc import Iterator -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Any, Optional import numpy as np import pandas as pd @@ -35,10 +35,10 @@ def __init__( id: Id, *, phases: str, - geometry: Optional[Point] = None, - potentials: Optional[ComplexArrayLike1D] = None, - min_voltage: Optional[float] = None, - max_voltage: Optional[float] = None, + geometry: Point | None = None, + potentials: ComplexArrayLike1D | None = None, + min_voltage: float | None = None, + max_voltage: float | None = None, **kwargs: Any, ) -> None: """Bus constructor. @@ -79,12 +79,12 @@ def __init__( potentials = [0] * len(phases) self.potentials = potentials self.geometry = geometry - self._min_voltage: Optional[float] = None - self._max_voltage: Optional[float] = None + self._min_voltage: float | None = None + self._max_voltage: float | None = None self.min_voltage = min_voltage self.max_voltage = max_voltage - self._res_potentials: Optional[ComplexArray] = None + self._res_potentials: ComplexArray | None = None self._short_circuits: list[dict[str, Any]] = [] def __repr__(self) -> str: @@ -141,13 +141,13 @@ def _get_potentials_of(self, phases: str, warning: bool) -> ComplexArray: return np.array([potentials[self.phases.index(p)] for p in phases]) @property - def min_voltage(self) -> Optional[Q_[float]]: + def min_voltage(self) -> Q_[float] | None: """The minimum voltage of the bus (V) if it is set.""" return None if self._min_voltage is None else Q_(self._min_voltage, "V") @min_voltage.setter @ureg_wraps(None, (None, "V")) - def min_voltage(self, value: Optional[Union[float, Q_[float]]]) -> None: + def min_voltage(self, value: float | Q_[float] | None) -> None: if value is not None and self._max_voltage is not None and value > self._max_voltage: msg = ( f"Cannot set min voltage of bus {self.id!r} to {value} V as it is higher than its " @@ -160,13 +160,13 @@ def min_voltage(self, value: Optional[Union[float, Q_[float]]]) -> None: self._min_voltage = value @property - def max_voltage(self) -> Optional[Q_[float]]: + def max_voltage(self) -> Q_[float] | None: """The maximum voltage of the bus (V) if it is set.""" return None if self._max_voltage is None else Q_(self._max_voltage, "V") @max_voltage.setter @ureg_wraps(None, (None, "V")) - def max_voltage(self, value: Optional[Union[float, Q_[float]]]) -> None: + def max_voltage(self, value: float | Q_[float] | None) -> None: if value is not None and self._min_voltage is not None and value < self._min_voltage: msg = ( f"Cannot set max voltage of bus {self.id!r} to {value} V as it is lower than its " @@ -179,7 +179,7 @@ def max_voltage(self, value: Optional[Union[float, Q_[float]]]) -> None: self._max_voltage = value @property - def res_violated(self) -> Optional[bool]: + def res_violated(self) -> bool | None: """Whether the bus has voltage limits violations. Returns ``None`` if the bus has no voltage limits are not set. @@ -221,7 +221,7 @@ def propagate_limits(self, force: bool = False) -> None: while remaining: branch = remaining.pop() visited.add(branch) - if not isinstance(branch, (Line, Switch)): + if not isinstance(branch, Line | Switch): continue for element in branch._connected_elements: if not isinstance(element, Bus) or element is self or element in buses: @@ -274,7 +274,7 @@ def get_connected_buses(self) -> Iterator[Id]: while remaining: branch = remaining.pop() visited.add(branch) - if not isinstance(branch, (Line, Switch)): + if not isinstance(branch, Line | Switch): continue for element in branch._connected_elements: if not isinstance(element, Bus) or element.id in visited_buses: diff --git a/roseau/load_flow/models/core.py b/roseau/load_flow/models/core.py index b117105d..74d176f4 100644 --- a/roseau/load_flow/models/core.py +++ b/roseau/load_flow/models/core.py @@ -1,7 +1,7 @@ import logging import warnings from abc import ABC -from typing import TYPE_CHECKING, Any, ClassVar, NoReturn, Optional, TypeVar, Union +from typing import TYPE_CHECKING, Any, ClassVar, NoReturn, Optional, TypeVar import shapely from shapely.geometry import shape @@ -39,7 +39,7 @@ def __init__(self, id: Id, **kwargs: Any) -> None: """ super().__init__(id) self._connected_elements: list[Element] = [] - self._network: Optional[ElectricalNetwork] = None + self._network: ElectricalNetwork | None = None @property def network(self) -> Optional["ElectricalNetwork"]: @@ -47,7 +47,7 @@ def network(self) -> Optional["ElectricalNetwork"]: return self._network @classmethod - def _check_phases(cls, id: Id, allowed_phases: Optional[frozenset[str]] = None, **kwargs: str) -> None: + def _check_phases(cls, id: Id, allowed_phases: frozenset[str] | None = None, **kwargs: str) -> None: if allowed_phases is None: allowed_phases = cls.allowed_phases name, phases = kwargs.popitem() # phases, phases1 or phases2 @@ -131,7 +131,7 @@ def _invalidate_network_results(self) -> None: if self.network is not None: self.network._results_valid = False - def _res_getter(self, value: Optional[_T], warning: bool) -> _T: + def _res_getter(self, value: _T | None, warning: bool) -> _T: """A safe getter for load flow results. Args: @@ -163,7 +163,7 @@ def _res_getter(self, value: Optional[_T], warning: bool) -> _T: return value @staticmethod - def _parse_geometry(geometry: Union[str, None, Any]) -> Optional[BaseGeometry]: + def _parse_geometry(geometry: str | None | Any) -> BaseGeometry | None: if geometry is None: return None elif isinstance(geometry, str): diff --git a/roseau/load_flow/models/grounds.py b/roseau/load_flow/models/grounds.py index 083534af..0e70c777 100644 --- a/roseau/load_flow/models/grounds.py +++ b/roseau/load_flow/models/grounds.py @@ -1,5 +1,5 @@ import logging -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any from typing_extensions import Self @@ -44,7 +44,7 @@ def __init__(self, id: Id, **kwargs: Any) -> None: super().__init__(id, **kwargs) # A map of bus id to phase connected to this ground. self._connected_buses: dict[Id, str] = {} - self._res_potential: Optional[complex] = None + self._res_potential: complex | None = None def __repr__(self) -> str: return f"{type(self).__name__}(id={self.id!r})" diff --git a/roseau/load_flow/models/lines/lines.py b/roseau/load_flow/models/lines/lines.py index 6a0a4d3e..42499dfb 100644 --- a/roseau/load_flow/models/lines/lines.py +++ b/roseau/load_flow/models/lines/lines.py @@ -1,6 +1,6 @@ import logging import warnings -from typing import Any, Optional, Union +from typing import Any import numpy as np from shapely import LineString, Point @@ -38,8 +38,8 @@ def __init__( bus1: Bus, bus2: Bus, *, - phases: Optional[str] = None, - geometry: Optional[Point] = None, + phases: str | None = None, + geometry: Point | None = None, **kwargs: Any, ) -> None: """Switch constructor. @@ -95,7 +95,7 @@ def _check_loop(self) -> None: element = elements.pop(-1) visited_1.add(element) for e in element._connected_elements: - if e not in visited_1 and (isinstance(e, (Bus, Switch))) and e != self: + if e not in visited_1 and (isinstance(e, Bus | Switch)) and e != self: elements.append(e) visited_2: set[Element] = set() elements = [self.bus2] @@ -103,7 +103,7 @@ def _check_loop(self) -> None: element = elements.pop(-1) visited_2.add(element) for e in element._connected_elements: - if e not in visited_2 and (isinstance(e, (Bus, Switch))) and e != self: + if e not in visited_2 and (isinstance(e, Bus | Switch)) and e != self: elements.append(e) if visited_1.intersection(visited_2): msg = f"There is a loop of switch involving the switch {self.id!r}. It is not allowed." @@ -144,10 +144,10 @@ def __init__( bus2: Bus, *, parameters: LineParameters, - length: Union[float, Q_[float]], - phases: Optional[str] = None, - ground: Optional[Ground] = None, - geometry: Optional[LineString] = None, + length: float | Q_[float], + phases: str | None = None, + ground: Ground | None = None, + geometry: LineString | None = None, **kwargs: Any, ) -> None: """Line constructor. @@ -231,7 +231,7 @@ def length(self) -> Q_[float]: @length.setter @ureg_wraps(None, (None, "km")) - def length(self, value: Union[float, Q_[float]]) -> None: + def length(self, value: float | Q_[float]) -> None: if value <= 0: msg = f"A line length must be greater than 0. {value:.2f} km provided." logger.error(msg) @@ -286,7 +286,7 @@ def y_shunt(self) -> Q_[ComplexArray]: return self.parameters._y_shunt * self._length @property - def max_current(self) -> Optional[Q_[float]]: + def max_current(self) -> Q_[float] | None: """The maximum current loading of the line in A.""" # Do not add a setter. The user must know that if they change the max_current, it changes # for all lines that share the parameters. It is better to set it on the parameters. @@ -370,7 +370,7 @@ def res_power_losses(self) -> Q_[ComplexArray]: return self._res_power_losses_getter(warning=True) @property - def res_violated(self) -> Optional[bool]: + def res_violated(self) -> bool | None: """Whether the line current exceeds the maximum current (loading > 100%). Returns ``None`` if the maximum current is not set. diff --git a/roseau/load_flow/models/lines/parameters.py b/roseau/load_flow/models/lines/parameters.py index a2776fe9..887cb885 100644 --- a/roseau/load_flow/models/lines/parameters.py +++ b/roseau/load_flow/models/lines/parameters.py @@ -1,6 +1,6 @@ import logging import re -from typing import NoReturn, Optional, Union +from typing import NoReturn import numpy as np import numpy.linalg as nplin @@ -44,8 +44,8 @@ def __init__( self, id: Id, z_line: ComplexArrayLike2D, - y_shunt: Optional[ComplexArrayLike2D] = None, - max_current: Optional[float] = None, + y_shunt: ComplexArrayLike2D | None = None, + max_current: float | None = None, ) -> None: """LineParameters constructor. @@ -106,13 +106,13 @@ def with_shunt(self) -> bool: return self._with_shunt @property - def max_current(self) -> Optional[Q_[float]]: + def max_current(self) -> Q_[float] | None: """The maximum current loading of the line (A) if it is set.""" return None if self._max_current is None else Q_(self._max_current, "A") @max_current.setter @ureg_wraps(None, (None, "A")) - def max_current(self, value: Optional[Union[float, Q_[float]]]) -> None: + def max_current(self, value: float | Q_[float] | None) -> None: self._max_current = value @classmethod @@ -120,15 +120,15 @@ def max_current(self, value: Optional[Union[float, Q_[float]]]) -> None: def from_sym( cls, id: Id, - z0: Union[complex, Q_[complex]], - z1: Union[complex, Q_[complex]], - y0: Union[complex, Q_[complex]], - y1: Union[complex, Q_[complex]], - zn: Optional[Union[complex, Q_[complex]]] = None, - xpn: Optional[Union[float, Q_[float]]] = None, - bn: Optional[Union[float, Q_[float]]] = None, - bpn: Optional[Union[float, Q_[float]]] = None, - max_current: Optional[Union[float, Q_[float]]] = None, + z0: complex | Q_[complex], + z1: complex | Q_[complex], + y0: complex | Q_[complex], + y1: complex | Q_[complex], + zn: complex | Q_[complex] | None = None, + xpn: float | Q_[float] | None = None, + bn: float | Q_[float] | None = None, + bpn: float | Q_[float] | None = None, + max_current: float | Q_[float] | None = None, ) -> Self: """Create line parameters from a symmetric model. @@ -181,10 +181,10 @@ def _sym_to_zy( z1: complex, y0: complex, y1: complex, - zn: Optional[complex] = None, - xpn: Optional[float] = None, - bn: Optional[float] = None, - bpn: Optional[float] = None, + zn: complex | None = None, + xpn: float | None = None, + bn: float | None = None, + bpn: float | None = None, ) -> tuple[ComplexArray, ComplexArray]: """Create impedance and admittance matrix from a symmetrical model. @@ -302,11 +302,11 @@ def from_geometry( line_type: LineType, conductor_type: ConductorType, insulator_type: InsulatorType, - section: Union[float, Q_[float]], - section_neutral: Union[float, Q_[float]], - height: Union[float, Q_[float]], - external_diameter: Union[float, Q_[float]], - max_current: Optional[Union[float, Q_[float]]] = None, + section: float | Q_[float], + section_neutral: float | Q_[float], + height: float | Q_[float], + external_diameter: float | Q_[float], + max_current: float | Q_[float] | None = None, ) -> Self: """Create line parameters from its geometry. @@ -500,10 +500,10 @@ def _geometry_to_zy( def from_name_lv( cls, name: str, - section_neutral: Optional[Union[float, Q_[float]]] = None, - height: Optional[Union[float, Q_[float]]] = None, - external_diameter: Optional[Union[float, Q_[float]]] = None, - max_current: Optional[Union[float, Q_[float]]] = None, + section_neutral: float | Q_[float] | None = None, + height: float | Q_[float] | None = None, + external_diameter: float | Q_[float] | None = None, + max_current: float | Q_[float] | None = None, ) -> Self: """Method to get the electrical parameters of a LV line from its canonical name. Some hypothesis will be made: the section of the neutral is the same as the other sections, the height and @@ -566,7 +566,7 @@ def from_name_lv( @classmethod @ureg_wraps(None, (None, None, "A")) - def from_name_mv(cls, name: str, max_current: Optional[Union[float, Q_[float]]] = None) -> Self: + def from_name_mv(cls, name: str, max_current: float | Q_[float] | None = None) -> Self: """Method to get the electrical parameters of a MV line from its canonical name. Args: diff --git a/roseau/load_flow/models/loads/flexible_parameters.py b/roseau/load_flow/models/loads/flexible_parameters.py index d687fbae..4c939d27 100644 --- a/roseau/load_flow/models/loads/flexible_parameters.py +++ b/roseau/load_flow/models/loads/flexible_parameters.py @@ -1,6 +1,6 @@ import logging import warnings -from typing import TYPE_CHECKING, NoReturn, Optional, Union +from typing import TYPE_CHECKING, NoReturn, Optional import numpy as np from numpy.typing import NDArray @@ -48,11 +48,11 @@ class Control(JsonMixin): def __init__( self, type: ControlType, - u_min: Union[float, Q_[float]], - u_down: Union[float, Q_[float]], - u_up: Union[float, Q_[float]], - u_max: Union[float, Q_[float]], - alpha: Union[float, Q_[float]] = _DEFAULT_ALPHA, + u_min: float | Q_[float], + u_down: float | Q_[float], + u_up: float | Q_[float], + u_max: float | Q_[float], + alpha: float | Q_[float] = _DEFAULT_ALPHA, ) -> None: """Control constructor. @@ -189,7 +189,7 @@ def constant(cls) -> Self: @classmethod @ureg_wraps(None, (None, "V", "V", None)) def p_max_u_production( - cls, u_up: Union[float, Q_[float]], u_max: Union[float, Q_[float]], alpha: float = _DEFAULT_ALPHA + cls, u_up: float | Q_[float], u_max: float | Q_[float], alpha: float = _DEFAULT_ALPHA ) -> Self: """Create a control of the type ``"p_max_u_production"``. @@ -220,7 +220,7 @@ def p_max_u_production( @classmethod @ureg_wraps(None, (None, "V", "V", None)) def p_max_u_consumption( - cls, u_min: Union[float, Q_[float]], u_down: Union[float, Q_[float]], alpha: float = _DEFAULT_ALPHA + cls, u_min: float | Q_[float], u_down: float | Q_[float], alpha: float = _DEFAULT_ALPHA ) -> Self: """Create a control of the type ``"p_max_u_consumption"``. @@ -252,10 +252,10 @@ def p_max_u_consumption( @ureg_wraps(None, (None, "V", "V", "V", "V", None)) def q_u( cls, - u_min: Union[float, Q_[float]], - u_down: Union[float, Q_[float]], - u_up: Union[float, Q_[float]], - u_max: Union[float, Q_[float]], + u_min: float | Q_[float], + u_down: float | Q_[float], + u_up: float | Q_[float], + u_max: float | Q_[float], alpha: float = _DEFAULT_ALPHA, ) -> Self: """Create a control of the type ``"q_u"``. @@ -464,9 +464,9 @@ def __init__( control_p: Control, control_q: Control, projection: Projection, - s_max: Union[float, Q_[float]], - q_min: Optional[Union[float, Q_[float]]] = None, - q_max: Optional[Union[float, Q_[float]]] = None, + s_max: float | Q_[float], + q_min: float | Q_[float] | None = None, + q_max: float | Q_[float] | None = None, ) -> None: """FlexibleParameter constructor. @@ -508,7 +508,7 @@ def s_max(self) -> Q_[float]: @s_max.setter @ureg_wraps(None, (None, "VA")) - def s_max(self, value: Union[float, Q_[float]]) -> None: + def s_max(self, value: float | Q_[float]) -> None: if value <= 0: s_max = Q_(value, "VA") msg = f"'s_max' must be greater than 0 but {s_max:P#~} was provided." @@ -530,7 +530,7 @@ def q_min(self) -> Q_[float]: @q_min.setter @ureg_wraps(None, (None, "VAr")) - def q_min(self, value: Optional[Union[float, Q_[float]]]) -> None: + def q_min(self, value: float | Q_[float] | None) -> None: if value is not None and value < -self._s_max: q_min = Q_(value, "VAr") msg = f"'q_min' must be greater than -s_max ({-self.s_max:P#~}) but {q_min:P#~} was provided." @@ -551,7 +551,7 @@ def q_max(self) -> Q_[float]: @q_max.setter @ureg_wraps(None, (None, "VAr")) - def q_max(self, value: Optional[Union[float, Q_[float]]]) -> None: + def q_max(self, value: float | Q_[float] | None) -> None: if value is not None and value > self._s_max: q_max = Q_(value, "VAr") msg = f"'q_max' must be less than s_max ({self.s_max:P#~}) but {q_max:P#~} was provided." @@ -582,9 +582,9 @@ def constant(cls) -> Self: @ureg_wraps(None, (None, "V", "V", "VA", None, None, None, None)) def p_max_u_production( cls, - u_up: Union[float, Q_[float]], - u_max: Union[float, Q_[float]], - s_max: Union[float, Q_[float]], + u_up: float | Q_[float], + u_max: float | Q_[float], + s_max: float | Q_[float], alpha_control: float = Control._DEFAULT_ALPHA, type_proj: ProjectionType = Projection._DEFAULT_TYPE, alpha_proj: float = Projection._DEFAULT_ALPHA, @@ -638,9 +638,9 @@ def p_max_u_production( @ureg_wraps(None, (None, "V", "V", "VA", None, None, None, None)) def p_max_u_consumption( cls, - u_min: Union[float, Q_[float]], - u_down: Union[float, Q_[float]], - s_max: Union[float, Q_[float]], + u_min: float | Q_[float], + u_down: float | Q_[float], + s_max: float | Q_[float], alpha_control: float = Control._DEFAULT_ALPHA, type_proj: ProjectionType = Projection._DEFAULT_TYPE, alpha_proj: float = Projection._DEFAULT_ALPHA, @@ -691,13 +691,13 @@ def p_max_u_consumption( @ureg_wraps(None, (None, "V", "V", "V", "V", "VA", "Var", "Var", None, None, None, None)) def q_u( cls, - u_min: Union[float, Q_[float]], - u_down: Union[float, Q_[float]], - u_up: Union[float, Q_[float]], - u_max: Union[float, Q_[float]], - s_max: Union[float, Q_[float]], - q_min: Optional[Union[float, Q_[float]]] = None, - q_max: Optional[Union[float, Q_[float]]] = None, + u_min: float | Q_[float], + u_down: float | Q_[float], + u_up: float | Q_[float], + u_max: float | Q_[float], + s_max: float | Q_[float], + q_min: float | Q_[float] | None = None, + q_max: float | Q_[float] | None = None, alpha_control: float = Control._DEFAULT_ALPHA, type_proj: ProjectionType = Projection._DEFAULT_TYPE, alpha_proj: float = Projection._DEFAULT_ALPHA, @@ -765,15 +765,15 @@ def q_u( @ureg_wraps(None, (None, "V", "V", "V", "V", "V", "V", "VA", "VAr", "VAr", None, None, None, None)) def pq_u_production( cls, - up_up: Union[float, Q_[float]], - up_max: Union[float, Q_[float]], - uq_min: Union[float, Q_[float]], - uq_down: Union[float, Q_[float]], - uq_up: Union[float, Q_[float]], - uq_max: Union[float, Q_[float]], - s_max: Union[float, Q_[float]], - q_min: Optional[Union[float, Q_[float]]] = None, - q_max: Optional[Union[float, Q_[float]]] = None, + up_up: float | Q_[float], + up_max: float | Q_[float], + uq_min: float | Q_[float], + uq_down: float | Q_[float], + uq_up: float | Q_[float], + uq_max: float | Q_[float], + s_max: float | Q_[float], + q_min: float | Q_[float] | None = None, + q_max: float | Q_[float] | None = None, alpha_control=Control._DEFAULT_ALPHA, type_proj: ProjectionType = Projection._DEFAULT_TYPE, alpha_proj=Projection._DEFAULT_ALPHA, @@ -852,16 +852,16 @@ def pq_u_production( @ureg_wraps(None, (None, "V", "V", "V", "V", "V", "V", "VA", "VAr", "VAr", None, None, None, None)) def pq_u_consumption( cls, - up_min: Union[float, Q_[float]], - up_down: Union[float, Q_[float]], - uq_min: Union[float, Q_[float]], - uq_down: Union[float, Q_[float]], - uq_up: Union[float, Q_[float]], - uq_max: Union[float, Q_[float]], - s_max: Union[float, Q_[float]], - q_min: Optional[Union[float, Q_[float]]] = None, - q_max: Optional[Union[float, Q_[float]]] = None, - alpha_control: Union[float, Q_[float]] = Control._DEFAULT_ALPHA, + up_min: float | Q_[float], + up_down: float | Q_[float], + uq_min: float | Q_[float], + uq_down: float | Q_[float], + uq_up: float | Q_[float], + uq_max: float | Q_[float], + s_max: float | Q_[float], + q_min: float | Q_[float] | None = None, + q_max: float | Q_[float] | None = None, + alpha_control: float | Q_[float] = Control._DEFAULT_ALPHA, type_proj: ProjectionType = Projection._DEFAULT_TYPE, alpha_proj: float = Projection._DEFAULT_ALPHA, epsilon_proj: float = Projection._DEFAULT_EPSILON, @@ -985,8 +985,8 @@ def compute_powers( self, auth: Authentication, voltages: ComplexArrayLike1D, - power: Union[complex, Q_[complex]], - solve_kwargs: Optional[JsonDict] = None, + power: complex | Q_[complex], + solve_kwargs: JsonDict | None = None, ) -> Q_[ComplexArray]: """Compute the flexible powers for different voltages (norms) @@ -1009,7 +1009,7 @@ def compute_powers( return self._compute_powers(auth=auth, voltages=voltages, power=power, solve_kwargs=solve_kwargs) def _compute_powers( - self, auth: Authentication, voltages: ComplexArrayLike1D, power: complex, solve_kwargs: Optional[JsonDict] + self, auth: Authentication, voltages: ComplexArrayLike1D, power: complex, solve_kwargs: JsonDict | None ) -> ComplexArray: from roseau.load_flow import Bus, ElectricalNetwork, PotentialRef, PowerLoad, VoltageSource @@ -1039,12 +1039,12 @@ def _compute_powers( def plot_pq( self, auth: Authentication, - voltages: Union[NDArray[np.float64], Q_[NDArray[np.float64]]], - power: Union[complex, Q_[complex]], + voltages: NDArray[np.float64] | Q_[NDArray[np.float64]], + power: complex | Q_[complex], ax: Optional["Axes"] = None, - solve_kwargs: Optional[JsonDict] = None, - voltages_labels_mask: Optional[NDArray[np.bool_]] = None, - res_flexible_powers: Optional[ComplexArray] = None, + solve_kwargs: JsonDict | None = None, + voltages_labels_mask: NDArray[np.bool_] | None = None, + res_flexible_powers: ComplexArray | None = None, ) -> tuple["Axes", ComplexArray]: """Plot the "trajectory" of the flexible powers (in the (P, Q) plane) for the provided voltages and theoretical power. @@ -1114,7 +1114,9 @@ def plot_pq( s=50, zorder=4, ) - for m, v, x, y in zip(voltages_labels_mask, voltages, res_flexible_powers.real, res_flexible_powers.imag): + for m, v, x, y in zip( + voltages_labels_mask, voltages, res_flexible_powers.real, res_flexible_powers.imag, strict=True + ): if not m: continue ax.annotate( @@ -1152,11 +1154,11 @@ def plot_pq( def plot_control_p( self, auth: Authentication, - voltages: Union[NDArray[np.float64], Q_[NDArray[np.float64]]], - power: Union[complex, Q_[complex]], + voltages: NDArray[np.float64] | Q_[NDArray[np.float64]], + power: complex | Q_[complex], ax: Optional["Axes"] = None, - solve_kwargs: Optional[JsonDict] = None, - res_flexible_powers: Optional[ComplexArray] = None, + solve_kwargs: JsonDict | None = None, + res_flexible_powers: ComplexArray | None = None, ) -> tuple["Axes", ComplexArray]: """Plot the flexible active power consumed (or produced) for the provided voltages and theoretical power. @@ -1218,11 +1220,11 @@ def plot_control_p( def plot_control_q( self, auth: Authentication, - voltages: Union[NDArray[np.float64], Q_[NDArray[np.float64]]], - power: Union[complex, Q_[complex]], + voltages: NDArray[np.float64] | Q_[NDArray[np.float64]], + power: complex | Q_[complex], ax: Optional["Axes"] = None, - solve_kwargs: Optional[JsonDict] = None, - res_flexible_powers: Optional[ComplexArray] = None, + solve_kwargs: JsonDict | None = None, + res_flexible_powers: ComplexArray | None = None, ) -> tuple["Axes", ComplexArray]: """Plot the flexible reactive power consumed (or produced) for the provided voltages and theoretical power. diff --git a/roseau/load_flow/models/loads/loads.py b/roseau/load_flow/models/loads/loads.py index dd92dc4d..45a7e8e6 100644 --- a/roseau/load_flow/models/loads/loads.py +++ b/roseau/load_flow/models/loads/loads.py @@ -1,6 +1,6 @@ import logging from abc import ABC -from typing import Any, Literal, Optional +from typing import Any, Literal import numpy as np @@ -34,7 +34,7 @@ class AbstractLoad(Element, ABC): allowed_phases = Bus.allowed_phases """The allowed phases for a load are the same as for a :attr:`bus`.""" - def __init__(self, id: Id, bus: Bus, *, phases: Optional[str] = None, **kwargs: Any) -> None: + def __init__(self, id: Id, bus: Bus, *, phases: str | None = None, **kwargs: Any) -> None: """AbstractLoad constructor. Args: @@ -78,7 +78,7 @@ def __init__(self, id: Id, bus: Bus, *, phases: Optional[str] = None, **kwargs: self._size = len(set(phases) - {"n"}) # Results - self._res_currents: Optional[ComplexArray] = None + self._res_currents: ComplexArray | None = None def __repr__(self) -> str: bus_id = self.bus.id if self.bus is not None else None @@ -210,8 +210,8 @@ def __init__( bus: Bus, *, powers: ComplexArrayLike1D, - phases: Optional[str] = None, - flexible_params: Optional[list[FlexibleParameter]] = None, + phases: str | None = None, + flexible_params: list[FlexibleParameter] | None = None, **kwargs: Any, ) -> None: """PowerLoad constructor. @@ -254,10 +254,10 @@ def __init__( self._flexible_params = flexible_params self.powers = powers - self._res_flexible_powers: Optional[ComplexArray] = None + self._res_flexible_powers: ComplexArray | None = None @property - def flexible_params(self) -> Optional[list[FlexibleParameter]]: + def flexible_params(self) -> list[FlexibleParameter] | None: return self._flexible_params @property @@ -275,7 +275,7 @@ def powers(self) -> Q_[ComplexArray]: def powers(self, value: ComplexArrayLike1D) -> None: value = self._validate_value(value) if self.is_flexible: - for power, fp in zip(value, self._flexible_params): + for power, fp in zip(value, self._flexible_params, strict=True): if fp.control_p.type == "constant" and fp.control_q.type == "constant": continue # No checks for this case if abs(power) > fp.s_max.m_as("VA"): @@ -350,7 +350,7 @@ class CurrentLoad(AbstractLoad): _type = "current" def __init__( - self, id: Id, bus: Bus, *, currents: ComplexArrayLike1D, phases: Optional[str] = None, **kwargs: Any + self, id: Id, bus: Bus, *, currents: ComplexArrayLike1D, phases: str | None = None, **kwargs: Any ) -> None: """CurrentLoad constructor. @@ -402,7 +402,7 @@ class ImpedanceLoad(AbstractLoad): _type = "impedance" def __init__( - self, id: Id, bus: Bus, *, impedances: ComplexArrayLike1D, phases: Optional[str] = None, **kwargs: Any + self, id: Id, bus: Bus, *, impedances: ComplexArrayLike1D, phases: str | None = None, **kwargs: Any ) -> None: """ImpedanceLoad constructor. diff --git a/roseau/load_flow/models/potential_refs.py b/roseau/load_flow/models/potential_refs.py index daba1a7f..dc8e4800 100644 --- a/roseau/load_flow/models/potential_refs.py +++ b/roseau/load_flow/models/potential_refs.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Optional, Union +from typing import Any from typing_extensions import Self @@ -25,7 +25,7 @@ class PotentialRef(Element): allowed_phases = frozenset({"a", "b", "c", "n"}) - def __init__(self, id: Id, element: Union[Bus, Ground], *, phase: Optional[str] = None, **kwargs: Any) -> None: + def __init__(self, id: Id, element: Bus | Ground, *, phase: str | None = None, **kwargs: Any) -> None: """PotentialRef constructor. Args: @@ -59,7 +59,7 @@ def __init__(self, id: Id, element: Union[Bus, Ground], *, phase: Optional[str] self.phase = phase self.element = element self._connect(element) - self._res_current: Optional[complex] = None + self._res_current: complex | None = None def __repr__(self) -> str: return f"{type(self).__name__}(id={self.id!r}, element={self.element!r}, phase={self.phase!r})" diff --git a/roseau/load_flow/models/sources.py b/roseau/load_flow/models/sources.py index 49a5e7bc..dbb3f885 100644 --- a/roseau/load_flow/models/sources.py +++ b/roseau/load_flow/models/sources.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Optional +from typing import Any import numpy as np from typing_extensions import Self @@ -22,7 +22,7 @@ class VoltageSource(Element): _floating_neutral_allowed: bool = False def __init__( - self, id: Id, bus: Bus, *, voltages: ComplexArrayLike1D, phases: Optional[str] = None, **kwargs: Any + self, id: Id, bus: Bus, *, voltages: ComplexArrayLike1D, phases: str | None = None, **kwargs: Any ) -> None: """Voltage source constructor. @@ -74,7 +74,7 @@ def __init__( self.voltages = voltages # Results - self._res_currents: Optional[ComplexArray] = None + self._res_currents: ComplexArray | None = None def __repr__(self) -> str: bus_id = self.bus.id if self.bus is not None else None diff --git a/roseau/load_flow/models/transformers/parameters.py b/roseau/load_flow/models/transformers/parameters.py index 31ec8d07..aa596ee5 100644 --- a/roseau/load_flow/models/transformers/parameters.py +++ b/roseau/load_flow/models/transformers/parameters.py @@ -4,7 +4,7 @@ from importlib import resources from itertools import cycle from pathlib import Path -from typing import NoReturn, Optional, Union +from typing import NoReturn import numpy as np import pandas as pd @@ -42,14 +42,14 @@ def __init__( self, id: Id, type: str, - uhv: Union[float, Q_[float]], - ulv: Union[float, Q_[float]], - sn: Union[float, Q_[float]], - p0: Union[float, Q_[float]], - i0: Union[float, Q_[float]], - psc: Union[float, Q_[float]], - vsc: Union[float, Q_[float]], - max_power: Optional[Union[float, Q_[float]]] = None, + uhv: float | Q_[float], + ulv: float | Q_[float], + sn: float | Q_[float], + p0: float | Q_[float], + i0: float | Q_[float], + psc: float | Q_[float], + vsc: float | Q_[float], + max_power: float | Q_[float] | None = None, ) -> None: """TransformerParameters constructor. @@ -198,13 +198,13 @@ def vsc(self) -> Q_[float]: return self._vsc @property - def max_power(self) -> Optional[Q_[float]]: + def max_power(self) -> Q_[float] | None: """The maximum power loading of the transformer (VA) if it is set.""" return None if self._max_power is None else Q_(self._max_power, "VA") @max_power.setter @ureg_wraps(None, (None, "VA")) - def max_power(self, value: Optional[Union[float, Q_[float]]]) -> None: + def max_power(self, value: float | Q_[float] | None) -> None: self._max_power = value @ureg_wraps(("ohm", "S", "", None), (None,)) @@ -318,14 +318,14 @@ def catalogue_data(cls) -> pd.DataFrame: @ureg_wraps(None, (None, None, None, None, None, None, "VA", "V", "V")) def from_catalogue( cls, - id: Optional[Union[str, re.Pattern[str]]] = None, - manufacturer: Optional[Union[str, re.Pattern[str]]] = None, - range: Optional[Union[str, re.Pattern[str]]] = None, - efficiency: Optional[Union[str, re.Pattern[str]]] = None, - type: Optional[Union[str, re.Pattern[str]]] = None, - sn: Optional[float] = None, - uhv: Optional[float] = None, - ulv: Optional[float] = None, + id: str | re.Pattern[str] | None = None, + manufacturer: str | re.Pattern[str] | None = None, + range: str | re.Pattern[str] | None = None, + efficiency: str | re.Pattern[str] | None = None, + type: str | re.Pattern[str] | None = None, + sn: float | None = None, + uhv: float | None = None, + ulv: float | None = None, ) -> Self: """Build a transformer parameters from one in the catalogue. @@ -463,14 +463,14 @@ def from_catalogue( @ureg_wraps(None, (None, None, None, None, None, None, "VA", "V", "V")) def print_catalogue( cls, - id: Optional[Union[str, re.Pattern[str]]] = None, - manufacturer: Optional[Union[str, re.Pattern[str]]] = None, - range: Optional[Union[str, re.Pattern[str]]] = None, - efficiency: Optional[Union[str, re.Pattern[str]]] = None, - type: Optional[Union[str, re.Pattern[str]]] = None, - sn: Optional[float] = None, - uhv: Optional[float] = None, - ulv: Optional[float] = None, + id: str | re.Pattern[str] | None = None, + manufacturer: str | re.Pattern[str] | None = None, + range: str | re.Pattern[str] | None = None, + efficiency: str | re.Pattern[str] | None = None, + type: str | re.Pattern[str] | None = None, + sn: float | None = None, + uhv: float | None = None, + ulv: float | None = None, ) -> None: """Print the catalogue of available transformers. @@ -567,7 +567,7 @@ def print_catalogue( @staticmethod def _filter_catalogue_str( - value: Union[str, re.Pattern[str]], catalogue_data: pd.DataFrame, column_name: str + value: str | re.Pattern[str], catalogue_data: pd.DataFrame, column_name: str ) -> pd.Series: """Filter the catalogue using a string/regexp value. diff --git a/roseau/load_flow/models/transformers/transformers.py b/roseau/load_flow/models/transformers/transformers.py index 3d73ccda..059b18ea 100644 --- a/roseau/load_flow/models/transformers/transformers.py +++ b/roseau/load_flow/models/transformers/transformers.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Optional +from typing import Any from shapely import Point @@ -41,9 +41,9 @@ def __init__( *, parameters: TransformerParameters, tap: float = 1.0, - phases1: Optional[str] = None, - phases2: Optional[str] = None, - geometry: Optional[Point] = None, + phases1: str | None = None, + phases2: str | None = None, + geometry: Point | None = None, **kwargs: Any, ) -> None: """Transformer constructor. @@ -130,7 +130,7 @@ def parameters(self, value: TransformerParameters) -> None: self._invalidate_network_results() @property - def max_power(self) -> Optional[Q_[float]]: + def max_power(self) -> Q_[float] | None: """The maximum power loading of the transformer (in VA).""" # Do not add a setter. The user must know that if they change the max_power, it changes # for all transformers that share the parameters. It is better to set it on the parameters. @@ -145,8 +145,8 @@ def _compute_phases_three( bus1: Bus, bus2: Bus, parameters: TransformerParameters, - phases1: Optional[str], - phases2: Optional[str], + phases1: str | None, + phases2: str | None, ) -> tuple[str, str]: w1_has_neutral = "y" in parameters.winding1.lower() or "z" in parameters.winding1.lower() w2_has_neutral = "y" in parameters.winding2.lower() or "z" in parameters.winding2.lower() @@ -187,7 +187,7 @@ def _compute_phases_three( return phases1, phases2 def _compute_phases_single( - self, id: Id, bus1: Bus, bus2: Bus, phases1: Optional[str], phases2: Optional[str] + self, id: Id, bus1: Bus, bus2: Bus, phases1: str | None, phases2: str | None ) -> tuple[str, str]: if phases1 is None: phases1 = "".join(p for p in bus1.phases if p in bus2.phases) # can't use set because order is important @@ -214,7 +214,7 @@ def _compute_phases_single( return phases1, phases2 def _compute_phases_center( - self, id: Id, bus1: Bus, bus2: Bus, phases1: Optional[str], phases2: Optional[str] + self, id: Id, bus1: Bus, bus2: Bus, phases1: str | None, phases2: str | None ) -> tuple[str, str]: if phases1 is None: phases1 = "".join(p for p in bus2.phases if p in bus1.phases and p != "n") @@ -253,7 +253,7 @@ def _check_bus_phases(id: Id, bus: Bus, **kwargs: str) -> None: raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_PHASE) @property - def res_violated(self) -> Optional[bool]: + def res_violated(self) -> bool | None: """Whether the transformer power exceeds the maximum power (loading > 100%). Returns ``None`` if the maximum power is not set. diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 72c2d162..93ad58b5 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -10,7 +10,7 @@ from importlib import resources from itertools import cycle from pathlib import Path -from typing import TYPE_CHECKING, NoReturn, Optional, TypeVar, Union +from typing import TYPE_CHECKING, NoReturn, TypeVar from urllib.parse import urljoin import geopandas as gpd @@ -175,7 +175,7 @@ def __init__( self.res_info: JsonDict = {} def __repr__(self) -> str: - def count_repr(__o: Sized, /, singular: str, plural: Optional[str] = None) -> str: + def count_repr(__o: Sized, /, singular: str, plural: str | None = None) -> str: """Singular/plural count representation: `1 bus` or `2 buses`.""" n = len(__o) if n == 1: @@ -496,7 +496,7 @@ def solve_load_flow( tolerance: float = _DEFAULT_TOLERANCE, warm_start: bool = _DEFAULT_WARM_START, solver: Solver = _DEFAULT_SOLVER, - solver_params: Optional[JsonDict] = None, + solver_params: JsonDict | None = None, ) -> int: """Solve the load flow for this network (Requires internet access). @@ -692,7 +692,7 @@ def res_buses(self) -> pd.DataFrame: res_dict = {"bus_id": [], "phase": [], "potential": []} dtypes = {c: _DTYPES[c] for c in res_dict} for bus_id, bus in self.buses.items(): - for potential, phase in zip(bus._res_potentials_getter(warning=False), bus.phases): + for potential, phase in zip(bus._res_potentials_getter(warning=False), bus.phases, strict=True): res_dict["bus_id"].append(bus_id) res_dict["phase"].append(phase) res_dict["potential"].append(potential) @@ -737,7 +737,7 @@ def res_buses_voltages(self) -> pd.DataFrame: max_voltage = float("nan") else: voltage_limits_set = True - for voltage, phase in zip(bus._res_voltages_getter(warning=False), bus.voltage_phases): + for voltage, phase in zip(bus._res_voltages_getter(warning=False), bus.voltage_phases, strict=True): voltage_abs = abs(voltage) violated = (voltage_abs < min_voltage or voltage_abs > max_voltage) if voltage_limits_set else None voltages_dict["bus_id"].append(bus_id) @@ -786,7 +786,7 @@ def res_branches(self) -> pd.DataFrame: "potential1": v1, "potential2": None, } - for i1, s1, v1, phase in zip(currents1, powers1, potentials1, branch.phases1) + for i1, s1, v1, phase in zip(currents1, powers1, potentials1, branch.phases1, strict=True) ) res_list.extend( { @@ -800,7 +800,7 @@ def res_branches(self) -> pd.DataFrame: "potential1": None, "potential2": v2, } - for i2, s2, v2, phase in zip(currents2, powers2, potentials2, branch.phases2) + for i2, s2, v2, phase in zip(currents2, powers2, potentials2, branch.phases2, strict=True) ) columns = [ @@ -876,7 +876,7 @@ def res_transformers(self) -> pd.DataFrame: "max_power": s_max, "violated": violated, } - for i1, s1, v1, phase in zip(currents1, powers1, potentials1, branch.phases1) + for i1, s1, v1, phase in zip(currents1, powers1, potentials1, branch.phases1, strict=True) ) res_list.extend( { @@ -891,7 +891,7 @@ def res_transformers(self) -> pd.DataFrame: "max_power": s_max, "violated": violated, } - for i2, s2, v2, phase in zip(currents2, powers2, potentials2, branch.phases2) + for i2, s2, v2, phase in zip(currents2, powers2, potentials2, branch.phases2, strict=True) ) columns = [ @@ -988,7 +988,7 @@ def res_lines(self) -> pd.DataFrame: series_currents = branch._res_series_currents_getter(warning=False) i_max = branch.parameters._max_current for i1, i2, s1, s2, v1, v2, s_series, i_series, phase in zip( - *currents, *powers, *potentials, series_losses, series_currents, branch.phases + *currents, *powers, *potentials, series_losses, series_currents, branch.phases, strict=True ): violated = None if i_max is None else max(abs(i1), abs(i2)) > i_max res_dict["line_id"].append(branch.id) @@ -1045,7 +1045,7 @@ def res_switches(self) -> pd.DataFrame: potentials = branch._res_potentials_getter(warning=False) currents = branch._res_currents_getter(warning=False) powers = branch._res_powers_getter(warning=False) - for i1, i2, s1, s2, v1, v2, phase in zip(*currents, *powers, *potentials, branch.phases): + for i1, i2, s1, s2, v1, v2, phase in zip(*currents, *powers, *potentials, branch.phases, strict=True): res_dict["switch_id"].append(branch.id) res_dict["phase"].append(phase) res_dict["current1"].append(i1) @@ -1075,7 +1075,7 @@ def res_loads(self) -> pd.DataFrame: currents = load._res_currents_getter(warning=False) powers = load._res_powers_getter(warning=False) potentials = load._res_potentials_getter(warning=False) - for i, s, v, phase in zip(currents, powers, potentials, load.phases): + for i, s, v, phase in zip(currents, powers, potentials, load.phases, strict=True): res_dict["load_id"].append(load_id) res_dict["phase"].append(phase) res_dict["current"].append(i) @@ -1098,7 +1098,7 @@ def res_loads_voltages(self) -> pd.DataFrame: voltages_dict = {"load_id": [], "phase": [], "voltage": []} dtypes = {c: _DTYPES[c] for c in voltages_dict} | {"phase": VoltagePhaseDtype} for load_id, load in self.loads.items(): - for voltage, phase in zip(load._res_voltages_getter(warning=False), load.voltage_phases): + for voltage, phase in zip(load._res_voltages_getter(warning=False), load.voltage_phases, strict=True): voltages_dict["load_id"].append(load_id) voltages_dict["phase"].append(phase) voltages_dict["voltage"].append(voltage) @@ -1124,7 +1124,7 @@ def res_loads_flexible_powers(self) -> pd.DataFrame: for load_id, load in self.loads.items(): if not (isinstance(load, PowerLoad) and load.is_flexible): continue - for power, phase in zip(load._res_flexible_powers_getter(warning=False), load.voltage_phases): + for power, phase in zip(load._res_flexible_powers_getter(warning=False), load.voltage_phases, strict=True): loads_dict["load_id"].append(load_id) loads_dict["phase"].append(phase) loads_dict["power"].append(power) @@ -1149,7 +1149,7 @@ def res_sources(self) -> pd.DataFrame: currents = source._res_currents_getter(warning=False) powers = source._res_powers_getter(warning=False) potentials = source._res_potentials_getter(warning=False) - for i, s, v, phase in zip(currents, powers, potentials, source.phases): + for i, s, v, phase in zip(currents, powers, potentials, source.phases, strict=True): res_dict["source_id"].append(source_id) res_dict["phase"].append(phase) res_dict["current"].append(i) @@ -1242,7 +1242,7 @@ def _disconnect_element(self, element: Element) -> None: The element to remove. """ # The C++ electrical network and the tape will be recomputed - if isinstance(element, (Bus, AbstractBranch)): + if isinstance(element, Bus | AbstractBranch): msg = f"{element!r} is a {type(element).__name__} and it cannot be disconnected from a network." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_ELEMENT_OBJECT) @@ -1476,7 +1476,7 @@ def catalogue_data(cls) -> JsonDict: return json.loads((cls.catalogue_path() / "Catalogue.json").read_text()) @classmethod - def from_catalogue(cls, name: Union[str, re.Pattern[str]], load_point_name: Union[str, re.Pattern[str]]) -> Self: + def from_catalogue(cls, name: str | re.Pattern[str], load_point_name: str | re.Pattern[str]) -> Self: """Build a network from one in the catalogue. Args: @@ -1563,8 +1563,8 @@ def from_catalogue(cls, name: Union[str, re.Pattern[str]], load_point_name: Unio @classmethod def print_catalogue( cls, - name: Optional[Union[str, re.Pattern[str]]] = None, - load_point_name: Optional[Union[str, re.Pattern[str]]] = None, + name: str | re.Pattern[str] | None = None, + load_point_name: str | re.Pattern[str] | None = None, ) -> None: """Print the catalogue of available networks. @@ -1650,7 +1650,7 @@ def match_load_point_function(x: str) -> bool: console.print(table) @staticmethod - def _filter_name(name: Optional[Union[str, re.Pattern[str]]], catalogue_data: JsonDict) -> list[str]: + def _filter_name(name: str | re.Pattern[str] | None, catalogue_data: JsonDict) -> list[str]: """Filter the catalogue using the network name. Args: diff --git a/roseau/load_flow/solvers.py b/roseau/load_flow/solvers.py index 4ef18260..6cab54eb 100644 --- a/roseau/load_flow/solvers.py +++ b/roseau/load_flow/solvers.py @@ -1,5 +1,4 @@ import logging -from typing import Optional from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow.typing import JsonDict, Solver @@ -13,7 +12,7 @@ SOLVERS = list(_SOLVERS_PARAMS) -def check_solver_params(solver: Solver, params: Optional[JsonDict]) -> JsonDict: +def check_solver_params(solver: Solver, params: JsonDict | None) -> JsonDict: """Strip and check the solver parameters. Args: diff --git a/roseau/load_flow/typing.py b/roseau/load_flow/typing.py index fc202af2..a18fd88f 100644 --- a/roseau/load_flow/typing.py +++ b/roseau/load_flow/typing.py @@ -54,39 +54,32 @@ """ import os from collections.abc import Mapping, Sequence -from typing import Any, Literal, TypeVar, Union +from typing import Any, Literal, TypeAlias, TypeVar import numpy as np from numpy.typing import NDArray from requests.auth import HTTPBasicAuth -from typing_extensions import TypeAlias from roseau.load_flow.units import Q_ T = TypeVar("T") -Id: TypeAlias = Union[int, str] +Id: TypeAlias = int | str JsonDict: TypeAlias = dict[str, Any] -StrPath: TypeAlias = Union[str, os.PathLike[str]] +StrPath: TypeAlias = str | os.PathLike[str] ControlType: TypeAlias = Literal["constant", "p_max_u_production", "p_max_u_consumption", "q_u"] ProjectionType: TypeAlias = Literal["euclidean", "keep_p", "keep_q"] Solver: TypeAlias = Literal["newton", "newton_goldstein"] -Authentication: TypeAlias = Union[tuple[str, str], HTTPBasicAuth] -MapOrSeq: TypeAlias = Union[Mapping[Id, T], Sequence[T]] +Authentication: TypeAlias = tuple[str, str] | HTTPBasicAuth +MapOrSeq: TypeAlias = Mapping[Id, T] | Sequence[T] ComplexArray: TypeAlias = NDArray[np.complex128] # TODO: improve the types below when shape-typing becomes supported -ComplexArrayLike1D: TypeAlias = Union[ - ComplexArray, - Q_[ComplexArray], - Q_[Sequence[complex]], - Sequence[Union[complex, Q_[complex]]], -] -ComplexArrayLike2D: TypeAlias = Union[ - ComplexArray, - Q_[ComplexArray], - Q_[Sequence[Sequence[complex]]], - Sequence[Sequence[Union[complex, Q_[complex]]]], -] +ComplexArrayLike1D: TypeAlias = ( + ComplexArray | Q_[ComplexArray] | Q_[Sequence[complex]] | Sequence[complex | Q_[complex]] +) +ComplexArrayLike2D: TypeAlias = ( + ComplexArray | Q_[ComplexArray] | Q_[Sequence[Sequence[complex]]] | Sequence[Sequence[complex | Q_[complex]]] +) __all__ = [ diff --git a/roseau/load_flow/units.py b/roseau/load_flow/units.py index f580f0e9..850b858d 100644 --- a/roseau/load_flow/units.py +++ b/roseau/load_flow/units.py @@ -22,11 +22,10 @@ """ from collections.abc import Callable, Iterable from types import GenericAlias -from typing import TYPE_CHECKING, TypeVar, Union +from typing import TYPE_CHECKING, TypeAlias, TypeVar from pint import Unit, UnitRegistry from pint.facets.plain import PlainQuantity -from typing_extensions import TypeAlias from roseau.load_flow._wrapper import wraps @@ -48,8 +47,8 @@ def ureg_wraps( - ret: Union[str, Unit, None, Iterable[Union[str, Unit, None]]], - args: Union[str, Unit, None, Iterable[Union[str, Unit, None]]], + ret: str | Unit | None | Iterable[str | Unit | None], + args: str | Unit | None | Iterable[str | Unit | None], strict: bool = True, ) -> Callable[[FuncT], FuncT]: """Wraps a function to become pint-aware. diff --git a/roseau/load_flow/utils/mixins.py b/roseau/load_flow/utils/mixins.py index 9f3c3f7d..af01846c 100644 --- a/roseau/load_flow/utils/mixins.py +++ b/roseau/load_flow/utils/mixins.py @@ -19,7 +19,7 @@ class Identifiable(metaclass=ABCMeta): """An identifiable object.""" def __init__(self, id: Id) -> None: - if not isinstance(id, (int, str)): + if not isinstance(id, int | str): msg = f"{type(self).__name__} expected id to be int or str, got {type(id)}" logger.error(msg) raise RoseauLoadFlowException(msg, code=RoseauLoadFlowExceptionCode.BAD_ID_TYPE) From 672d540fa7581b980009c85d969724804d07ad68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 17:50:06 +0000 Subject: [PATCH 03/51] Bump conda-incubator/setup-miniconda from 2 to 3 Bumps [conda-incubator/setup-miniconda](https://github.com/conda-incubator/setup-miniconda) from 2 to 3. - [Release notes](https://github.com/conda-incubator/setup-miniconda/releases) - [Changelog](https://github.com/conda-incubator/setup-miniconda/blob/main/CHANGELOG.md) - [Commits](https://github.com/conda-incubator/setup-miniconda/compare/v2...v3) --- updated-dependencies: - dependency-name: conda-incubator/setup-miniconda dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/conda.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/conda.yml b/.github/workflows/conda.yml index c840d7fe..cd0bd682 100644 --- a/.github/workflows/conda.yml +++ b/.github/workflows/conda.yml @@ -43,7 +43,7 @@ jobs: git lfs pull git lfs prune --verify-remote - - uses: conda-incubator/setup-miniconda@v2 + - uses: conda-incubator/setup-miniconda@v3 with: auto-update-conda: true python-version: ${{ matrix.python-version }} From cb0ece089e14c425a25a351ba87370273312a6cc Mon Sep 17 00:00:00 2001 From: Saelyos Date: Tue, 12 Dec 2023 16:53:43 +0100 Subject: [PATCH 04/51] Start fusion of the python code --- roseau/load_flow/models/branches.py | 19 ++ roseau/load_flow/models/buses.py | 19 ++ roseau/load_flow/models/core.py | 4 + roseau/load_flow/models/grounds.py | 5 + roseau/load_flow/models/lines/lines.py | 34 +++ .../models/loads/flexible_parameters.py | 40 +-- roseau/load_flow/models/loads/loads.py | 52 ++++ roseau/load_flow/models/potential_refs.py | 14 + roseau/load_flow/models/sources.py | 19 ++ .../models/transformers/transformers.py | 50 ++++ roseau/load_flow/network.py | 261 ++++++++++++------ roseau/load_flow/solvers.py | 198 +++++++++++++ 12 files changed, 615 insertions(+), 100 deletions(-) diff --git a/roseau/load_flow/models/branches.py b/roseau/load_flow/models/branches.py index 55d0effd..ce44e22e 100644 --- a/roseau/load_flow/models/branches.py +++ b/roseau/load_flow/models/branches.py @@ -77,6 +77,7 @@ def __repr__(self) -> str: return s def _res_currents_getter(self, warning: bool) -> tuple[ComplexArray, ComplexArray]: + self._res_currents = self.cy_element.get_currents(len(self.phases1), len(self.phases2)) return self._res_getter(value=self._res_currents, warning=warning) @property @@ -119,6 +120,24 @@ def res_voltages(self) -> tuple[Q_[ComplexArray], Q_[ComplexArray]]: """The load flow result of the branch voltages (V).""" return self._res_voltages_getter(warning=True) + def _cy_connect(self) -> None: + """Connect the Cython elements of the buses and the branch""" + connections = [] + assert isinstance(self.bus1, Bus) + for i, phase in enumerate(self.phases1): + if phase in self.bus1.phases: + j = self.bus1.phases.find(phase) + connections.append((i, j)) + self.cy_element.connect(self.bus1.cy_element, connections, True) + + connections = [] + assert isinstance(self.bus2, Bus) + for i, phase in enumerate(self.phases2): + if phase in self.bus2.phases: + j = self.bus2.phases.find(phase) + connections.append((i, j)) + self.cy_element.connect(self.bus2.cy_element, connections, False) + # # Json Mixin interface # diff --git a/roseau/load_flow/models/buses.py b/roseau/load_flow/models/buses.py index a4e6d2f5..160a7359 100644 --- a/roseau/load_flow/models/buses.py +++ b/roseau/load_flow/models/buses.py @@ -12,6 +12,7 @@ from roseau.load_flow.models.core import Element from roseau.load_flow.typing import ComplexArray, ComplexArrayLike1D, Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps +from roseau.load_flow_engine.models.buses.cy_buses import CyBus logger = logging.getLogger(__name__) @@ -75,6 +76,7 @@ def __init__( super().__init__(id, **kwargs) self._check_phases(id, phases=phases) self.phases = phases + initialized = potentials is not None if potentials is None: potentials = [0] * len(phases) self.potentials = potentials @@ -87,6 +89,10 @@ def __init__( self._res_potentials: ComplexArray | None = None self._short_circuits: list[dict[str, Any]] = [] + self.n = len(self.phases) + self._initialized = initialized + self.cy_element = CyBus(n=self.n, potentials=self._potentials) + def __repr__(self) -> str: return f"{type(self).__name__}(id={self.id!r}, phases={self.phases!r})" @@ -105,8 +111,12 @@ def potentials(self, value: ComplexArrayLike1D) -> None: raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_POTENTIALS_SIZE) self._potentials = np.array(value, dtype=np.complex128) self._invalidate_network_results() + self._initialized = True + if hasattr(self, "cy_element"): + self.cy_element.initialize_potentials(self._potentials) def _res_potentials_getter(self, warning: bool) -> ComplexArray: + self._res_potentials = self.cy_element.get_potentials(self.n) return self._res_getter(value=self._res_potentials, warning=warning) @property @@ -391,6 +401,12 @@ def add_short_circuit(self, *phases: str, ground: Optional["Ground"] = None) -> if self.network is not None: self.network._valid = False + phases_index = np.array([self.phases.find(p) for p in phases], dtype=np.int32) + self.cy_element.connect_ports(phases_index, len(phases)) + + if ground is not None: + self.cy_element.connect(ground.cy_element, [(phases_index[0], 0)]) + @property def short_circuits(self) -> list[dict[str, Any]]: """Return the list of short-circuits of this bus.""" @@ -399,3 +415,6 @@ def short_circuits(self) -> list[dict[str, Any]]: def clear_short_circuits(self) -> None: """Remove the short-circuits of this bus.""" self._short_circuits = [] + msg = "Short circuits cannot be cleared for the engine part." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_SHORT_CIRCUIT) # TODO diff --git a/roseau/load_flow/models/core.py b/roseau/load_flow/models/core.py index 74d176f4..4071e41b 100644 --- a/roseau/load_flow/models/core.py +++ b/roseau/load_flow/models/core.py @@ -40,6 +40,7 @@ def __init__(self, id: Id, **kwargs: Any) -> None: super().__init__(id) self._connected_elements: list[Element] = [] self._network: ElectricalNetwork | None = None + # self.cy_element = None TODO replace hasattr with "if self.cy_element is not None" @property def network(self) -> Optional["ElectricalNetwork"]: @@ -125,6 +126,9 @@ def _disconnect(self) -> None: element._connected_elements.remove(self) self._connected_elements = [] self._set_network(None) + self.cy_element.disconnect() + # The cpp element has been disconnected and can't be reconnected easily, it's safer to delete it + self.cy_element = None def _invalidate_network_results(self) -> None: """Invalidate the network making the result""" diff --git a/roseau/load_flow/models/grounds.py b/roseau/load_flow/models/grounds.py index 0e70c777..344bfb4b 100644 --- a/roseau/load_flow/models/grounds.py +++ b/roseau/load_flow/models/grounds.py @@ -7,6 +7,7 @@ from roseau.load_flow.models.core import Element from roseau.load_flow.typing import Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps +from roseau.load_flow_engine.models.grounds.cy_grounds import CyGround if TYPE_CHECKING: from roseau.load_flow.models.buses import Bus @@ -45,11 +46,13 @@ def __init__(self, id: Id, **kwargs: Any) -> None: # A map of bus id to phase connected to this ground. self._connected_buses: dict[Id, str] = {} self._res_potential: complex | None = None + self.cy_element = CyGround() def __repr__(self) -> str: return f"{type(self).__name__}(id={self.id!r})" def _res_potential_getter(self, warning: bool) -> complex: + self._res_potential = self.cy_element.get_potentials(1)[0] return self._res_getter(self._res_potential, warning) @property @@ -85,6 +88,8 @@ def connect(self, bus: "Bus", phase: str = "n") -> None: raise RoseauLoadFlowException(msg, RoseauLoadFlowExceptionCode.BAD_PHASE) self._connect(bus) self._connected_buses[bus.id] = phase + p = bus.phases.find(phase) + bus.cy_element.connect(self.cy_element, [(p, 0)]) # # Json Mixin interface diff --git a/roseau/load_flow/models/lines/lines.py b/roseau/load_flow/models/lines/lines.py index 42499dfb..f381acf5 100644 --- a/roseau/load_flow/models/lines/lines.py +++ b/roseau/load_flow/models/lines/lines.py @@ -14,6 +14,7 @@ from roseau.load_flow.models.sources import VoltageSource from roseau.load_flow.typing import ComplexArray, Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps +from roseau.load_flow_engine.models.lines.cy_lines import CyShuntLine, CySimplifiedLine, CySwitch logger = logging.getLogger(__name__) @@ -86,6 +87,9 @@ def __init__( self.phases = phases self._check_elements() self._check_loop() + self.n = len(self.phases) + self.cy_element = CySwitch(self.n) + self._cy_connect() def _check_loop(self) -> None: """Check that there are no switch loop, raise an exception if it is the case""" @@ -224,6 +228,21 @@ def __init__( # Connect the ground self._connect(self.ground) + self.n = len(self.phases) + if parameters.with_shunt: + self.cy_element = CyShuntLine( + n=self.n, + y_shunt=(parameters.y_shunt.reshape(self.n * self.n) * self.length).m_as("S"), + z_line=(parameters.z_line.reshape(self.n * self.n) * self.length).m_as("ohm"), + ) + else: + self.cy_element = CySimplifiedLine( + n=self.n, z_line=(parameters.z_line.reshape(self.n * self.n) * self.length).m_as("ohm") + ) + self._cy_connect() + if parameters.with_shunt: + ground.cy_element.connect(self.cy_element, [(0, self.n + self.n)]) + @property @ureg_wraps("km", (None,)) def length(self) -> Q_[float]: @@ -239,6 +258,10 @@ def length(self, value: float | Q_[float]) -> None: self._length = value self._invalidate_network_results() + if hasattr(self, "cy_element"): + # Reassign the same parameters with the new length + self.parameters = self.parameters + @property def parameters(self) -> LineParameters: """The parameters of the line.""" @@ -273,6 +296,17 @@ def parameters(self, value: LineParameters) -> None: self._parameters = value self._invalidate_network_results() + if hasattr(self, "cy_element"): + if value.with_shunt: + self.cy_element.update_line_parameters( + (value.y_shunt.reshape(self.n * self.n) * self.length).m_as("S"), + (value.z_line.reshape(self.n * self.n) * self.length).m_as("ohm"), + ) + else: + self.cy_element.update_line_parameters( + (value.z_line.reshape(self.n * self.n) * self.length).m_as("ohm") + ) + @property @ureg_wraps("ohm", (None,)) def z_line(self) -> Q_[ComplexArray]: diff --git a/roseau/load_flow/models/loads/flexible_parameters.py b/roseau/load_flow/models/loads/flexible_parameters.py index 4c939d27..0f51545f 100644 --- a/roseau/load_flow/models/loads/flexible_parameters.py +++ b/roseau/load_flow/models/loads/flexible_parameters.py @@ -17,6 +17,7 @@ ) from roseau.load_flow.units import Q_, ureg_wraps from roseau.load_flow.utils import JsonMixin, _optional_deps +from roseau.load_flow_engine.models.loads.cy_loads import CyControl, CyFlexibleParameter, CyProjection logger = logging.getLogger(__name__) @@ -89,6 +90,9 @@ def __init__( self._u_max = u_max self._alpha = alpha self._check_values() + self.cy_control = CyControl( + t=type, u_min=self._u_min, u_down=self._u_down, u_up=self._u_up, u_max=self._u_max, alpha=self._alpha + ) def _check_values(self) -> None: """Check the provided values.""" @@ -380,6 +384,7 @@ def __init__(self, type: ProjectionType, alpha: float = _DEFAULT_ALPHA, epsilon: self._alpha = alpha self._epsilon = epsilon self._check_values() + self.cy_projection = CyProjection(t=type, alpha=self._alpha, epsilon=self._epsilon) def _check_values(self) -> None: """Check the provided values.""" @@ -499,6 +504,14 @@ def __init__( self.s_max = s_max self.q_min = q_min self.q_max = q_max + self.cy_fp = CyFlexibleParameter( + control_p=control_p.cy_control, + control_q=control_q.cy_control, + projection=projection.cy_projection, + s_max=self._s_max, + q_min=self.q_min.m_as("VAr"), + q_max=self.q_max.m_as("VAr"), + ) @property @ureg_wraps("VA", (None,)) @@ -521,6 +534,8 @@ def s_max(self, value: float | Q_[float]) -> None: if self._q_min is not None and self._q_min < -self._s_max: logger.warning("'s_max' has been updated but now 'q_min' is less than -s_max. 'q_min' is set to -s_max") self._q_min = -self._s_max + if hasattr(self, "cy_fp"): + self.cy_fp.update_parameters(self._s_max, self.q_min.m_as("VAr"), self.q_max.m_as("VAr")) @property @ureg_wraps("VAr", (None,)) @@ -542,6 +557,8 @@ def q_min(self, value: float | Q_[float] | None) -> None: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_FLEXIBLE_PARAMETER_VALUE) self._q_min = value + if hasattr(self, "cy_fp"): + self.cy_fp.update_parameters(self._s_max, self.q_min.m_as("VAr"), self.q_max.m_as("VAr")) @property @ureg_wraps("VAr", (None,)) @@ -563,6 +580,8 @@ def q_max(self, value: float | Q_[float] | None) -> None: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_FLEXIBLE_PARAMETER_VALUE) self._q_max = value + if hasattr(self, "cy_fp"): + self.cy_fp.update_parameters(self._s_max, self.q_min.m_as("VAr"), self.q_max.m_as("VAr")) @classmethod def constant(cls) -> Self: @@ -1011,29 +1030,16 @@ def compute_powers( def _compute_powers( self, auth: Authentication, voltages: ComplexArrayLike1D, power: complex, solve_kwargs: JsonDict | None ) -> ComplexArray: - from roseau.load_flow import Bus, ElectricalNetwork, PotentialRef, PowerLoad, VoltageSource - # Format the input - if solve_kwargs is None: - solve_kwargs = {} - voltages = np.array(np.abs(voltages), dtype=np.float64) - - # Simple network - bus = Bus(id="bus", phases="an") - vs = VoltageSource(id="source", bus=bus, voltages=[voltages[0]]) - PotentialRef(id="pref", element=bus, phase="n") - fp = FlexibleParameter.from_dict(data=self.to_dict(_lf_only=True)) - load = PowerLoad(id="load", bus=bus, powers=[power], flexible_params=[fp]) - en = ElectricalNetwork.from_element(bus) + voltages = np.array(np.abs(voltages), dtype=float) # Iterate over the provided voltages to get the associated flexible powers res_flexible_powers = [] for v in voltages: - vs.voltages = [v] - en.solve_load_flow(auth=auth, **solve_kwargs) - res_flexible_powers.append(load.res_flexible_powers.m_as("VA")[0]) + s = self.cy_fp.compute_power(v, power) + res_flexible_powers.append(s) - return np.array(res_flexible_powers, dtype=np.complex128) + return np.array(res_flexible_powers, dtype=complex) @ureg_wraps((None, "VA"), (None, None, "V", "VA", None, None, None, "VA")) def plot_pq( diff --git a/roseau/load_flow/models/loads/loads.py b/roseau/load_flow/models/loads/loads.py index 45a7e8e6..c38d897f 100644 --- a/roseau/load_flow/models/loads/loads.py +++ b/roseau/load_flow/models/loads/loads.py @@ -11,6 +11,16 @@ from roseau.load_flow.models.loads.flexible_parameters import FlexibleParameter from roseau.load_flow.typing import ComplexArray, ComplexArrayLike1D, Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps +from roseau.load_flow_engine.models.loads.cy_loads import ( + CyAdmittanceLoad, + CyCurrentLoad, + CyDeltaAdmittanceLoad, + CyDeltaCurrentLoad, + CyDeltaFlexibleLoad, + CyDeltaPowerLoad, + CyFlexibleLoad, + CyPowerLoad, +) logger = logging.getLogger(__name__) @@ -70,6 +80,7 @@ def __init__(self, id: Id, bus: Bus, *, phases: str | None = None, **kwargs: Any self.phases = phases self.bus = bus + self.n = len(self.phases) self._symbol = {"power": "S", "current": "I", "impedance": "Z"}[self._type] if len(phases) == 2 and "n" not in phases: # This is a delta load that has one element connected between two phases @@ -95,6 +106,7 @@ def voltage_phases(self) -> list[str]: return calculate_voltage_phases(self.phases) def _res_currents_getter(self, warning: bool) -> ComplexArray: + self._res_currents = self.cy_element.get_currents(self.n) return self._res_getter(value=self._res_currents, warning=warning) @property @@ -148,6 +160,14 @@ def res_powers(self) -> Q_[ComplexArray]: """The load flow result of the load powers (VA).""" return self._res_powers_getter(warning=True) + def _cy_connect(self): + connections = [] + for i, phase in enumerate(self.bus.phases): + if phase in self.phases: + j = self.phases.find(phase) + connections.append((i, j)) + self.bus.cy_element.connect(self.cy_element, connections) + # # Disconnect # @@ -256,6 +276,21 @@ def __init__( self.powers = powers self._res_flexible_powers: ComplexArray | None = None + if self.is_flexible: + cy_parameters = [] + for p in flexible_params: + cy_parameters.append(p.cy_fp) + if self.phases == "abc": + self.cy_element = CyDeltaFlexibleLoad(n=self.n, powers=self._powers, parameters=np.array(cy_parameters)) + else: + self.cy_element = CyFlexibleLoad(n=self.n, powers=self._powers, parameters=np.array(cy_parameters)) + else: + if self.phases == "abc": + self.cy_element = CyDeltaPowerLoad(n=self.n, powers=self._powers) + else: + self.cy_element = CyPowerLoad(n=self.n, powers=self._powers) + self._cy_connect() + @property def flexible_params(self) -> list[FlexibleParameter] | None: return self._flexible_params @@ -304,8 +339,11 @@ def powers(self, value: ComplexArrayLike1D) -> None: raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_S_VALUE) self._powers = value self._invalidate_network_results() + if hasattr(self, "cy_element"): + self.cy_element.update_powers(self._powers) def _res_flexible_powers_getter(self, warning: bool) -> ComplexArray: + self._res_flexible_powers = self.cy_element.get_powers(self.n) return self._res_getter(value=self._res_flexible_powers, warning=warning) @property @@ -373,6 +411,11 @@ def __init__( """ super().__init__(id=id, phases=phases, bus=bus, **kwargs) self.currents = currents # handles size checks and unit conversion + if self.phases == "abc": + self.cy_element = CyDeltaCurrentLoad(n=self.n, currents=self._currents) + else: + self.cy_element = CyCurrentLoad(n=self.n, currents=self._currents) + self._cy_connect() @property @ureg_wraps("A", (None,)) @@ -385,6 +428,8 @@ def currents(self) -> Q_[ComplexArray]: def currents(self, value: ComplexArrayLike1D) -> None: self._currents = self._validate_value(value) self._invalidate_network_results() + if hasattr(self, "cy_element"): + self.cy_element.update_currents(self._currents) def to_dict(self, *, _lf_only: bool = False) -> JsonDict: self._raise_disconnected_error() @@ -425,6 +470,11 @@ def __init__( """ super().__init__(id=id, phases=phases, bus=bus, **kwargs) self.impedances = impedances + if self.phases == "abc": + self.cy_element = CyDeltaAdmittanceLoad(n=self.n, admittances=1.0 / self._impedances) + else: + self.cy_element = CyAdmittanceLoad(n=self.n, admittances=1.0 / self._impedances) + self._cy_connect() @property @ureg_wraps("ohm", (None,)) @@ -437,6 +487,8 @@ def impedances(self) -> Q_[ComplexArray]: def impedances(self, impedances: ComplexArrayLike1D) -> None: self._impedances = self._validate_value(impedances) self._invalidate_network_results() + if hasattr(self, "cy_element"): + self.cy_element.update_admittances(1.0 / self._impedances) def to_dict(self, *, _lf_only: bool = False) -> JsonDict: self._raise_disconnected_error() diff --git a/roseau/load_flow/models/potential_refs.py b/roseau/load_flow/models/potential_refs.py index dc8e4800..ee956448 100644 --- a/roseau/load_flow/models/potential_refs.py +++ b/roseau/load_flow/models/potential_refs.py @@ -9,6 +9,7 @@ from roseau.load_flow.models.grounds import Ground from roseau.load_flow.typing import Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps +from roseau.load_flow_engine.models.potential_refs.cy_potential_refs import CyDeltaPotentialRef, CyPotentialRef logger = logging.getLogger(__name__) @@ -60,11 +61,24 @@ def __init__(self, id: Id, element: Bus | Ground, *, phase: str | None = None, * self.element = element self._connect(element) self._res_current: complex | None = None + if isinstance(element, Bus) and self.phase is None: + n = len(element.phases) + self.cy_element = CyDeltaPotentialRef(n) + connections = [(i, i) for i in range(n)] + element.cy_element.connect(self.cy_element, connections) + else: + self.cy_element = CyPotentialRef() + if isinstance(element, Ground): + element.cy_element.connect(self.cy_element, [(0, 0)]) + else: + p = element.phases.find(self.phase) + element.cy_element.connect(self.cy_element, [(p, 0)]) def __repr__(self) -> str: return f"{type(self).__name__}(id={self.id!r}, element={self.element!r}, phase={self.phase!r})" def _res_current_getter(self, warning: bool) -> complex: + self._res_current = self.cy_element.get_current() return self._res_getter(self._res_current, warning) @property diff --git a/roseau/load_flow/models/sources.py b/roseau/load_flow/models/sources.py index dbb3f885..bc546aae 100644 --- a/roseau/load_flow/models/sources.py +++ b/roseau/load_flow/models/sources.py @@ -10,6 +10,7 @@ from roseau.load_flow.models.core import Element from roseau.load_flow.typing import ComplexArray, ComplexArrayLike1D, Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps +from roseau.load_flow_engine.models.sources.cy_sources import CyDeltaVoltageSource, CyVoltageSource logger = logging.getLogger(__name__) @@ -73,6 +74,13 @@ def __init__( self.bus = bus self.voltages = voltages + self.n = len(self.phases) + if self.phases == "abc": + self.cy_element = CyDeltaVoltageSource(n=self.n, voltages=self._voltages) + else: + self.cy_element = CyVoltageSource(n=self.n, voltages=self._voltages) + self._cy_connect() + # Results self._res_currents: ComplexArray | None = None @@ -98,6 +106,8 @@ def voltages(self, voltages: ComplexArrayLike1D) -> None: raise RoseauLoadFlowException(msg, code=RoseauLoadFlowExceptionCode.BAD_VOLTAGES_SIZE) self._voltages = np.array(voltages, dtype=np.complex128) self._invalidate_network_results() + if hasattr(self, "cy_element"): + self.cy_element.update_voltages(self._voltages) @property def voltage_phases(self) -> list[str]: @@ -105,6 +115,7 @@ def voltage_phases(self) -> list[str]: return calculate_voltage_phases(self.phases) def _res_currents_getter(self, warning: bool) -> ComplexArray: + self._res_currents = self.cy_element.get_currents(self.n) return self._res_getter(value=self._res_currents, warning=warning) @property @@ -134,6 +145,14 @@ def res_powers(self) -> Q_[ComplexArray]: """The load flow result of the source powers (VA).""" return self._res_powers_getter(warning=True) + def _cy_connect(self): + connections = [] + for i, phase in enumerate(self.bus.phases): + if phase in self.phases: + j = self.phases.find(phase) + connections.append((i, j)) + self.bus.cy_element.connect(self.cy_element, connections) + # # Disconnect # diff --git a/roseau/load_flow/models/transformers/transformers.py b/roseau/load_flow/models/transformers/transformers.py index 059b18ea..495e6000 100644 --- a/roseau/load_flow/models/transformers/transformers.py +++ b/roseau/load_flow/models/transformers/transformers.py @@ -9,6 +9,12 @@ from roseau.load_flow.models.transformers.parameters import TransformerParameters from roseau.load_flow.typing import Id, JsonDict from roseau.load_flow.units import Q_ +from roseau.load_flow_engine.models.transformers.cy_transformers import ( + CyCenterTransformer, + CyExtendedTransformer, + CyReducedTransformer, + CySingleTransformer, +) logger = logging.getLogger(__name__) @@ -99,6 +105,44 @@ def __init__( self.tap = tap self._parameters = parameters + z2, ym, k, orientation = parameters.to_zyk() + z2 = z2.m_as("ohm") + ym = ym.m_as("S") + if parameters.type == "single": + self.cy_element = CySingleTransformer(z2=z2, ym=ym, k=k * tap) + elif parameters.type == "center": + self.cy_element = CyCenterTransformer(z2=z2, ym=ym, k=k * tap) + else: + if "Y" in parameters.winding1 and "y" in parameters.winding2: + self.cy_element = CyReducedTransformer( + n1=4, n2=4, prim="Y", sec="y", z2=z2, ym=ym, k=k * tap, orientation=orientation + ) + elif "D" in parameters.winding1 and "y" in parameters.winding2: + self.cy_element = CyReducedTransformer( + n1=3, n2=4, prim="D", sec="y", z2=z2, ym=ym, k=k * tap, orientation=orientation + ) + elif "D" in parameters.winding1 and "d" in parameters.winding2: + self.cy_element = CyReducedTransformer( + n1=3, n2=3, prim="D", sec="d", z2=z2, ym=ym, k=k * tap, orientation=orientation + ) + elif "Y" in parameters.winding1 and "d" in parameters.winding2: + self.cy_element = CyReducedTransformer( + n1=4, n2=3, prim="Y", sec="d", z2=z2, ym=ym, k=k * tap, orientation=orientation + ) + elif "Y" in parameters.winding1 and "z" in parameters.winding2: + self.cy_element = CyExtendedTransformer( + n1=4, n2=4, prim="Y", sec="z", z2=z2, ym=ym, k=k * tap, orientation=orientation + ) + elif "D" in parameters.winding1 and "z" in parameters.winding2: + self.cy_element = CyExtendedTransformer( + n1=3, n2=4, prim="D", sec="z", z2=z2, ym=ym, k=k * tap, orientation=orientation + ) + else: + msg = f"Transformer {parameters.type} is not implemented yet..." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_WINDINGS) + self._cy_connect() + @property def tap(self) -> float: """The tap of the transformer, for example 1.02.""" @@ -112,6 +156,9 @@ def tap(self, value: float) -> None: logger.warning(f"The provided tap {value:.2f} is lower than 0.9. A good value is between 0.9 and 1.1.") self._tap = value self._invalidate_network_results() + if hasattr(self, "cy_element"): + z2, ym, k, _ = self.parameters.to_zyk() + self.cy_element.update_transformer_parameters(z2.m_as("ohm"), ym.m_as("S"), k * value) @property def parameters(self) -> TransformerParameters: @@ -128,6 +175,9 @@ def parameters(self, value: TransformerParameters) -> None: raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_TYPE) self._parameters = value self._invalidate_network_results() + if hasattr(self, "cy_element"): + z2, ym, k, _ = value.to_zyk() + self.cy_element.update_transformer_parameters(z2.m_as("ohm"), ym.m_as("S"), k * self.tap) @property def max_power(self) -> Q_[float] | None: diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 93ad58b5..5e0e4312 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -5,17 +5,17 @@ import logging import re import textwrap +import time import warnings from collections.abc import Mapping, Sized from importlib import resources from itertools import cycle from pathlib import Path from typing import TYPE_CHECKING, NoReturn, TypeVar -from urllib.parse import urljoin import geopandas as gpd +import numpy as np import pandas as pd -import requests from pyproj import CRS from requests import Response from rich.table import Table @@ -36,10 +36,11 @@ Transformer, VoltageSource, ) -from roseau.load_flow.solvers import check_solver_params -from roseau.load_flow.typing import Authentication, Id, JsonDict, MapOrSeq, Solver, StrPath +from roseau.load_flow.solvers import AbstractSolver, NewtonGoldstein +from roseau.load_flow.typing import Id, JsonDict, MapOrSeq, Solver, StrPath from roseau.load_flow.utils import CatalogueMixin, JsonMixin, _optional_deps, console, palette from roseau.load_flow.utils.types import _DTYPES, VoltagePhaseDtype +from roseau.load_flow_engine.network.cy_network import CyElectricalNetwork if TYPE_CHECKING: from networkx import Graph @@ -168,11 +169,13 @@ def __init__( self.grounds = self._elements_as_dict(grounds, RoseauLoadFlowExceptionCode.BAD_GROUND_ID) self.potential_refs = self._elements_as_dict(potential_refs, RoseauLoadFlowExceptionCode.BAD_POTENTIAL_REF_ID) + self._elements: list[Element] = [] self._check_validity(constructed=False) self._create_network() self._valid = True self._results_valid: bool = False self.res_info: JsonDict = {} + self._solver: AbstractSolver | None = None def __repr__(self) -> str: def count_repr(__o: Sized, /, singular: str, plural: str | None = None) -> str: @@ -490,116 +493,124 @@ def to_graph(self) -> "Graph": # def solve_load_flow( self, - auth: Authentication, - base_url: str = _DEFAULT_BASE_URL, max_iterations: int = _DEFAULT_MAX_ITERATIONS, tolerance: float = _DEFAULT_TOLERANCE, warm_start: bool = _DEFAULT_WARM_START, - solver: Solver = _DEFAULT_SOLVER, - solver_params: JsonDict | None = None, + solver: AbstractSolver | None = None, + **kwargs, ) -> int: - """Solve the load flow for this network (Requires internet access). + """Solve the load flow for this network. To get the results of the load flow for the whole network, use the `res_` properties on the network (e.g. ``print(net.res_buses``). To get the results for a specific element, use the `res_` properties on the element (e.g. ``print(net.buses["bus1"].res_potentials)``. Args: - auth: - The login and password for the roseau load flow api. - - base_url: - The base url to request the load flow solver. - max_iterations: - The maximum number of allowed iterations. + The maximum number of allowed iterations tolerance: - Tolerance needed for the convergence. + Required tolerance value on the residuals for the convergence. warm_start: - If true, initialize the solver with the potentials of the last successful load flow - result (if any). + Should we use the values of potentials of the last successful load flow result (if any)? solver: - The name of the solver to use for the load flow. The options are: - - ``'newton'``: the classical Newton-Raphson solver. - - ``'newton_goldstein'``: the Newton-Raphson solver with the Goldstein and - Price linear search. - - solver_params: - A dictionary of parameters used by the solver. Available parameters depend on the - solver chosen. For more information, see the :ref:`solvers` page. + The solver to use for the load flow. By default, a Newton-Raphson algorithm is performed with the + Goldstein and Price linear search. Returns: - The number of iterations taken. + The number of iterations taken """ - from roseau.load_flow import __version__ - - solver_params = check_solver_params(solver=solver, params=solver_params) if not self._valid: - warm_start = False # Otherwise, we may get an error when calling self.results_to_dict() - self._check_validity(constructed=True) + self._check_validity(constructed=False) self._create_network() + if self._solver is not None: + self._solver.update_network(self) + + # Update solver TODO solver params + if solver is not None: + self._solver = solver + if self._solver is None: + self._solver = NewtonGoldstein(self) + if self._solver.network != self: + msg = "The solver has been constructed with a different network than the one it is intending to solve." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.NETWORK_SOLVER_MISMATCH) + + if not warm_start: + self._propagate_potentials(force=True) + + start = time.perf_counter() + try: + iterations, residual = self._solver.solve_load_flow(max_iterations=max_iterations, tolerance=tolerance) + except RuntimeError as e: + msg = e.args[0] + zero_elements_index, inf_elements_index = self._solver.cy_solver.analyse_jacobian() + if zero_elements_index: + zero_elements = [self._elements[i] for i in zero_elements_index] + printable_elements = ", ".join(f"{type(e).__name__}({e.id!r})" for e in zero_elements) + msg += ( + f"The problem seems to come from the elements [{printable_elements}] that have at least one " + f"disconnected phase. " + ) + if inf_elements_index: + inf_elements = [self._elements[i] for i in inf_elements_index] + printable_elements = ", ".join(f"{type(e).__name__}({e.id!r})" for e in inf_elements) + msg += ( + f"The problem seems to come from the elements [{printable_elements}] that induce infinite " + f"values. This might be caused by flexible loads with very high alpha." + ) + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_JACOBIAN) from e - # Get the data - data = { - "network": self.to_dict(_lf_only=True), - "solver": { - "name": solver, - "params": solver_params, - "max_iterations": max_iterations, - "tolerance": tolerance, - "warm_start": warm_start, - }, - } - if warm_start and self.res_info.get("status", "failure") == "success": - # Ignore warnings because results may be invalid (a load power has been changed, etc.) - data["results"] = self._results_to_dict(False) - - # Request the server - response = requests.post( - url=urljoin(base_url, "solve/"), - json=data, - auth=auth, - headers={"accept": "application/json", "rlf-version": __version__}, - ) - - # Read the response - # Check the response headers - remote_rlf_version = response.headers.get("rlf-new-version") - if remote_rlf_version is not None: - warnings.warn( - message=f"A new version ({remote_rlf_version}) of the library roseau-load-flow is available. Please " - f"visit https://roseautechnologies.github.io/Roseau_Load_Flow/Installation.html for more information.", - category=UserWarning, - stacklevel=2, - ) - - # HTTP 4xx,5xx - if not response.ok: - self._parse_error(response=response) + end = time.perf_counter() - # HTTP 200 - results: JsonDict = response.json() - self.res_info = results["info"] - if self.res_info["status"] != "success": + if iterations == max_iterations: msg = ( - f"The load flow did not converge after {self.res_info['iterations']} iterations. The norm of " - f"the residuals is {self.res_info['residual']:.5n}" + f"The load flow did not converge after {iterations} iterations. The norm of the residuals is " + f"{residual:5n}" ) logger.error(msg=msg) + + self.res_info = { + # Input + "solver": self._solver.name, + "solver_params": self._solver.params(), + "tolerance": tolerance, + "max_iterations": max_iterations, + "warm_start": warm_start, + # Output + "status": "failure", + "iterations": iterations, + "residual": residual, + } raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.NO_LOAD_FLOW_CONVERGENCE) - logger.info( - f"The load flow converged after {self.res_info['iterations']} iterations (residual=" - f"{self.res_info['residual']:.5n})." - ) + logger.debug(f"The load flow converged after {iterations} iterations and {end - start:.3n} s.") + + self.res_info = { + # Input + "solver": self._solver.name, + "solver_params": self._solver.params(), + "tolerance": tolerance, + "max_iterations": max_iterations, + "warm_start": warm_start, + # Output + "status": "success", + "iterations": iterations, + "residual": residual, + } + + # The results are now valid + self._results_valid = True - # Dispatch the results - self._results_from_dict(data=results) + return iterations - return self.res_info["iterations"] + def reset_inputs(self) -> None: + """Reset the input vector (which is used for the first step of the newton algorithm) to its initial value""" + if self._solver is not None: + self._solver.reset_inputs() @staticmethod def _parse_error(response: Response) -> NoReturn: @@ -1261,6 +1272,28 @@ def _disconnect_element(self, element: Element) -> None: def _create_network(self) -> None: """Create the Cython and C++ electrical network of all the passed elements.""" self._valid = True + cy_elements = [] + self._elements = [] + for bus in self.buses.values(): + cy_elements.append(bus.cy_element) + self._elements.append(bus) + for line in self.branches.values(): + cy_elements.append(line.cy_element) + self._elements.append(line) + for load in self.loads.values(): + cy_elements.append(load.cy_element) + self._elements.append(load) + for ground in self.grounds.values(): + cy_elements.append(ground.cy_element) + self._elements.append(ground) + for p_ref in self.potential_refs.values(): + cy_elements.append(p_ref.cy_element) + self._elements.append(p_ref) + for source in self.sources.values(): + cy_elements.append(source.cy_element) + self._elements.append(source) + self._propagate_potentials(force=False) + self.cy_electrical_network = CyElectricalNetwork(elements=np.array(cy_elements), nb_elements=len(cy_elements)) def _check_validity(self, constructed: bool) -> None: """Check the validity of the network to avoid having a singular jacobian matrix. It also assigns the `self` @@ -1315,6 +1348,68 @@ def _check_validity(self, constructed: bool) -> None: elif element.network != self: element._raise_several_network() + def _propagate_potentials(self, force: bool) -> None: + """Set the bus potentials that have not been initialized yet + + Args: + force: + If True, the `_initialized` status of the buses are ignored. If False, only uninitialized + potentials of buses will be overwritten. + """ + if force: + uninitialized = True + else: + uninitialized = False + for bus in self.buses.values(): + if not bus._initialized: + uninitialized = True + + if uninitialized: + max_voltages = 0.0 + voltage_source = None + potentials = None + for source in self.sources.values(): + # if there are multiple voltage sources, start from the higher one + source_voltages = source.voltages.m_as("V") + if np.average(np.abs(source_voltages)) > max_voltages: + max_voltages = np.average(np.abs(source_voltages)) + voltage_source = source + if "n" in source.phases: + # Assume Vn = 0 + potentials = np.append(source_voltages, 0.0) + elif len(source.phases) == 2: + # Assume V1 + V2 = 0 + u = source_voltages[0] + potentials = np.array([u / 2, -u / 2]) + else: + assert len(source.phases) == 3 + # Assume Va + Vb + Vc = 0 + u_ab = source_voltages[0] + u_bc = source_voltages[1] + v_b = (u_bc - u_ab) / 3 + v_c = v_b - u_bc + v_a = v_b + u_ab + potentials = np.array([v_a, v_b, v_c, 0.0]) + + elements = [(voltage_source, potentials)] + visited = set() + while elements: + element, potentials = elements.pop(-1) + visited.add(element) + if isinstance(element, Bus) and (force or not element._initialized): + bus_n = element.n + element.potentials = potentials[0:bus_n] + for e in element._connected_elements: + if e not in visited: + if isinstance(element, Transformer): + k = (element.parameters.ulv / element.parameters.uhv).m_as("") + phase_displacement = element.parameters.phase_displacement + if phase_displacement is None: + phase_displacement = 0 + elements.append((e, potentials * k * np.exp(phase_displacement * -1j * np.pi / 6.0))) + else: + elements.append((e, potentials)) + @staticmethod def _check_ref(elements: list[Element]) -> None: """Check the number of potential references to avoid having a singular jacobian matrix.""" diff --git a/roseau/load_flow/solvers.py b/roseau/load_flow/solvers.py index 6cab54eb..32c2d54f 100644 --- a/roseau/load_flow/solvers.py +++ b/roseau/load_flow/solvers.py @@ -1,7 +1,12 @@ import logging +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Self + +import numpy as np from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow.typing import JsonDict, Solver +from roseau.load_flow_engine.network.cy_network import CyAbstractSolver, CyNewton, CyNewtonGoldstein logger = logging.getLogger(__name__) @@ -11,6 +16,199 @@ } SOLVERS = list(_SOLVERS_PARAMS) +if TYPE_CHECKING: + from roseau.load_flow_engine.network import ElectricalNetwork + + +class AbstractSolver(ABC): + """This is an abstract class for all the solvers.""" + + name: str | None = None + + def __init__(self, network: "ElectricalNetwork", **kwargs): + """AbstractSolver constructor. + + Args: + network: + The electrical network for which the load flow needs to be solved. + """ + self.network = network + self.cy_solver: CyAbstractSolver | None = None + + @classmethod + def from_dict(cls, data: JsonDict, network: "ElectricalNetwork") -> Self: + """AbstractSolver constructor from dict. + + Args: + data: + The solver data. + + network: + The electrical network for which the load flow needs to be solved. + + Returns: + The constructed solver. + """ + if data["name"] == "newton": + return Newton(network=network) + elif data["name"] == "newton_goldstein": + m1 = data["params"].get("m1", NewtonGoldstein.DEFAULT_M1) + m2 = data["params"].get("m2", NewtonGoldstein.DEFAULT_M2) + return NewtonGoldstein(network=network, m1=m1, m2=m2) + else: + msg = f"Solver {data['name']!r} is not implemented." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_SOLVER_NAME) + + def solve_load_flow(self, max_iterations: int, tolerance: float) -> tuple[int, float]: + """Solve the load flow for the network the solver was constructed with. + + Args: + tolerance: + Required tolerance value on the residuals for the convergence. + + max_iterations: + The maximum number of allowed iterations + + Returns: + The number of iterations and the final residual + """ + return self.cy_solver.solve_load_flow(max_iterations=max_iterations, tolerance=tolerance) + + def reset_inputs(self): + """Reset the input vector (which is used for the first step of the newton algorithm) to its initial value""" + self.cy_solver.reset_inputs() + + @abstractmethod + def update_network(self, network: "ElectricalNetwork") -> None: + """If the network has changed, we need to re-create a solver for this new network.""" + raise NotImplementedError + + def to_dict(self) -> JsonDict: + """Return the solver information as a dictionary format.""" + return {"name": self.name, "params": self.params()} + + def params(self) -> JsonDict: + """Return the parameters of the solver.""" + return {} + + +class AbstractNewton(AbstractSolver, ABC): + """This is an abstract class for all the Newton-Raphson solvers.""" + + DEFAULT_TAPE_OPTIMIZATION: bool = True + + def __init__(self, network: "ElectricalNetwork", optimize_tape: bool = DEFAULT_TAPE_OPTIMIZATION, **kwargs: Any): + """AbstractNewton constructor. + + Args: + network: + The electrical network for which the load flow needs to be solved. + + optimize_tape: + If True, a tape optimization will be performed. This operation might take a bit of time, but will make + every subsequent load flow to run faster. + """ + super().__init__(network=network, **kwargs) + self.optimize_tape = optimize_tape + + def save_matrix(self, prefix: str) -> None: + """Output files of the jacobian and vector matrix of the first newton step. Those files can be used to launch an + eigen solver benchmark (see https://eigen.tuxfamily.org/dox/group__TopicSparseSystems.html) + + Args: + prefix: + The prefix of the name of the files. They will be output as prefix.mtx and prefix_m.mtx to follow Eigen + solver benchmark convention. + """ + self.cy_solver.save_matrix(prefix) + + def current_jacobian(self) -> np.ndarray: + """Show the jacobian of the current iteration (useful for debugging)""" + return self.cy_solver.current_jacobian() + + +class Newton(AbstractNewton): + """The classical Newton-Raphson algorithm.""" + + name = "newton" + + def __init__( + self, + network: "ElectricalNetwork", + optimize_tape: bool = AbstractNewton.DEFAULT_TAPE_OPTIMIZATION, + **kwargs: Any, + ): + """Newton constructor. + + Args: + network: + The electrical network for which the load flow needs to be solved. + + optimize_tape: + If True, a tape optimization will be performed. This operation might take a bit of time, but will make + every subsequent load flow to run faster. + """ + super().__init__(network=network, optimize_tape=optimize_tape, **kwargs) + self.cy_solver = CyNewton(network=network.cy_electrical_network, optimize_tape=optimize_tape) + + def update_network(self, network: "ElectricalNetwork") -> None: + self.cy_solver = CyNewton(network=network.cy_electrical_network, optimize_tape=self.optimize_tape) + + +class NewtonGoldstein(AbstractNewton): + """The Newton-Raphson algorithm with the Goldstein and Price linear search. It has better stability than the + classical Newton-Raphson, without losing performance. + """ + + name = "newton_goldstein" + + DEFAULT_M1 = 0.1 + DEFAULT_M2 = 0.9 + + def __init__( + self, + network: "ElectricalNetwork", + m1: float = DEFAULT_M1, + m2: float = DEFAULT_M2, + optimize_tape: bool = AbstractNewton.DEFAULT_TAPE_OPTIMIZATION, + **kwargs: Any, + ): + """NewtonGoldstein constructor. + + Args: + network: + The electrical network for which the load flow needs to be solved. + + optimize_tape: + If True, a tape optimization will be performed. This operation might take a bit of time, but will make + every subsequent load flow iteration to run faster. + + m1: + The first constant of the Goldstein and Price linear search. + + m2: + The second constant of the Goldstein and Price linear search. + """ + super().__init__(network=network, optimize_tape=optimize_tape, **kwargs) + if m1 >= m2: + msg = "For the 'newton_goldstein' solver, the inequality m1 < m2 should be respected." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_SOLVER_PARAMS) + self.m1 = m1 + self.m2 = m2 + self.cy_solver = CyNewtonGoldstein( + network=network.cy_electrical_network, optimize_tape=optimize_tape, m1=m1, m2=m2 + ) + + def update_network(self, network: "ElectricalNetwork") -> None: + self.cy_solver = CyNewtonGoldstein( + network=network.cy_electrical_network, optimize_tape=self.optimize_tape, m1=self.m1, m2=self.m2 + ) + + def params(self) -> JsonDict: + return {"m1": self.m1, "m2": self.m2} + def check_solver_params(solver: Solver, params: JsonDict | None) -> JsonDict: """Strip and check the solver parameters. From 683567461e3677a27b1528f0d5ced9fe6d192a37 Mon Sep 17 00:00:00 2001 From: Saelyos Date: Wed, 13 Dec 2023 11:06:03 +0100 Subject: [PATCH 05/51] Update solvers --- roseau/load_flow/network.py | 49 +++++++++++--------- roseau/load_flow/solvers.py | 61 +++++++------------------ roseau/load_flow/tests/test_solvers.py | 63 +++++++++++++++++++++----- 3 files changed, 96 insertions(+), 77 deletions(-) diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 5e0e4312..7fe93c5b 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -36,7 +36,7 @@ Transformer, VoltageSource, ) -from roseau.load_flow.solvers import AbstractSolver, NewtonGoldstein +from roseau.load_flow.solvers import AbstractSolver from roseau.load_flow.typing import Id, JsonDict, MapOrSeq, Solver, StrPath from roseau.load_flow.utils import CatalogueMixin, JsonMixin, _optional_deps, console, palette from roseau.load_flow.utils.types import _DTYPES, VoltagePhaseDtype @@ -175,7 +175,9 @@ def __init__( self._valid = True self._results_valid: bool = False self.res_info: JsonDict = {} - self._solver: AbstractSolver | None = None + self._solver: AbstractSolver = AbstractSolver.from_dict( + data={"name": self._DEFAULT_SOLVER, "params": {}}, network=self + ) def __repr__(self) -> str: def count_repr(__o: Sized, /, singular: str, plural: str | None = None) -> str: @@ -496,8 +498,8 @@ def solve_load_flow( max_iterations: int = _DEFAULT_MAX_ITERATIONS, tolerance: float = _DEFAULT_TOLERANCE, warm_start: bool = _DEFAULT_WARM_START, - solver: AbstractSolver | None = None, - **kwargs, + solver: Solver = _DEFAULT_SOLVER, + solver_params: JsonDict | None = None, ) -> int: """Solve the load flow for this network. @@ -507,36 +509,39 @@ def solve_load_flow( Args: max_iterations: - The maximum number of allowed iterations + The maximum number of allowed iterations. tolerance: - Required tolerance value on the residuals for the convergence. + Tolerance needed for the convergence. warm_start: - Should we use the values of potentials of the last successful load flow result (if any)? + If true, initialize the solver with the potentials of the last successful load flow + result (if any). solver: - The solver to use for the load flow. By default, a Newton-Raphson algorithm is performed with the - Goldstein and Price linear search. + The name of the solver to use for the load flow. The options are: + - ``'newton'``: the classical Newton-Raphson solver. + - ``'newton_goldstein'``: the Newton-Raphson solver with the Goldstein and + Price linear search. + + solver_params: + A dictionary of parameters used by the solver. Available parameters depend on the + solver chosen. For more information, see the :ref:`solvers` page. Returns: - The number of iterations taken + The number of iterations taken. """ if not self._valid: self._check_validity(constructed=False) self._create_network() - if self._solver is not None: - self._solver.update_network(self) - - # Update solver TODO solver params - if solver is not None: - self._solver = solver - if self._solver is None: - self._solver = NewtonGoldstein(self) - if self._solver.network != self: - msg = "The solver has been constructed with a different network than the one it is intending to solve." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.NETWORK_SOLVER_MISMATCH) + self._solver.update_network(self) + + # Update solver + if solver != self._solver.name: + solver_params = solver_params if solver_params is not None else {} + self._solver = AbstractSolver.from_dict(data={"name": solver, "params": solver_params}, network=self) + elif solver_params is not None: + self._solver.update_params(solver_params) if not warm_start: self._propagate_potentials(force=True) diff --git a/roseau/load_flow/solvers.py b/roseau/load_flow/solvers.py index 32c2d54f..ce68028c 100644 --- a/roseau/load_flow/solvers.py +++ b/roseau/load_flow/solvers.py @@ -17,7 +17,7 @@ SOLVERS = list(_SOLVERS_PARAMS) if TYPE_CHECKING: - from roseau.load_flow_engine.network import ElectricalNetwork + from roseau.load_flow.network import ElectricalNetwork class AbstractSolver(ABC): @@ -84,6 +84,11 @@ def update_network(self, network: "ElectricalNetwork") -> None: """If the network has changed, we need to re-create a solver for this new network.""" raise NotImplementedError + @abstractmethod + def update_params(self, params: JsonDict) -> None: + """If the network has changed, we need to re-create a solver for this new network.""" + raise NotImplementedError + def to_dict(self) -> JsonDict: """Return the solver information as a dictionary format.""" return {"name": self.name, "params": self.params()} @@ -155,6 +160,9 @@ def __init__( def update_network(self, network: "ElectricalNetwork") -> None: self.cy_solver = CyNewton(network=network.cy_electrical_network, optimize_tape=self.optimize_tape) + def update_params(self, params: JsonDict) -> None: + pass + class NewtonGoldstein(AbstractNewton): """The Newton-Raphson algorithm with the Goldstein and Price linear search. It has better stability than the @@ -206,48 +214,13 @@ def update_network(self, network: "ElectricalNetwork") -> None: network=network.cy_electrical_network, optimize_tape=self.optimize_tape, m1=self.m1, m2=self.m2 ) + def update_params(self, params: JsonDict) -> None: + m1 = params.get("m1", NewtonGoldstein.DEFAULT_M1) + m2 = params.get("m2", NewtonGoldstein.DEFAULT_M2) + if m1 != self.m1 or m2 != self.m2: + self.cy_solver.update_params(m1=m1, m2=m2) + self.m1 = m1 + self.m2 = m2 + def params(self) -> JsonDict: return {"m1": self.m1, "m2": self.m2} - - -def check_solver_params(solver: Solver, params: JsonDict | None) -> JsonDict: - """Strip and check the solver parameters. - - Args: - solver: - The name of the solver used by the solver. - - params: - The solver parameters dictionary. - - Returns: - The updated solver parameters. - """ - params = {} if params is None else params.copy() - - # Check the solver - if solver not in _SOLVERS_PARAMS: - msg = f"Solver {solver!r} is not implemented. Available solvers are: {SOLVERS}" - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_SOLVER_NAME) - - # Warn about and remove unexpected parameters - param_list = _SOLVERS_PARAMS[solver] - to_delete: list[str] = [] - for key in params: - if key not in param_list: - msg = "Unexpected solver parameter %r for the %r solver. Available params are: %s" - logger.warning(msg, key, solver, param_list) - to_delete.append(key) - for key in to_delete: - del params[key] - - # Extra checks per solver - if solver == "newton": - pass # Nothing more to check - elif solver == "newton_goldstein" and params.get("m1", 0.1) >= params.get("m2", 0.9): - msg = "For the 'newton_goldstein' solver, the inequality m1 < m2 should be respected." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_SOLVER_PARAMS) - - return params diff --git a/roseau/load_flow/tests/test_solvers.py b/roseau/load_flow/tests/test_solvers.py index ac03fa59..e405ad3b 100644 --- a/roseau/load_flow/tests/test_solvers.py +++ b/roseau/load_flow/tests/test_solvers.py @@ -1,29 +1,70 @@ import pytest -from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode -from roseau.load_flow.solvers import check_solver_params +from roseau.load_flow import ( + Bus, + ElectricalNetwork, + PotentialRef, + RoseauLoadFlowException, + RoseauLoadFlowExceptionCode, + VoltageSource, +) +from roseau.load_flow.solvers import AbstractSolver, Newton, NewtonGoldstein def test_solver(): - # Additional key - solver_params = check_solver_params(solver="newton", params={"m1": 0.1, "toto": ""}) - assert "m1" not in solver_params - assert "toto" not in solver_params + bus = Bus(id="bus", phases="abcn") + VoltageSource(id="vs", bus=bus, voltages=[20000.0 + 0.0j, -10000.0 - 17320.508076j, -10000.0 + 17320.508076j]) + PotentialRef(id="pref", element=bus) + en = ElectricalNetwork.from_element(bus) - # Bad solver + # Bad solvers with pytest.raises(RoseauLoadFlowException) as e: - check_solver_params(solver="toto", params={}) - assert "Solver 'toto' is not implemented" in e.value.msg + AbstractSolver.from_dict(data={"name": "toto", "params": {}}, network=en) + assert "'toto' is not implemented" in e.value.msg assert e.value.code == RoseauLoadFlowExceptionCode.BAD_SOLVER_NAME # Bad Goldstein and Price parameters with pytest.raises(RoseauLoadFlowException) as e: # m1 and m2 provided - check_solver_params(solver="newton_goldstein", params={"m1": 0.9, "m2": 0.1}) + AbstractSolver.from_dict(data={"name": "newton_goldstein", "params": {"m1": 0.9, "m2": 0.1}}, network=en) assert "the inequality m1 < m2 should be respected" in e.value.msg assert e.value.code == RoseauLoadFlowExceptionCode.BAD_SOLVER_PARAMS with pytest.raises(RoseauLoadFlowException) as e: # only m1 provided (m2 defaults to 0.9) - check_solver_params(solver="newton_goldstein", params={"m1": 0.9}) + AbstractSolver.from_dict(data={"name": "newton_goldstein", "params": {"m1": 0.9}}, network=en) assert "the inequality m1 < m2 should be respected" in e.value.msg assert e.value.code == RoseauLoadFlowExceptionCode.BAD_SOLVER_PARAMS + + # Good ones + data = {"name": "newton_goldstein", "params": {"m1": 0.1, "m2": 0.9}} + solver = AbstractSolver.from_dict(data=data, network=en) + data2 = solver.to_dict() + assert data == data2 + + data = {"name": "newton", "params": {}} + solver = AbstractSolver.from_dict(data=data, network=en) + data2 = solver.to_dict() + assert data == data2 + + +def test_network_solver(): + bus = Bus(id="bus", phases="abcn") + VoltageSource(id="vs", bus=bus, voltages=[20000.0 + 0.0j, -10000.0 - 17320.508076j, -10000.0 + 17320.508076j]) + PotentialRef(id="pref", element=bus) + en = ElectricalNetwork.from_element(bus) + + en.solve_load_flow() + solver = en._solver + assert isinstance(solver, NewtonGoldstein) + + en.solve_load_flow(solver="newton_goldstein", solver_params={"m1": 0.2}) + assert solver == en._solver # Solver did not change + assert solver.m1 == 0.2 + assert solver.m2 == NewtonGoldstein.DEFAULT_M2 + + en.solve_load_flow(solver="newton") + assert solver != en._solver + assert isinstance(en._solver, Newton) + + en.solve_load_flow() # Reset to default + assert isinstance(en._solver, NewtonGoldstein) From 625bf99af6a0e0c901d58f885e2a6dacc802f0a7 Mon Sep 17 00:00:00 2001 From: Saelyos Date: Wed, 13 Dec 2023 15:41:03 +0100 Subject: [PATCH 06/51] Make more members private --- roseau/load_flow/{solvers.py => _solvers.py} | 24 ++++++------ roseau/load_flow/models/branches.py | 6 +-- roseau/load_flow/models/buses.py | 12 +++--- roseau/load_flow/models/core.py | 7 ++-- roseau/load_flow/models/grounds.py | 6 +-- roseau/load_flow/models/lines/lines.py | 16 ++++---- .../models/loads/flexible_parameters.py | 27 ++++++------- roseau/load_flow/models/loads/loads.py | 38 ++++++++++--------- roseau/load_flow/models/potential_refs.py | 12 +++--- roseau/load_flow/models/sources.py | 12 +++--- .../models/transformers/transformers.py | 24 ++++++------ roseau/load_flow/network.py | 18 ++++----- roseau/load_flow/tests/test_solvers.py | 2 +- 13 files changed, 104 insertions(+), 100 deletions(-) rename roseau/load_flow/{solvers.py => _solvers.py} (89%) diff --git a/roseau/load_flow/solvers.py b/roseau/load_flow/_solvers.py similarity index 89% rename from roseau/load_flow/solvers.py rename to roseau/load_flow/_solvers.py index ce68028c..429a0d04 100644 --- a/roseau/load_flow/solvers.py +++ b/roseau/load_flow/_solvers.py @@ -33,7 +33,7 @@ def __init__(self, network: "ElectricalNetwork", **kwargs): The electrical network for which the load flow needs to be solved. """ self.network = network - self.cy_solver: CyAbstractSolver | None = None + self._cy_solver: CyAbstractSolver | None = None @classmethod def from_dict(cls, data: JsonDict, network: "ElectricalNetwork") -> Self: @@ -73,11 +73,11 @@ def solve_load_flow(self, max_iterations: int, tolerance: float) -> tuple[int, f Returns: The number of iterations and the final residual """ - return self.cy_solver.solve_load_flow(max_iterations=max_iterations, tolerance=tolerance) + return self._cy_solver.solve_load_flow(max_iterations=max_iterations, tolerance=tolerance) def reset_inputs(self): """Reset the input vector (which is used for the first step of the newton algorithm) to its initial value""" - self.cy_solver.reset_inputs() + self._cy_solver.reset_inputs() @abstractmethod def update_network(self, network: "ElectricalNetwork") -> None: @@ -126,11 +126,11 @@ def save_matrix(self, prefix: str) -> None: The prefix of the name of the files. They will be output as prefix.mtx and prefix_m.mtx to follow Eigen solver benchmark convention. """ - self.cy_solver.save_matrix(prefix) + self._cy_solver.save_matrix(prefix) def current_jacobian(self) -> np.ndarray: """Show the jacobian of the current iteration (useful for debugging)""" - return self.cy_solver.current_jacobian() + return self._cy_solver.current_jacobian() class Newton(AbstractNewton): @@ -155,10 +155,10 @@ def __init__( every subsequent load flow to run faster. """ super().__init__(network=network, optimize_tape=optimize_tape, **kwargs) - self.cy_solver = CyNewton(network=network.cy_electrical_network, optimize_tape=optimize_tape) + self._cy_solver = CyNewton(network=network._cy_electrical_network, optimize_tape=optimize_tape) def update_network(self, network: "ElectricalNetwork") -> None: - self.cy_solver = CyNewton(network=network.cy_electrical_network, optimize_tape=self.optimize_tape) + self._cy_solver = CyNewton(network=network._cy_electrical_network, optimize_tape=self.optimize_tape) def update_params(self, params: JsonDict) -> None: pass @@ -205,20 +205,20 @@ def __init__( raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_SOLVER_PARAMS) self.m1 = m1 self.m2 = m2 - self.cy_solver = CyNewtonGoldstein( - network=network.cy_electrical_network, optimize_tape=optimize_tape, m1=m1, m2=m2 + self._cy_solver = CyNewtonGoldstein( + network=network._cy_electrical_network, optimize_tape=optimize_tape, m1=m1, m2=m2 ) def update_network(self, network: "ElectricalNetwork") -> None: - self.cy_solver = CyNewtonGoldstein( - network=network.cy_electrical_network, optimize_tape=self.optimize_tape, m1=self.m1, m2=self.m2 + self._cy_solver = CyNewtonGoldstein( + network=network._cy_electrical_network, optimize_tape=self.optimize_tape, m1=self.m1, m2=self.m2 ) def update_params(self, params: JsonDict) -> None: m1 = params.get("m1", NewtonGoldstein.DEFAULT_M1) m2 = params.get("m2", NewtonGoldstein.DEFAULT_M2) if m1 != self.m1 or m2 != self.m2: - self.cy_solver.update_params(m1=m1, m2=m2) + self._cy_solver.update_params(m1=m1, m2=m2) self.m1 = m1 self.m2 = m2 diff --git a/roseau/load_flow/models/branches.py b/roseau/load_flow/models/branches.py index ce44e22e..ea5f7dc5 100644 --- a/roseau/load_flow/models/branches.py +++ b/roseau/load_flow/models/branches.py @@ -77,7 +77,7 @@ def __repr__(self) -> str: return s def _res_currents_getter(self, warning: bool) -> tuple[ComplexArray, ComplexArray]: - self._res_currents = self.cy_element.get_currents(len(self.phases1), len(self.phases2)) + self._res_currents = self._cy_element.get_currents(len(self.phases1), len(self.phases2)) return self._res_getter(value=self._res_currents, warning=warning) @property @@ -128,7 +128,7 @@ def _cy_connect(self) -> None: if phase in self.bus1.phases: j = self.bus1.phases.find(phase) connections.append((i, j)) - self.cy_element.connect(self.bus1.cy_element, connections, True) + self._cy_element.connect(self.bus1._cy_element, connections, True) connections = [] assert isinstance(self.bus2, Bus) @@ -136,7 +136,7 @@ def _cy_connect(self) -> None: if phase in self.bus2.phases: j = self.bus2.phases.find(phase) connections.append((i, j)) - self.cy_element.connect(self.bus2.cy_element, connections, False) + self._cy_element.connect(self.bus2._cy_element, connections, False) # # Json Mixin interface diff --git a/roseau/load_flow/models/buses.py b/roseau/load_flow/models/buses.py index 160a7359..4bc126f1 100644 --- a/roseau/load_flow/models/buses.py +++ b/roseau/load_flow/models/buses.py @@ -91,7 +91,7 @@ def __init__( self.n = len(self.phases) self._initialized = initialized - self.cy_element = CyBus(n=self.n, potentials=self._potentials) + self._cy_element = CyBus(n=self.n, potentials=self._potentials) def __repr__(self) -> str: return f"{type(self).__name__}(id={self.id!r}, phases={self.phases!r})" @@ -112,11 +112,11 @@ def potentials(self, value: ComplexArrayLike1D) -> None: self._potentials = np.array(value, dtype=np.complex128) self._invalidate_network_results() self._initialized = True - if hasattr(self, "cy_element"): - self.cy_element.initialize_potentials(self._potentials) + if self._cy_element is not None: + self._cy_element.initialize_potentials(self._potentials) def _res_potentials_getter(self, warning: bool) -> ComplexArray: - self._res_potentials = self.cy_element.get_potentials(self.n) + self._res_potentials = self._cy_element.get_potentials(self.n) return self._res_getter(value=self._res_potentials, warning=warning) @property @@ -402,10 +402,10 @@ def add_short_circuit(self, *phases: str, ground: Optional["Ground"] = None) -> self.network._valid = False phases_index = np.array([self.phases.find(p) for p in phases], dtype=np.int32) - self.cy_element.connect_ports(phases_index, len(phases)) + self._cy_element.connect_ports(phases_index, len(phases)) if ground is not None: - self.cy_element.connect(ground.cy_element, [(phases_index[0], 0)]) + self._cy_element.connect(ground._cy_element, [(phases_index[0], 0)]) @property def short_circuits(self) -> list[dict[str, Any]]: diff --git a/roseau/load_flow/models/core.py b/roseau/load_flow/models/core.py index 4071e41b..bfe52a6c 100644 --- a/roseau/load_flow/models/core.py +++ b/roseau/load_flow/models/core.py @@ -10,6 +10,7 @@ from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow.typing import Id from roseau.load_flow.utils import Identifiable, JsonMixin +from roseau.load_flow_engine.models.core.cy_core import CyElement if TYPE_CHECKING: from roseau.load_flow.network import ElectricalNetwork @@ -40,7 +41,7 @@ def __init__(self, id: Id, **kwargs: Any) -> None: super().__init__(id) self._connected_elements: list[Element] = [] self._network: ElectricalNetwork | None = None - # self.cy_element = None TODO replace hasattr with "if self.cy_element is not None" + self._cy_element: CyElement | None = None @property def network(self) -> Optional["ElectricalNetwork"]: @@ -126,9 +127,9 @@ def _disconnect(self) -> None: element._connected_elements.remove(self) self._connected_elements = [] self._set_network(None) - self.cy_element.disconnect() + self._cy_element.disconnect() # The cpp element has been disconnected and can't be reconnected easily, it's safer to delete it - self.cy_element = None + self._cy_element = None def _invalidate_network_results(self) -> None: """Invalidate the network making the result""" diff --git a/roseau/load_flow/models/grounds.py b/roseau/load_flow/models/grounds.py index 344bfb4b..a2520e43 100644 --- a/roseau/load_flow/models/grounds.py +++ b/roseau/load_flow/models/grounds.py @@ -46,13 +46,13 @@ def __init__(self, id: Id, **kwargs: Any) -> None: # A map of bus id to phase connected to this ground. self._connected_buses: dict[Id, str] = {} self._res_potential: complex | None = None - self.cy_element = CyGround() + self._cy_element = CyGround() def __repr__(self) -> str: return f"{type(self).__name__}(id={self.id!r})" def _res_potential_getter(self, warning: bool) -> complex: - self._res_potential = self.cy_element.get_potentials(1)[0] + self._res_potential = self._cy_element.get_potentials(1)[0] return self._res_getter(self._res_potential, warning) @property @@ -89,7 +89,7 @@ def connect(self, bus: "Bus", phase: str = "n") -> None: self._connect(bus) self._connected_buses[bus.id] = phase p = bus.phases.find(phase) - bus.cy_element.connect(self.cy_element, [(p, 0)]) + bus._cy_element.connect(self._cy_element, [(p, 0)]) # # Json Mixin interface diff --git a/roseau/load_flow/models/lines/lines.py b/roseau/load_flow/models/lines/lines.py index f381acf5..508d9f20 100644 --- a/roseau/load_flow/models/lines/lines.py +++ b/roseau/load_flow/models/lines/lines.py @@ -88,7 +88,7 @@ def __init__( self._check_elements() self._check_loop() self.n = len(self.phases) - self.cy_element = CySwitch(self.n) + self._cy_element = CySwitch(self.n) self._cy_connect() def _check_loop(self) -> None: @@ -230,18 +230,18 @@ def __init__( self.n = len(self.phases) if parameters.with_shunt: - self.cy_element = CyShuntLine( + self._cy_element = CyShuntLine( n=self.n, y_shunt=(parameters.y_shunt.reshape(self.n * self.n) * self.length).m_as("S"), z_line=(parameters.z_line.reshape(self.n * self.n) * self.length).m_as("ohm"), ) else: - self.cy_element = CySimplifiedLine( + self._cy_element = CySimplifiedLine( n=self.n, z_line=(parameters.z_line.reshape(self.n * self.n) * self.length).m_as("ohm") ) self._cy_connect() if parameters.with_shunt: - ground.cy_element.connect(self.cy_element, [(0, self.n + self.n)]) + ground._cy_element.connect(self._cy_element, [(0, self.n + self.n)]) @property @ureg_wraps("km", (None,)) @@ -258,7 +258,7 @@ def length(self, value: float | Q_[float]) -> None: self._length = value self._invalidate_network_results() - if hasattr(self, "cy_element"): + if self._cy_element is not None: # Reassign the same parameters with the new length self.parameters = self.parameters @@ -296,14 +296,14 @@ def parameters(self, value: LineParameters) -> None: self._parameters = value self._invalidate_network_results() - if hasattr(self, "cy_element"): + if self._cy_element is not None: if value.with_shunt: - self.cy_element.update_line_parameters( + self._cy_element.update_line_parameters( (value.y_shunt.reshape(self.n * self.n) * self.length).m_as("S"), (value.z_line.reshape(self.n * self.n) * self.length).m_as("ohm"), ) else: - self.cy_element.update_line_parameters( + self._cy_element.update_line_parameters( (value.z_line.reshape(self.n * self.n) * self.length).m_as("ohm") ) diff --git a/roseau/load_flow/models/loads/flexible_parameters.py b/roseau/load_flow/models/loads/flexible_parameters.py index 0f51545f..2857083a 100644 --- a/roseau/load_flow/models/loads/flexible_parameters.py +++ b/roseau/load_flow/models/loads/flexible_parameters.py @@ -90,7 +90,7 @@ def __init__( self._u_max = u_max self._alpha = alpha self._check_values() - self.cy_control = CyControl( + self._cy_control = CyControl( t=type, u_min=self._u_min, u_down=self._u_down, u_up=self._u_up, u_max=self._u_max, alpha=self._alpha ) @@ -384,7 +384,7 @@ def __init__(self, type: ProjectionType, alpha: float = _DEFAULT_ALPHA, epsilon: self._alpha = alpha self._epsilon = epsilon self._check_values() - self.cy_projection = CyProjection(t=type, alpha=self._alpha, epsilon=self._epsilon) + self._cy_projection = CyProjection(t=type, alpha=self._alpha, epsilon=self._epsilon) def _check_values(self) -> None: """Check the provided values.""" @@ -499,15 +499,16 @@ def __init__( self.control_p = control_p self.control_q = control_q self.projection = projection + self._cy_fp = None self._q_min = None self._q_max = None self.s_max = s_max self.q_min = q_min self.q_max = q_max - self.cy_fp = CyFlexibleParameter( - control_p=control_p.cy_control, - control_q=control_q.cy_control, - projection=projection.cy_projection, + self._cy_fp = CyFlexibleParameter( + control_p=control_p._cy_control, + control_q=control_q._cy_control, + projection=projection._cy_projection, s_max=self._s_max, q_min=self.q_min.m_as("VAr"), q_max=self.q_max.m_as("VAr"), @@ -534,8 +535,8 @@ def s_max(self, value: float | Q_[float]) -> None: if self._q_min is not None and self._q_min < -self._s_max: logger.warning("'s_max' has been updated but now 'q_min' is less than -s_max. 'q_min' is set to -s_max") self._q_min = -self._s_max - if hasattr(self, "cy_fp"): - self.cy_fp.update_parameters(self._s_max, self.q_min.m_as("VAr"), self.q_max.m_as("VAr")) + if self._cy_fp is not None: + self._cy_fp.update_parameters(self._s_max, self.q_min.m_as("VAr"), self.q_max.m_as("VAr")) @property @ureg_wraps("VAr", (None,)) @@ -557,8 +558,8 @@ def q_min(self, value: float | Q_[float] | None) -> None: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_FLEXIBLE_PARAMETER_VALUE) self._q_min = value - if hasattr(self, "cy_fp"): - self.cy_fp.update_parameters(self._s_max, self.q_min.m_as("VAr"), self.q_max.m_as("VAr")) + if self._cy_fp is not None: + self._cy_fp.update_parameters(self._s_max, self.q_min.m_as("VAr"), self.q_max.m_as("VAr")) @property @ureg_wraps("VAr", (None,)) @@ -580,8 +581,8 @@ def q_max(self, value: float | Q_[float] | None) -> None: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_FLEXIBLE_PARAMETER_VALUE) self._q_max = value - if hasattr(self, "cy_fp"): - self.cy_fp.update_parameters(self._s_max, self.q_min.m_as("VAr"), self.q_max.m_as("VAr")) + if self._cy_fp is not None: + self._cy_fp.update_parameters(self._s_max, self.q_min.m_as("VAr"), self.q_max.m_as("VAr")) @classmethod def constant(cls) -> Self: @@ -1036,7 +1037,7 @@ def _compute_powers( # Iterate over the provided voltages to get the associated flexible powers res_flexible_powers = [] for v in voltages: - s = self.cy_fp.compute_power(v, power) + s = self._cy_fp.compute_power(v, power) res_flexible_powers.append(s) return np.array(res_flexible_powers, dtype=complex) diff --git a/roseau/load_flow/models/loads/loads.py b/roseau/load_flow/models/loads/loads.py index c38d897f..f5ff743b 100644 --- a/roseau/load_flow/models/loads/loads.py +++ b/roseau/load_flow/models/loads/loads.py @@ -106,7 +106,7 @@ def voltage_phases(self) -> list[str]: return calculate_voltage_phases(self.phases) def _res_currents_getter(self, warning: bool) -> ComplexArray: - self._res_currents = self.cy_element.get_currents(self.n) + self._res_currents = self._cy_element.get_currents(self.n) return self._res_getter(value=self._res_currents, warning=warning) @property @@ -166,7 +166,7 @@ def _cy_connect(self): if phase in self.phases: j = self.phases.find(phase) connections.append((i, j)) - self.bus.cy_element.connect(self.cy_element, connections) + self.bus._cy_element.connect(self._cy_element, connections) # # Disconnect @@ -279,16 +279,18 @@ def __init__( if self.is_flexible: cy_parameters = [] for p in flexible_params: - cy_parameters.append(p.cy_fp) + cy_parameters.append(p._cy_fp) if self.phases == "abc": - self.cy_element = CyDeltaFlexibleLoad(n=self.n, powers=self._powers, parameters=np.array(cy_parameters)) + self._cy_element = CyDeltaFlexibleLoad( + n=self.n, powers=self._powers, parameters=np.array(cy_parameters) + ) else: - self.cy_element = CyFlexibleLoad(n=self.n, powers=self._powers, parameters=np.array(cy_parameters)) + self._cy_element = CyFlexibleLoad(n=self.n, powers=self._powers, parameters=np.array(cy_parameters)) else: if self.phases == "abc": - self.cy_element = CyDeltaPowerLoad(n=self.n, powers=self._powers) + self._cy_element = CyDeltaPowerLoad(n=self.n, powers=self._powers) else: - self.cy_element = CyPowerLoad(n=self.n, powers=self._powers) + self._cy_element = CyPowerLoad(n=self.n, powers=self._powers) self._cy_connect() @property @@ -339,11 +341,11 @@ def powers(self, value: ComplexArrayLike1D) -> None: raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_S_VALUE) self._powers = value self._invalidate_network_results() - if hasattr(self, "cy_element"): - self.cy_element.update_powers(self._powers) + if self._cy_element is not None: + self._cy_element.update_powers(self._powers) def _res_flexible_powers_getter(self, warning: bool) -> ComplexArray: - self._res_flexible_powers = self.cy_element.get_powers(self.n) + self._res_flexible_powers = self._cy_element.get_powers(self.n) return self._res_getter(value=self._res_flexible_powers, warning=warning) @property @@ -412,9 +414,9 @@ def __init__( super().__init__(id=id, phases=phases, bus=bus, **kwargs) self.currents = currents # handles size checks and unit conversion if self.phases == "abc": - self.cy_element = CyDeltaCurrentLoad(n=self.n, currents=self._currents) + self._cy_element = CyDeltaCurrentLoad(n=self.n, currents=self._currents) else: - self.cy_element = CyCurrentLoad(n=self.n, currents=self._currents) + self._cy_element = CyCurrentLoad(n=self.n, currents=self._currents) self._cy_connect() @property @@ -428,8 +430,8 @@ def currents(self) -> Q_[ComplexArray]: def currents(self, value: ComplexArrayLike1D) -> None: self._currents = self._validate_value(value) self._invalidate_network_results() - if hasattr(self, "cy_element"): - self.cy_element.update_currents(self._currents) + if self._cy_element is not None: + self._cy_element.update_currents(self._currents) def to_dict(self, *, _lf_only: bool = False) -> JsonDict: self._raise_disconnected_error() @@ -471,9 +473,9 @@ def __init__( super().__init__(id=id, phases=phases, bus=bus, **kwargs) self.impedances = impedances if self.phases == "abc": - self.cy_element = CyDeltaAdmittanceLoad(n=self.n, admittances=1.0 / self._impedances) + self._cy_element = CyDeltaAdmittanceLoad(n=self.n, admittances=1.0 / self._impedances) else: - self.cy_element = CyAdmittanceLoad(n=self.n, admittances=1.0 / self._impedances) + self._cy_element = CyAdmittanceLoad(n=self.n, admittances=1.0 / self._impedances) self._cy_connect() @property @@ -487,8 +489,8 @@ def impedances(self) -> Q_[ComplexArray]: def impedances(self, impedances: ComplexArrayLike1D) -> None: self._impedances = self._validate_value(impedances) self._invalidate_network_results() - if hasattr(self, "cy_element"): - self.cy_element.update_admittances(1.0 / self._impedances) + if self._cy_element is not None: + self._cy_element.update_admittances(1.0 / self._impedances) def to_dict(self, *, _lf_only: bool = False) -> JsonDict: self._raise_disconnected_error() diff --git a/roseau/load_flow/models/potential_refs.py b/roseau/load_flow/models/potential_refs.py index ee956448..1391bda2 100644 --- a/roseau/load_flow/models/potential_refs.py +++ b/roseau/load_flow/models/potential_refs.py @@ -63,22 +63,22 @@ def __init__(self, id: Id, element: Bus | Ground, *, phase: str | None = None, * self._res_current: complex | None = None if isinstance(element, Bus) and self.phase is None: n = len(element.phases) - self.cy_element = CyDeltaPotentialRef(n) + self._cy_element = CyDeltaPotentialRef(n) connections = [(i, i) for i in range(n)] - element.cy_element.connect(self.cy_element, connections) + element._cy_element.connect(self._cy_element, connections) else: - self.cy_element = CyPotentialRef() + self._cy_element = CyPotentialRef() if isinstance(element, Ground): - element.cy_element.connect(self.cy_element, [(0, 0)]) + element._cy_element.connect(self._cy_element, [(0, 0)]) else: p = element.phases.find(self.phase) - element.cy_element.connect(self.cy_element, [(p, 0)]) + element._cy_element.connect(self._cy_element, [(p, 0)]) def __repr__(self) -> str: return f"{type(self).__name__}(id={self.id!r}, element={self.element!r}, phase={self.phase!r})" def _res_current_getter(self, warning: bool) -> complex: - self._res_current = self.cy_element.get_current() + self._res_current = self._cy_element.get_current() return self._res_getter(self._res_current, warning) @property diff --git a/roseau/load_flow/models/sources.py b/roseau/load_flow/models/sources.py index bc546aae..96fc0a8b 100644 --- a/roseau/load_flow/models/sources.py +++ b/roseau/load_flow/models/sources.py @@ -76,9 +76,9 @@ def __init__( self.n = len(self.phases) if self.phases == "abc": - self.cy_element = CyDeltaVoltageSource(n=self.n, voltages=self._voltages) + self._cy_element = CyDeltaVoltageSource(n=self.n, voltages=self._voltages) else: - self.cy_element = CyVoltageSource(n=self.n, voltages=self._voltages) + self._cy_element = CyVoltageSource(n=self.n, voltages=self._voltages) self._cy_connect() # Results @@ -106,8 +106,8 @@ def voltages(self, voltages: ComplexArrayLike1D) -> None: raise RoseauLoadFlowException(msg, code=RoseauLoadFlowExceptionCode.BAD_VOLTAGES_SIZE) self._voltages = np.array(voltages, dtype=np.complex128) self._invalidate_network_results() - if hasattr(self, "cy_element"): - self.cy_element.update_voltages(self._voltages) + if self._cy_element is not None: + self._cy_element.update_voltages(self._voltages) @property def voltage_phases(self) -> list[str]: @@ -115,7 +115,7 @@ def voltage_phases(self) -> list[str]: return calculate_voltage_phases(self.phases) def _res_currents_getter(self, warning: bool) -> ComplexArray: - self._res_currents = self.cy_element.get_currents(self.n) + self._res_currents = self._cy_element.get_currents(self.n) return self._res_getter(value=self._res_currents, warning=warning) @property @@ -151,7 +151,7 @@ def _cy_connect(self): if phase in self.phases: j = self.phases.find(phase) connections.append((i, j)) - self.bus.cy_element.connect(self.cy_element, connections) + self.bus._cy_element.connect(self._cy_element, connections) # # Disconnect diff --git a/roseau/load_flow/models/transformers/transformers.py b/roseau/load_flow/models/transformers/transformers.py index 495e6000..303360d7 100644 --- a/roseau/load_flow/models/transformers/transformers.py +++ b/roseau/load_flow/models/transformers/transformers.py @@ -109,32 +109,32 @@ def __init__( z2 = z2.m_as("ohm") ym = ym.m_as("S") if parameters.type == "single": - self.cy_element = CySingleTransformer(z2=z2, ym=ym, k=k * tap) + self._cy_element = CySingleTransformer(z2=z2, ym=ym, k=k * tap) elif parameters.type == "center": - self.cy_element = CyCenterTransformer(z2=z2, ym=ym, k=k * tap) + self._cy_element = CyCenterTransformer(z2=z2, ym=ym, k=k * tap) else: if "Y" in parameters.winding1 and "y" in parameters.winding2: - self.cy_element = CyReducedTransformer( + self._cy_element = CyReducedTransformer( n1=4, n2=4, prim="Y", sec="y", z2=z2, ym=ym, k=k * tap, orientation=orientation ) elif "D" in parameters.winding1 and "y" in parameters.winding2: - self.cy_element = CyReducedTransformer( + self._cy_element = CyReducedTransformer( n1=3, n2=4, prim="D", sec="y", z2=z2, ym=ym, k=k * tap, orientation=orientation ) elif "D" in parameters.winding1 and "d" in parameters.winding2: - self.cy_element = CyReducedTransformer( + self._cy_element = CyReducedTransformer( n1=3, n2=3, prim="D", sec="d", z2=z2, ym=ym, k=k * tap, orientation=orientation ) elif "Y" in parameters.winding1 and "d" in parameters.winding2: - self.cy_element = CyReducedTransformer( + self._cy_element = CyReducedTransformer( n1=4, n2=3, prim="Y", sec="d", z2=z2, ym=ym, k=k * tap, orientation=orientation ) elif "Y" in parameters.winding1 and "z" in parameters.winding2: - self.cy_element = CyExtendedTransformer( + self._cy_element = CyExtendedTransformer( n1=4, n2=4, prim="Y", sec="z", z2=z2, ym=ym, k=k * tap, orientation=orientation ) elif "D" in parameters.winding1 and "z" in parameters.winding2: - self.cy_element = CyExtendedTransformer( + self._cy_element = CyExtendedTransformer( n1=3, n2=4, prim="D", sec="z", z2=z2, ym=ym, k=k * tap, orientation=orientation ) else: @@ -156,9 +156,9 @@ def tap(self, value: float) -> None: logger.warning(f"The provided tap {value:.2f} is lower than 0.9. A good value is between 0.9 and 1.1.") self._tap = value self._invalidate_network_results() - if hasattr(self, "cy_element"): + if self._cy_element is not None: z2, ym, k, _ = self.parameters.to_zyk() - self.cy_element.update_transformer_parameters(z2.m_as("ohm"), ym.m_as("S"), k * value) + self._cy_element.update_transformer_parameters(z2.m_as("ohm"), ym.m_as("S"), k * value) @property def parameters(self) -> TransformerParameters: @@ -175,9 +175,9 @@ def parameters(self, value: TransformerParameters) -> None: raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_TYPE) self._parameters = value self._invalidate_network_results() - if hasattr(self, "cy_element"): + if self._cy_element is not None: z2, ym, k, _ = value.to_zyk() - self.cy_element.update_transformer_parameters(z2.m_as("ohm"), ym.m_as("S"), k * self.tap) + self._cy_element.update_transformer_parameters(z2.m_as("ohm"), ym.m_as("S"), k * self.tap) @property def max_power(self) -> Q_[float] | None: diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 7fe93c5b..070a7e04 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -21,6 +21,7 @@ from rich.table import Table from typing_extensions import Self +from roseau.load_flow._solvers import AbstractSolver from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow.io import network_from_dgs, network_from_dict, network_to_dict from roseau.load_flow.models import ( @@ -36,7 +37,6 @@ Transformer, VoltageSource, ) -from roseau.load_flow.solvers import AbstractSolver from roseau.load_flow.typing import Id, JsonDict, MapOrSeq, Solver, StrPath from roseau.load_flow.utils import CatalogueMixin, JsonMixin, _optional_deps, console, palette from roseau.load_flow.utils.types import _DTYPES, VoltagePhaseDtype @@ -551,7 +551,7 @@ def solve_load_flow( iterations, residual = self._solver.solve_load_flow(max_iterations=max_iterations, tolerance=tolerance) except RuntimeError as e: msg = e.args[0] - zero_elements_index, inf_elements_index = self._solver.cy_solver.analyse_jacobian() + zero_elements_index, inf_elements_index = self._solver._cy_solver.analyse_jacobian() if zero_elements_index: zero_elements = [self._elements[i] for i in zero_elements_index] printable_elements = ", ".join(f"{type(e).__name__}({e.id!r})" for e in zero_elements) @@ -1280,25 +1280,25 @@ def _create_network(self) -> None: cy_elements = [] self._elements = [] for bus in self.buses.values(): - cy_elements.append(bus.cy_element) + cy_elements.append(bus._cy_element) self._elements.append(bus) for line in self.branches.values(): - cy_elements.append(line.cy_element) + cy_elements.append(line._cy_element) self._elements.append(line) for load in self.loads.values(): - cy_elements.append(load.cy_element) + cy_elements.append(load._cy_element) self._elements.append(load) for ground in self.grounds.values(): - cy_elements.append(ground.cy_element) + cy_elements.append(ground._cy_element) self._elements.append(ground) for p_ref in self.potential_refs.values(): - cy_elements.append(p_ref.cy_element) + cy_elements.append(p_ref._cy_element) self._elements.append(p_ref) for source in self.sources.values(): - cy_elements.append(source.cy_element) + cy_elements.append(source._cy_element) self._elements.append(source) self._propagate_potentials(force=False) - self.cy_electrical_network = CyElectricalNetwork(elements=np.array(cy_elements), nb_elements=len(cy_elements)) + self._cy_electrical_network = CyElectricalNetwork(elements=np.array(cy_elements), nb_elements=len(cy_elements)) def _check_validity(self, constructed: bool) -> None: """Check the validity of the network to avoid having a singular jacobian matrix. It also assigns the `self` diff --git a/roseau/load_flow/tests/test_solvers.py b/roseau/load_flow/tests/test_solvers.py index e405ad3b..3080e562 100644 --- a/roseau/load_flow/tests/test_solvers.py +++ b/roseau/load_flow/tests/test_solvers.py @@ -8,7 +8,7 @@ RoseauLoadFlowExceptionCode, VoltageSource, ) -from roseau.load_flow.solvers import AbstractSolver, Newton, NewtonGoldstein +from roseau.load_flow._solvers import AbstractSolver, Newton, NewtonGoldstein def test_solver(): From 71110299deddc316d24411391130a40d29eb198b Mon Sep 17 00:00:00 2001 From: Saelyos Date: Wed, 13 Dec 2023 15:47:44 +0100 Subject: [PATCH 07/51] Fix test --- .../tests/test_electrical_network.py | 135 +----------------- 1 file changed, 1 insertion(+), 134 deletions(-) diff --git a/roseau/load_flow/tests/test_electrical_network.py b/roseau/load_flow/tests/test_electrical_network.py index 0d8ef238..cc04c130 100644 --- a/roseau/load_flow/tests/test_electrical_network.py +++ b/roseau/load_flow/tests/test_electrical_network.py @@ -515,139 +515,6 @@ def test_bad_networks(): assert e.value.code == RoseauLoadFlowExceptionCode.BAD_BUS_ID -def test_solve_load_flow(small_network, good_json_results): - load: PowerLoad = small_network.loads["load"] - load_bus = small_network.buses["bus1"] - - # Good result - # Request the server - solve_url = urljoin(ElectricalNetwork._DEFAULT_BASE_URL, "solve/") - with requests_mock.Mocker() as m: - m.post(solve_url, status_code=200, json=good_json_results, headers={"content-type": "application/json"}) - small_network.solve_load_flow(auth=("", "")) - assert len(load_bus.res_potentials) == 4 - assert small_network.results_to_dict() == good_json_results - - # No convergence - load.powers = [10000000, 100, 100] - json_result = { - "info": { - "status": "failure", - "solver": "newton", - "iterations": 50, - "wam_start": False, - "tolerance": 1e-06, - "residual": 14037.977318668112, - "max_iterations": 20, - }, - "buses": [ - { - "id": "bus0", - "phases": "abcn", - "potentials": [ - [20000.0, 0.0], - [-10000.0, -17320.508076], - [-10000.0, 17320.508076], - [0.0, 0.0], - ], - }, - { - "id": "bus1", - "phases": "abcn", - "potentials": [ - [110753.81558442864, 1.5688245436058308e-26], - [-9999.985548801811, -17320.50568183019], - [-9999.985548801811, 17320.50568183019], - [-90753.844486825, -2.6687106473172017e-26], - ], - }, - ], - "branches": [ - { - "id": "line", - "phases1": "abcn", - "phases2": "abcn", - "currents1": [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], - "currents2": [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], - } - ], - "loads": [ - { - "id": "load", - "phases": "abcn", - "currents": [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], - }, - ], - "sources": [ - { - "id": "vs", - "phases": "abcn", - "currents": [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], - }, - ], - "grounds": [ - { - "id": "ground", - "potential": [1.3476526914363477e-12, 0.0], - } - ], - "potential_refs": [ - { - "id": "pref", - "current": [0.0, 0.0], - }, - ], - } - with requests_mock.Mocker() as m: - m.post(solve_url, status_code=200, json=json_result, headers={"content-type": "application/json"}) - with pytest.raises(RoseauLoadFlowException) as e: - small_network.solve_load_flow(auth=("", "")) - assert "The load flow did not converge after 50 iterations" in e.value.msg - assert e.value.code == RoseauLoadFlowExceptionCode.NO_LOAD_FLOW_CONVERGENCE - - -def test_solve_load_flow_error(small_network): - # Solve url - solve_url = urljoin(ElectricalNetwork._DEFAULT_BASE_URL, "solve/") - - # Parse RLF error - json_result = {"msg": "toto", "code": "roseau.load_flow.bad_branch_type"} - with requests_mock.Mocker() as m: - m.post(solve_url, status_code=400, json=json_result, headers={"content-type": "application/json"}) - with pytest.raises(RoseauLoadFlowException) as e: - small_network.solve_load_flow(auth=("", "")) - assert e.value.msg == json_result["msg"] - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_BRANCH_TYPE - - # Load flow error (other than official exceptions of RoseauLoadFlowException) - json_result = {"msg": "Error while solving the load flow", "code": "load_flow_error"} - with requests_mock.Mocker() as m: - m.post(solve_url, status_code=400, json=json_result, headers={"content-type": "application/json"}) - with pytest.raises(RoseauLoadFlowException) as e: - small_network.solve_load_flow(auth=("", "")) - assert json_result["msg"] in e.value.msg - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_REQUEST - - # Authentication fail - json_result = {"detail": "not_authenticated"} - with requests_mock.Mocker() as m: - m.post(solve_url, status_code=401, json=json_result, headers={"content-type": "application/json"}) - with pytest.raises(RoseauLoadFlowException) as e: - small_network.solve_load_flow(auth=("", "")) - assert "Authentication failed." in e.value.msg - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_REQUEST - - # Bad request - json_result = {"msg": "Error while parsing the provided JSON", "code": "parse_error"} - with requests_mock.Mocker() as m: - m.post(solve_url, status_code=400, json=json_result, headers={"content-type": "application/json"}) - with pytest.raises(RoseauLoadFlowException) as e: - small_network.solve_load_flow(auth=("", "")) - assert "There is a problem in the request" in e.value.msg - assert "Error while parsing the provided JSON" in e.value.msg - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_REQUEST - - def test_frame(small_network: ElectricalNetwork): # Buses buses_gdf = small_network.buses_frame @@ -951,7 +818,7 @@ def test_single_phase_network(single_phase_network: ElectricalNetwork): solve_url = urljoin(ElectricalNetwork._DEFAULT_BASE_URL, "solve/") with requests_mock.Mocker() as m: m.post(solve_url, status_code=200, json=json_results, headers={"content-type": "application/json"}) - single_phase_network.solve_load_flow(auth=("", "")) + single_phase_network.solve_load_flow() # Test results of elements # ------------------------ From d3ee8b4f84cae11f60779a72868e81b689e25f8b Mon Sep 17 00:00:00 2001 From: Saelyos Date: Thu, 21 Dec 2023 12:18:19 +0100 Subject: [PATCH 08/51] Fix merging --- roseau/load_flow/__init__.py | 6 ++ roseau/load_flow/_solvers.py | 2 +- roseau/load_flow/license.py | 96 +++++++++++++++++++ roseau/load_flow/models/buses.py | 2 +- roseau/load_flow/models/core.py | 2 +- roseau/load_flow/models/grounds.py | 2 +- roseau/load_flow/models/lines/lines.py | 2 +- .../models/loads/flexible_parameters.py | 2 +- roseau/load_flow/models/loads/loads.py | 2 +- roseau/load_flow/models/potential_refs.py | 2 +- roseau/load_flow/models/sources.py | 2 +- .../models/transformers/transformers.py | 2 +- roseau/load_flow/network.py | 2 +- roseau/load_flow/tests/test_solvers.py | 14 ++- roseau/load_flow/utils/log.py | 85 ++++++++++++++++ 15 files changed, 208 insertions(+), 15 deletions(-) create mode 100644 roseau/load_flow/license.py create mode 100644 roseau/load_flow/utils/log.py diff --git a/roseau/load_flow/__init__.py b/roseau/load_flow/__init__.py index c00522f4..0aeb006e 100644 --- a/roseau/load_flow/__init__.py +++ b/roseau/load_flow/__init__.py @@ -18,6 +18,7 @@ __url__, ) from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode +from roseau.load_flow.license import License, activate_license, deactivate_license, get_license from roseau.load_flow.models import ( AbstractBranch, AbstractLoad, @@ -91,4 +92,9 @@ "LineType", "ConductorType", "InsulatorType", + # License + "activate_license", + "deactivate_license", + "get_license", + "License", ] diff --git a/roseau/load_flow/_solvers.py b/roseau/load_flow/_solvers.py index 429a0d04..a6baa900 100644 --- a/roseau/load_flow/_solvers.py +++ b/roseau/load_flow/_solvers.py @@ -6,7 +6,7 @@ from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow.typing import JsonDict, Solver -from roseau.load_flow_engine.network.cy_network import CyAbstractSolver, CyNewton, CyNewtonGoldstein +from roseau.load_flow_engine.cy_engine import CyAbstractSolver, CyNewton, CyNewtonGoldstein logger = logging.getLogger(__name__) diff --git a/roseau/load_flow/license.py b/roseau/load_flow/license.py new file mode 100644 index 00000000..1f0706a7 --- /dev/null +++ b/roseau/load_flow/license.py @@ -0,0 +1,96 @@ +import datetime as dt + +import certifi +from platformdirs import user_cache_dir + +from roseau.load_flow_engine.cy_engine import ( + CyLicense, + cy_activate_license, + cy_deactivate_license, + cy_get_license, +) + +__all__ = ["activate_license", "deactivate_license", "get_license", "License"] + + +# +# License class accessor +# +class License: + """A class to access the main data of the License.""" + + def __init__(self, cy_license: CyLicense): + """Constructor for a License + + Args: + cy_license: + The Cython license object + """ + self.cy_license = cy_license + + @property + def key(self) -> str: + """The key of the license""" + return self.cy_license.key + + @property + def expiry_datetime(self) -> dt.datetime | None: + """The expiry date of the license.""" + exp_dt = self.cy_license.expiry_datetime + if exp_dt is None: + return None + try: + return dt.datetime.fromisoformat(exp_dt) + except ValueError: + return None + + @property + def valid(self) -> bool: + """Is the license valid?""" + return self.cy_license.valid + + def validate(self) -> None: + """Validate the license.""" + return self.cy_license.validate() + + # + # The following methods are used to identify your PC + # + @staticmethod + def get_machine_fingerprint() -> str: + """This method retrieves your machine fingerprint.""" + return CyLicense.get_machine_fingerprint() + + @staticmethod + def get_hostname() -> str: + """This method retrieves the hostname of your computer.""" + return CyLicense.get_hostname() + + @staticmethod + def get_username() -> str: + """This method retrieves your username.""" + return CyLicense.get_username() + + +def activate_license(key: str) -> None: + """Activate the license from the given key. + + Args: + key: + The key of the license to activate. + """ + cy_activate_license(key=key, cacert_filepath=certifi.where(), cache_folderpath=user_cache_dir()) + + +def deactivate_license() -> None: + """Deactivate the license of the current process.""" + cy_deactivate_license() + + +def get_license() -> License | None: + """A function to retrieve the currently active license.""" + cy_license = cy_get_license() + if cy_license is None: + return None + else: + return License(cy_license=cy_license) diff --git a/roseau/load_flow/models/buses.py b/roseau/load_flow/models/buses.py index 4bc126f1..a4235118 100644 --- a/roseau/load_flow/models/buses.py +++ b/roseau/load_flow/models/buses.py @@ -12,7 +12,7 @@ from roseau.load_flow.models.core import Element from roseau.load_flow.typing import ComplexArray, ComplexArrayLike1D, Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps -from roseau.load_flow_engine.models.buses.cy_buses import CyBus +from roseau.load_flow_engine.cy_engine import CyBus logger = logging.getLogger(__name__) diff --git a/roseau/load_flow/models/core.py b/roseau/load_flow/models/core.py index bfe52a6c..586d9a15 100644 --- a/roseau/load_flow/models/core.py +++ b/roseau/load_flow/models/core.py @@ -10,7 +10,7 @@ from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow.typing import Id from roseau.load_flow.utils import Identifiable, JsonMixin -from roseau.load_flow_engine.models.core.cy_core import CyElement +from roseau.load_flow_engine.cy_engine import CyElement if TYPE_CHECKING: from roseau.load_flow.network import ElectricalNetwork diff --git a/roseau/load_flow/models/grounds.py b/roseau/load_flow/models/grounds.py index a2520e43..f3bba98f 100644 --- a/roseau/load_flow/models/grounds.py +++ b/roseau/load_flow/models/grounds.py @@ -7,7 +7,7 @@ from roseau.load_flow.models.core import Element from roseau.load_flow.typing import Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps -from roseau.load_flow_engine.models.grounds.cy_grounds import CyGround +from roseau.load_flow_engine.cy_engine import CyGround if TYPE_CHECKING: from roseau.load_flow.models.buses import Bus diff --git a/roseau/load_flow/models/lines/lines.py b/roseau/load_flow/models/lines/lines.py index 508d9f20..01dbf1c8 100644 --- a/roseau/load_flow/models/lines/lines.py +++ b/roseau/load_flow/models/lines/lines.py @@ -14,7 +14,7 @@ from roseau.load_flow.models.sources import VoltageSource from roseau.load_flow.typing import ComplexArray, Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps -from roseau.load_flow_engine.models.lines.cy_lines import CyShuntLine, CySimplifiedLine, CySwitch +from roseau.load_flow_engine.cy_engine import CyShuntLine, CySimplifiedLine, CySwitch logger = logging.getLogger(__name__) diff --git a/roseau/load_flow/models/loads/flexible_parameters.py b/roseau/load_flow/models/loads/flexible_parameters.py index 2857083a..dece49bb 100644 --- a/roseau/load_flow/models/loads/flexible_parameters.py +++ b/roseau/load_flow/models/loads/flexible_parameters.py @@ -17,7 +17,7 @@ ) from roseau.load_flow.units import Q_, ureg_wraps from roseau.load_flow.utils import JsonMixin, _optional_deps -from roseau.load_flow_engine.models.loads.cy_loads import CyControl, CyFlexibleParameter, CyProjection +from roseau.load_flow_engine.cy_engine import CyControl, CyFlexibleParameter, CyProjection logger = logging.getLogger(__name__) diff --git a/roseau/load_flow/models/loads/loads.py b/roseau/load_flow/models/loads/loads.py index f5ff743b..ce3ec9db 100644 --- a/roseau/load_flow/models/loads/loads.py +++ b/roseau/load_flow/models/loads/loads.py @@ -11,7 +11,7 @@ from roseau.load_flow.models.loads.flexible_parameters import FlexibleParameter from roseau.load_flow.typing import ComplexArray, ComplexArrayLike1D, Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps -from roseau.load_flow_engine.models.loads.cy_loads import ( +from roseau.load_flow_engine.cy_engine import ( CyAdmittanceLoad, CyCurrentLoad, CyDeltaAdmittanceLoad, diff --git a/roseau/load_flow/models/potential_refs.py b/roseau/load_flow/models/potential_refs.py index 1391bda2..8e0a90b9 100644 --- a/roseau/load_flow/models/potential_refs.py +++ b/roseau/load_flow/models/potential_refs.py @@ -9,7 +9,7 @@ from roseau.load_flow.models.grounds import Ground from roseau.load_flow.typing import Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps -from roseau.load_flow_engine.models.potential_refs.cy_potential_refs import CyDeltaPotentialRef, CyPotentialRef +from roseau.load_flow_engine.cy_engine import CyDeltaPotentialRef, CyPotentialRef logger = logging.getLogger(__name__) diff --git a/roseau/load_flow/models/sources.py b/roseau/load_flow/models/sources.py index 96fc0a8b..a621c2c0 100644 --- a/roseau/load_flow/models/sources.py +++ b/roseau/load_flow/models/sources.py @@ -10,7 +10,7 @@ from roseau.load_flow.models.core import Element from roseau.load_flow.typing import ComplexArray, ComplexArrayLike1D, Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps -from roseau.load_flow_engine.models.sources.cy_sources import CyDeltaVoltageSource, CyVoltageSource +from roseau.load_flow_engine.cy_engine import CyDeltaVoltageSource, CyVoltageSource logger = logging.getLogger(__name__) diff --git a/roseau/load_flow/models/transformers/transformers.py b/roseau/load_flow/models/transformers/transformers.py index 303360d7..dca30bad 100644 --- a/roseau/load_flow/models/transformers/transformers.py +++ b/roseau/load_flow/models/transformers/transformers.py @@ -9,7 +9,7 @@ from roseau.load_flow.models.transformers.parameters import TransformerParameters from roseau.load_flow.typing import Id, JsonDict from roseau.load_flow.units import Q_ -from roseau.load_flow_engine.models.transformers.cy_transformers import ( +from roseau.load_flow_engine.cy_engine import ( CyCenterTransformer, CyExtendedTransformer, CyReducedTransformer, diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 070a7e04..25633734 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -40,7 +40,7 @@ from roseau.load_flow.typing import Id, JsonDict, MapOrSeq, Solver, StrPath from roseau.load_flow.utils import CatalogueMixin, JsonMixin, _optional_deps, console, palette from roseau.load_flow.utils.types import _DTYPES, VoltagePhaseDtype -from roseau.load_flow_engine.network.cy_network import CyElectricalNetwork +from roseau.load_flow_engine.cy_engine import CyElectricalNetwork if TYPE_CHECKING: from networkx import Graph diff --git a/roseau/load_flow/tests/test_solvers.py b/roseau/load_flow/tests/test_solvers.py index 3080e562..a9eadff4 100644 --- a/roseau/load_flow/tests/test_solvers.py +++ b/roseau/load_flow/tests/test_solvers.py @@ -1,3 +1,5 @@ +import contextlib + import pytest from roseau.load_flow import ( @@ -53,18 +55,22 @@ def test_network_solver(): PotentialRef(id="pref", element=bus) en = ElectricalNetwork.from_element(bus) - en.solve_load_flow() + with contextlib.suppress(RoseauLoadFlowException): # No valid license + en.solve_load_flow() solver = en._solver assert isinstance(solver, NewtonGoldstein) - en.solve_load_flow(solver="newton_goldstein", solver_params={"m1": 0.2}) + with contextlib.suppress(RoseauLoadFlowException): # No valid license + en.solve_load_flow(solver="newton_goldstein", solver_params={"m1": 0.2}) assert solver == en._solver # Solver did not change assert solver.m1 == 0.2 assert solver.m2 == NewtonGoldstein.DEFAULT_M2 - en.solve_load_flow(solver="newton") + with contextlib.suppress(RoseauLoadFlowException): # No valid license + en.solve_load_flow(solver="newton") assert solver != en._solver assert isinstance(en._solver, Newton) - en.solve_load_flow() # Reset to default + with contextlib.suppress(RoseauLoadFlowException): # No valid license + en.solve_load_flow() # Reset to default assert isinstance(en._solver, NewtonGoldstein) diff --git a/roseau/load_flow/utils/log.py b/roseau/load_flow/utils/log.py new file mode 100644 index 00000000..6f0c70c7 --- /dev/null +++ b/roseau/load_flow/utils/log.py @@ -0,0 +1,85 @@ +import logging +import sys + +from rich.console import Console +from rich.logging import RichHandler +from rich.traceback import install + +from roseau.load_flow_engine.cy_engine import cy_set_logging_config + +# Human logging levels +log_levels = { + "trace": logging.DEBUG, # No deeper log value for Python + "debug": logging.DEBUG, + "info": logging.INFO, + "warning": logging.WARNING, + "error": logging.ERROR, + "critical": logging.CRITICAL, +} + +# Rich console +console = Console() + +palette = [ + "#4c72b0", + "#dd8452", + "#55a868", + "#c44e52", + "#8172b3", + "#937860", + "#da8bc3", + "#8c8c8c", + "#ccb974", + "#64b5cd", +] +"""Color palette for the catalogue tables. + +This is seaborn's default color palette. Generated with: +```python +import seaborn as sns +sns.set_theme() +list(sns.color_palette().as_hex()) +``` +""" + + +def set_logging_config(verbosity: str): + """A function to define the configuration of the logging module + + Args: + verbosity: + A valid verbosity level as defined in `log_levels` + """ + level = log_levels[verbosity] + rich_handler_kwargs = { + "show_time": True, + "show_level": True, + "rich_tracebacks": True, + "tracebacks_show_locals": True, + "locals_max_string": None, + } + if verbosity in ("debug", "trace"): + rich_handler_kwargs["show_path"] = True + log_time_format = "%x %X" + else: + rich_handler_kwargs["show_path"] = False + log_time_format = "%x %X" + + # Rich traceback color formatter + error_console = Console(file=sys.stderr, log_time_format=log_time_format) + install(console=error_console, width=None) + + # A first handler on the main console to have output synchronized with progress bar (which are also printed on the + # main console) + handlers = [RichHandler(level=level, console=console, **rich_handler_kwargs)] + + # Define the basic config + logging.basicConfig( + level=log_levels[verbosity], handlers=handlers, datefmt=log_time_format, format="{message}", style="{" + ) + + # Capture the warnings + logging.captureWarnings(True) + + # Define the logger at C++ level + cy_set_logging_config(verbosity) From 118e95bc29fd6d3d74ad1df5d3eb34f0aa66fdde Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 21:17:16 +0100 Subject: [PATCH 09/51] Bump actions/upload-pages-artifact from 2 to 3 (#153) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 2 to 3.
Release notes

Sourced from actions/upload-pages-artifact's releases.

v3.0.0

Changelog

To deploy a GitHub Pages site which has been uploaded with this version of actions/upload-pages-artifact, you must also use actions/deploy-pages@v4 or newer.

See details of all code changes since previous release.

Commits
  • 0252fc4 Merge pull request #81 from actions/artifacts-next
  • 2a5c144 Use actions/download-artifact@v4 in test
  • 7e3f6bb Merge pull request #80 from robherley/patch-1
  • 257e666 Use v4 upload-artifact tag
  • 0313a19 Merge pull request #78 from konradpabjan/main
  • 1228e65 Update action.yml
  • eb31309 Update artifact names in tests
  • 241a975 Correct artifact name during download
  • ef95519 Unique artifact name per job
  • ecdd3ed Switch to using download@v4-beta
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-pages-artifact&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/doc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 65d867c6..adfbf1ab 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -77,7 +77,7 @@ jobs: - name: Upload pages artifact if: ${{ github.ref == 'refs/heads/main' || inputs.forceDeploy == true }} - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 with: path: "build/html/" From fe127fb93fd42aec06fc94ae23c41fdf7e75ae9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 21:17:27 +0100 Subject: [PATCH 10/51] Bump actions/deploy-pages from 2 to 4 (#154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 2 to 4.
Release notes

Sourced from actions/deploy-pages's releases.

v4.0.0

Changelog

  • Deploy pages using artifact IDs @​konradpabjan (#251)
  • This version requires the permission actions: read in the workflows which use it.

ℹ️ This version of actions/deploy-pages is ONLY compatible with artifacts uploaded by either:

See details of all code changes since previous release.

:warning: For use with products other than GitHub.com, such as GitHub Enterprise Server, please consult the compatibility table.

v3.0.1

Changelog

🧰 Maintenance


See details of all code changes since previous release.

:warning: For use with products other than GitHub.com, such as GitHub Enterprise Server, please consult the compatibility table.

v3.0.0

Changelog


See details of all code changes since previous release.

:warning: For use with products other than GitHub.com, such as GitHub Enterprise Server, please consult the compatibility table.

v2.0.5

Changelog

... (truncated)

Commits
  • 7a9bd94 Merge pull request #290 from actions/dependabot/npm_and_yarn/undici-6.2.1
  • eee8a27 Update distributables after Dependabot 🤖
  • b6e5c85 Bump undici from 6.0.1 to 6.2.1
  • b8d2528 Merge pull request #282 from actions/dependabot/github_actions/github/codeql-...
  • 53d1eac Bump github/codeql-action from 2 to 3
  • 3f0ef9d Merge pull request #281 from actions/dependabot/github_actions/actions/upload...
  • 8275104 Bump actions/upload-artifact from 3 to 4
  • 9be9d73 Merge pull request #280 from actions/dependabot/npm_and_yarn/eslint-8.56.0
  • d8afefa Bump eslint from 8.55.0 to 8.56.0
  • 304d0b7 Merge pull request #277 from actions/dependabot/github_actions/actions/publis...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/deploy-pages&package-manager=github_actions&previous-version=2&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/doc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index adfbf1ab..fbc469f9 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -98,4 +98,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 From 01fb5d6fbecc4ebc21f56ced0ec3daba5116eb5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 21:17:36 +0100 Subject: [PATCH 11/51] Bump actions/setup-python from 4 to 5 (#155) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
Release notes

Sourced from actions/setup-python's releases.

v5.0.0

What's Changed

In scope of this release, we update node version runtime from node16 to node20 (actions/setup-python#772). Besides, we update dependencies to the latest versions.

Full Changelog: https://github.com/actions/setup-python/compare/v4.8.0...v5.0.0

v4.8.0

What's Changed

In scope of this release we added support for GraalPy (actions/setup-python#694). You can use this snippet to set up GraalPy:

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
  with:
    python-version: 'graalpy-22.3'
- run: python my_script.py

Besides, the release contains such changes as:

New Contributors

Full Changelog: https://github.com/actions/setup-python/compare/v4...v4.8.0

v4.7.1

What's Changed

Full Changelog: https://github.com/actions/setup-python/compare/v4...v4.7.1

v4.7.0

In scope of this release, the support for reading python version from pyproject.toml was added (actions/setup-python#669).

      - name: Setup Python
        uses: actions/setup-python@v4
</tr></table>

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/doc.yml | 2 +- .github/workflows/pre-commit.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 962c0c4c..52685274 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: run: pipx install poetry - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: "poetry" diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index fbc469f9..ce6e31df 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -58,7 +58,7 @@ jobs: run: pipx install poetry - name: Set up Python 3.12 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.12" cache: "poetry" diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 706a29fc..65814084 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v4 with: lfs: false - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.12" - uses: pre-commit/action@v3.0.0 From 482119a59a5c8b0ea7cf454ac8fbef07a0d4fc0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 21:17:46 +0100 Subject: [PATCH 12/51] Bump actions/configure-pages from 3 to 4 (#156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 3 to 4.
Release notes

Sourced from actions/configure-pages's releases.

v4.0.0

Changelog

See details of all code changes since previous release.

v3.0.7

Changelog

See details of all code changes since previous release.

v3.0.6

Changelog

See details of all code changes since previous release.

v3.0.5

Changelog

See details of all code changes since previous release.

v3.0.4

Changelog

... (truncated)

Commits
  • 1f0c5cd Merge pull request #117 from actions/use-node-version-file
  • 591bb0d Merge branch 'main' into use-node-version-file
  • 1465f01 Merge pull request #108 from takost/update-to-node-20
  • f2fc553 Merge branch 'main' into update-to-node-20
  • 373694e Use a centralized .node-version file
  • 3a01413 Update action to node20
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/configure-pages&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/doc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index ce6e31df..76c76479 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -52,7 +52,7 @@ jobs: git lfs prune --verify-remote - name: Setup Pages - uses: actions/configure-pages@v3 + uses: actions/configure-pages@v4 - name: Install poetry run: pipx install poetry From 9665328eee3d14e22e127889853c5c49a85c9993 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 21:17:58 +0100 Subject: [PATCH 13/51] Bump actions/upload-artifact from 3 to 4 (#157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
Release notes

Sourced from actions/upload-artifact's releases.

v4.0.0

What's Changed

The release of upload-artifact@v4 and download-artifact@v4 are major changes to the backend architecture of Artifacts. They have numerous performance and behavioral improvements.

For more information, see the @​actions/artifact documentation.

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v3...v4.0.0

v3.1.3

What's Changed

Full Changelog: https://github.com/actions/upload-artifact/compare/v3...v3.1.3

v3.1.2

  • Update all @actions/* NPM packages to their latest versions- #374
  • Update all dev dependencies to their most recent versions - #375

v3.1.1

  • Update actions/core package to latest version to remove set-output deprecation warning #351

v3.1.0

What's Changed

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/conda.yml | 2 +- .github/workflows/doc.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52685274..adda5477 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: --cov-config pyproject.toml --cov-fail-under 75 roseau - name: Archive code coverage results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ always() }} with: name: code-coverage-report-${{ runner.os }}-python-${{ matrix.python-version }} diff --git a/.github/workflows/conda.yml b/.github/workflows/conda.yml index cd0bd682..30a5d483 100644 --- a/.github/workflows/conda.yml +++ b/.github/workflows/conda.yml @@ -62,7 +62,7 @@ jobs: echo "CONDA_ARCHIVE=$(mamba build --output-folder dist/ --output conda/)" >> $GITHUB_OUTPUT - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: package-python-${{ matrix.python-version }} path: ${{ steps.conda-build.outputs.CONDA_ARCHIVE }} diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 76c76479..4544f11e 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -82,7 +82,7 @@ jobs: path: "build/html/" - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ !(github.ref == 'refs/heads/main' || inputs.forceDeploy == true) }} with: path: "build/html/" From a80974b2da4b9048d979d413bfc1bbfb3b25436c Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Wed, 10 Jan 2024 13:35:08 +0100 Subject: [PATCH 14/51] Update the tests, the doc and the dependencies --- .pre-commit-config.yaml | 8 +- README.md | 33 +- conda/environment.yml | 3 +- conda/meta.yaml | 3 +- doc/License.md | 38 + doc/conf.py | 3 +- doc/index.md | 21 + doc/models/Bus.md | 3 +- doc/models/Ground.md | 3 +- doc/models/Line/ShuntLine.md | 3 +- doc/models/Line/SimplifiedLine.md | 3 +- doc/models/Load/CurrentLoad.md | 5 +- .../Load/FlexibleLoad/FeasibleDomain.md | 96 +-- doc/models/Load/ImpedanceLoad.md | 5 +- doc/models/Load/PowerLoad.md | 5 +- doc/models/Switch.md | 3 +- .../Transformer/Three_Phase_Transformer.md | 3 +- doc/usage/Connecting_Elements.md | 5 +- doc/usage/Flexible_Loads.md | 7 +- doc/usage/Getting_Started.md | 14 +- doc/usage/Short_Circuit.md | 13 +- poetry.lock | 800 +++++++++--------- pyproject.toml | 14 +- roseau/load_flow/__about__.py | 2 +- roseau/load_flow/conftest.py | 5 + roseau/load_flow/exceptions.py | 1 + roseau/load_flow/io/dgs.py | 73 +- roseau/load_flow/io/dict.py | 31 +- roseau/load_flow/license.py | 15 +- roseau/load_flow/models/branches.py | 32 +- roseau/load_flow/models/buses.py | 23 +- roseau/load_flow/models/core.py | 2 + roseau/load_flow/models/grounds.py | 4 +- roseau/load_flow/models/lines/lines.py | 36 +- .../models/loads/flexible_parameters.py | 139 +-- roseau/load_flow/models/loads/loads.py | 60 +- roseau/load_flow/models/potential_refs.py | 11 +- roseau/load_flow/models/sources.py | 26 +- roseau/load_flow/models/tests/test_buses.py | 15 +- .../models/tests/test_flexible_parameters.py | 81 +- roseau/load_flow/models/tests/test_phases.py | 142 ++-- roseau/load_flow/network.py | 112 ++- .../tests/test_electrical_network.py | 267 ++---- roseau/load_flow/typing.py | 7 - roseau/load_flow/utils/_versions.py | 2 +- 45 files changed, 974 insertions(+), 1203 deletions(-) create mode 100644 doc/License.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 684b603c..d3c57805 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: hooks: - id: poetry-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.6 # keep in sync with pyproject.toml + rev: v0.1.11 # keep in sync with pyproject.toml hooks: - id: ruff types_or: [python, pyi, jupyter] @@ -27,11 +27,11 @@ repos: rev: 1.16.0 hooks: - id: blacken-docs - entry: bash -c "blacken-docs -l 90 $(find doc/ -name '*.md')" + files: ^doc/.*\.md$ args: [-l 90] - additional_dependencies: [black==23.11.0] + additional_dependencies: [black==23.12.1] - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.1.0 + rev: v4.0.0-alpha.8 hooks: - id: prettier args: ["--print-width", "120"] diff --git a/README.md b/README.md index b8a9b591..407c111a 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,29 @@ [![Documentation](https://github.com/RoseauTechnologies/Roseau_Load_Flow/actions/workflows/doc.yml/badge.svg)](https://github.com/RoseauTechnologies/Roseau_Load_Flow/actions/workflows/doc.yml) [![pre-commit](https://github.com/RoseauTechnologies/Roseau_Load_Flow/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/RoseauTechnologies/Roseau_Load_Flow/actions/workflows/pre-commit.yml) -_Roseau Load Flow_ is a highly capable three-phase load flow solver. This project is compatible with Python 3.10 and -above. +_Roseau Load Flow_ is a highly capable three-phase load flow solver with an ergonomic Python API +for unbalanced power flow analysis. -Please take a look at our documentation to see how to install and use `roseau-load-flow`. +This project is compatible with Python version 3.10 and newer. The +[installation instructions](https://roseautechnologies.github.io/Roseau_Load_Flow/Installation.html) +will guide you through the installation process. If you are new to _Roseau Load Flow_, we recommend you start with the +[getting started tutorial](https://roseautechnologies.github.io/Roseau_Load_Flow/usage/Getting_Started.html). +You can find the complete documentation at https://roseautechnologies.github.io/Roseau_Load_Flow. -- [Installation](https://roseautechnologies.github.io/Roseau_Load_Flow/Installation.html) -- [Usage](https://roseautechnologies.github.io/Roseau_Load_Flow/usage/index.html) +> [!IMPORTANT] +> Starting with version 0.7.0, Roseau Load Flow will no longer be supplied as a SaaS. The software will +> be available as a standalone Python library. -# Accessing the solver +## License -This is the client library for the -[_Roseau Load Flow_](https://www.roseautechnologies.com/en/roseau-load-flow-en/) solver. To use the solver, you -need to sign up for an account. For inquiry, please contact us at contact@roseautechnologies.com. +The project is _partially_ open source but to use the solver you will need a license. Please contact +us at contact@roseautechnologies.com to obtain a license. -If you are a **student or a teacher, free API credentials are provided**. Please contact us at -contact@roseautechnologies.com. +> [!NOTE] +> Licenses are given free of charge for **students and teachers**. Please contact us at +> contact@roseautechnologies.com for more information. -# Network data +## Network data With this library, there is a sample of 20 low-voltage and 20 medium-voltage feeders included for an easy start! Each network is given with its summer and winter load point. At _Roseau Technologies_, we are able to provide @@ -30,12 +35,12 @@ contact@roseautechnologies.com. ![Catalogue of networks](https://github.com/RoseauTechnologies/Roseau_Load_Flow/blob/main/doc/_static/Network/Catalogue.png?raw=True) -# Bug reports / Feature requests +## Bug reports / Feature requests If you find a bug or have a feature request, please open an issue on [GitHub](https://github.com/RoseauTechnologies/Roseau_Load_Flow/issues) -# Credits +## Credits This software is developed by [Roseau Technologies](https://www.roseautechnologies.com/en). [![Linkedin](https://i.stack.imgur.com/gVE0j.png) LinkedIn](https://www.linkedin.com/company/roseau-technologies/) diff --git a/conda/environment.yml b/conda/environment.yml index 10c01870..efa95a45 100644 --- a/conda/environment.yml +++ b/conda/environment.yml @@ -10,9 +10,10 @@ dependencies: - shapely >=2.0.0 - regex >=2022.1.18 - pint >=0.21.0 - - requests >=2.28.1 - typing_extensions >=4.6.2 - rich >=13.5.1 - pyproj >=3.3.0 - matplotlib-base >=3.7.2 - networkx >=3.0.0 + - certifi >=2023.5.7 + - platformdirs >=4.0.0 diff --git a/conda/meta.yaml b/conda/meta.yaml index a53a8f7a..9bebd633 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -32,12 +32,13 @@ requirements: - shapely >=2.0.0 - regex >=2022.1.18 - pint >=0.21.0 - - requests >=2.28.1 - typing_extensions >=4.6.2 - rich >=13.5.1 - pyproj >=3.3.0 - matplotlib-base >=3.7.2 - networkx >=3.0.0 + - certifi >=2023.5.7 + - platformdirs >=4.0.0 test: imports: diff --git a/doc/License.md b/doc/License.md new file mode 100644 index 00000000..d708f8de --- /dev/null +++ b/doc/License.md @@ -0,0 +1,38 @@ +(license)= + +# License + +This project is partially open source. The source code of this repository is available under the +[BSD 3-Clause License](https://github.com/RoseauTechnologies/Roseau_Load_Flow/blob/main/LICENSE.md). + +The solver used in this project is not open source. A license has to be purchased to use it. To get a license, please contact us at contact@roseautechnologies.com. + +```{note} +Licenses are given **free of charge** for _students and teachers_. Please contact us at +contact@roseautechnologies.com to get a license key. +``` + +(license-activation)= + +## How to activate the license in your project + +There are two ways to activate the license in your project: + +1. Set the environment variable `ROSEAU_LOAD_FLOW_LICENSE_KEY` to the license key. When this + environment variable is defined, it will be automatically used by the solver to validate the + license, no further action is required. + **This is the recommended approach.** +2. Call the function `activate_license` with the license key as argument. This function will + activate the license for the current session. If you use this approach, it is recommended to + store the license key in a file and read it from there to avoid hardcoding it in your code and + accidentally committing it to your repository. Example: + + ```python + import roseau.load_flow as lf + + with open("my_license_key.txt", "r") as f: + license_key = f.read().strip() + lf.activate_license(license_key) + + # Rest of your code here + ``` diff --git a/doc/conf.py b/doc/conf.py index 098cb6b9..ac236690 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -18,7 +18,7 @@ # -- Project information ----------------------------------------------------- project = "Roseau Load Flow" -copyright = "2022--2023, Roseau Technologies SAS" +copyright = "2022--2024, Roseau Technologies SAS" # author = "Benoît Vinot" # The full version, including alpha/beta/rc tags @@ -126,7 +126,6 @@ "numpy": ("https://numpy.org/doc/stable/", None), "pandas": ("https://pandas.pydata.org/docs/", None), "geopandas": ("https://geopandas.org/en/stable/", None), - "requests": ("https://requests.readthedocs.io/en/latest/", None), "pint": ("https://pint.readthedocs.io/en/stable/", None), "typing_extensions": ("https://typing-extensions.readthedocs.io/en/stable/", None), "rich": ("https://rich.readthedocs.io/en/stable/", None), diff --git a/doc/index.md b/doc/index.md index 23e9db54..ba530be6 100644 --- a/doc/index.md +++ b/doc/index.md @@ -65,6 +65,18 @@ caption: Solvers Solvers ``` +## License + +Read more about the license of this project: + +```{toctree} +--- +maxdepth: 2 +caption: License +--- +License +``` + ## Changelog ```{toctree} @@ -87,3 +99,12 @@ caption: API Reference --- autoapi/roseau/load_flow/index ``` + + + +```{toctree} +--- +hidden: +--- +autoapi/index +``` diff --git a/doc/models/Bus.md b/doc/models/Bus.md index e55951cc..07e5f1c8 100644 --- a/doc/models/Bus.md +++ b/doc/models/Bus.md @@ -88,8 +88,7 @@ bus2.add_short_circuit("a", "b") # Create a network and solve a load flow en = ElectricalNetwork.from_element(bus1) -auth = ("username", "password") -en.solve_load_flow(auth=auth) +en.solve_load_flow() # Get the currents flowing to the line from bus1 # Notice the extremely high currents in phases "a" and "b" diff --git a/doc/models/Ground.md b/doc/models/Ground.md index 7f222bca..f18d89e0 100644 --- a/doc/models/Ground.md +++ b/doc/models/Ground.md @@ -107,8 +107,7 @@ pref = PotentialRef(id="pref", element=g1) # Create a network and solve a load flow en = ElectricalNetwork.from_element(bus1) -auth = ("username", "password") -en.solve_load_flow(auth=auth) +en.solve_load_flow() # Get the ground potentials # The potential of g1 is 0 as defined by the potential reference element diff --git a/doc/models/Line/ShuntLine.md b/doc/models/Line/ShuntLine.md index 363a5645..9db51fe2 100644 --- a/doc/models/Line/ShuntLine.md +++ b/doc/models/Line/ShuntLine.md @@ -152,8 +152,7 @@ line.with_shunt # Create a network and solve a load flow en = ElectricalNetwork.from_element(bus1) -auth = ("username", "password") -en.solve_load_flow(auth=auth) +en.solve_load_flow() # The current "entering" into the line from the bus1 en.res_branches[["current1"]].transform([np.abs, ft.partial(np.angle, deg=True)]) diff --git a/doc/models/Line/SimplifiedLine.md b/doc/models/Line/SimplifiedLine.md index fa83c1bf..091dd693 100644 --- a/doc/models/Line/SimplifiedLine.md +++ b/doc/models/Line/SimplifiedLine.md @@ -97,8 +97,7 @@ line.y_shunt # Create a network and solve a load flow en = ElectricalNetwork.from_element(bus1) -auth = ("username", "password") -en.solve_load_flow(auth=auth) +en.solve_load_flow() # The current flowing into the line from bus1 en.res_branches[["current1"]].transform([np.abs, ft.partial(np.angle, deg=True)]) diff --git a/doc/models/Load/CurrentLoad.md b/doc/models/Load/CurrentLoad.md index 4953642e..8b0bbee2 100644 --- a/doc/models/Load/CurrentLoad.md +++ b/doc/models/Load/CurrentLoad.md @@ -71,8 +71,7 @@ load = CurrentLoad( # Create a network and solve a load flow en = ElectricalNetwork.from_element(bus1) -auth = ("username", "password") -en.solve_load_flow(auth=auth) +en.solve_load_flow() # Get the current of the load (equal to the one provided) en.res_loads["current"].transform([np.abs, ft.partial(np.angle, deg=True)]) @@ -98,7 +97,7 @@ en.res_buses_voltages.transform([np.abs, ft.partial(np.angle, deg=True)]) load.currents = Q_( np.array([5.0, 2.5, 0]) * np.exp([0, -2j * np.pi / 3, 2j * np.pi / 3]), "A" ) -en.solve_load_flow(auth=auth) +en.solve_load_flow() # Get the currents of the loads of the network en.res_loads["current"].transform([np.abs, ft.partial(np.angle, deg=True)]) diff --git a/doc/models/Load/FlexibleLoad/FeasibleDomain.md b/doc/models/Load/FlexibleLoad/FeasibleDomain.md index 103ca4ad..2b02f39a 100644 --- a/doc/models/Load/FlexibleLoad/FeasibleDomain.md +++ b/doc/models/Load/FlexibleLoad/FeasibleDomain.md @@ -56,8 +56,7 @@ load = PowerLoad( # Build a network and solve a load flow en = ElectricalNetwork.from_element(bus) -auth = ("username", "password") -en.solve_load_flow(auth=auth) +en.solve_load_flow() # The voltage source provided 1 kVA per phase for the load vs.res_powers @@ -85,7 +84,7 @@ load = PowerLoad( powers=Q_(np.array([1000, 1000, 1000]), "VA"), flexible_params=[fp, fp, fp], ) -en.solve_load_flow(auth=auth) +en.solve_load_flow() # Again the voltage source provided 1 kVA per phase vs.res_powers @@ -103,7 +102,7 @@ load = PowerLoad( powers=Q_(np.array([6, 4.5, 6]), "kVA"), # Above 5 kVA -> also OK! flexible_params=[fp, fp, fp], ) -en.solve_load_flow(auth=auth) +en.solve_load_flow() # The load provides exactly the power consumed by the load even if it is greater than s_max vs.res_powers @@ -191,8 +190,7 @@ voltages = np.arange(205, 256, dtype=float) power = Q_(-2.5 + 1j, "kVA") # Get the resulting flexible powers for the given theoretical power and voltages list. -auth = ("username", "password") -res_flexible_powers = fp.compute_powers(auth=auth, voltages=voltages, power=power) +res_flexible_powers = fp.compute_powers(voltages=voltages, power=power) ``` Plotting the control curve $P(U)$ using the variables `voltages` and `res_flexible_powers` of the @@ -203,12 +201,6 @@ example above produces the following plot: :align: center ``` -```{note} -Using `compute_powers` actually requests the solver to solve a load flow for each voltage in the list. -It needs an internet connection to access the server and may take some time (similar to the -{meth}`roseau.load_flow.ElectricalNetwork.solve_load_flow` method). -``` - The non-smooth theoretical control function is the control function applied to $S^{\max}$. The "Actual power" plotted is the power actually produced by the load for each voltage. Below 240 V, there is no variation in the produced power which is expected. Between 240 V and approximately @@ -222,16 +214,11 @@ The same plot can be obtained with: ```python from matplotlib import pyplot as plt -ax, res_flexible_powers = fp.plot_control_p( - auth=auth, voltages=voltages, power=power, res_flexible_powers=res_flexible_powers -) +ax, res_flexible_powers = fp.plot_control_p(voltages=voltages, power=power) plt.show() ``` -Note that in this example, `res_flexible_powers` is provided as input to the plotting function. If -it was not provided, the powers would have been computed by requesting the server (using the -`compute_powers()` method above). The method returns a 2-tuple with the _matplotlib axis_ of the -plot and the computed powers. +The method returns a 2-tuple with the _matplotlib axis_ of the plot and the computed powers. `````{tip} To install _matplotlib_ along side _roseau-load-flow_, you can use the `plot` extra: @@ -267,10 +254,8 @@ from matplotlib import pyplot as plt ax = plt.subplot() # New axes ax, res_flexible_powers = fp.plot_pq( - auth=auth, voltages=voltages, power=power, - res_flexible_powers=res_flexible_powers, voltages_labels_mask=np.isin(voltages, [240, 250]), ax=ax, ) @@ -320,8 +305,7 @@ voltages = np.arange(205, 256, dtype=float) power = Q_(-2.5, "kVA") # Get the resulting flexible powers for the given theoretical power and voltages list. -auth = ("username", "password") -res_flexible_powers = fp.compute_powers(auth=auth, voltages=voltages, power=power) +res_flexible_powers = fp.compute_powers(voltages=voltages, power=power) ``` The variable `res_flexible_powers` contains the powers that have been actually produced by @@ -344,13 +328,7 @@ The same plot can be obtained with: from matplotlib import pyplot as plt ax = plt.subplot() # New axes -ax, res_flexible_powers = fp.plot_control_q( - auth=auth, - voltages=voltages, - power=power, - res_flexible_powers=res_flexible_powers, - ax=ax, -) +ax, res_flexible_powers = fp.plot_control_q(voltages=voltages, power=power, ax=ax) plt.show() ``` @@ -376,10 +354,8 @@ from matplotlib import pyplot as plt ax = plt.subplot() # New axes ax, res_flexible_powers = fp.plot_pq( - auth=auth, voltages=voltages, power=power, - res_flexible_powers=res_flexible_powers, voltages_labels_mask=np.isin(voltages, [210, 215, 230, 245, 250]), ax=ax, ) @@ -425,8 +401,7 @@ voltages = np.arange(205, 256, dtype=float) power = Q_(-2.5, "kVA") # Get the resulting flexible powers for the given theoretical power and voltages list. -auth = ("username", "password") -res_flexible_powers = fp.compute_powers(auth=auth, voltages=voltages, power=power) +res_flexible_powers = fp.compute_powers(voltages=voltages, power=power) ``` The variable `res_flexible_powers` contains the powers that have been actually produced by @@ -448,13 +423,7 @@ The same plot can be obtained with: from matplotlib import pyplot as plt ax = plt.subplot() # New axes -ax, res_flexible_powers = fp.plot_control_q( - auth=auth, - voltages=voltages, - power=power, - res_flexible_powers=res_flexible_powers, - ax=ax, -) +ax, res_flexible_powers = fp.plot_control_q(voltages=voltages, power=power, ax=ax) plt.show() ``` @@ -472,10 +441,8 @@ from matplotlib import pyplot as plt ax = plt.subplot() # New axes ax, res_flexible_powers = fp.plot_pq( - auth=auth, voltages=voltages, power=power, - res_flexible_powers=res_flexible_powers, voltages_labels_mask=np.isin(voltages, [210, 215, 230, 245, 250]), ax=ax, ) @@ -524,8 +491,7 @@ voltages = np.arange(205, 256, dtype=float) power = Q_(-2.5, "kVA") # Get the resulting flexible powers for the given theoretical power and voltages list. -auth = ("username", "password") -res_flexible_powers = fp.compute_powers(auth=auth, voltages=voltages, power=power) +res_flexible_powers = fp.compute_powers(voltages=voltages, power=power) ``` The variable `res_flexible_powers` contains the powers that have been actually produced by @@ -546,13 +512,7 @@ The same plot can be obtained with: from matplotlib import pyplot as plt ax = plt.subplot() # New axes -ax, res_flexible_powers = fp.plot_control_q( - auth=auth, - voltages=voltages, - power=power, - res_flexible_powers=res_flexible_powers, - ax=ax, -) +ax, res_flexible_powers = fp.plot_control_q(voltages=voltages, power=power, ax=ax) plt.show() ``` @@ -570,10 +530,8 @@ from matplotlib import pyplot as plt ax = plt.subplot() # New axes ax, res_flexible_powers = fp.plot_pq( - auth=auth, voltages=voltages, power=power, - res_flexible_powers=res_flexible_powers, voltages_labels_mask=np.isin(voltages, [210, 215, 230, 245, 250]), ax=ax, ) @@ -618,8 +576,7 @@ voltages = np.arange(205, 256, dtype=float) power = Q_(-2.5, "kVA") # Get the resulting flexible powers for the given theoretical power and voltages list. -auth = ("username", "password") -res_flexible_powers = fp.compute_powers(auth=auth, voltages=voltages, power=power) +res_flexible_powers = fp.compute_powers(voltages=voltages, power=power) ``` The variable `res_flexible_powers` contains the powers that have been actually produced by @@ -641,13 +598,7 @@ The same plot can be obtained with: from matplotlib import pyplot as plt ax = plt.subplot() # New axes -ax, res_flexible_powers = fp.plot_control_q( - auth=auth, - voltages=voltages, - power=power, - res_flexible_powers=res_flexible_powers, - ax=ax, -) +ax, res_flexible_powers = fp.plot_control_q(voltages=voltages, power=power, ax=ax) plt.show() ``` @@ -665,10 +616,8 @@ from matplotlib import pyplot as plt ax = plt.subplot() # New axes ax, res_flexible_powers = fp.plot_pq( - auth=auth, voltages=voltages, power=power, - res_flexible_powers=res_flexible_powers, voltages_labels_mask=np.isin(voltages, [210, 215, 230, 245, 250]), ax=ax, ) @@ -723,8 +672,7 @@ voltages = np.arange(205, 256, dtype=float) power = Q_(-2.5, "kVA") # Get the resulting flexible powers for the given theoretical power and voltages list. -auth = ("username", "password") -res_flexible_powers = fp.compute_powers(auth=auth, voltages=voltages, power=power) +res_flexible_powers = fp.compute_powers(voltages=voltages, power=power) ``` If we plot the trajectory of the control in the $(P, Q)$ space, we get: @@ -741,10 +689,8 @@ from matplotlib import pyplot as plt ax = plt.subplot() # New axes ax, res_flexible_powers = fp.plot_pq( - auth=auth, voltages=voltages, power=power, - res_flexible_powers=res_flexible_powers, voltages_labels_mask=np.isin(voltages, [210, 215, 230, 245, 250]), ax=ax, ) @@ -791,8 +737,7 @@ voltages = np.arange(205, 256, dtype=float) power = Q_(-2.5, "kVA") # Get the resulting flexible powers for the given theoretical power and voltages list. -auth = ("username", "password") -res_flexible_powers = fp.compute_powers(auth=auth, voltages=voltages, power=power) +res_flexible_powers = fp.compute_powers(voltages=voltages, power=power) ``` If we plot the trajectory of the control in the $(P, Q)$ space, we get: @@ -809,10 +754,8 @@ from matplotlib import pyplot as plt ax = plt.subplot() # New axes ax, res_flexible_powers = fp.plot_pq( - auth=auth, voltages=voltages, power=power, - res_flexible_powers=res_flexible_powers, voltages_labels_mask=np.isin(voltages, [210, 215, 230, 245, 250]), ax=ax, ) @@ -858,8 +801,7 @@ voltages = np.arange(205, 256, dtype=float) power = Q_(-2.5, "kVA") # Get the resulting flexible powers for the given theoretical power and voltages list. -auth = ("username", "password") -res_flexible_powers = fp.compute_powers(auth=auth, voltages=voltages, power=power) +res_flexible_powers = fp.compute_powers(voltages=voltages, power=power) ``` If we plot the trajectory of the control in the $(P, Q)$ space, we get: @@ -876,10 +818,8 @@ from matplotlib import pyplot as plt ax = plt.subplot() # New axes ax, res_flexible_powers = fp.plot_pq( - auth=auth, voltages=voltages, power=power, - res_flexible_powers=res_flexible_powers, voltages_labels_mask=np.isin(voltages, [210, 215, 230, 245, 250]), ax=ax, ) @@ -904,10 +844,8 @@ from matplotlib import pyplot as plt ax = plt.subplot() # New axes ax, res_flexible_powers = fp.plot_pq( - auth=auth, voltages=voltages, power=Q_(-4, "kVA"), # <------ New power - # res_flexible_powers=res_flexible_powers, # Must be computed again! voltages_labels_mask=np.isin(voltages, [210, 215, 230, 245, 250]), ax=ax, ) diff --git a/doc/models/Load/ImpedanceLoad.md b/doc/models/Load/ImpedanceLoad.md index 43481aca..894e904f 100644 --- a/doc/models/Load/ImpedanceLoad.md +++ b/doc/models/Load/ImpedanceLoad.md @@ -70,8 +70,7 @@ load = ImpedanceLoad( # Create a network and solve a load flow en = ElectricalNetwork.from_element(bus1) -auth = ("username", "password") -en.solve_load_flow(auth=auth) +en.solve_load_flow() # Get the impedances of the load (the result is equal to the provided impedance load.res_voltages / load.res_currents[:3] @@ -90,7 +89,7 @@ en.res_buses_voltages.transform([np.abs, ft.partial(np.angle, deg=True)]) # Modify the load value to create an unbalanced load load.impedances = Q_(np.array([40 + 4j, 20 + 2j, 10 + 1j]), "ohm") -en.solve_load_flow(auth=auth) +en.solve_load_flow() # Get the impedance of the load load.res_voltages / load.res_currents[:3] diff --git a/doc/models/Load/PowerLoad.md b/doc/models/Load/PowerLoad.md index 72564734..e3b9dccd 100644 --- a/doc/models/Load/PowerLoad.md +++ b/doc/models/Load/PowerLoad.md @@ -71,8 +71,7 @@ load = PowerLoad(id="load", bus=bus2, powers=Q_((1000 - 300j) * np.ones(3), "VA" # Create a network and solve a load flow en = ElectricalNetwork.from_element(bus1) -auth = ("username", "password") -en.solve_load_flow(auth=auth) +en.solve_load_flow() # Get the powers of the loads in the network en.res_loads["power"] @@ -96,7 +95,7 @@ en.res_buses_voltages.transform([np.abs, ft.partial(np.angle, deg=True)]) # Modify the load value to create an unbalanced load load.powers = Q_(np.array([5.0, 2.5, 0]) * (1 - 0.3j), "kVA") -en.solve_load_flow(auth=auth) +en.solve_load_flow() # Get the powers of the loads in the network en.res_loads["power"] diff --git a/doc/models/Switch.md b/doc/models/Switch.md index efc0f6ba..51379741 100644 --- a/doc/models/Switch.md +++ b/doc/models/Switch.md @@ -60,8 +60,7 @@ load = PowerLoad(id="load", bus=bus2, powers=[5000 + 1600j, 2500 + 800j, 0]) # Create a network and solve a load flow en = ElectricalNetwork.from_element(bus1) -auth = ("username", "password") -en.solve_load_flow(auth=auth) +en.solve_load_flow() # The current flowing into the line from bus1 en.res_branches[["current1"]].transform([np.abs, ft.partial(np.angle, deg=True)]) diff --git a/doc/models/Transformer/Three_Phase_Transformer.md b/doc/models/Transformer/Three_Phase_Transformer.md index 03309d26..e7367703 100644 --- a/doc/models/Transformer/Three_Phase_Transformer.md +++ b/doc/models/Transformer/Three_Phase_Transformer.md @@ -646,8 +646,7 @@ load = PowerLoad(id="load", bus=bus_lv, phases="abcn", powers=[3e3, 3e3, 3e3]) # Create the network and solve the load flow en = ElectricalNetwork.from_element(bus_mv) -auth = ("username", "password") -en.solve_load_flow(auth=auth) +en.solve_load_flow() # The current flowing into the transformer from the MV bus en.res_branches[["current1"]].dropna().transform([np.abs, ft.partial(np.angle, deg=True)]) diff --git a/doc/usage/Connecting_Elements.md b/doc/usage/Connecting_Elements.md index 4c27df69..2892c2d4 100644 --- a/doc/usage/Connecting_Elements.md +++ b/doc/usage/Connecting_Elements.md @@ -52,8 +52,7 @@ roseau.load_flow.exceptions.RoseauLoadFlowException: The Bus 'lb' is already ass The load flow can be solved: ```pycon ->>> auth = ("username", "password") ->>> en.solve_load_flow(auth=auth) +>>> en.solve_load_flow() 2 ``` @@ -161,7 +160,7 @@ Line(id='new_line', phases1='abcn', phases2='abcn', bus1='lb', bus2='new_bus') And now if you run the load flow, you can see that the new elements are taken into account. ```pycon ->>> en.solve_load_flow(auth=auth) +>>> en.solve_load_flow() 3 >>> abs(new_load.res_voltages) array([216.54956226]) diff --git a/doc/usage/Flexible_Loads.md b/doc/usage/Flexible_Loads.md index 1b4a11ef..345e28a6 100644 --- a/doc/usage/Flexible_Loads.md +++ b/doc/usage/Flexible_Loads.md @@ -128,8 +128,7 @@ a Delta-Wye transformer and a small LV network. Then, the load flow can be solved and the results can be retrieved. ```pycon ->>> auth = ("username", "password") ->>> en.solve_load_flow(auth=auth) +>>> en.solve_load_flow() 2 >>> abs(load_bus3.res_voltages) array([243.66463933, 232.20612714, 233.55093129]) @@ -177,7 +176,7 @@ the voltage magnitude for phase `'a'` was 240 V above without the $P(U)$ control has been activated in this run. ```pycon ->>> en.solve_load_flow(auth=auth) +>>> en.solve_load_flow() 4 >>> abs(load_bus3.res_voltages) array([243.08225748, 232.46046866, 233.62854073]) @@ -242,7 +241,7 @@ production is totally shut down. The load flow can be solved again. ```pycon ->>> en.solve_load_flow(auth=auth) +>>> en.solve_load_flow() 6 >>> abs(load_bus3.res_voltages) array([239.5133208 , 230.2108052 , 237.59184615]) diff --git a/doc/usage/Getting_Started.md b/doc/usage/Getting_Started.md index 6d8de653..5d97dc3f 100644 --- a/doc/usage/Getting_Started.md +++ b/doc/usage/Getting_Started.md @@ -140,16 +140,14 @@ automatically included into the network. ## Solving a load flow -An authentication is required. Please contact us at contact@roseautechnologies.com to get the necessary credentials. -Then, the load flow can be solved by requesting our server **(requires Internet access)**. +A license is required. Please contact us at contact@roseautechnologies.com to get a license key. +Once you have a license key, you can activate by following the instructions in the +[License activation page](license-activation). -```{note} -The server takes some time to warm up the first time it is requested. Subsequent requests will execute faster. -``` +Then, the load flow can be solved by calling the `solve_load_flow` method of the `ElectricalNetwork` ```pycon ->>> auth = ("username", "password") ->>> en.solve_load_flow(auth=auth) +>>> en.solve_load_flow() 2 ``` @@ -504,7 +502,7 @@ unbalanced situation. ```pycon >>> load.powers = Q_([15, 0, 0], "kVA") ->>> en.solve_load_flow(auth=auth) +>>> en.solve_load_flow() 3 >>> load_bus.res_potentials array([ 216.02252269 +0.j, -115.47005384-200.j, -115.47005384+200.j, 14.91758499 +0.j]) diff --git a/doc/usage/Short_Circuit.md b/doc/usage/Short_Circuit.md index 8c8a8e5b..6e6d3760 100644 --- a/doc/usage/Short_Circuit.md +++ b/doc/usage/Short_Circuit.md @@ -70,13 +70,18 @@ We can now add a short-circuit. Let's first create a phase-to-phase short-circui Let's run the load flow, and get the current results. +```{note} +If you get an error saying +`roseau.load_flow.RoseauLoadFlowException: The license is not valid. Please use the activate_license(key="...")`, +make sure you follow the instructions in [Solving a load flow](gs-solving-load-flow). +``` + ```{note} All the following tables are rounded to 2 decimals to be properly displayed. ``` ```pycon ->>> auth = ("username", "password") ->>> en.solve_load_flow(auth=auth) +>>> en.solve_load_flow() 1 >>> en.res_branches ``` @@ -109,7 +114,7 @@ short-circuit then create a new one between phases "a", "b", and "c". ```pycon >>> bus2.clear_short_circuits() >>> bus2.add_short_circuit("a", "b", "c") ->>> en.solve_load_flow(auth=auth) +>>> en.solve_load_flow() 1 >>> en.res_branches ``` @@ -137,7 +142,7 @@ between phase "a" and ground. >>> bus2.clear_short_circuits() >>> # ground MUST be passed as a keyword argument ... bus2.add_short_circuit("a", ground=ground) ->>> en.solve_load_flow(auth=auth) +>>> en.solve_load_flow() 1 >>> en.res_branches ``` diff --git a/poetry.lock b/poetry.lock index c040570c..84cdefc6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "alabaster" -version = "0.7.13" -description = "A configurable sidebar-enabled Sphinx theme" +version = "0.7.15" +description = "A light, configurable Sphinx theme" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" files = [ - {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, - {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, + {file = "alabaster-0.7.15-py3-none-any.whl", hash = "sha256:d99c6fd0f7a86fca68ecc5231c9de45227991c10ee6facfb894cf6afb953b142"}, + {file = "alabaster-0.7.15.tar.gz", hash = "sha256:0127f4b1db0afc914883f930e3d40763131aebac295522fc4a04d9e77c703705"}, ] [[package]] @@ -24,13 +24,13 @@ files = [ [[package]] name = "astroid" -version = "3.0.1" +version = "3.0.2" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.8.0" files = [ - {file = "astroid-3.0.1-py3-none-any.whl", hash = "sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca"}, - {file = "astroid-3.0.1.tar.gz", hash = "sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e"}, + {file = "astroid-3.0.2-py3-none-any.whl", hash = "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c"}, + {file = "astroid-3.0.2.tar.gz", hash = "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91"}, ] [package.dependencies] @@ -38,36 +38,34 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} [[package]] name = "attrs" -version = "23.1.0" +version = "23.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] [package.extras] cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] +dev = ["attrs[tests]", "pre-commit"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] [[package]] name = "babel" -version = "2.13.1" +version = "2.14.0" description = "Internationalization utilities" optional = false python-versions = ">=3.7" files = [ - {file = "Babel-2.13.1-py3-none-any.whl", hash = "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"}, - {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"}, + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, ] -[package.dependencies] -setuptools = {version = "*", markers = "python_version >= \"3.12\""} - [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] @@ -334,63 +332,63 @@ test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] [[package]] name = "coverage" -version = "7.3.2" +version = "7.4.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, - {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, - {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, - {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, - {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, - {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, - {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, - {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, - {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, - {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, - {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, - {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, - {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, - {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, + {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, + {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, + {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, + {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, + {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, + {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, + {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, + {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, + {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, + {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, + {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, + {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, + {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, + {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, ] [package.dependencies] @@ -416,13 +414,13 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "distlib" -version = "0.3.7" +version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] [[package]] @@ -530,59 +528,59 @@ test = ["Fiona[s3]", "pytest (>=7)", "pytest-cov", "pytz"] [[package]] name = "fonttools" -version = "4.45.1" +version = "4.47.0" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.45.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:45fa321c458ea29224067700954ec44493ae869b47e7c5485a350a149a19fb53"}, - {file = "fonttools-4.45.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0dc7617d96b1e668eea9250e1c1fe62d0c78c3f69573ce7e3332cc40e6d84356"}, - {file = "fonttools-4.45.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ed3bda541e86725f6b4e1b94213f13ed1ae51a5a1f167028534cedea38c010"}, - {file = "fonttools-4.45.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4f4a5870e3b56788fb196da8cf30d0dfd51a76dc3b907861d018165f76ae4c2"}, - {file = "fonttools-4.45.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a3c11d9687479f01eddef729aa737abcdea0a44fdaffb62a930a18892f186c9b"}, - {file = "fonttools-4.45.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:316cec50581e844c3ab69d7c82455b54c7cf18236b2f09e722faf665fbfcac58"}, - {file = "fonttools-4.45.1-cp310-cp310-win32.whl", hash = "sha256:e2277cba9f0b525e30de2a9ad3cb4219aa4bc697230c1645666b0deee9f914f0"}, - {file = "fonttools-4.45.1-cp310-cp310-win_amd64.whl", hash = "sha256:1b9e9ad2bcded9a1431afaa57c8d3c39143ac1f050862d66bddd863c515464a2"}, - {file = "fonttools-4.45.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff6a698bdd435d24c379f6e8a54908cd9bb7dda23719084d56bf8c87709bf3bd"}, - {file = "fonttools-4.45.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c980d60cd6ec1376206fe55013d166e5627ad0b149b5c81e74eaa913ab6134f"}, - {file = "fonttools-4.45.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a12dee6523c02ca78aeedd0a5e12bfa9b7b29896350edd5241542897b072ae23"}, - {file = "fonttools-4.45.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37cd1ced6efb3dd6fe82e9f9bf92fd74ac58a5aefc284045f59ecd517a5fb9ab"}, - {file = "fonttools-4.45.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e3d24248221bd7151dfff0d88b1b5da02dccd7134bd576ce8888199827bbaa19"}, - {file = "fonttools-4.45.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ba6c23591427844dfb0a13658f1718489de75de6a46b64234584c0d17573162d"}, - {file = "fonttools-4.45.1-cp311-cp311-win32.whl", hash = "sha256:cebcddbe9351b67166292b4f71ffdbfcce01ba4b07d4267824eb46b277aeb19a"}, - {file = "fonttools-4.45.1-cp311-cp311-win_amd64.whl", hash = "sha256:f22eb69996a0bd49f76bdefb30be54ce8dbb89a0d1246874d610f05c2aa2e69e"}, - {file = "fonttools-4.45.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:794de93e83297db7b4943f2431e206d8b1ea69cb3ae14638a49cc50332bf0db8"}, - {file = "fonttools-4.45.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4ba17822a6681d06849078daaf6e03eccc9f467efe7c4c60280e28a78e8e5df9"}, - {file = "fonttools-4.45.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e50f794d09df0675da8d9dbd7c66bfcab2f74a708343aabcad41936d26556891"}, - {file = "fonttools-4.45.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b07b857d4f9de3199a8c3d1b1bf2078c0f37447891ca1a8d9234106b9a27aff"}, - {file = "fonttools-4.45.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:777ba42b94a27bb7fb2b4082522fccfd345667c32a56011e1c3e105979af5b79"}, - {file = "fonttools-4.45.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:21e96b99878348c74aa58059b8578d7586f9519cbcdadacf56486737038aa043"}, - {file = "fonttools-4.45.1-cp312-cp312-win32.whl", hash = "sha256:5cbf02cda8465b69769d07385f5d11e7bba19954e7787792f46fe679ec755ebb"}, - {file = "fonttools-4.45.1-cp312-cp312-win_amd64.whl", hash = "sha256:800e354e0c3afaeb8d9552769773d02f228e98c37b8cb03041157c3d0687cffc"}, - {file = "fonttools-4.45.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6eb2c54f7a07c92108daabcf02caf31df97825738db02a28270633946bcda4d0"}, - {file = "fonttools-4.45.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:43a3d267334109ff849c37cf3629476b5feb392ef1d2e464a167b83de8cd599c"}, - {file = "fonttools-4.45.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e1aefc2bf3c43e0f33f995f828a7bbeff4adc9393a7760b11456dbcf14388f6"}, - {file = "fonttools-4.45.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f53a19dcdd5737440839b8394eeebb35da9ec8109f7926cb6456639b5b58e47"}, - {file = "fonttools-4.45.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a17706b9cc24b27721613fe5773d93331ab7f0ecaca9955aead89c6b843d3a7"}, - {file = "fonttools-4.45.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fb36e5f40191274a95938b40c0a1fa7f895e36935aea8709e1d6deff0b2d0d4f"}, - {file = "fonttools-4.45.1-cp38-cp38-win32.whl", hash = "sha256:46eabddec12066829b8a1efe45ae552ba2f1796981ecf538d5f68284c354c589"}, - {file = "fonttools-4.45.1-cp38-cp38-win_amd64.whl", hash = "sha256:b6de2f0fcd3302fb82f94801002cb473959e998c14c24ec28234adb674aed345"}, - {file = "fonttools-4.45.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:392d0e3cc23daee910193625f7cf1b387aff9dd5b6f1a5f4a925680acb6dcbc2"}, - {file = "fonttools-4.45.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4b9544b1346d99848ac0e9b05b5d45ee703d7562fc4c9c48cf4b781de9632e57"}, - {file = "fonttools-4.45.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8717db3e4895e4820ade64ea379187738827ee60748223cb0438ef044ee208c6"}, - {file = "fonttools-4.45.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e29d5f298d616a93a4c5963682dc6cc8cc09f6d89cad2c29019fc5fb3b4d9472"}, - {file = "fonttools-4.45.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cb472905da3049960e80fc1cf808231880d79727a8410e156bf3e5063a1c574f"}, - {file = "fonttools-4.45.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ba299f1fbaa2a1e33210aaaf6fa816d4059e4d3cfe2ae9871368d4ab548c1c6a"}, - {file = "fonttools-4.45.1-cp39-cp39-win32.whl", hash = "sha256:105099968b58a5b4cef6f3eb409db8ea8578b302a9d05e23fecba1b8b0177b5f"}, - {file = "fonttools-4.45.1-cp39-cp39-win_amd64.whl", hash = "sha256:847f3f49dd3423e5a678c098e2ba92c7f4955d4aab3044f6a507b0bb0ecb07e0"}, - {file = "fonttools-4.45.1-py3-none-any.whl", hash = "sha256:3bdd7dfca8f6c9f4779384064027e8477ad6a037d6a327b09381f43e0247c6f3"}, - {file = "fonttools-4.45.1.tar.gz", hash = "sha256:6e441286d55fe7ec7c4fb36812bf914924813776ff514b744b510680fc2733f2"}, + {file = "fonttools-4.47.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2d2404107626f97a221dc1a65b05396d2bb2ce38e435f64f26ed2369f68675d9"}, + {file = "fonttools-4.47.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01f409be619a9a0f5590389e37ccb58b47264939f0e8d58bfa1f3ba07d22671"}, + {file = "fonttools-4.47.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d986b66ff722ef675b7ee22fbe5947a41f60a61a4da15579d5e276d897fbc7fa"}, + {file = "fonttools-4.47.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8acf6dd0434b211b3bd30d572d9e019831aae17a54016629fa8224783b22df8"}, + {file = "fonttools-4.47.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:495369c660e0c27233e3c572269cbe520f7f4978be675f990f4005937337d391"}, + {file = "fonttools-4.47.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c59227d7ba5b232281c26ae04fac2c73a79ad0e236bca5c44aae904a18f14faf"}, + {file = "fonttools-4.47.0-cp310-cp310-win32.whl", hash = "sha256:59a6c8b71a245800e923cb684a2dc0eac19c56493e2f896218fcf2571ed28984"}, + {file = "fonttools-4.47.0-cp310-cp310-win_amd64.whl", hash = "sha256:52c82df66201f3a90db438d9d7b337c7c98139de598d0728fb99dab9fd0495ca"}, + {file = "fonttools-4.47.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:854421e328d47d70aa5abceacbe8eef231961b162c71cbe7ff3f47e235e2e5c5"}, + {file = "fonttools-4.47.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:511482df31cfea9f697930f61520f6541185fa5eeba2fa760fe72e8eee5af88b"}, + {file = "fonttools-4.47.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0e2c88c8c985b7b9a7efcd06511fb0a1fe3ddd9a6cd2895ef1dbf9059719d7"}, + {file = "fonttools-4.47.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7a0a8848726956e9d9fb18c977a279013daadf0cbb6725d2015a6dd57527992"}, + {file = "fonttools-4.47.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e869da810ae35afb3019baa0d0306cdbab4760a54909c89ad8904fa629991812"}, + {file = "fonttools-4.47.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dd23848f877c3754f53a4903fb7a593ed100924f9b4bff7d5a4e2e8a7001ae11"}, + {file = "fonttools-4.47.0-cp311-cp311-win32.whl", hash = "sha256:bf1810635c00f7c45d93085611c995fc130009cec5abdc35b327156aa191f982"}, + {file = "fonttools-4.47.0-cp311-cp311-win_amd64.whl", hash = "sha256:61df4dee5d38ab65b26da8efd62d859a1eef7a34dcbc331299a28e24d04c59a7"}, + {file = "fonttools-4.47.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e3f4d61f3a8195eac784f1d0c16c0a3105382c1b9a74d99ac4ba421da39a8826"}, + {file = "fonttools-4.47.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:174995f7b057e799355b393e97f4f93ef1f2197cbfa945e988d49b2a09ecbce8"}, + {file = "fonttools-4.47.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea592e6a09b71cb7a7661dd93ac0b877a6228e2d677ebacbad0a4d118494c86d"}, + {file = "fonttools-4.47.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40bdbe90b33897d9cc4a39f8e415b0fcdeae4c40a99374b8a4982f127ff5c767"}, + {file = "fonttools-4.47.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:843509ae9b93db5aaf1a6302085e30bddc1111d31e11d724584818f5b698f500"}, + {file = "fonttools-4.47.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9acfa1cdc479e0dde528b61423855913d949a7f7fe09e276228298fef4589540"}, + {file = "fonttools-4.47.0-cp312-cp312-win32.whl", hash = "sha256:66c92ec7f95fd9732550ebedefcd190a8d81beaa97e89d523a0d17198a8bda4d"}, + {file = "fonttools-4.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8fa20748de55d0021f83754b371432dca0439e02847962fc4c42a0e444c2d78"}, + {file = "fonttools-4.47.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c75e19971209fbbce891ebfd1b10c37320a5a28e8d438861c21d35305aedb81c"}, + {file = "fonttools-4.47.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e79f1a3970d25f692bbb8c8c2637e621a66c0d60c109ab48d4a160f50856deff"}, + {file = "fonttools-4.47.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:562681188c62c024fe2c611b32e08b8de2afa00c0c4e72bed47c47c318e16d5c"}, + {file = "fonttools-4.47.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a77a60315c33393b2bd29d538d1ef026060a63d3a49a9233b779261bad9c3f71"}, + {file = "fonttools-4.47.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4fabb8cc9422efae1a925160083fdcbab8fdc96a8483441eb7457235df625bd"}, + {file = "fonttools-4.47.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2a78dba8c2a1e9d53a0fb5382979f024200dc86adc46a56cbb668a2249862fda"}, + {file = "fonttools-4.47.0-cp38-cp38-win32.whl", hash = "sha256:e6b968543fde4119231c12c2a953dcf83349590ca631ba8216a8edf9cd4d36a9"}, + {file = "fonttools-4.47.0-cp38-cp38-win_amd64.whl", hash = "sha256:4a9a51745c0439516d947480d4d884fa18bd1458e05b829e482b9269afa655bc"}, + {file = "fonttools-4.47.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:62d8ddb058b8e87018e5dc26f3258e2c30daad4c87262dfeb0e2617dd84750e6"}, + {file = "fonttools-4.47.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5dde0eab40faaa5476133123f6a622a1cc3ac9b7af45d65690870620323308b4"}, + {file = "fonttools-4.47.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4da089f6dfdb822293bde576916492cd708c37c2501c3651adde39804630538"}, + {file = "fonttools-4.47.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:253bb46bab970e8aae254cebf2ae3db98a4ef6bd034707aa68a239027d2b198d"}, + {file = "fonttools-4.47.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1193fb090061efa2f9e2d8d743ae9850c77b66746a3b32792324cdce65784154"}, + {file = "fonttools-4.47.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:084511482dd265bce6dca24c509894062f0117e4e6869384d853f46c0e6d43be"}, + {file = "fonttools-4.47.0-cp39-cp39-win32.whl", hash = "sha256:97620c4af36e4c849e52661492e31dc36916df12571cb900d16960ab8e92a980"}, + {file = "fonttools-4.47.0-cp39-cp39-win_amd64.whl", hash = "sha256:e77bdf52185bdaf63d39f3e1ac3212e6cfa3ab07d509b94557a8902ce9c13c82"}, + {file = "fonttools-4.47.0-py3-none-any.whl", hash = "sha256:d6477ba902dd2d7adda7f0fd3bfaeb92885d45993c9e1928c9f28fc3961415f7"}, + {file = "fonttools-4.47.0.tar.gz", hash = "sha256:ec13a10715eef0e031858c1c23bfaee6cba02b97558e4a7bfa089dba4a8c2ebf"}, ] [package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "scipy"] +interpolatable = ["munkres", "pycairo", "scipy"] lxml = ["lxml (>=4.0,<5)"] pathops = ["skia-pathops (>=0.5.0)"] plot = ["matplotlib"] @@ -612,13 +610,13 @@ sphinx-basic-ng = "*" [[package]] name = "geopandas" -version = "0.14.1" +version = "0.14.2" description = "Geographic pandas extensions" optional = false python-versions = ">=3.9" files = [ - {file = "geopandas-0.14.1-py3-none-any.whl", hash = "sha256:ed5a7cae7874bfc3238fb05e0501cc1760e1b7b11e5b76ecad29da644ca305da"}, - {file = "geopandas-0.14.1.tar.gz", hash = "sha256:4853ff89ecb6d1cfc43e7b3671092c8160e8a46a3dd7368f25906283314e42bb"}, + {file = "geopandas-0.14.2-py3-none-any.whl", hash = "sha256:0efa61235a68862c1c6be89fc3707cdeba67667d5676bb19e24f3c57a8c2f723"}, + {file = "geopandas-0.14.2.tar.gz", hash = "sha256:6e71d57b8376f9fdc9f1c3aa3170e7e420e91778de854f51013ae66fd371ccdb"}, ] [package.dependencies] @@ -630,13 +628,13 @@ shapely = ">=1.8.0" [[package]] name = "identify" -version = "2.5.32" +version = "2.5.33" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.32-py2.py3-none-any.whl", hash = "sha256:0b7656ef6cba81664b783352c73f8c24b39cf82f926f78f4550eda928e5e0545"}, - {file = "identify-2.5.32.tar.gz", hash = "sha256:5d9979348ec1a21c768ae07e0a652924538e8bce67313a73cb0f681cf08ba407"}, + {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, + {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, ] [package.extras] @@ -1040,47 +1038,47 @@ setuptools = "*" [[package]] name = "numpy" -version = "1.26.2" +version = "1.26.3" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "numpy-1.26.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3703fc9258a4a122d17043e57b35e5ef1c5a5837c3db8be396c82e04c1cf9b0f"}, - {file = "numpy-1.26.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc392fdcbd21d4be6ae1bb4475a03ce3b025cd49a9be5345d76d7585aea69440"}, - {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36340109af8da8805d8851ef1d74761b3b88e81a9bd80b290bbfed61bd2b4f75"}, - {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc008217145b3d77abd3e4d5ef586e3bdfba8fe17940769f8aa09b99e856c00"}, - {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ced40d4e9e18242f70dd02d739e44698df3dcb010d31f495ff00a31ef6014fe"}, - {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b272d4cecc32c9e19911891446b72e986157e6a1809b7b56518b4f3755267523"}, - {file = "numpy-1.26.2-cp310-cp310-win32.whl", hash = "sha256:22f8fc02fdbc829e7a8c578dd8d2e15a9074b630d4da29cda483337e300e3ee9"}, - {file = "numpy-1.26.2-cp310-cp310-win_amd64.whl", hash = "sha256:26c9d33f8e8b846d5a65dd068c14e04018d05533b348d9eaeef6c1bd787f9919"}, - {file = "numpy-1.26.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b96e7b9c624ef3ae2ae0e04fa9b460f6b9f17ad8b4bec6d7756510f1f6c0c841"}, - {file = "numpy-1.26.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa18428111fb9a591d7a9cc1b48150097ba6a7e8299fb56bdf574df650e7d1f1"}, - {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06fa1ed84aa60ea6ef9f91ba57b5ed963c3729534e6e54055fc151fad0423f0a"}, - {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ca5482c3dbdd051bcd1fce8034603d6ebfc125a7bd59f55b40d8f5d246832b"}, - {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:854ab91a2906ef29dc3925a064fcd365c7b4da743f84b123002f6139bcb3f8a7"}, - {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f43740ab089277d403aa07567be138fc2a89d4d9892d113b76153e0e412409f8"}, - {file = "numpy-1.26.2-cp311-cp311-win32.whl", hash = "sha256:a2bbc29fcb1771cd7b7425f98b05307776a6baf43035d3b80c4b0f29e9545186"}, - {file = "numpy-1.26.2-cp311-cp311-win_amd64.whl", hash = "sha256:2b3fca8a5b00184828d12b073af4d0fc5fdd94b1632c2477526f6bd7842d700d"}, - {file = "numpy-1.26.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a4cd6ed4a339c21f1d1b0fdf13426cb3b284555c27ac2f156dfdaaa7e16bfab0"}, - {file = "numpy-1.26.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d5244aabd6ed7f312268b9247be47343a654ebea52a60f002dc70c769048e75"}, - {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3cdb4d9c70e6b8c0814239ead47da00934666f668426fc6e94cce869e13fd7"}, - {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa317b2325f7aa0a9471663e6093c210cb2ae9c0ad824732b307d2c51983d5b6"}, - {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:174a8880739c16c925799c018f3f55b8130c1f7c8e75ab0a6fa9d41cab092fd6"}, - {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f79b231bf5c16b1f39c7f4875e1ded36abee1591e98742b05d8a0fb55d8a3eec"}, - {file = "numpy-1.26.2-cp312-cp312-win32.whl", hash = "sha256:4a06263321dfd3598cacb252f51e521a8cb4b6df471bb12a7ee5cbab20ea9167"}, - {file = "numpy-1.26.2-cp312-cp312-win_amd64.whl", hash = "sha256:b04f5dc6b3efdaab541f7857351aac359e6ae3c126e2edb376929bd3b7f92d7e"}, - {file = "numpy-1.26.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4eb8df4bf8d3d90d091e0146f6c28492b0be84da3e409ebef54349f71ed271ef"}, - {file = "numpy-1.26.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a13860fdcd95de7cf58bd6f8bc5a5ef81c0b0625eb2c9a783948847abbef2c2"}, - {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64308ebc366a8ed63fd0bf426b6a9468060962f1a4339ab1074c228fa6ade8e3"}, - {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf8aab04a2c0e859da118f0b38617e5ee65d75b83795055fb66c0d5e9e9b818"}, - {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d73a3abcac238250091b11caef9ad12413dab01669511779bc9b29261dd50210"}, - {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b361d369fc7e5e1714cf827b731ca32bff8d411212fccd29ad98ad622449cc36"}, - {file = "numpy-1.26.2-cp39-cp39-win32.whl", hash = "sha256:bd3f0091e845164a20bd5a326860c840fe2af79fa12e0469a12768a3ec578d80"}, - {file = "numpy-1.26.2-cp39-cp39-win_amd64.whl", hash = "sha256:2beef57fb031dcc0dc8fa4fe297a742027b954949cabb52a2a376c144e5e6060"}, - {file = "numpy-1.26.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1cc3d5029a30fb5f06704ad6b23b35e11309491c999838c31f124fee32107c79"}, - {file = "numpy-1.26.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94cc3c222bb9fb5a12e334d0479b97bb2df446fbe622b470928f5284ffca3f8d"}, - {file = "numpy-1.26.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe6b44fb8fcdf7eda4ef4461b97b3f63c466b27ab151bec2366db8b197387841"}, - {file = "numpy-1.26.2.tar.gz", hash = "sha256:f65738447676ab5777f11e6bbbdb8ce11b785e105f690bc45966574816b6d3ea"}, + {file = "numpy-1.26.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:806dd64230dbbfaca8a27faa64e2f414bf1c6622ab78cc4264f7f5f028fee3bf"}, + {file = "numpy-1.26.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02f98011ba4ab17f46f80f7f8f1c291ee7d855fcef0a5a98db80767a468c85cd"}, + {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d45b3ec2faed4baca41c76617fcdcfa4f684ff7a151ce6fc78ad3b6e85af0a6"}, + {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdd2b45bf079d9ad90377048e2747a0c82351989a2165821f0c96831b4a2a54b"}, + {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:211ddd1e94817ed2d175b60b6374120244a4dd2287f4ece45d49228b4d529178"}, + {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1240f767f69d7c4c8a29adde2310b871153df9b26b5cb2b54a561ac85146485"}, + {file = "numpy-1.26.3-cp310-cp310-win32.whl", hash = "sha256:21a9484e75ad018974a2fdaa216524d64ed4212e418e0a551a2d83403b0531d3"}, + {file = "numpy-1.26.3-cp310-cp310-win_amd64.whl", hash = "sha256:9e1591f6ae98bcfac2a4bbf9221c0b92ab49762228f38287f6eeb5f3f55905ce"}, + {file = "numpy-1.26.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b831295e5472954104ecb46cd98c08b98b49c69fdb7040483aff799a755a7374"}, + {file = "numpy-1.26.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e87562b91f68dd8b1c39149d0323b42e0082db7ddb8e934ab4c292094d575d6"}, + {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c66d6fec467e8c0f975818c1796d25c53521124b7cfb760114be0abad53a0a2"}, + {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f25e2811a9c932e43943a2615e65fc487a0b6b49218899e62e426e7f0a57eeda"}, + {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af36e0aa45e25c9f57bf684b1175e59ea05d9a7d3e8e87b7ae1a1da246f2767e"}, + {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:51c7f1b344f302067b02e0f5b5d2daa9ed4a721cf49f070280ac202738ea7f00"}, + {file = "numpy-1.26.3-cp311-cp311-win32.whl", hash = "sha256:7ca4f24341df071877849eb2034948459ce3a07915c2734f1abb4018d9c49d7b"}, + {file = "numpy-1.26.3-cp311-cp311-win_amd64.whl", hash = "sha256:39763aee6dfdd4878032361b30b2b12593fb445ddb66bbac802e2113eb8a6ac4"}, + {file = "numpy-1.26.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a7081fd19a6d573e1a05e600c82a1c421011db7935ed0d5c483e9dd96b99cf13"}, + {file = "numpy-1.26.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12c70ac274b32bc00c7f61b515126c9205323703abb99cd41836e8125ea0043e"}, + {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f784e13e598e9594750b2ef6729bcd5a47f6cfe4a12cca13def35e06d8163e3"}, + {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f24750ef94d56ce6e33e4019a8a4d68cfdb1ef661a52cdaee628a56d2437419"}, + {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:77810ef29e0fb1d289d225cabb9ee6cf4d11978a00bb99f7f8ec2132a84e0166"}, + {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8ed07a90f5450d99dad60d3799f9c03c6566709bd53b497eb9ccad9a55867f36"}, + {file = "numpy-1.26.3-cp312-cp312-win32.whl", hash = "sha256:f73497e8c38295aaa4741bdfa4fda1a5aedda5473074369eca10626835445511"}, + {file = "numpy-1.26.3-cp312-cp312-win_amd64.whl", hash = "sha256:da4b0c6c699a0ad73c810736303f7fbae483bcb012e38d7eb06a5e3b432c981b"}, + {file = "numpy-1.26.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1666f634cb3c80ccbd77ec97bc17337718f56d6658acf5d3b906ca03e90ce87f"}, + {file = "numpy-1.26.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18c3319a7d39b2c6a9e3bb75aab2304ab79a811ac0168a671a62e6346c29b03f"}, + {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b7e807d6888da0db6e7e75838444d62495e2b588b99e90dd80c3459594e857b"}, + {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4d362e17bcb0011738c2d83e0a65ea8ce627057b2fdda37678f4374a382a137"}, + {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b8c275f0ae90069496068c714387b4a0eba5d531aace269559ff2b43655edd58"}, + {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cc0743f0302b94f397a4a65a660d4cd24267439eb16493fb3caad2e4389bccbb"}, + {file = "numpy-1.26.3-cp39-cp39-win32.whl", hash = "sha256:9bc6d1a7f8cedd519c4b7b1156d98e051b726bf160715b769106661d567b3f03"}, + {file = "numpy-1.26.3-cp39-cp39-win_amd64.whl", hash = "sha256:867e3644e208c8922a3be26fc6bbf112a035f50f0a86497f98f228c50c607bb2"}, + {file = "numpy-1.26.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3c67423b3703f8fbd90f5adaa37f85b5794d3366948efe9a5190a5f3a83fc34e"}, + {file = "numpy-1.26.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46f47ee566d98849323f01b349d58f2557f02167ee301e5e28809a8c0e27a2d0"}, + {file = "numpy-1.26.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a8474703bffc65ca15853d5fd4d06b18138ae90c17c8d12169968e998e448bb5"}, + {file = "numpy-1.26.3.tar.gz", hash = "sha256:697df43e2b6310ecc9d95f05d5ef20eacc09c7c4ecc9da3f235d39e71b7da1e4"}, ] [[package]] @@ -1096,36 +1094,36 @@ files = [ [[package]] name = "pandas" -version = "2.1.3" +version = "2.1.4" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" files = [ - {file = "pandas-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:acf08a73b5022b479c1be155d4988b72f3020f308f7a87c527702c5f8966d34f"}, - {file = "pandas-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3cc4469ff0cf9aa3a005870cb49ab8969942b7156e0a46cc3f5abd6b11051dfb"}, - {file = "pandas-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35172bff95f598cc5866c047f43c7f4df2c893acd8e10e6653a4b792ed7f19bb"}, - {file = "pandas-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59dfe0e65a2f3988e940224e2a70932edc964df79f3356e5f2997c7d63e758b4"}, - {file = "pandas-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0296a66200dee556850d99b24c54c7dfa53a3264b1ca6f440e42bad424caea03"}, - {file = "pandas-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:465571472267a2d6e00657900afadbe6097c8e1dc43746917db4dfc862e8863e"}, - {file = "pandas-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04d4c58e1f112a74689da707be31cf689db086949c71828ef5da86727cfe3f82"}, - {file = "pandas-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fa2ad4ff196768ae63a33f8062e6838efed3a319cf938fdf8b95e956c813042"}, - {file = "pandas-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4441ac94a2a2613e3982e502ccec3bdedefe871e8cea54b8775992485c5660ef"}, - {file = "pandas-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5ded6ff28abbf0ea7689f251754d3789e1edb0c4d0d91028f0b980598418a58"}, - {file = "pandas-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca5680368a5139d4920ae3dc993eb5106d49f814ff24018b64d8850a52c6ed2"}, - {file = "pandas-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:de21e12bf1511190fc1e9ebc067f14ca09fccfb189a813b38d63211d54832f5f"}, - {file = "pandas-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a5d53c725832e5f1645e7674989f4c106e4b7249c1d57549023ed5462d73b140"}, - {file = "pandas-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7cf4cf26042476e39394f1f86868d25b265ff787c9b2f0d367280f11afbdee6d"}, - {file = "pandas-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72c84ec1b1d8e5efcbff5312abe92bfb9d5b558f11e0cf077f5496c4f4a3c99e"}, - {file = "pandas-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f539e113739a3e0cc15176bf1231a553db0239bfa47a2c870283fd93ba4f683"}, - {file = "pandas-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc77309da3b55732059e484a1efc0897f6149183c522390772d3561f9bf96c00"}, - {file = "pandas-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:08637041279b8981a062899da0ef47828df52a1838204d2b3761fbd3e9fcb549"}, - {file = "pandas-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b99c4e51ef2ed98f69099c72c75ec904dd610eb41a32847c4fcbc1a975f2d2b8"}, - {file = "pandas-2.1.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7ea8ae8004de0381a2376662c0505bb0a4f679f4c61fbfd122aa3d1b0e5f09d"}, - {file = "pandas-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcd76d67ca2d48f56e2db45833cf9d58f548f97f61eecd3fdc74268417632b8a"}, - {file = "pandas-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1329dbe93a880a3d7893149979caa82d6ba64a25e471682637f846d9dbc10dd2"}, - {file = "pandas-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:321ecdb117bf0f16c339cc6d5c9a06063854f12d4d9bc422a84bb2ed3207380a"}, - {file = "pandas-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:11a771450f36cebf2a4c9dbd3a19dfa8c46c4b905a3ea09dc8e556626060fe71"}, - {file = "pandas-2.1.3.tar.gz", hash = "sha256:22929f84bca106921917eb73c1521317ddd0a4c71b395bcf767a106e3494209f"}, + {file = "pandas-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bdec823dc6ec53f7a6339a0e34c68b144a7a1fd28d80c260534c39c62c5bf8c9"}, + {file = "pandas-2.1.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:294d96cfaf28d688f30c918a765ea2ae2e0e71d3536754f4b6de0ea4a496d034"}, + {file = "pandas-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b728fb8deba8905b319f96447a27033969f3ea1fea09d07d296c9030ab2ed1d"}, + {file = "pandas-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00028e6737c594feac3c2df15636d73ace46b8314d236100b57ed7e4b9ebe8d9"}, + {file = "pandas-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:426dc0f1b187523c4db06f96fb5c8d1a845e259c99bda74f7de97bd8a3bb3139"}, + {file = "pandas-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:f237e6ca6421265643608813ce9793610ad09b40154a3344a088159590469e46"}, + {file = "pandas-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b7d852d16c270e4331f6f59b3e9aa23f935f5c4b0ed2d0bc77637a8890a5d092"}, + {file = "pandas-2.1.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd7d5f2f54f78164b3d7a40f33bf79a74cdee72c31affec86bfcabe7e0789821"}, + {file = "pandas-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa6e92e639da0d6e2017d9ccff563222f4eb31e4b2c3cf32a2a392fc3103c0d"}, + {file = "pandas-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d797591b6846b9db79e65dc2d0d48e61f7db8d10b2a9480b4e3faaddc421a171"}, + {file = "pandas-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2d3e7b00f703aea3945995ee63375c61b2e6aa5aa7871c5d622870e5e137623"}, + {file = "pandas-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:dc9bf7ade01143cddc0074aa6995edd05323974e6e40d9dbde081021ded8510e"}, + {file = "pandas-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:482d5076e1791777e1571f2e2d789e940dedd927325cc3cb6d0800c6304082f6"}, + {file = "pandas-2.1.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8a706cfe7955c4ca59af8c7a0517370eafbd98593155b48f10f9811da440248b"}, + {file = "pandas-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0513a132a15977b4a5b89aabd304647919bc2169eac4c8536afb29c07c23540"}, + {file = "pandas-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9f17f2b6fc076b2a0078862547595d66244db0f41bf79fc5f64a5c4d635bead"}, + {file = "pandas-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:45d63d2a9b1b37fa6c84a68ba2422dc9ed018bdaa668c7f47566a01188ceeec1"}, + {file = "pandas-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:f69b0c9bb174a2342818d3e2778584e18c740d56857fc5cdb944ec8bbe4082cf"}, + {file = "pandas-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3f06bda01a143020bad20f7a85dd5f4a1600112145f126bc9e3e42077c24ef34"}, + {file = "pandas-2.1.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab5796839eb1fd62a39eec2916d3e979ec3130509930fea17fe6f81e18108f6a"}, + {file = "pandas-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edbaf9e8d3a63a9276d707b4d25930a262341bca9874fcb22eff5e3da5394732"}, + {file = "pandas-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ebfd771110b50055712b3b711b51bee5d50135429364d0498e1213a7adc2be8"}, + {file = "pandas-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8ea107e0be2aba1da619cc6ba3f999b2bfc9669a83554b1904ce3dd9507f0860"}, + {file = "pandas-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:d65148b14788b3758daf57bf42725caa536575da2b64df9964c563b015230984"}, + {file = "pandas-2.1.4.tar.gz", hash = "sha256:fcb68203c833cc735321512e13861358079a96c174a61f5116a1de89c58c0ef7"}, ] [package.dependencies] @@ -1164,80 +1162,98 @@ xml = ["lxml (>=4.8.0)"] [[package]] name = "pillow" -version = "10.1.0" +version = "10.2.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "Pillow-10.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106"}, - {file = "Pillow-10.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273"}, - {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666"}, - {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2"}, - {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593"}, - {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db"}, - {file = "Pillow-10.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f"}, - {file = "Pillow-10.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818"}, - {file = "Pillow-10.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57"}, - {file = "Pillow-10.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7"}, - {file = "Pillow-10.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7"}, - {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610"}, - {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839"}, - {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172"}, - {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061"}, - {file = "Pillow-10.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262"}, - {file = "Pillow-10.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992"}, - {file = "Pillow-10.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a"}, - {file = "Pillow-10.1.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b"}, - {file = "Pillow-10.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d"}, - {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27"}, - {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312"}, - {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de"}, - {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651"}, - {file = "Pillow-10.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b"}, - {file = "Pillow-10.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f"}, - {file = "Pillow-10.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996"}, - {file = "Pillow-10.1.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793"}, - {file = "Pillow-10.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e"}, - {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2"}, - {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a"}, - {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01"}, - {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d"}, - {file = "Pillow-10.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80"}, - {file = "Pillow-10.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212"}, - {file = "Pillow-10.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14"}, - {file = "Pillow-10.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099"}, - {file = "Pillow-10.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616"}, - {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb"}, - {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219"}, - {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34"}, - {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd"}, - {file = "Pillow-10.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28"}, - {file = "Pillow-10.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2"}, - {file = "Pillow-10.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256"}, - {file = "Pillow-10.1.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7"}, - {file = "Pillow-10.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba"}, - {file = "Pillow-10.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4"}, - {file = "Pillow-10.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9"}, - {file = "Pillow-10.1.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e"}, - {file = "Pillow-10.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412"}, - {file = "Pillow-10.1.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b"}, - {file = "Pillow-10.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f"}, - {file = "Pillow-10.1.0.tar.gz", hash = "sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, + {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, + {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, + {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, + {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, + {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, + {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, + {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, + {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, + {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, + {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, + {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, + {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, + {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, + {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, + {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, + {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, ] [package.extras] docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] [[package]] name = "pint" -version = "0.22" +version = "0.23" description = "Physical quantities module" optional = false python-versions = ">=3.9" files = [ - {file = "Pint-0.22-py3-none-any.whl", hash = "sha256:6e2b3c5c2b4d9b516608bc860a417a39d66eb99c958f36540cf931d2c2e9f80f"}, - {file = "Pint-0.22.tar.gz", hash = "sha256:2d139f6abbcf3016cad7d3cec05707fe908ac4f99cf59aedfd6ee667b7a64433"}, + {file = "Pint-0.23-py3-none-any.whl", hash = "sha256:df79b6b5f1beb7ed0cd55d91a0766fc55f972f757a9364e844958c05e8eb66f9"}, + {file = "Pint-0.23.tar.gz", hash = "sha256:e1509b91606dbc52527c600a4ef74ffac12fff70688aff20e9072409346ec9b4"}, ] [package.dependencies] @@ -1245,23 +1261,25 @@ typing-extensions = "*" [package.extras] babel = ["babel (<=2.8)"] +bench = ["pytest", "pytest-codspeed"] dask = ["dask"] mip = ["mip (>=1.13)"] numpy = ["numpy (>=1.19.5)"] pandas = ["pint-pandas (>=0.3)"] -test = ["pytest", "pytest-cov", "pytest-mpl", "pytest-subtests"] +test = ["pytest", "pytest-benchmark", "pytest-cov", "pytest-mpl", "pytest-subtests"] +testbase = ["pytest", "pytest-benchmark", "pytest-cov", "pytest-subtests"] uncertainties = ["uncertainties (>=3.1.6)"] xarray = ["xarray"] [[package]] name = "platformdirs" -version = "4.0.0" +version = "4.1.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, - {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, + {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, + {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, ] [package.extras] @@ -1285,13 +1303,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "3.5.0" +version = "3.6.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, - {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, + {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, + {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, ] [package.dependencies] @@ -1405,13 +1423,13 @@ certifi = "*" [[package]] name = "pytest" -version = "7.4.3" +version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -1539,99 +1557,104 @@ files = [ [[package]] name = "regex" -version = "2023.10.3" +version = "2023.12.25" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.7" files = [ - {file = "regex-2023.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc"}, - {file = "regex-2023.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a"}, - {file = "regex-2023.10.3-cp310-cp310-win32.whl", hash = "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec"}, - {file = "regex-2023.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353"}, - {file = "regex-2023.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e"}, - {file = "regex-2023.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54"}, - {file = "regex-2023.10.3-cp311-cp311-win32.whl", hash = "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2"}, - {file = "regex-2023.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c"}, - {file = "regex-2023.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037"}, - {file = "regex-2023.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a"}, - {file = "regex-2023.10.3-cp312-cp312-win32.whl", hash = "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a"}, - {file = "regex-2023.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b"}, - {file = "regex-2023.10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf"}, - {file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991"}, - {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302"}, - {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971"}, - {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11"}, - {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597"}, - {file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb"}, - {file = "regex-2023.10.3-cp37-cp37m-win32.whl", hash = "sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a"}, - {file = "regex-2023.10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed"}, - {file = "regex-2023.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533"}, - {file = "regex-2023.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4"}, - {file = "regex-2023.10.3-cp38-cp38-win32.whl", hash = "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d"}, - {file = "regex-2023.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b"}, - {file = "regex-2023.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af"}, - {file = "regex-2023.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48"}, - {file = "regex-2023.10.3-cp39-cp39-win32.whl", hash = "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd"}, - {file = "regex-2023.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988"}, - {file = "regex-2023.10.3.tar.gz", hash = "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"}, + {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"}, + {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"}, + {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"}, + {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"}, + {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"}, + {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"}, + {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"}, + {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"}, + {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"}, + {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"}, + {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"}, + {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"}, + {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"}, + {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"}, ] [[package]] @@ -1655,25 +1678,6 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "requests-mock" -version = "1.11.0" -description = "Mock out responses from the requests package" -optional = false -python-versions = "*" -files = [ - {file = "requests-mock-1.11.0.tar.gz", hash = "sha256:ef10b572b489a5f28e09b708697208c4a3b2b89ef80a9f01584340ea357ec3c4"}, - {file = "requests_mock-1.11.0-py2.py3-none-any.whl", hash = "sha256:f7fae383f228633f6bececebdab236c478ace2284d6292c6e7e2867b9ab74d15"}, -] - -[package.dependencies] -requests = ">=2.3,<3" -six = "*" - -[package.extras] -fixture = ["fixtures"] -test = ["fixtures", "mock", "purl", "pytest", "requests-futures", "sphinx", "testtools"] - [[package]] name = "rich" version = "13.7.0" @@ -1694,39 +1698,39 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.1.6" +version = "0.1.11" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703"}, - {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462"}, - {file = "ruff-0.1.6-py3-none-win32.whl", hash = "sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a"}, - {file = "ruff-0.1.6-py3-none-win_amd64.whl", hash = "sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33"}, - {file = "ruff-0.1.6-py3-none-win_arm64.whl", hash = "sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc"}, - {file = "ruff-0.1.6.tar.gz", hash = "sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184"}, + {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a7f772696b4cdc0a3b2e527fc3c7ccc41cdcb98f5c80fdd4f2b8c50eb1458196"}, + {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:934832f6ed9b34a7d5feea58972635c2039c7a3b434fe5ba2ce015064cb6e955"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea0d3e950e394c4b332bcdd112aa566010a9f9c95814844a7468325290aabfd9"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bd4025b9c5b429a48280785a2b71d479798a69f5c2919e7d274c5f4b32c3607"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ad00662305dcb1e987f5ec214d31f7d6a062cae3e74c1cbccef15afd96611d"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4b077ce83f47dd6bea1991af08b140e8b8339f0ba8cb9b7a484c30ebab18a23f"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a88efecec23c37b11076fe676e15c6cdb1271a38f2b415e381e87fe4517f18"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b25093dad3b055667730a9b491129c42d45e11cdb7043b702e97125bcec48a1"}, + {file = "ruff-0.1.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231d8fb11b2cc7c0366a326a66dafc6ad449d7fcdbc268497ee47e1334f66f77"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:09c415716884950080921dd6237767e52e227e397e2008e2bed410117679975b"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f58948c6d212a6b8d41cd59e349751018797ce1727f961c2fa755ad6208ba45"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:190a566c8f766c37074d99640cd9ca3da11d8deae2deae7c9505e68a4a30f740"}, + {file = "ruff-0.1.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6464289bd67b2344d2a5d9158d5eb81025258f169e69a46b741b396ffb0cda95"}, + {file = "ruff-0.1.11-py3-none-win32.whl", hash = "sha256:9b8f397902f92bc2e70fb6bebfa2139008dc72ae5177e66c383fa5426cb0bf2c"}, + {file = "ruff-0.1.11-py3-none-win_amd64.whl", hash = "sha256:eb85ee287b11f901037a6683b2374bb0ec82928c5cbc984f575d0437979c521a"}, + {file = "ruff-0.1.11-py3-none-win_arm64.whl", hash = "sha256:97ce4d752f964ba559c7023a86e5f8e97f026d511e48013987623915431c7ea9"}, + {file = "ruff-0.1.11.tar.gz", hash = "sha256:f9d4d88cb6eeb4dfe20f9f0519bd2eaba8119bde87c3d5065c541dbae2b5a2cb"}, ] [[package]] name = "setuptools" -version = "69.0.2" +version = "69.0.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, - {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, ] [package.extras] @@ -2097,24 +2101,24 @@ files = [ [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.9.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] [[package]] name = "tzdata" -version = "2023.3" +version = "2023.4" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, - {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, + {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, + {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, ] [[package]] @@ -2135,13 +2139,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.24.7" +version = "20.25.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.7-py3-none-any.whl", hash = "sha256:a18b3fd0314ca59a2e9f4b556819ed07183b3e9a3702ecfe213f593d44f7b3fd"}, - {file = "virtualenv-20.24.7.tar.gz", hash = "sha256:69050ffb42419c91f6c1284a7b24e0475d793447e35929b488bf6a0aade39353"}, + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, ] [package.dependencies] @@ -2160,4 +2164,4 @@ plot = ["matplotlib"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "d216daa4b15d67093d163277ed61afe6d156d4a9e7b7edff0d978248c4711b93" +content-hash = "c467c96774a04a058c3b480f1a8d5c6a250bc6f82b8fe6f13dde61e2e7d765f3" diff --git a/pyproject.toml b/pyproject.toml index de1f8dd0..6bbf33fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,20 +38,17 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.dependencies] python = "^3.10" -numpy = [ - { version = ">=1.21.5", python = "<3.12" }, - { version = ">=1.26.0", python = ">=3.12,<3.13" }, - { version = "*", python = ">=3.13" }, -] +numpy = ">=1.21.5" pandas = ">=1.4.0" geopandas = ">=0.10.2" shapely = ">=2.0.0" regex = ">=2022.1.18" -requests = ">=2.28.1" pint = ">=0.21.0" typing-extensions = ">=4.6.2" rich = ">=13.5.1" -pyproj = "^3.3.0" +pyproj = ">=3.3.0" +certifi = ">=2023.5.7" +platformdirs = ">=4.0.0" # Optional dependencies matplotlib = { version = ">=3.7.2", optional = true } @@ -66,14 +63,13 @@ graph = ["networkx"] pytest = "^7.1.2" pytest-cov = "^4.0.0" pytest-xdist = "^3.1.0" -requests-mock = "^1.9.3" coverage = { version = "^7.0.5", extras = ["toml"] } matplotlib = ">=3.7.2" networkx = ">=3.0.0" [tool.poetry.group.dev.dependencies] pre-commit = "^3.0.0" -ruff = "==0.1.6" # keep in sync with .pre-commit-config.yaml +ruff = "==0.1.11" # keep in sync with .pre-commit-config.yaml [tool.poetry.group.doc.dependencies] sphinx = "^7.0.1" diff --git a/roseau/load_flow/__about__.py b/roseau/load_flow/__about__.py index ce864993..eb091007 100644 --- a/roseau/load_flow/__about__.py +++ b/roseau/load_flow/__about__.py @@ -7,7 +7,7 @@ "Victor Gouin", ) ) -__copyright__ = "Roseau Technologies 2018--2023" +__copyright__ = "Roseau Technologies 2018--2024" __credits__ = "Roseau Technologies" __license__ = "Proprietary" __maintainer__ = "Ali Hamdan" diff --git a/roseau/load_flow/conftest.py b/roseau/load_flow/conftest.py index e2181a46..2990baa2 100644 --- a/roseau/load_flow/conftest.py +++ b/roseau/load_flow/conftest.py @@ -1,11 +1,16 @@ +import os from pathlib import Path import numpy as np import pytest from pandas.testing import assert_frame_equal +from roseau.load_flow import activate_license from roseau.load_flow.utils import console +if "ROSEAU_LOAD_FLOW_TEST_LICENSE_KEY" in os.environ: + activate_license(os.environ["ROSEAU_LOAD_FLOW_TEST_LICENSE_KEY"]) + # Variable to test the network HERE = Path(__file__).parent.expanduser().absolute() TEST_ALL_NETWORKS_DATA_FOLDER = HERE / "tests" / "data" / "networks" diff --git a/roseau/load_flow/exceptions.py b/roseau/load_flow/exceptions.py index 31d8ed0d..8ccd1003 100644 --- a/roseau/load_flow/exceptions.py +++ b/roseau/load_flow/exceptions.py @@ -75,6 +75,7 @@ class RoseauLoadFlowExceptionCode(Enum): SWITCHES_LOOP = auto() NO_POTENTIAL_REFERENCE = auto() SEVERAL_POTENTIAL_REFERENCE = auto() + EMPTY_NETWORK = auto() UNKNOWN_ELEMENT = auto() NO_VOLTAGE_SOURCE = auto() BAD_ELEMENT_OBJECT = auto() diff --git a/roseau/load_flow/io/dgs.py b/roseau/load_flow/io/dgs.py index 873e4a64..52fee519 100644 --- a/roseau/load_flow/io/dgs.py +++ b/roseau/load_flow/io/dgs.py @@ -1,6 +1,10 @@ +""" +This module is not for public use. + +Use the `ElectricalNetwork.from_dgs` method to read a network from a dgs file. +""" import json import logging -from typing import TYPE_CHECKING import numpy as np import pandas as pd @@ -11,30 +15,30 @@ AbstractLoad, Bus, Ground, + Line, LineParameters, PotentialRef, + PowerLoad, + Switch, + Transformer, TransformerParameters, VoltageSource, ) -from roseau.load_flow.typing import StrPath +from roseau.load_flow.typing import Id, StrPath from roseau.load_flow.units import Q_ -if TYPE_CHECKING: - from roseau.load_flow.network import ElectricalNetwork - logger = logging.getLogger(__name__) def network_from_dgs( # noqa: C901 filename: StrPath, - en_class: type["ElectricalNetwork"], ) -> tuple[ - dict[str, Bus], - dict[str, AbstractBranch], - dict[str, AbstractLoad], - dict[str, VoltageSource], - dict[str, Ground], - dict[str, PotentialRef], + dict[Id, Bus], + dict[Id, AbstractBranch], + dict[Id, AbstractLoad], + dict[Id, VoltageSource], + dict[Id, Ground], + dict[Id, PotentialRef], ]: """Create the electrical elements from a JSON file in DGS format. @@ -61,14 +65,14 @@ def network_from_dgs( # noqa: C901 ) = _read_dgs_json_file(filename=filename) # Ground and potential reference - ground = en_class._ground_class("ground") - p_ref = en_class._pref_class("pref", element=ground) + ground = Ground("ground") + p_ref = PotentialRef("pref", element=ground) grounds = {ground.id: ground} potential_refs = {p_ref.id: p_ref} # Buses - buses: dict[str, Bus] = {} + buses: dict[Id, Bus] = {} for bus_id in elm_term.index: ph_tech = elm_term.at[bus_id, "phtech"] if ph_tech == 0: @@ -79,10 +83,10 @@ def network_from_dgs( # noqa: C901 msg = f"The Ph tech {ph_tech!r} for bus {bus_id!r} cannot be handled." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.DGS_BAD_PHASE_TECHNOLOGY) - buses[bus_id] = en_class._bus_class(id=bus_id, phases=phases) + buses[bus_id] = Bus(id=bus_id, phases=phases) # Sources - sources: dict[str, VoltageSource] = {} + sources: dict[Id, VoltageSource] = {} for source_id in elm_xnet.index: id_sta_cubic_source = elm_xnet.at[source_id, "bus1"] # id of the cubicle connecting the source and its bus bus_id = sta_cubic.at[id_sta_cubic_source, "cterm"] # id of the bus to which the source is connected @@ -91,30 +95,28 @@ def network_from_dgs( # noqa: C901 voltages = [un * tap, un * np.exp(-np.pi * 2 / 3 * 1j) * tap, un * np.exp(np.pi * 2 / 3 * 1j) * tap] source_bus = buses[bus_id] - sources[source_id] = en_class._voltage_source_class( - id=source_id, phases="abcn", bus=source_bus, voltages=voltages - ) + sources[source_id] = VoltageSource(id=source_id, phases="abcn", bus=source_bus, voltages=voltages) source_bus._connect(ground) # LV loads - loads: dict[str, AbstractLoad] = {} + loads: dict[Id, AbstractLoad] = {} if elm_lod_lv is not None: - _generate_loads(en_class, elm_lod_lv, loads, buses, sta_cubic, 1e3, production=False) + _generate_loads(elm_lod_lv, loads, buses, sta_cubic, 1e3, production=False) # LV Production loads if elm_pv_sys is not None: - _generate_loads(en_class, elm_pv_sys, loads, buses, sta_cubic, 1e3, production=True) + _generate_loads(elm_pv_sys, loads, buses, sta_cubic, 1e3, production=True) if elm_gen_stat is not None: - _generate_loads(en_class, elm_gen_stat, loads, buses, sta_cubic, 1e3, production=True) + _generate_loads(elm_gen_stat, loads, buses, sta_cubic, 1e3, production=True) # MV loads if elm_lod_mv is not None: - _generate_loads(en_class, elm_lod_mv, loads, buses, sta_cubic, 1e6, production=False) + _generate_loads(elm_lod_mv, loads, buses, sta_cubic, 1e6, production=False) # Lines - branches: dict[str, AbstractBranch] = {} + branches: dict[Id, AbstractBranch] = {} if elm_lne is not None: - lines_params_dict: dict[str, LineParameters] = {} + lines_params_dict: dict[Id, LineParameters] = {} for type_id in typ_lne.index: # TODO: use the detailed phase information instead of n n = typ_lne.at[type_id, "nlnph"] + typ_lne.at[type_id, "nneutral"] @@ -159,7 +161,7 @@ def network_from_dgs( # noqa: C901 for line_id in elm_lne.index: type_id = elm_lne.at[line_id, "typ_id"] # id of the line type lp = lines_params_dict[type_id] - branches[line_id] = en_class._line_class( + branches[line_id] = Line( id=line_id, bus1=buses[sta_cubic.at[elm_lne.at[line_id, "bus1"], "cterm"]], bus2=buses[sta_cubic.at[elm_lne.at[line_id, "bus2"], "cterm"]], @@ -171,8 +173,8 @@ def network_from_dgs( # noqa: C901 # Transformers if elm_tr is not None: # Transformers type - transformers_params_dict: dict[str, TransformerParameters] = {} - transformers_tap: dict[str, int] = {} + transformers_params_dict: dict[Id, TransformerParameters] = {} + transformers_tap: dict[Id, int] = {} for idx in typ_tr.index: # Extract data name = typ_tr.at[idx, "loc_name"] @@ -196,7 +198,7 @@ def network_from_dgs( # noqa: C901 for idx in elm_tr.index: type_id = elm_tr.at[idx, "typ_id"] # id of the line type tap = 1.0 + elm_tr.at[idx, "nntap"] * transformers_tap[type_id] / 100 - branches[idx] = en_class._transformer_class( + branches[idx] = Transformer( id=idx, bus1=buses[sta_cubic.at[elm_tr.at[idx, "bushv"], "cterm"]], bus2=buses[sta_cubic.at[elm_tr.at[idx, "buslv"], "cterm"]], @@ -210,7 +212,7 @@ def network_from_dgs( # noqa: C901 for switch_id in elm_coup.index: # TODO: use the detailed phase information instead of n n = elm_coup.at[switch_id, "nphase"] + elm_coup.at[switch_id, "nneutral"] - branches[switch_id] = en_class._switch_class( + branches[switch_id] = Switch( id=switch_id, phases="abc" if n == 3 else "abcn", bus1=buses[sta_cubic.at[elm_coup.at[switch_id, "bus1"], "cterm"]], @@ -332,10 +334,9 @@ def _read_dgs_json_file(filename: StrPath): def _generate_loads( - en_class: type["ElectricalNetwork"], elm_lod: pd.DataFrame, - loads: dict[str, AbstractLoad], - buses: dict[str, Bus], + loads: dict[Id, AbstractLoad], + buses: dict[Id, Bus], sta_cubic: pd.DataFrame, factor: float, production: bool, @@ -376,7 +377,7 @@ def _generate_loads( # Balanced or Unbalanced s = [s_phase / 3, s_phase / 3, s_phase / 3] if sa == 0 and sb == 0 and sc == 0 else [sa, sb, sc] - loads[load_id] = en_class._load_class._power_load_class(id=load_id, phases="abcn", bus=buses[bus_id], powers=s) + loads[load_id] = PowerLoad(id=load_id, phases="abcn", bus=buses[bus_id], powers=s) def _compute_load_power(elm_lod: pd.DataFrame, load_id: str, suffix: str) -> complex: diff --git a/roseau/load_flow/io/dict.py b/roseau/load_flow/io/dict.py index 8db7ac4f..7292ea25 100644 --- a/roseau/load_flow/io/dict.py +++ b/roseau/load_flow/io/dict.py @@ -1,3 +1,10 @@ +""" +This module is not for public use. + +Use the `ElectricalNetwork.from_dict` and `ElectricalNetwork.to_dict` methods to serialize networks +from and to dictionaries, or the methods `ElectricalNetwork.from_json` and `ElectricalNetwork.to_json` +to read and write networks from and to JSON files. +""" import logging from typing import TYPE_CHECKING @@ -10,6 +17,7 @@ Line, LineParameters, PotentialRef, + Switch, Transformer, TransformerParameters, VoltageSource, @@ -26,7 +34,7 @@ def network_from_dict( - data: JsonDict, en_class: type["ElectricalNetwork"] + data: JsonDict, ) -> tuple[ dict[Id, Bus], dict[Id, AbstractBranch], @@ -41,9 +49,6 @@ def network_from_dict( data: The dictionary containing the network data. - en_class: - The ElectricalNetwork class to create. - Returns: The buses, branches, loads, sources, grounds and potential refs to construct the electrical network. @@ -63,16 +68,14 @@ def network_from_dict( transformers_params = {tp["id"]: TransformerParameters.from_dict(tp) for tp in data["transformers_params"]} # Buses, loads and sources - buses = {bd["id"]: en_class._bus_class.from_dict(bd) for bd in data["buses"]} - loads = {ld["id"]: en_class._load_class.from_dict(ld | {"bus": buses[ld["bus"]]}) for ld in data["loads"]} - sources = { - sd["id"]: en_class._voltage_source_class.from_dict(sd | {"bus": buses[sd["bus"]]}) for sd in data["sources"] - } + buses = {bd["id"]: Bus.from_dict(bd) for bd in data["buses"]} + loads = {ld["id"]: AbstractLoad.from_dict(ld | {"bus": buses[ld["bus"]]}) for ld in data["loads"]} + sources = {sd["id"]: VoltageSource.from_dict(sd | {"bus": buses[sd["bus"]]}) for sd in data["sources"]} # Grounds and potential refs grounds: dict[Id, Ground] = {} for ground_data in data["grounds"]: - ground = en_class._ground_class(ground_data["id"]) + ground = Ground(ground_data["id"]) for ground_bus in ground_data["buses"]: ground.connect(buses[ground_bus["id"]], ground_bus["phase"]) grounds[ground_data["id"]] = ground @@ -86,7 +89,7 @@ def network_from_dict( msg = f"Potential reference data {pref_data['id']} missing bus or ground." logger.error(msg) raise RoseauLoadFlowException(msg, RoseauLoadFlowExceptionCode.JSON_PREF_INVALID) - potential_refs[pref_data["id"]] = en_class._pref_class( + potential_refs[pref_data["id"]] = PotentialRef( pref_data["id"], element=bus_or_ground, phase=pref_data.get("phases") ) @@ -105,17 +108,17 @@ def network_from_dict( lp = lines_params[branch_data["params_id"]] gid = branch_data.get("ground") ground = grounds[gid] if gid is not None else None - branches_dict[id] = en_class._line_class( + branches_dict[id] = Line( id, bus1, bus2, parameters=lp, phases=phases1, length=length, ground=ground, geometry=geometry ) elif branch_data["type"] == "transformer": tp = transformers_params[branch_data["params_id"]] - branches_dict[id] = en_class._transformer_class( + branches_dict[id] = Transformer( id, bus1, bus2, parameters=tp, phases1=phases1, phases2=phases2, geometry=geometry ) elif branch_data["type"] == "switch": assert phases1 == phases2 - branches_dict[id] = en_class._switch_class(id, bus1, bus2, phases=phases1, geometry=geometry) + branches_dict[id] = Switch(id, bus1, bus2, phases=phases1, geometry=geometry) else: msg = f"Unknown branch type for branch {id}: {branch_data['type']}" logger.error(msg) diff --git a/roseau/load_flow/license.py b/roseau/load_flow/license.py index 1f0706a7..e522b3aa 100644 --- a/roseau/load_flow/license.py +++ b/roseau/load_flow/license.py @@ -19,7 +19,7 @@ class License: """A class to access the main data of the License.""" - def __init__(self, cy_license: CyLicense): + def __init__(self, cy_license: CyLicense) -> None: """Constructor for a License Args: @@ -49,16 +49,9 @@ def valid(self) -> bool: """Is the license valid?""" return self.cy_license.valid - def validate(self) -> None: - """Validate the license.""" - return self.cy_license.validate() - - # - # The following methods are used to identify your PC - # @staticmethod def get_machine_fingerprint() -> str: - """This method retrieves your machine fingerprint.""" + """This method retrieves your machine fingerprint for license validation.""" return CyLicense.get_machine_fingerprint() @staticmethod @@ -73,7 +66,7 @@ def get_username() -> str: def activate_license(key: str) -> None: - """Activate the license from the given key. + """Activate the license with the given key in the current process. Args: key: @@ -83,7 +76,7 @@ def activate_license(key: str) -> None: def deactivate_license() -> None: - """Deactivate the license of the current process.""" + """Deactivate the license in the current process.""" cy_deactivate_license() diff --git a/roseau/load_flow/models/branches.py b/roseau/load_flow/models/branches.py index ea5f7dc5..3aef18fd 100644 --- a/roseau/load_flow/models/branches.py +++ b/roseau/load_flow/models/branches.py @@ -60,10 +60,10 @@ def __init__( super().__init__(id, **kwargs) self._check_phases(id, phases1=phases1) self._check_phases(id, phases2=phases2) - self.phases1 = phases1 - self.phases2 = phases2 - self.bus1 = bus1 - self.bus2 = bus2 + self._phases1 = phases1 + self._phases2 = phases2 + self._bus1 = bus1 + self._bus2 = bus2 self.geometry = geometry self._connect(bus1, bus2) self._res_currents: tuple[ComplexArray, ComplexArray] | None = None @@ -76,8 +76,29 @@ def __repr__(self) -> str: s += ")" return s + @property + def phases1(self) -> str: + """The phases of the branch at the first bus.""" + return self._phases1 + + @property + def phases2(self) -> str: + """The phases of the branch at the second bus.""" + return self._phases2 + + @property + def bus1(self) -> Bus: + """The first bus of the branch.""" + return self._bus1 + + @property + def bus2(self) -> Bus: + """The second bus of the branch.""" + return self._bus2 + def _res_currents_getter(self, warning: bool) -> tuple[ComplexArray, ComplexArray]: - self._res_currents = self._cy_element.get_currents(len(self.phases1), len(self.phases2)) + if self._fetch_results: + self._res_currents = self._cy_element.get_currents(len(self.phases1), len(self.phases2)) return self._res_getter(value=self._res_currents, warning=warning) @property @@ -162,6 +183,7 @@ def results_from_dict(self, data: JsonDict) -> None: currents1 = np.array([complex(i[0], i[1]) for i in data["currents1"]], dtype=np.complex128) currents2 = np.array([complex(i[0], i[1]) for i in data["currents2"]], dtype=np.complex128) self._res_currents = (currents1, currents2) + self._fetch_results = False def _results_to_dict(self, warning: bool) -> JsonDict: currents1, currents2 = self._res_currents_getter(warning) diff --git a/roseau/load_flow/models/buses.py b/roseau/load_flow/models/buses.py index a4235118..590f413e 100644 --- a/roseau/load_flow/models/buses.py +++ b/roseau/load_flow/models/buses.py @@ -75,7 +75,7 @@ def __init__( """ super().__init__(id, **kwargs) self._check_phases(id, phases=phases) - self.phases = phases + self._phases = phases initialized = potentials is not None if potentials is None: potentials = [0] * len(phases) @@ -89,13 +89,19 @@ def __init__( self._res_potentials: ComplexArray | None = None self._short_circuits: list[dict[str, Any]] = [] - self.n = len(self.phases) + self._n = len(self._phases) self._initialized = initialized - self._cy_element = CyBus(n=self.n, potentials=self._potentials) + self._initialized_by_the_user = initialized # only used for serialization + self._cy_element = CyBus(n=self._n, potentials=self._potentials) def __repr__(self) -> str: return f"{type(self).__name__}(id={self.id!r}, phases={self.phases!r})" + @property + def phases(self) -> str: + """The phases of the bus.""" + return self._phases + @property @ureg_wraps("V", (None,)) def potentials(self) -> Q_[ComplexArray]: @@ -112,11 +118,13 @@ def potentials(self, value: ComplexArrayLike1D) -> None: self._potentials = np.array(value, dtype=np.complex128) self._invalidate_network_results() self._initialized = True + self._initialized_by_the_user = True if self._cy_element is not None: self._cy_element.initialize_potentials(self._potentials) def _res_potentials_getter(self, warning: bool) -> ComplexArray: - self._res_potentials = self._cy_element.get_potentials(self.n) + if self._fetch_results: + self._res_potentials = self._cy_element.get_potentials(self._n) return self._res_getter(value=self._res_potentials, warning=warning) @property @@ -337,7 +345,7 @@ def from_dict(cls, data: JsonDict) -> Self: def to_dict(self, *, _lf_only: bool = False) -> JsonDict: res = {"id": self.id, "phases": self.phases} - if not np.allclose(self.potentials, 0): + if self._initialized_by_the_user: res["potentials"] = [[v.real, v.imag] for v in self._potentials] if not _lf_only: if self.geometry is not None: @@ -350,6 +358,7 @@ def to_dict(self, *, _lf_only: bool = False) -> JsonDict: def results_from_dict(self, data: JsonDict) -> None: self._res_potentials = np.array([complex(v[0], v[1]) for v in data["potentials"]], dtype=np.complex128) + self._fetch_results = False def _results_to_dict(self, warning: bool) -> JsonDict: return { @@ -414,7 +423,7 @@ def short_circuits(self) -> list[dict[str, Any]]: def clear_short_circuits(self) -> None: """Remove the short-circuits of this bus.""" - self._short_circuits = [] - msg = "Short circuits cannot be cleared for the engine part." + # self._short_circuits = [] + msg = "Short circuits cannot be cleared for now. Please recreate the bus without the short circuits instead." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_SHORT_CIRCUIT) # TODO diff --git a/roseau/load_flow/models/core.py b/roseau/load_flow/models/core.py index 586d9a15..f6762aa2 100644 --- a/roseau/load_flow/models/core.py +++ b/roseau/load_flow/models/core.py @@ -42,6 +42,7 @@ def __init__(self, id: Id, **kwargs: Any) -> None: self._connected_elements: list[Element] = [] self._network: ElectricalNetwork | None = None self._cy_element: CyElement | None = None + self._fetch_results = False @property def network(self) -> Optional["ElectricalNetwork"]: @@ -165,6 +166,7 @@ def _res_getter(self, value: _T | None, warning: bool) -> _T: category=UserWarning, stacklevel=2, ) + self._fetch_results = False return value @staticmethod diff --git a/roseau/load_flow/models/grounds.py b/roseau/load_flow/models/grounds.py index f3bba98f..95d554e0 100644 --- a/roseau/load_flow/models/grounds.py +++ b/roseau/load_flow/models/grounds.py @@ -52,7 +52,8 @@ def __repr__(self) -> str: return f"{type(self).__name__}(id={self.id!r})" def _res_potential_getter(self, warning: bool) -> complex: - self._res_potential = self._cy_element.get_potentials(1)[0] + if self._fetch_results: + self._res_potential = self._cy_element.get_potentials(1)[0] return self._res_getter(self._res_potential, warning) @property @@ -109,6 +110,7 @@ def to_dict(self, *, _lf_only: bool = False) -> JsonDict: def results_from_dict(self, data: JsonDict) -> None: self._res_potential = complex(*data["potential"]) + self._fetch_results = False def _results_to_dict(self, warning: bool) -> JsonDict: v = self._res_potential_getter(warning) diff --git a/roseau/load_flow/models/lines/lines.py b/roseau/load_flow/models/lines/lines.py index 01dbf1c8..2ce149b4 100644 --- a/roseau/load_flow/models/lines/lines.py +++ b/roseau/load_flow/models/lines/lines.py @@ -84,13 +84,18 @@ def __init__( logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_GEOMETRY_TYPE) super().__init__(id=id, phases1=phases, phases2=phases, bus1=bus1, bus2=bus2, geometry=geometry, **kwargs) - self.phases = phases + self._phases = phases self._check_elements() self._check_loop() - self.n = len(self.phases) - self._cy_element = CySwitch(self.n) + self._n = len(self._phases) + self._cy_element = CySwitch(self._n) self._cy_connect() + @property + def phases(self) -> str: + """The phases of the source.""" + return self._phases + def _check_loop(self) -> None: """Check that there are no switch loop, raise an exception if it is the case""" visited_1: set[Element] = set() @@ -207,7 +212,7 @@ def __init__( self._initialized = False super().__init__(id, bus1, bus2, phases1=phases, phases2=phases, geometry=geometry, **kwargs) - self.phases = phases + self._phases = phases self.ground = ground self.length = length self.parameters = parameters @@ -228,20 +233,25 @@ def __init__( # Connect the ground self._connect(self.ground) - self.n = len(self.phases) + self._n = len(self._phases) if parameters.with_shunt: self._cy_element = CyShuntLine( - n=self.n, - y_shunt=(parameters.y_shunt.reshape(self.n * self.n) * self.length).m_as("S"), - z_line=(parameters.z_line.reshape(self.n * self.n) * self.length).m_as("ohm"), + n=self._n, + y_shunt=(parameters.y_shunt.reshape(self._n * self._n) * self.length).m_as("S"), + z_line=(parameters.z_line.reshape(self._n * self._n) * self.length).m_as("ohm"), ) else: self._cy_element = CySimplifiedLine( - n=self.n, z_line=(parameters.z_line.reshape(self.n * self.n) * self.length).m_as("ohm") + n=self._n, z_line=(parameters.z_line.reshape(self._n * self._n) * self.length).m_as("ohm") ) self._cy_connect() if parameters.with_shunt: - ground._cy_element.connect(self._cy_element, [(0, self.n + self.n)]) + ground._cy_element.connect(self._cy_element, [(0, self._n + self._n)]) + + @property + def phases(self) -> str: + """The phases of the source.""" + return self._phases @property @ureg_wraps("km", (None,)) @@ -299,12 +309,12 @@ def parameters(self, value: LineParameters) -> None: if self._cy_element is not None: if value.with_shunt: self._cy_element.update_line_parameters( - (value.y_shunt.reshape(self.n * self.n) * self.length).m_as("S"), - (value.z_line.reshape(self.n * self.n) * self.length).m_as("ohm"), + (value.y_shunt.reshape(self._n * self._n) * self.length).m_as("S"), + (value.z_line.reshape(self._n * self._n) * self.length).m_as("ohm"), ) else: self._cy_element.update_line_parameters( - (value.z_line.reshape(self.n * self.n) * self.length).m_as("ohm") + (value.z_line.reshape(self._n * self._n) * self.length).m_as("ohm") ) @property diff --git a/roseau/load_flow/models/loads/flexible_parameters.py b/roseau/load_flow/models/loads/flexible_parameters.py index dece49bb..76050ad0 100644 --- a/roseau/load_flow/models/loads/flexible_parameters.py +++ b/roseau/load_flow/models/loads/flexible_parameters.py @@ -7,14 +7,7 @@ from typing_extensions import Self from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode -from roseau.load_flow.typing import ( - Authentication, - ComplexArray, - ComplexArrayLike1D, - ControlType, - JsonDict, - ProjectionType, -) +from roseau.load_flow.typing import ComplexArray, ComplexArrayLike1D, ControlType, JsonDict, ProjectionType from roseau.load_flow.units import Q_, ureg_wraps from roseau.load_flow.utils import JsonMixin, _optional_deps from roseau.load_flow_engine.cy_engine import CyControl, CyFlexibleParameter, CyProjection @@ -460,9 +453,6 @@ class FlexibleParameter(JsonMixin): For multi-phase loads, you need to use a `FlexibleParameter` instance per phase. """ - _control_class: type[Control] = Control - _projection_class: type[Projection] = Projection - @ureg_wraps(None, (None, None, None, None, "VA", "VAr", "VAr")) def __init__( self, @@ -592,9 +582,9 @@ def constant(cls) -> Self: A constant control i.e. no control at all. It is an equivalent of the constant power load. """ return cls( - control_p=cls._control_class.constant(), - control_q=cls._control_class.constant(), - projection=cls._projection_class(type=cls._projection_class._DEFAULT_TYPE), + control_p=Control.constant(), + control_q=Control.constant(), + projection=Projection(type=Projection._DEFAULT_TYPE), s_max=1.0, ) @@ -646,11 +636,11 @@ def p_max_u_production( Returns: A flexible parameter which performs "p_max_u_production" control. """ - control_p = cls._control_class.p_max_u_production(u_up=u_up, u_max=u_max, alpha=alpha_control) + control_p = Control.p_max_u_production(u_up=u_up, u_max=u_max, alpha=alpha_control) return cls( control_p=control_p, - control_q=cls._control_class.constant(), - projection=cls._projection_class(type=type_proj, alpha=alpha_proj, epsilon=epsilon_proj), + control_q=Control.constant(), + projection=Projection(type=type_proj, alpha=alpha_proj, epsilon=epsilon_proj), s_max=s_max, ) @@ -699,11 +689,11 @@ def p_max_u_consumption( Returns: A flexible parameter which performs "p_max_u_consumption" control. """ - control_p = cls._control_class.p_max_u_consumption(u_min=u_min, u_down=u_down, alpha=alpha_control) + control_p = Control.p_max_u_consumption(u_min=u_min, u_down=u_down, alpha=alpha_control) return cls( control_p=control_p, - control_q=cls._control_class.constant(), - projection=cls._projection_class(type=type_proj, alpha=alpha_proj, epsilon=epsilon_proj), + control_q=Control.constant(), + projection=Projection(type=type_proj, alpha=alpha_proj, epsilon=epsilon_proj), s_max=s_max, ) @@ -771,11 +761,11 @@ def q_u( Returns: A flexible parameter which performs "q_u" control. """ - control_q = cls._control_class.q_u(u_min=u_min, u_down=u_down, u_up=u_up, u_max=u_max, alpha=alpha_control) + control_q = Control.q_u(u_min=u_min, u_down=u_down, u_up=u_up, u_max=u_max, alpha=alpha_control) return cls( - control_p=cls._control_class.constant(), + control_p=Control.constant(), control_q=control_q, - projection=cls._projection_class(type=type_proj, alpha=alpha_proj, epsilon=epsilon_proj), + projection=Projection(type=type_proj, alpha=alpha_proj, epsilon=epsilon_proj), s_max=s_max, q_min=q_min, q_max=q_max, @@ -857,12 +847,12 @@ def pq_u_production( See Also: :meth:`p_max_u_production` and :meth:`q_u` for more details. """ - control_p = cls._control_class.p_max_u_production(u_up=up_up, u_max=up_max, alpha=alpha_control) - control_q = cls._control_class.q_u(u_min=uq_min, u_down=uq_down, u_up=uq_up, u_max=uq_max, alpha=alpha_control) + control_p = Control.p_max_u_production(u_up=up_up, u_max=up_max, alpha=alpha_control) + control_q = Control.q_u(u_min=uq_min, u_down=uq_down, u_up=uq_up, u_max=uq_max, alpha=alpha_control) return cls( control_p=control_p, control_q=control_q, - projection=cls._projection_class(type=type_proj, alpha=alpha_proj, epsilon=epsilon_proj), + projection=Projection(type=type_proj, alpha=alpha_proj, epsilon=epsilon_proj), s_max=s_max, q_min=q_min, q_max=q_max, @@ -944,12 +934,12 @@ def pq_u_consumption( See Also: :meth:`p_max_u_consumption` and :meth:`q_u` for more details. """ - control_p = cls._control_class.p_max_u_consumption(u_min=up_min, u_down=up_down, alpha=alpha_control) - control_q = cls._control_class.q_u(u_min=uq_min, u_down=uq_down, u_up=uq_up, u_max=uq_max, alpha=alpha_control) + control_p = Control.p_max_u_consumption(u_min=up_min, u_down=up_down, alpha=alpha_control) + control_q = Control.q_u(u_min=uq_min, u_down=uq_down, u_up=uq_up, u_max=uq_max, alpha=alpha_control) return cls( control_p=control_p, control_q=control_q, - projection=cls._projection_class(type=type_proj, alpha=alpha_proj, epsilon=epsilon_proj), + projection=Projection(type=type_proj, alpha=alpha_proj, epsilon=epsilon_proj), s_max=s_max, q_min=q_min, q_max=q_max, @@ -960,9 +950,9 @@ def pq_u_consumption( # @classmethod def from_dict(cls, data: JsonDict) -> Self: - control_p = cls._control_class.from_dict(data["control_p"]) - control_q = cls._control_class.from_dict(data["control_q"]) - projection = cls._projection_class.from_dict(data["projection"]) + control_p = Control.from_dict(data["control_p"]) + control_q = Control.from_dict(data["control_q"]) + projection = Projection.from_dict(data["projection"]) q_min = data.get("q_min", None) q_max = data.get("q_max", None) return cls( @@ -1000,66 +990,42 @@ def results_from_dict(self, data: JsonDict) -> NoReturn: # # Equivalent Python method # - @ureg_wraps("VA", (None, None, "V", "VA", None)) - def compute_powers( - self, - auth: Authentication, - voltages: ComplexArrayLike1D, - power: complex | Q_[complex], - solve_kwargs: JsonDict | None = None, - ) -> Q_[ComplexArray]: + @ureg_wraps("VA", (None, "V", "VA")) + def compute_powers(self, voltages: ComplexArrayLike1D, power: complex | Q_[complex]) -> Q_[ComplexArray]: """Compute the flexible powers for different voltages (norms) Args: - auth: - The login and password for the roseau load flow api. - voltages: The array of voltage norms to test with this flexible parameter. power: The input theoretical power of the load. - solve_kwargs: - Keywords arguments passed to the :meth:`~roseau.load_flow.ElectricalNetwork.solve_load_flow` method. - Returns: The flexible powers really consumed taking into account the control. One value per provided voltage norm. """ - return self._compute_powers(auth=auth, voltages=voltages, power=power, solve_kwargs=solve_kwargs) + return self._compute_powers(voltages=voltages, power=power) - def _compute_powers( - self, auth: Authentication, voltages: ComplexArrayLike1D, power: complex, solve_kwargs: JsonDict | None - ) -> ComplexArray: + def _compute_powers(self, voltages: ComplexArrayLike1D, power: complex) -> ComplexArray: # Format the input voltages = np.array(np.abs(voltages), dtype=float) # Iterate over the provided voltages to get the associated flexible powers - res_flexible_powers = [] - for v in voltages: - s = self._cy_fp.compute_power(v, power) - res_flexible_powers.append(s) - + res_flexible_powers = [self._cy_fp.compute_power(v, power) for v in voltages] return np.array(res_flexible_powers, dtype=complex) - @ureg_wraps((None, "VA"), (None, None, "V", "VA", None, None, None, "VA")) + @ureg_wraps((None, "VA"), (None, "V", "VA", None, None)) def plot_pq( self, - auth: Authentication, voltages: NDArray[np.float64] | Q_[NDArray[np.float64]], power: complex | Q_[complex], ax: Optional["Axes"] = None, - solve_kwargs: JsonDict | None = None, voltages_labels_mask: NDArray[np.bool_] | None = None, - res_flexible_powers: ComplexArray | None = None, ) -> tuple["Axes", ComplexArray]: """Plot the "trajectory" of the flexible powers (in the (P, Q) plane) for the provided voltages and theoretical power. Args: - auth: - The login and password for the roseau load flow api. - voltages: The array of voltage norms to test with this flexible parameter. @@ -1069,15 +1035,9 @@ def plot_pq( ax: The optional axis to use for the plot. The current axis is used by default. - solve_kwargs: - The keywords arguments of the :meth:`~roseau.load_flow.ElectricalNetwork.solve_load_flow` method. - voltages_labels_mask: A mask to activate the plot of voltages labels. By default, no voltages annotations. - res_flexible_powers: - If None is provided, the `res_flexible_powers` are computed. Otherwise, the provided values are used. - Returns: The axis on which the plot has been drawn and the resulting flexible powers (the input if not `None` else the computed values). @@ -1099,10 +1059,7 @@ def plot_pq( v_max = voltages.max() # Compute the powers for the voltages norms - if res_flexible_powers is None: - res_flexible_powers = self._compute_powers( - auth=auth, voltages=voltages, power=power, solve_kwargs=solve_kwargs - ) + res_flexible_powers = self._compute_powers(voltages=voltages, power=power) # Draw a circle circle = plt.Circle((0, 0), radius=s_max, color="black", fill=False) @@ -1157,22 +1114,16 @@ def plot_pq( return ax, res_flexible_powers - @ureg_wraps((None, "VA"), (None, None, "V", "VA", None, None, "VA")) + @ureg_wraps((None, "VA"), (None, "V", "VA", None)) def plot_control_p( self, - auth: Authentication, voltages: NDArray[np.float64] | Q_[NDArray[np.float64]], power: complex | Q_[complex], ax: Optional["Axes"] = None, - solve_kwargs: JsonDict | None = None, - res_flexible_powers: ComplexArray | None = None, ) -> tuple["Axes", ComplexArray]: """Plot the flexible active power consumed (or produced) for the provided voltages and theoretical power. Args: - auth: - The login and password for the roseau load flow api. - voltages: The array of voltage norms to test with this flexible parameter. @@ -1182,12 +1133,6 @@ def plot_control_p( ax: The optional axis to use for the plot. The current axis is used by default. - solve_kwargs: - The keywords arguments of the :meth:`~roseau.load_flow.ElectricalNetwork.solve_load_flow` method. - - res_flexible_powers: - If None is provided, the `res_flexible_powers` are computed. Otherwise, the provided values are used. - Returns: The axis on which the plot has been drawn and the resulting flexible powers (the input if not `None` else the computed values). @@ -1204,10 +1149,7 @@ def plot_control_p( ) # Compute the powers for the voltages norms - if res_flexible_powers is None: - res_flexible_powers = self._compute_powers( - auth=auth, voltages=voltages, power=power, solve_kwargs=solve_kwargs - ) + res_flexible_powers = self._compute_powers(voltages=voltages, power=power) ax.scatter(voltages, res_flexible_powers.real, marker=".", c="blue", zorder=2, label="Actual power") # Add the theoretical non-smooth curve @@ -1223,22 +1165,16 @@ def plot_control_p( return ax, res_flexible_powers - @ureg_wraps((None, "VA"), (None, None, "V", "VA", None, None, "VA")) + @ureg_wraps((None, "VA"), (None, "V", "VA", None)) def plot_control_q( self, - auth: Authentication, voltages: NDArray[np.float64] | Q_[NDArray[np.float64]], power: complex | Q_[complex], ax: Optional["Axes"] = None, - solve_kwargs: JsonDict | None = None, - res_flexible_powers: ComplexArray | None = None, ) -> tuple["Axes", ComplexArray]: """Plot the flexible reactive power consumed (or produced) for the provided voltages and theoretical power. Args: - auth: - The login and password for the roseau load flow api. - voltages: The array of voltage norms to test with this flexible parameter. @@ -1248,12 +1184,6 @@ def plot_control_q( ax: The optional axis to use for the plot. The current axis is used by default. - solve_kwargs: - The keywords arguments of the :meth:`~roseau.load_flow.ElectricalNetwork.solve_load_flow` method - - res_flexible_powers: - If None is provided, the `res_flexible_powers` are computed. Otherwise, the provided values are used. - Returns: The axis on which the plot has been drawn and the resulting flexible powers (the input if not `None` else the computed values). @@ -1270,10 +1200,7 @@ def plot_control_q( ) # Compute the powers for the voltages norms - if res_flexible_powers is None: - res_flexible_powers = self._compute_powers( - auth=auth, voltages=voltages, power=power, solve_kwargs=solve_kwargs - ) + res_flexible_powers = self._compute_powers(voltages=voltages, power=power) ax.scatter(voltages, res_flexible_powers.imag, marker=".", c="blue", zorder=2, label="Actual power") # Add the theoretical non-smooth curve diff --git a/roseau/load_flow/models/loads/loads.py b/roseau/load_flow/models/loads/loads.py index ce3ec9db..4ad0b40c 100644 --- a/roseau/load_flow/models/loads/loads.py +++ b/roseau/load_flow/models/loads/loads.py @@ -33,11 +33,6 @@ class AbstractLoad(Element, ABC): * delta-connected loads using a `phases` constructor argument not containing `"n"` """ - _power_load_class: type["PowerLoad"] - _current_load_class: type["CurrentLoad"] - _impedance_load_class: type["ImpedanceLoad"] - _flexible_parameter_class = FlexibleParameter - _type: Literal["power", "current", "impedance"] _floating_neutral_allowed: bool = False @@ -78,9 +73,9 @@ def __init__(self, id: Id, bus: Bus, *, phases: str | None = None, **kwargs: Any raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_PHASE) self._connect(bus) - self.phases = phases - self.bus = bus - self.n = len(self.phases) + self._phases = phases + self._bus = bus + self._n = len(self._phases) self._symbol = {"power": "S", "current": "I", "impedance": "Z"}[self._type] if len(phases) == 2 and "n" not in phases: # This is a delta load that has one element connected between two phases @@ -95,6 +90,16 @@ def __repr__(self) -> str: bus_id = self.bus.id if self.bus is not None else None return f"{type(self).__name__}(id={self.id!r}, phases={self.phases!r}, bus={bus_id!r})" + @property + def phases(self) -> str: + """The phases of the load.""" + return self._phases + + @property + def bus(self) -> Bus: + """The bus of the load.""" + return self._bus + @property def is_flexible(self) -> bool: """Whether the load is flexible or not. Only :class:`PowerLoad` can be flexible.""" @@ -106,7 +111,8 @@ def voltage_phases(self) -> list[str]: return calculate_voltage_phases(self.phases) def _res_currents_getter(self, warning: bool) -> ComplexArray: - self._res_currents = self._cy_element.get_currents(self.n) + if self._fetch_results: + self._res_currents = self._cy_element.get_currents(self._n) return self._res_getter(value=self._res_currents, warning=warning) @property @@ -174,7 +180,7 @@ def _cy_connect(self): def disconnect(self) -> None: """Disconnect this load from the network. It cannot be used afterwards.""" self._disconnect() - self.bus = None + self._bus = None def _raise_disconnected_error(self) -> None: """Raise an error if the load is disconnected.""" @@ -191,18 +197,16 @@ def from_dict(cls, data: JsonDict) -> "AbstractLoad": if (s_list := data.get("powers")) is not None: powers = [complex(s[0], s[1]) for s in s_list] if (fp_data_list := data.get("flexible_params")) is not None: - fp = [cls._flexible_parameter_class.from_dict(fp_dict) for fp_dict in fp_data_list] + fp = [FlexibleParameter.from_dict(fp_dict) for fp_dict in fp_data_list] else: fp = None - return cls._power_load_class( - data["id"], data["bus"], powers=powers, phases=data["phases"], flexible_params=fp - ) + return PowerLoad(data["id"], data["bus"], powers=powers, phases=data["phases"], flexible_params=fp) elif (i_list := data.get("currents")) is not None: currents = [complex(i[0], i[1]) for i in i_list] - return cls._current_load_class(data["id"], data["bus"], currents=currents, phases=data["phases"]) + return CurrentLoad(data["id"], data["bus"], currents=currents, phases=data["phases"]) elif (z_list := data.get("impedances")) is not None: impedances = [complex(z[0], z[1]) for z in z_list] - return cls._impedance_load_class(data["id"], data["bus"], impedances=impedances, phases=data["phases"]) + return ImpedanceLoad(data["id"], data["bus"], impedances=impedances, phases=data["phases"]) else: msg = f"Unknown load type for load {data['id']!r}" logger.error(msg) @@ -282,15 +286,15 @@ def __init__( cy_parameters.append(p._cy_fp) if self.phases == "abc": self._cy_element = CyDeltaFlexibleLoad( - n=self.n, powers=self._powers, parameters=np.array(cy_parameters) + n=self._n, powers=self._powers, parameters=np.array(cy_parameters) ) else: - self._cy_element = CyFlexibleLoad(n=self.n, powers=self._powers, parameters=np.array(cy_parameters)) + self._cy_element = CyFlexibleLoad(n=self._n, powers=self._powers, parameters=np.array(cy_parameters)) else: if self.phases == "abc": - self._cy_element = CyDeltaPowerLoad(n=self.n, powers=self._powers) + self._cy_element = CyDeltaPowerLoad(n=self._n, powers=self._powers) else: - self._cy_element = CyPowerLoad(n=self.n, powers=self._powers) + self._cy_element = CyPowerLoad(n=self._n, powers=self._powers) self._cy_connect() @property @@ -345,7 +349,8 @@ def powers(self, value: ComplexArrayLike1D) -> None: self._cy_element.update_powers(self._powers) def _res_flexible_powers_getter(self, warning: bool) -> ComplexArray: - self._res_flexible_powers = self._cy_element.get_powers(self.n) + if self._fetch_results: + self._res_flexible_powers = self._cy_element.get_powers(self._n) return self._res_getter(value=self._res_flexible_powers, warning=warning) @property @@ -414,9 +419,9 @@ def __init__( super().__init__(id=id, phases=phases, bus=bus, **kwargs) self.currents = currents # handles size checks and unit conversion if self.phases == "abc": - self._cy_element = CyDeltaCurrentLoad(n=self.n, currents=self._currents) + self._cy_element = CyDeltaCurrentLoad(n=self._n, currents=self._currents) else: - self._cy_element = CyCurrentLoad(n=self.n, currents=self._currents) + self._cy_element = CyCurrentLoad(n=self._n, currents=self._currents) self._cy_connect() @property @@ -473,9 +478,9 @@ def __init__( super().__init__(id=id, phases=phases, bus=bus, **kwargs) self.impedances = impedances if self.phases == "abc": - self._cy_element = CyDeltaAdmittanceLoad(n=self.n, admittances=1.0 / self._impedances) + self._cy_element = CyDeltaAdmittanceLoad(n=self._n, admittances=1.0 / self._impedances) else: - self._cy_element = CyAdmittanceLoad(n=self.n, admittances=1.0 / self._impedances) + self._cy_element = CyAdmittanceLoad(n=self._n, admittances=1.0 / self._impedances) self._cy_connect() @property @@ -500,8 +505,3 @@ def to_dict(self, *, _lf_only: bool = False) -> JsonDict: "phases": self.phases, "impedances": [[z.real, z.imag] for z in self._impedances], } - - -AbstractLoad._power_load_class = PowerLoad -AbstractLoad._current_load_class = CurrentLoad -AbstractLoad._impedance_load_class = ImpedanceLoad diff --git a/roseau/load_flow/models/potential_refs.py b/roseau/load_flow/models/potential_refs.py index 8e0a90b9..3a01ee0e 100644 --- a/roseau/load_flow/models/potential_refs.py +++ b/roseau/load_flow/models/potential_refs.py @@ -57,7 +57,7 @@ def __init__(self, id: Id, element: Bus | Ground, *, phase: str | None = None, * msg = f"Potential reference {self.id!r} is connected to {element!r} which is not a ground nor a bus." logger.error(msg) raise RoseauLoadFlowException(msg, RoseauLoadFlowExceptionCode.BAD_ELEMENT_OBJECT) - self.phase = phase + self._phase = phase self.element = element self._connect(element) self._res_current: complex | None = None @@ -77,8 +77,14 @@ def __init__(self, id: Id, element: Bus | Ground, *, phase: str | None = None, * def __repr__(self) -> str: return f"{type(self).__name__}(id={self.id!r}, element={self.element!r}, phase={self.phase!r})" + @property + def phase(self) -> str | None: + """The phase of the bus set as a potential reference.""" + return self._phase + def _res_current_getter(self, warning: bool) -> complex: - self._res_current = self._cy_element.get_current() + if self._fetch_results: + self._res_current = self._cy_element.get_current() return self._res_getter(self._res_current, warning) @property @@ -111,6 +117,7 @@ def to_dict(self, *, _lf_only: bool = False) -> JsonDict: def results_from_dict(self, data: JsonDict) -> None: self._res_current = complex(*data["current"]) + self._fetch_results = False def _results_to_dict(self, warning: bool) -> JsonDict: i = self._res_current_getter(warning) diff --git a/roseau/load_flow/models/sources.py b/roseau/load_flow/models/sources.py index a621c2c0..a7f003dd 100644 --- a/roseau/load_flow/models/sources.py +++ b/roseau/load_flow/models/sources.py @@ -70,15 +70,15 @@ def __init__( else: self._size = len(set(phases) - {"n"}) - self.phases = phases - self.bus = bus + self._phases = phases + self._bus = bus self.voltages = voltages - self.n = len(self.phases) + self._n = len(self._phases) if self.phases == "abc": - self._cy_element = CyDeltaVoltageSource(n=self.n, voltages=self._voltages) + self._cy_element = CyDeltaVoltageSource(n=self._n, voltages=self._voltages) else: - self._cy_element = CyVoltageSource(n=self.n, voltages=self._voltages) + self._cy_element = CyVoltageSource(n=self._n, voltages=self._voltages) self._cy_connect() # Results @@ -91,6 +91,16 @@ def __repr__(self) -> str: f"phases={self.phases!r})" ) + @property + def phases(self) -> str: + """The phases of the source.""" + return self._phases + + @property + def bus(self) -> Bus: + """The bus of the source.""" + return self._bus + @property @ureg_wraps("V", (None,)) def voltages(self) -> Q_[ComplexArray]: @@ -115,7 +125,8 @@ def voltage_phases(self) -> list[str]: return calculate_voltage_phases(self.phases) def _res_currents_getter(self, warning: bool) -> ComplexArray: - self._res_currents = self._cy_element.get_currents(self.n) + if self._fetch_results: + self._res_currents = self._cy_element.get_currents(self._n) return self._res_getter(value=self._res_currents, warning=warning) @property @@ -159,7 +170,7 @@ def _cy_connect(self): def disconnect(self) -> None: """Disconnect this voltage source from the network. It cannot be used afterwards.""" self._disconnect() - self.bus = None + self._bus = None def _raise_disconnected_error(self) -> None: """Raise an error if the voltage source is disconnected.""" @@ -187,6 +198,7 @@ def to_dict(self, *, _lf_only: bool = False) -> JsonDict: def results_from_dict(self, data: JsonDict) -> None: self._res_currents = np.array([complex(i[0], i[1]) for i in data["currents"]], dtype=np.complex128) + self._fetch_results = False def _results_to_dict(self, warning: bool) -> JsonDict: return { diff --git a/roseau/load_flow/models/tests/test_buses.py b/roseau/load_flow/models/tests/test_buses.py index 5fd92ff9..468db7be 100644 --- a/roseau/load_flow/models/tests/test_buses.py +++ b/roseau/load_flow/models/tests/test_buses.py @@ -74,13 +74,14 @@ def test_short_circuit(): assert len(bus.short_circuits) == 2 # With power load - bus.clear_short_circuits() - assert not bus.short_circuits - PowerLoad(id="load", bus=bus, powers=[10, 10, 10]) - with pytest.raises(RoseauLoadFlowException) as e: - bus.add_short_circuit("a", "b") - assert "is already connected on bus" in e.value.msg - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_SHORT_CIRCUIT + # TODO: clear_short_circuits no longer works + # bus.clear_short_circuits() + # assert not bus.short_circuits + # PowerLoad(id="load", bus=bus, powers=[10, 10, 10]) + # with pytest.raises(RoseauLoadFlowException) as e: + # bus.add_short_circuit("a", "b") + # assert "is already connected on bus" in e.value.msg + # assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_SHORT_CIRCUIT def test_voltage_limits(): diff --git a/roseau/load_flow/models/tests/test_flexible_parameters.py b/roseau/load_flow/models/tests/test_flexible_parameters.py index ba9194a0..b395e5f5 100644 --- a/roseau/load_flow/models/tests/test_flexible_parameters.py +++ b/roseau/load_flow/models/tests/test_flexible_parameters.py @@ -1,17 +1,13 @@ import warnings -from contextlib import contextmanager import numpy as np -import numpy.testing as npt import pytest from matplotlib import pyplot as plt from roseau.load_flow import ( Q_, Control, - ElectricalNetwork, FlexibleParameter, - PowerLoad, Projection, RoseauLoadFlowException, RoseauLoadFlowExceptionCode, @@ -260,7 +256,7 @@ def test_flexible_parameter(): assert e.value.code == RoseauLoadFlowExceptionCode.BAD_FLEXIBLE_PARAMETER_VALUE -@pytest.fixture(params=["constant", "p_max_u_production", "p_max_u_consumption"]) +@pytest.fixture(params=["constant", "p_max_u_production"]) def control_p(request) -> Control: if request.param == "constant": return Control.constant() @@ -280,7 +276,7 @@ def control_q(request) -> Control: raise NotImplementedError(request.param) -@pytest.fixture(params=["keep_p", "keep_q", "euclidean"]) +@pytest.fixture(params=["keep_p", "euclidean"]) def projection(request) -> Projection: return Projection(type=request.param) @@ -290,86 +286,31 @@ def flexible_parameter(control_p, control_q, projection) -> FlexibleParameter: return FlexibleParameter(control_p=control_p, control_q=control_q, projection=projection, s_max=Q_(5, "kVA")) -@pytest.fixture() -def monkeypatch_flexible_parameter_compute_powers(monkeypatch, rg): - @contextmanager - def inner(): - nonlocal monkeypatch - with monkeypatch.context() as m: - m.setattr(target=ElectricalNetwork, name="solve_load_flow", value=lambda *args, **kwargs: 2) - m.setattr( - target=PowerLoad, - name="res_flexible_powers", - value=property( - lambda x: Q_([rg.normal(loc=-2500, scale=1000) + 1j * rg.normal(loc=0, scale=2500)], "VA") - ), - ) - yield m - - return inner - - -def test_plot(flexible_parameter, monkeypatch_flexible_parameter_compute_powers): +def test_plot(flexible_parameter): voltages = np.array(range(205, 256, 1), dtype=float) power = Q_(-2.5 + 1j, "kVA") - auth = ("username", "password") - # # Test compute powers - # - with monkeypatch_flexible_parameter_compute_powers(): - res_flexible_powers = flexible_parameter.compute_powers(auth=auth, voltages=voltages, power=power) + res_flexible_powers = flexible_parameter.compute_powers(voltages=voltages, power=power) - # # Plot control P - # fig, ax = plt.subplots() - ax, res_flexible_powers_1 = flexible_parameter.plot_control_p( - auth=auth, voltages=voltages, power=power, res_flexible_powers=res_flexible_powers, ax=ax - ) - npt.assert_allclose(res_flexible_powers.m_as("VA"), res_flexible_powers_1.m_as("VA")) - plt.close(fig) - - # The same but do not provide the res_flexible_powers - fig, ax = plt.subplots() - with monkeypatch_flexible_parameter_compute_powers(): - ax, res_flexible_powers_2 = flexible_parameter.plot_control_p(auth=auth, voltages=voltages, power=power, ax=ax) - assert not np.allclose(res_flexible_powers.m_as("VA"), res_flexible_powers_2.m_as("VA")) + ax, res_flexible_powers_1 = flexible_parameter.plot_control_p(voltages=voltages, power=power, ax=ax) + assert np.allclose(res_flexible_powers.m_as("VA"), res_flexible_powers_1.m_as("VA")) plt.close(fig) # Plot control Q - ax, res_flexible_powers = flexible_parameter.plot_control_q( - auth=auth, voltages=voltages, power=power, res_flexible_powers=res_flexible_powers, ax=ax - ) - - # The same but do not provide the res_flexible_powers fig, ax = plt.subplots() - with monkeypatch_flexible_parameter_compute_powers(): - ax, res_flexible_powers_3 = flexible_parameter.plot_control_q(auth=auth, voltages=voltages, power=power, ax=ax) - assert not np.allclose(res_flexible_powers.m_as("VA"), res_flexible_powers_3.m_as("VA")) + ax, res_flexible_powers_2 = flexible_parameter.plot_control_q(voltages=voltages, power=power, ax=ax) + assert np.allclose(res_flexible_powers.m_as("VA"), res_flexible_powers_2.m_as("VA")) plt.close(fig) # Plot trajectory in the (P, Q) plane - fig, ax = plt.subplots() - ax, res_flexible_powers_4 = flexible_parameter.plot_pq( - auth=auth, + fig, ax = plt.subplots() # Create a new ax that is not used directly in the following function call + ax, res_flexible_powers_3 = flexible_parameter.plot_pq( voltages=voltages, power=power, - res_flexible_powers=res_flexible_powers, voltages_labels_mask=np.isin(voltages, [240, 250]), - ax=ax, ) - npt.assert_allclose(res_flexible_powers.m_as("VA"), res_flexible_powers_4.m_as("VA")) - plt.close(fig) - - # The same but do not provide the res_flexible_powers - fig, ax = plt.subplots() # Create a new ax that is not used directly in the following function call - with monkeypatch_flexible_parameter_compute_powers(): - ax, res_flexible_powers_5 = flexible_parameter.plot_pq( - auth=auth, - voltages=voltages, - power=power, - voltages_labels_mask=np.isin(voltages, [240, 250]), - ) - assert not np.allclose(res_flexible_powers.m_as("VA"), res_flexible_powers_5.m_as("VA")) + assert np.allclose(res_flexible_powers.m_as("VA"), res_flexible_powers_3.m_as("VA")) plt.close(fig) diff --git a/roseau/load_flow/models/tests/test_phases.py b/roseau/load_flow/models/tests/test_phases.py index 945e6675..5763885f 100644 --- a/roseau/load_flow/models/tests/test_phases.py +++ b/roseau/load_flow/models/tests/test_phases.py @@ -68,7 +68,7 @@ def test_loads_phases(): PowerLoad("load1", bus, phases=ph, powers=[100] * n) # Not in bus - bus.phases = "ab" + bus = Bus("bus", phases="ab") for phase, missing, n in (("abc", "c", 3), ("abn", "n", 2), ("an", "n", 1)): with pytest.raises(RoseauLoadFlowException) as e: PowerLoad("load1", bus, phases=phase, powers=[100] * n) @@ -77,7 +77,7 @@ def test_loads_phases(): # Default for ph, n in (("ab", 1), ("abc", 3), ("abcn", 3)): - bus.phases = ph + bus = Bus("bus", phases=ph) load = PowerLoad("load1", bus, phases=ph, powers=[100] * n) assert load.phases == ph @@ -85,7 +85,7 @@ def test_loads_phases(): class PowerLoadEngine(PowerLoad): _floating_neutral_allowed = True - bus.phases = "ab" + bus = Bus("bus", phases="ab") PowerLoadEngine("load1", bus, phases="abn", powers=[100, 100]) # single-phase floating neutral does not make sense with pytest.raises(RoseauLoadFlowException) as e: @@ -111,7 +111,7 @@ def test_sources_phases(): VoltageSource("source1", bus, phases=ph, voltages=[100] * n) # Not in bus - bus.phases = "ab" + bus = Bus("bus", phases="ab") for phase, missing, n in (("abc", "c", 3), ("abn", "n", 2), ("an", "n", 1)): with pytest.raises(RoseauLoadFlowException) as e: VoltageSource("source1", bus, phases=phase, voltages=[100] * n) @@ -120,7 +120,7 @@ def test_sources_phases(): # Default for ph, n in (("ab", 1), ("abc", 3), ("abcn", 3)): - bus.phases = ph + bus = Bus("bus", phases=ph) vs = VoltageSource("source1", bus, voltages=[100] * n) assert vs.phases == ph @@ -128,7 +128,7 @@ def test_sources_phases(): class VoltageSourceEngine(VoltageSource): _floating_neutral_allowed = True - bus.phases = "ab" + bus = Bus("bus", phases="ab") VoltageSourceEngine("source1", bus, phases="abn", voltages=[100, 100]) # single-phase floating neutral does not make sense with pytest.raises(RoseauLoadFlowException) as e: @@ -157,7 +157,7 @@ def test_lines_phases(): Line("line1", bus1, bus2, phases=ph, parameters=lp, length=10) # Not in bus - bus1.phases = "abc" + bus1 = Bus("bus-1", phases="abc") with pytest.raises(RoseauLoadFlowException) as e: Line("line1", bus1, bus2, phases="abcn", parameters=lp, length=10) assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE @@ -167,8 +167,8 @@ def test_lines_phases(): ) # Default - bus1.phases = "abcn" - bus2.phases = "ca" + bus1 = Bus("bus-1", phases="abcn") + bus2 = Bus("bus-2", phases="ca") lp = LineParameters("test", z_line=10 * np.eye(2, dtype=complex)) line = Line("line1", bus1, bus2, parameters=lp, length=10) assert line.phases == line.phases1 == line.phases2 == "ca" @@ -237,30 +237,30 @@ def test_transformer_three_phases(): Transformer("tr1", bus1, bus2, phases1="abc", phases2="abcn", parameters=tp) # Not in bus - bus2.phases = "abc" + bus2 = Bus("bus-2", phases="abc") with pytest.raises(RoseauLoadFlowException) as e: Transformer("tr1", bus1, bus2, phases1="abc", phases2="abcn", parameters=tp) assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE assert e.value.msg == "Phases (2) ['n'] of transformer 'tr1' are not in phases 'abc' of bus 'bus-2'." # Not in transformer - bus1.phases = "abcn" - bus2.phases = "abcn" + bus1 = Bus("bus-1", phases="abcn") + bus2 = Bus("bus-2", phases="abcn") with pytest.raises(RoseauLoadFlowException) as e: Transformer("tr1", bus1, bus2, phases1="abcn", phases2="abcn", parameters=tp) assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE assert e.value.msg == "Phases (1) 'abcn' of transformer 'tr1' are not compatible with its winding 'D'." # Default - bus1.phases = "abc" - bus2.phases = "abcn" + bus1 = Bus("bus-1", phases="abc") + bus2 = Bus("bus-2", phases="abcn") transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) assert transformer.phases1 == "abc" assert transformer.phases2 == "abcn" # Intersection - bus1.phases = "abcn" - bus2.phases = "abcn" + bus1 = Bus("bus-1", phases="abcn") + bus2 = Bus("bus-2", phases="abcn") transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) assert transformer.phases1 == "abc" assert transformer.phases2 == "abcn" @@ -284,59 +284,52 @@ def test_transformer_single_phases(): Transformer("tr1", bus1, bus2, phases1="an", phases2="an", parameters=tp) # Not in bus - bus2.phases = "ab" + bus2 = Bus("bus-2", phases="ab") with pytest.raises(RoseauLoadFlowException) as e: Transformer("tr1", bus1, bus2, phases1="an", phases2="an", parameters=tp) assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE assert e.value.msg == "Phases (2) ['n'] of transformer 'tr1' are not in phases 'ab' of bus 'bus-2'." # Default - bus1.phases = "ab" - bus2.phases = "ab" + bus1 = Bus("bus-1", phases="ab") + bus2 = Bus("bus-2", phases="ab") transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) assert transformer.phases1 == "ab" assert transformer.phases2 == "ab" # Intersection - bus1.phases = "abcn" - bus2.phases = "ab" + bus1 = Bus("bus-1", phases="abcn") + bus2 = Bus("bus-2", phases="ab") transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) assert transformer.phases1 == "ab" assert transformer.phases2 == "ab" - bus1.phases = "abc" - bus2.phases = "bcn" + bus1 = Bus("bus-1", phases="abc") + bus2 = Bus("bus-2", phases="bcn") transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) assert transformer.phases1 == "bc" assert transformer.phases2 == "bc" - bus1.phases = "abc" - bus2.phases = "ca" + bus1 = Bus("bus-1", phases="abc") + bus2 = Bus("bus-2", phases="ca") transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) assert transformer.phases1 == "ca" assert transformer.phases2 == "ca" # Cannot be deduced - bus1.phases = "abc" - bus2.phases = "abc" - with pytest.raises(RoseauLoadFlowException) as e: - Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE - assert e.value.msg == "Phases (1) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." - - bus1.phases = "abcn" - bus2.phases = "abn" - with pytest.raises(RoseauLoadFlowException) as e: - Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE - assert e.value.msg == "Phases (1) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." - - bus1.phases = "abcn" - bus2.phases = "a" - with pytest.raises(RoseauLoadFlowException) as e: - Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE - assert e.value.msg == "Phases (1) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." + for ph1, ph2 in ( + ("abc", "abc"), + ("abcn", "abn"), + ("abcn", "abc"), + ): + bus1 = Bus("bus-1", phases=ph1) + bus2 = Bus("bus-2", phases=ph2) + with pytest.raises(RoseauLoadFlowException) as e: + Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.msg == ( + "Phases (1) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." + ) def test_transformer_center_phases(): @@ -357,62 +350,49 @@ def test_transformer_center_phases(): Transformer("tr1", bus1, bus2, phases1="ab", phases2="abn", parameters=tp) # Not in bus 1 - bus1.phases = "acn" + bus1 = Bus("bus-1", phases="can") with pytest.raises(RoseauLoadFlowException) as e: Transformer("tr1", bus1, bus2, phases1="ab", phases2="abn", parameters=tp) assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE - assert e.value.msg == "Phases (1) ['b'] of transformer 'tr1' are not in phases 'acn' of bus 'bus-1'." + assert e.value.msg == "Phases (1) ['b'] of transformer 'tr1' are not in phases 'can' of bus 'bus-1'." # Not in bus 2 - bus1.phases = "abc" - bus2.phases = "acn" + bus1 = Bus("bus-1", phases="abc") + bus2 = Bus("bus-2", phases="can") with pytest.raises(RoseauLoadFlowException) as e: Transformer("tr1", bus1, bus2, phases1="ab", phases2="abn", parameters=tp) assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE - assert e.value.msg == "Phases (2) ['b'] of transformer 'tr1' are not in phases 'acn' of bus 'bus-2'." + assert e.value.msg == "Phases (2) ['b'] of transformer 'tr1' are not in phases 'can' of bus 'bus-2'." # Default - bus1.phases = "ab" - bus2.phases = "abn" + bus1 = Bus("bus-1", phases="ab") + bus2 = Bus("bus-2", phases="abn") transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) assert transformer.phases1 == "ab" assert transformer.phases2 == "abn" # Intersection - bus1.phases = "abcn" - bus2.phases = "can" + bus1 = Bus("bus-1", phases="abcn") + bus2 = Bus("bus-2", phases="can") transformer = Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) assert transformer.phases1 == "ca" assert transformer.phases2 == "can" # Cannot be deduced - bus1.phases = "abc" - bus2.phases = "abcn" - with pytest.raises(RoseauLoadFlowException) as e: - Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE - assert e.value.msg == "Phases (1) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." - - bus1.phases = "a" - bus2.phases = "abn" - with pytest.raises(RoseauLoadFlowException) as e: - Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE - assert e.value.msg == "Phases (1) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." - - bus1.phases = "ab" - bus2.phases = "ab" - with pytest.raises(RoseauLoadFlowException) as e: - Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE - assert e.value.msg == "Phases (2) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." - - bus1.phases = "ab" - bus2.phases = "abc" - with pytest.raises(RoseauLoadFlowException) as e: - Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE - assert e.value.msg == "Phases (2) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." + for ph1, ph2, err_ph in ( + ("abc", "abcn", 1), + ("ca", "abn", 1), + ("ab", "ab", 2), + ("ab", "abc", 2), + ): + bus1 = Bus("bus-1", phases=ph1) + bus2 = Bus("bus-2", phases=ph2) + with pytest.raises(RoseauLoadFlowException) as e: + Transformer(id="tr1", bus1=bus1, bus2=bus2, parameters=tp) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.msg == ( + f"Phases ({err_ph}) of transformer 'tr1' cannot be deduced from the buses, they need to be specified." + ) def test_voltage_phases(): diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 25633734..7c38f26d 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -9,15 +9,14 @@ import warnings from collections.abc import Mapping, Sized from importlib import resources -from itertools import cycle +from itertools import chain, cycle from pathlib import Path -from typing import TYPE_CHECKING, NoReturn, TypeVar +from typing import TYPE_CHECKING, TypeVar import geopandas as gpd import numpy as np import pandas as pd from pyproj import CRS -from requests import Response from rich.table import Table from typing_extensions import Self @@ -132,23 +131,8 @@ class ElectricalNetwork(JsonMixin, CatalogueMixin[JsonDict]): } """ - _DEFAULT_TOLERANCE: float = 1e-6 - _DEFAULT_MAX_ITERATIONS: int = 20 - _DEFAULT_BASE_URL: str = "https://load-flow-api-dev.roseautechnologies.com/" - _DEFAULT_WARM_START: bool = True _DEFAULT_SOLVER: Solver = "newton_goldstein" - # Elements classes (for internal use only) - _branch_class = AbstractBranch - _line_class = Line - _transformer_class = Transformer - _switch_class = Switch - _load_class = AbstractLoad - _voltage_source_class = VoltageSource - _bus_class = Bus - _ground_class = Ground - _pref_class = PotentialRef - # # Methods to build an electrical network # @@ -160,7 +144,6 @@ def __init__( sources: MapOrSeq[VoltageSource], grounds: MapOrSeq[Ground], potential_refs: MapOrSeq[PotentialRef], - **kwargs, ) -> None: self.buses = self._elements_as_dict(buses, RoseauLoadFlowExceptionCode.BAD_BUS_ID) self.branches = self._elements_as_dict(branches, RoseauLoadFlowExceptionCode.BAD_BRANCH_ID) @@ -175,9 +158,7 @@ def __init__( self._valid = True self._results_valid: bool = False self.res_info: JsonDict = {} - self._solver: AbstractSolver = AbstractSolver.from_dict( - data={"name": self._DEFAULT_SOLVER, "params": {}}, network=self - ) + self._solver = AbstractSolver.from_dict(data={"name": self._DEFAULT_SOLVER, "params": {}}, network=self) def __repr__(self) -> str: def count_repr(__o: Sized, /, singular: str, plural: str | None = None) -> str: @@ -495,9 +476,9 @@ def to_graph(self) -> "Graph": # def solve_load_flow( self, - max_iterations: int = _DEFAULT_MAX_ITERATIONS, - tolerance: float = _DEFAULT_TOLERANCE, - warm_start: bool = _DEFAULT_WARM_START, + max_iterations: int = 20, + tolerance: float = 1e-6, + warm_start: bool = True, solver: Solver = _DEFAULT_SOLVER, solver_params: JsonDict | None = None, ) -> int: @@ -507,6 +488,11 @@ def solve_load_flow( network (e.g. ``print(net.res_buses``). To get the results for a specific element, use the `res_` properties on the element (e.g. ``print(net.buses["bus1"].res_potentials)``. + You need to activate the license before calling this method. Alternatively you may set the + environment variable ``ROSEAU_LOAD_FLOW_LICENSE_KEY`` to your license key and it will be + picked automatically when calling this method. See the :ref:`license` page for more + information. + Args: max_iterations: The maximum number of allowed iterations. @@ -535,6 +521,9 @@ def solve_load_flow( self._check_validity(constructed=False) self._create_network() self._solver.update_network(self) + warm_start = False + if not self.res_info: + warm_start = False # Update solver if solver != self._solver.name: @@ -607,49 +596,27 @@ def solve_load_flow( "residual": residual, } + # Lazily update the results of the elements + for element in chain( + self.buses.values(), + self.branches.values(), + self.loads.values(), + self.sources.values(), + self.grounds.values(), + self.potential_refs.values(), + ): + element._fetch_results = True + # The results are now valid self._results_valid = True return iterations def reset_inputs(self) -> None: - """Reset the input vector (which is used for the first step of the newton algorithm) to its initial value""" + """Reset the input vector which is used for the first step of the newton algorithm to its initial value.""" if self._solver is not None: self._solver.reset_inputs() - @staticmethod - def _parse_error(response: Response) -> NoReturn: - """Parse a response when its status is not "ok". - - Args: - response: - The response to parse. - """ - content_type = response.headers.get("content-type", None) - code = RoseauLoadFlowExceptionCode.BAD_REQUEST - if response.status_code == 401: - msg = "Authentication failed." - else: - msg = f"There is a problem in the request. Error code {response.status_code}." - if content_type == "application/json": - result_dict: JsonDict = response.json() - if "msg" in result_dict and "code" in result_dict: - # If we have a valid Roseau Load Flow Exception, raise it - try: - code = RoseauLoadFlowExceptionCode.from_string(result_dict["code"]) - except Exception: - msg += f" {result_dict['code']!r} - {result_dict['msg']!r}" - else: - msg = result_dict["msg"] - else: - # Otherwise, raise a generic "Bad request" - msg += response.text - else: - # Non JSON response, raise a generic "Bad request" - msg += response.text - logger.error(msg=msg) - raise RoseauLoadFlowException(msg=msg, code=code) - def _results_from_dict(self, data: JsonDict) -> None: """Dispatch the results to all the elements of the network. @@ -866,6 +833,10 @@ def res_transformers(self) -> pd.DataFrame: - `potential1`: The complex potential of the first bus (in Volts) for the given phase. - `potential2`: The complex potential of the second bus (in Volts) for the given phase. - `max_power`: The maximum power loading (in VoltAmps) of the transformer. + + Note that values for missing phases are set to ``nan``. For example, a "Dyn" transformer + has the phases "abc" on the primary side and "abcn" on the secondary side, so the primary + side values for current, power, and potential for phase "n" will be ``nan``. """ self._warn_invalid_results() res_list = [] @@ -1212,8 +1183,14 @@ def res_potential_refs(self) -> pd.DataFrame: def clear_short_circuits(self) -> None: """Remove the short-circuits of all the buses.""" - for bus in self.buses.values(): - bus.clear_short_circuits() + # for bus in self.buses.values(): + # bus.clear_short_circuits() + msg = ( + "Short circuits cannot be cleared for now. Please recreate the network without the " + "short circuits instead." + ) + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_SHORT_CIRCUIT) # TODO # # Internal methods, please do not use @@ -1317,6 +1294,11 @@ def _check_validity(self, constructed: bool) -> None: elements.update(self.grounds.values()) elements.update(self.potential_refs.values()) + if not elements: + msg = "Cannot create a network without elements." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.EMPTY_NETWORK) + found_source = False for element in elements: # Check connected elements and check network assignment @@ -1402,8 +1384,10 @@ def _propagate_potentials(self, force: bool) -> None: element, potentials = elements.pop(-1) visited.add(element) if isinstance(element, Bus) and (force or not element._initialized): - bus_n = element.n + bus_n = element._n + bus_initialized_by_the_user = element._initialized element.potentials = potentials[0:bus_n] + element._initialized_by_the_user = bus_initialized_by_the_user for e in element._connected_elements: if e not in visited: if isinstance(element, Transformer): @@ -1467,7 +1451,7 @@ def from_dict(cls, data: JsonDict) -> Self: Returns: The constructed network. """ - buses, branches, loads, sources, grounds, p_refs = network_from_dict(data, en_class=cls) + buses, branches, loads, sources, grounds, p_refs = network_from_dict(data) return cls( buses=buses, branches=branches, @@ -1554,7 +1538,7 @@ def from_dgs(cls, path: StrPath) -> Self: Returns: The constructed network. """ - buses, branches, loads, sources, grounds, potential_refs = network_from_dgs(path, en_class=cls) + buses, branches, loads, sources, grounds, potential_refs = network_from_dgs(path) return cls( buses=buses, branches=branches, diff --git a/roseau/load_flow/tests/test_electrical_network.py b/roseau/load_flow/tests/test_electrical_network.py index cc04c130..393e3092 100644 --- a/roseau/load_flow/tests/test_electrical_network.py +++ b/roseau/load_flow/tests/test_electrical_network.py @@ -2,14 +2,12 @@ import re import warnings from contextlib import contextmanager -from urllib.parse import urljoin import geopandas as gpd import networkx as nx import numpy as np import pandas as pd import pytest -import requests_mock from pandas.testing import assert_frame_equal from shapely import LineString, Point @@ -103,7 +101,8 @@ def single_phase_network() -> ElectricalNetwork: def good_json_results() -> dict: return { "info": { - "solver": "newton", + "solver": "newton_goldstein", + "solver_params": {"m1": 0.1, "m2": 0.9}, "tolerance": 1e-06, "max_iterations": 20, "warm_start": True, @@ -169,10 +168,10 @@ def good_json_results() -> dict: "id": "vs", "phases": "abcn", "currents": [ - [-0.005000012500031251, 0.0], + [-0.00500001250003125, -8.673617379884035e-19], [0.0025000062499482426, 0.004330137844227901], - [0.0025000062499482435, -0.004330137844227901], - [1.3476481215690672e-13, -2.891210611954938e-19], + [0.0025000062499482426, -0.0043301378442279], + [1.3476481215690672e-13, -2.891203383964549e-19], ], } ], @@ -572,90 +571,18 @@ def test_frame(small_network: ElectricalNetwork): assert sources_df.index.name == "id" -def test_frame_empty_network(monkeypatch): - # Test that we can create dataframes even if a certain element is not present in the network - monkeypatch.setattr(ElectricalNetwork, "_check_validity", lambda self, constructed: None) - monkeypatch.setattr(ElectricalNetwork, "_warn_invalid_results", lambda self: None) - empty_network = ElectricalNetwork( - buses={}, - branches={}, - loads={}, - sources={}, - grounds={}, - potential_refs={}, - ) - # Buses - buses = empty_network.buses_frame - assert buses.shape == (0, 4) - assert buses.empty - - # Branches - branches = empty_network.branches_frame - assert branches.shape == (0, 6) - assert branches.empty - - # Transformers - transformers = empty_network.transformers_frame - assert transformers.shape == (0, 7) - assert transformers.empty - - # Lines - lines = empty_network.lines_frame - assert lines.shape == (0, 6) - assert lines.empty - - # Switches - switches = empty_network.switches_frame - assert switches.shape == (0, 4) - assert switches.empty - - # Loads - loads = empty_network.loads_frame - assert loads.shape == (0, 2) - assert loads.empty - - # Sources - sources = empty_network.sources_frame - assert sources.shape == (0, 2) - assert sources.empty - - # Res buses - res_buses = empty_network.res_buses - assert res_buses.shape == (0, 1) - assert res_buses.empty - res_buses_voltages = empty_network.res_buses_voltages - assert res_buses_voltages.shape == (0, 4) - assert res_buses_voltages.empty - - # Res branches - res_branches = empty_network.res_branches - assert res_branches.shape == (0, 7) - assert res_branches.empty - - # Res transformers - res_transformers = empty_network.res_transformers - assert res_transformers.shape == (0, 8) - assert res_transformers.empty - - # Res lines - res_lines = empty_network.res_lines - assert res_lines.shape == (0, 10) - assert res_lines.empty - - # Res switches - res_switches = empty_network.res_switches - assert res_switches.shape == (0, 6) - assert res_switches.empty - - # Res loads - res_loads = empty_network.res_loads - assert res_loads.shape == (0, 3) - assert res_loads.empty - - # Res sources - res_sources = empty_network.res_sources - assert res_sources.shape == (0, 3) - assert res_sources.empty +def test_empty_network(): + with pytest.raises(RoseauLoadFlowException) as exc_info: + ElectricalNetwork( + buses={}, + branches={}, + loads={}, + sources={}, + grounds={}, + potential_refs={}, + ) + assert exc_info.value.code == RoseauLoadFlowExceptionCode.EMPTY_NETWORK + assert exc_info.value.msg == "Cannot create a network without elements." def test_buses_voltages(small_network: ElectricalNetwork, good_json_results): @@ -767,58 +694,7 @@ def test_single_phase_network(single_phase_network: ElectricalNetwork): line = single_phase_network.branches["line"] load = single_phase_network.loads["load"] - json_results = { - "info": { - "solver": "newton", - "tolerance": 1e-06, - "max_iterations": 20, - "status": "success", - "iterations": 1, - "warm_start": True, - "residual": 1.3239929985697785e-13, - }, - "buses": [ - { - "id": "bus0", - "phases": "bn", - "potentials": [[19999.94999975, 0.0], [-0.050000250001249996, 0.0]], - }, - {"id": "bus1", "phases": "bn", "potentials": [[19999.899999499998, 0.0], [0.0, 0.0]]}, - ], - "branches": [ - { - "id": "line", - "phases1": "bn", - "phases2": "bn", - "currents1": [[0.005000025000117603, 0.0], [-0.005000025000125, 0.0]], - "currents2": [[-0.005000025000117603, -0.0], [0.005000025000125, -0.0]], - } - ], - "loads": [ - { - "id": "load", - "phases": "bn", - "currents": [[0.005000025000250002, -0.0], [-0.005000025000250002, 0.0]], - } - ], - "sources": [ - { - "id": "vs", - "phases": "bn", - "currents": [[-0.005000025000125, 0.0], [0.005000025000125, 0.0]], - }, - ], - "grounds": [ - {"id": "ground", "potential": [0.0, 0.0]}, - ], - "potential_refs": [ - {"id": "pref", "current": [-1.2500243895541274e-13, 0.0]}, - ], - } - solve_url = urljoin(ElectricalNetwork._DEFAULT_BASE_URL, "solve/") - with requests_mock.Mocker() as m: - m.post(solve_url, status_code=200, json=json_results, headers={"content-type": "application/json"}) - single_phase_network.solve_load_flow() + single_phase_network.solve_load_flow() # Test results of elements # ------------------------ @@ -1118,7 +994,7 @@ def test_network_elements(small_network: ElectricalNetwork): assert element.network == small_network_2 -def test_network_results_warning(small_network: ElectricalNetwork, good_json_results, recwarn): # noqa: C901 +def test_network_results_warning(small_network: ElectricalNetwork, recwarn): # noqa: C901 # network well-defined using the constructor for bus in small_network.buses.values(): assert bus.network == small_network @@ -1167,10 +1043,7 @@ def test_network_results_warning(small_network: ElectricalNetwork, good_json_res assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN # Solve a load flow - solve_url = urljoin(ElectricalNetwork._DEFAULT_BASE_URL, "solve/") - with requests_mock.Mocker() as m: - m.post(solve_url, status_code=200, json=good_json_results, headers={"content-type": "application/json"}) - small_network.solve_load_flow(auth=("", "")) + small_network.solve_load_flow() # No warning when getting results (they are up-to-date) recwarn.clear() @@ -1728,72 +1601,81 @@ def test_load_flow_results_frames(small_network: ElectricalNetwork, good_json_re assert_frame_equal(small_network.res_loads_flexible_powers, expected_res_flex_powers, rtol=1e-4) -def test_solver_warm_start(small_network: ElectricalNetwork, good_json_results): +def test_solver_warm_start(small_network: ElectricalNetwork, good_json_results, monkeypatch): load: PowerLoad = small_network.loads["load"] load_bus = small_network.buses["bus1"] - solve_url = urljoin(ElectricalNetwork._DEFAULT_BASE_URL, "solve/") - headers = {"Content-Type": "application/json"} - - def json_callback(request, context): - request_json_data = request.json() - warm_start = request_json_data["solver"]["warm_start"] - assert isinstance(request_json_data, dict) - assert "network" in request_json_data - if should_warm_start: - assert warm_start # Make sure the warm start flag is set by the user - assert "results" in request_json_data + + original_propagate_potentials = small_network._propagate_potentials + + def compare_results(expected, obtained): + if isinstance(expected, dict): + assert isinstance(obtained, dict) + for key, value in expected.items(): + assert key in obtained + compare_results(value, obtained[key]) + elif isinstance(expected, list | tuple): + assert isinstance(obtained, list | tuple) + for i, item in enumerate(expected): + compare_results(item, obtained[i]) + elif isinstance(expected, complex | float | int): + assert isinstance(obtained, complex | float | int) + assert np.isclose(expected, obtained, atol=1e-1) else: - assert "results" not in request_json_data - if not warm_start: - # Make sure to not warm start if the user does not want warm start - assert not should_warm_start - assert "results" not in request_json_data - return good_json_results + assert isinstance(obtained, type(expected)) + assert expected == obtained + + def _propagate_potentials(force): + if should_not_propagate_potentials: + raise AssertionError("Should not propagate potentials") + return original_propagate_potentials(force) + + monkeypatch.setattr(small_network, "_propagate_potentials", _propagate_potentials) # First case: network is valid, no results yet -> no warm start - should_warm_start = False + should_not_propagate_potentials = False + good_json_results["info"]["warm_start"] = False assert small_network._valid assert not small_network.res_info # No results assert not small_network._results_valid # Results are not valid by default - with requests_mock.Mocker() as m, warnings.catch_warnings(): + with warnings.catch_warnings(): warnings.simplefilter("error") # Make sure there is no warning - m.post(solve_url, status_code=200, json=json_callback, headers=headers) - small_network.solve_load_flow(auth=("", ""), warm_start=True) - assert small_network.results_to_dict() == good_json_results + good_json_results["info"]["iterations"] = small_network.solve_load_flow(warm_start=True) + compare_results(good_json_results, small_network.results_to_dict()) # Second case: the user requested no warm start (even though the network and results are valid) - should_warm_start = False + should_not_propagate_potentials = False + good_json_results["info"]["warm_start"] = False assert small_network._valid assert small_network._results_valid - with requests_mock.Mocker() as m, warnings.catch_warnings(): + with warnings.catch_warnings(): warnings.simplefilter("error") # Make sure there is no warning - m.post(solve_url, status_code=200, json=json_callback, headers=headers) - small_network.solve_load_flow(auth=("", ""), warm_start=False) - assert small_network.results_to_dict() == good_json_results + good_json_results["info"]["iterations"] = small_network.solve_load_flow(warm_start=False) + compare_results(good_json_results, small_network.results_to_dict()) # Third case: network is valid, results are valid -> warm start - should_warm_start = True + should_not_propagate_potentials = True + good_json_results["info"]["warm_start"] = True assert small_network._valid assert small_network._results_valid - with requests_mock.Mocker() as m, warnings.catch_warnings(): + with warnings.catch_warnings(): warnings.simplefilter("error") # Make sure there is no warning - m.post(solve_url, status_code=200, json=json_callback, headers=headers) - small_network.solve_load_flow(auth=("", ""), warm_start=True) - assert small_network.results_to_dict() == good_json_results + good_json_results["info"]["iterations"] = small_network.solve_load_flow(warm_start=True) + compare_results(good_json_results, small_network.results_to_dict()) # Fourth case (load powers changes): network is valid, results are not valid -> warm start - should_warm_start = True + should_not_propagate_potentials = True + good_json_results["info"]["warm_start"] = True load.powers = load.powers + Q_(1 + 1j, "VA") assert small_network._valid assert not small_network._results_valid - with requests_mock.Mocker() as m, warnings.catch_warnings(): + with warnings.catch_warnings(): warnings.simplefilter("error") # Make sure there is no warning - m.post(solve_url, status_code=200, json=json_callback, headers=headers) - small_network.solve_load_flow(auth=("", ""), warm_start=True) - assert small_network.results_to_dict() == good_json_results + good_json_results["info"]["iterations"] = small_network.solve_load_flow(warm_start=True) + compare_results(good_json_results, small_network.results_to_dict()) # Fifth case: network is not valid -> no warm start - should_warm_start = False + should_not_propagate_potentials = False + good_json_results["info"]["warm_start"] = False new_load = PowerLoad("new_load", load_bus, powers=[100, 200, 300], phases=load.phases) new_load_result = good_json_results["loads"][0].copy() new_load_result["id"] = "new_load" @@ -1801,15 +1683,14 @@ def json_callback(request, context): assert new_load.network is small_network assert not small_network._valid assert not small_network._results_valid - with requests_mock.Mocker() as m, warnings.catch_warnings(): + with warnings.catch_warnings(): # We could warn here that the user requested warm start but the network is not valid # but this will be disruptive for the user especially that warm start is the default warnings.simplefilter("error") # Make sure there is no warning - m.post(solve_url, status_code=200, json=json_callback, headers=headers) assert not small_network._valid assert not small_network._results_valid - small_network.solve_load_flow(auth=("", ""), warm_start=True) - assert small_network.results_to_dict() == good_json_results + good_json_results["info"]["iterations"] = small_network.solve_load_flow(warm_start=True) + compare_results(good_json_results, small_network.results_to_dict()) def test_short_circuits(): @@ -1827,8 +1708,10 @@ def test_short_circuits(): assert_frame_equal(en.short_circuits_frame, df) assert bus.short_circuits - en.clear_short_circuits() - assert not bus.short_circuits + + # TODO: clear_short_circuits no longer works + # en.clear_short_circuits() + # assert not bus.short_circuits def test_catalogue_data(): diff --git a/roseau/load_flow/typing.py b/roseau/load_flow/typing.py index a18fd88f..696ae186 100644 --- a/roseau/load_flow/typing.py +++ b/roseau/load_flow/typing.py @@ -30,10 +30,6 @@ Available solvers for the load flow computation. -.. class:: Authentication - - Valid authentication types used to connect to the Roseau Load Flow solver API. - .. class:: MapOrSeq A mapping from element IDs to elements or a sequence of elements of unique IDs. @@ -58,7 +54,6 @@ import numpy as np from numpy.typing import NDArray -from requests.auth import HTTPBasicAuth from roseau.load_flow.units import Q_ @@ -70,7 +65,6 @@ ControlType: TypeAlias = Literal["constant", "p_max_u_production", "p_max_u_consumption", "q_u"] ProjectionType: TypeAlias = Literal["euclidean", "keep_p", "keep_q"] Solver: TypeAlias = Literal["newton", "newton_goldstein"] -Authentication: TypeAlias = tuple[str, str] | HTTPBasicAuth MapOrSeq: TypeAlias = Mapping[Id, T] | Sequence[T] ComplexArray: TypeAlias = NDArray[np.complex128] # TODO: improve the types below when shape-typing becomes supported @@ -89,7 +83,6 @@ "ControlType", "ProjectionType", "Solver", - "Authentication", "MapOrSeq", "ComplexArray", "ComplexArrayLike1D", diff --git a/roseau/load_flow/utils/_versions.py b/roseau/load_flow/utils/_versions.py index 8b1cce24..a2af81e2 100644 --- a/roseau/load_flow/utils/_versions.py +++ b/roseau/load_flow/utils/_versions.py @@ -18,7 +18,7 @@ def _get_sys_info() -> JsonDict: def _get_dependency_info() -> JsonDict: """Get versions of dependencies.""" - return {dist: version(dist) for dist in ("pandas", "numpy", "geopandas", "shapely", "regex", "pint", "requests")} + return {dist: version(dist) for dist in ("pandas", "numpy", "geopandas", "shapely", "regex", "pint")} def show_versions() -> None: From 44aae0a625a8cf521eac1de9d5b73d523258985e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Vinot?= Date: Wed, 10 Jan 2024 14:05:41 +0100 Subject: [PATCH 15/51] New year! --- LICENSE.md | 2 +- doc/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index aed1a24a..305daf9a 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2023, Roseau Technologies +Copyright (c) 2018--2024, Roseau Technologies Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/doc/conf.py b/doc/conf.py index ac236690..8df59650 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -18,7 +18,7 @@ # -- Project information ----------------------------------------------------- project = "Roseau Load Flow" -copyright = "2022--2024, Roseau Technologies SAS" +copyright = "2018--2024, Roseau Technologies SAS" # author = "Benoît Vinot" # The full version, including alpha/beta/rc tags From 064b1f0503e71031e76cd3bdf9817ceca83e2086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Vinot?= Date: Wed, 10 Jan 2024 14:06:39 +0100 Subject: [PATCH 16/51] Change the license in pyproject.toml file --- poetry.lock | 32 ++++++++++++++++++++++++++------ pyproject.toml | 2 +- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 84cdefc6..5b656fed 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "alabaster" -version = "0.7.15" +version = "0.7.16" description = "A light, configurable Sphinx theme" optional = false python-versions = ">=3.9" files = [ - {file = "alabaster-0.7.15-py3-none-any.whl", hash = "sha256:d99c6fd0f7a86fca68ecc5231c9de45227991c10ee6facfb894cf6afb953b142"}, - {file = "alabaster-0.7.15.tar.gz", hash = "sha256:0127f4b1db0afc914883f930e3d40763131aebac295522fc4a04d9e77c703705"}, + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] [[package]] @@ -868,6 +868,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1518,6 +1528,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1525,8 +1536,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1543,6 +1561,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1550,6 +1569,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1973,13 +1993,13 @@ test = ["pytest"] [[package]] name = "sphinxcontrib-bibtex" -version = "2.6.1" +version = "2.6.2" description = "Sphinx extension for BibTeX style citations." optional = false python-versions = ">=3.7" files = [ - {file = "sphinxcontrib-bibtex-2.6.1.tar.gz", hash = "sha256:046b49f070ae5276af34c1b8ddb9bc9562ef6de2f7a52d37a91cb8e53f54b863"}, - {file = "sphinxcontrib_bibtex-2.6.1-py3-none-any.whl", hash = "sha256:094c772098fe6b030cda8618c45722b2957cad0c04f328ba2b154aa08dfe720a"}, + {file = "sphinxcontrib-bibtex-2.6.2.tar.gz", hash = "sha256:f487af694336f28bfb7d6a17070953a7d264bec43000a2379724274f5f8d70ae"}, + {file = "sphinxcontrib_bibtex-2.6.2-py3-none-any.whl", hash = "sha256:10d45ebbb19207c5665396c9446f8012a79b8a538cb729f895b5910ab2d0b2da"}, ] [package.dependencies] diff --git a/pyproject.toml b/pyproject.toml index 6bbf33fa..2ec5ffce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ "Victor Gouin", ] maintainers = ["Ali Hamdan "] -license = "Proprietary" +license = "BSD-3-Clause" repository = "https://github.com/RoseauTechnologies/Roseau_Load_Flow/" readme = "README.md" include = [ From db1847f4c567342a989cc8064b8f4ec28bce30b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Vinot?= Date: Wed, 10 Jan 2024 14:07:25 +0100 Subject: [PATCH 17/51] Activate the license from the environment --- roseau/load_flow/_solvers.py | 3 +++ roseau/load_flow/license.py | 33 ++++++++++++++++++++------------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/roseau/load_flow/_solvers.py b/roseau/load_flow/_solvers.py index a6baa900..0061cf90 100644 --- a/roseau/load_flow/_solvers.py +++ b/roseau/load_flow/_solvers.py @@ -5,6 +5,7 @@ import numpy as np from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode +from roseau.load_flow.license import activate_license, get_license from roseau.load_flow.typing import JsonDict, Solver from roseau.load_flow_engine.cy_engine import CyAbstractSolver, CyNewton, CyNewtonGoldstein @@ -73,6 +74,8 @@ def solve_load_flow(self, max_iterations: int, tolerance: float) -> tuple[int, f Returns: The number of iterations and the final residual """ + if get_license() is None: + activate_license(None) return self._cy_solver.solve_load_flow(max_iterations=max_iterations, tolerance=tolerance) def reset_inputs(self): diff --git a/roseau/load_flow/license.py b/roseau/load_flow/license.py index e522b3aa..10f1afaf 100644 --- a/roseau/load_flow/license.py +++ b/roseau/load_flow/license.py @@ -1,17 +1,16 @@ import datetime as dt +import os import certifi from platformdirs import user_cache_dir -from roseau.load_flow_engine.cy_engine import ( - CyLicense, - cy_activate_license, - cy_deactivate_license, - cy_get_license, -) +from roseau.load_flow_engine.cy_engine import CyLicense, cy_activate_license, cy_deactivate_license, cy_get_license __all__ = ["activate_license", "deactivate_license", "get_license", "License"] +_license = None +"""str|None: The Python copy of the activated license.""" + # # License class accessor @@ -65,25 +64,33 @@ def get_username() -> str: return CyLicense.get_username() -def activate_license(key: str) -> None: +def activate_license(key: str | None) -> None: """Activate the license with the given key in the current process. Args: key: - The key of the license to activate. + The key of the license to activate. If None is provided, the environment variable + `ROSEAU_LOAD_FLOW_LICENSE_KEY` is read. """ + if key is None: + key = os.getenv("ROSEAU_LOAD_FLOW_LICENSE_KEY", "") cy_activate_license(key=key, cacert_filepath=certifi.where(), cache_folderpath=user_cache_dir()) def deactivate_license() -> None: """Deactivate the license in the current process.""" + global _license cy_deactivate_license() + _license = None def get_license() -> License | None: """A function to retrieve the currently active license.""" - cy_license = cy_get_license() - if cy_license is None: - return None - else: - return License(cy_license=cy_license) + global _license + if _license is None: + cy_license = cy_get_license() + if cy_license is None: + return None + else: + _license = License(cy_license=cy_license) + return _license From 7cd77182905f458884ccf3c1122196fa414d9d9d Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Wed, 10 Jan 2024 15:22:58 +0100 Subject: [PATCH 18/51] Fix res_transformers returning an empty dataframe (#163) Fixes #158 The property `res_branches` was using similar technique to `res_transformers`. I changed both to avoid future breakage. --- doc/Changelog.md | 2 + roseau/load_flow/network.py | 186 ++++++++++++++---------------------- 2 files changed, 73 insertions(+), 115 deletions(-) diff --git a/doc/Changelog.md b/doc/Changelog.md index b6dfec49..df1e3a36 100644 --- a/doc/Changelog.md +++ b/doc/Changelog.md @@ -2,6 +2,8 @@ ## Unreleased +- {gh-pr}`163` {gh-issue}`158` Fix `ElectricalNetwork.res_transformers` returning an empty dataframe + when max_power is not set. - {gh-pr}`151` Require Python 3.10 or newer. ## Version 0.6.0 diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 93ad58b5..3bac5605 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -769,63 +769,44 @@ def res_branches(self) -> pd.DataFrame: - `potential2`: The complex potential of the second bus (in Volts) for the given phase. """ self._warn_invalid_results() - res_list = [] + res_dict = { + "branch_id": [], + "phase": [], + "branch_type": [], + "current1": [], + "current2": [], + "power1": [], + "power2": [], + "potential1": [], + "potential2": [], + } + dtypes = {c: _DTYPES[c] for c in res_dict} for branch_id, branch in self.branches.items(): currents1, currents2 = branch._res_currents_getter(warning=False) powers1, powers2 = branch._res_powers_getter(warning=False) potentials1, potentials2 = branch._res_potentials_getter(warning=False) - res_list.extend( - { - "branch_id": branch_id, - "phase": phase, - "branch_type": branch.branch_type, - "current1": i1, - "current2": None, - "power1": s1, - "power2": None, - "potential1": v1, - "potential2": None, - } - for i1, s1, v1, phase in zip(currents1, powers1, potentials1, branch.phases1, strict=True) - ) - res_list.extend( - { - "branch_id": branch_id, - "phase": phase, - "branch_type": branch.branch_type, - "current1": None, - "current2": i2, - "power1": None, - "power2": s2, - "potential1": None, - "potential2": v2, - } - for i2, s2, v2, phase in zip(currents2, powers2, potentials2, branch.phases2, strict=True) - ) - - columns = [ - "branch_id", - "phase", - "branch_type", - "current1", - "current2", - "power1", - "power2", - "potential1", - "potential2", - ] - dtypes = {c: _DTYPES[c] for c in columns} - return ( - pd.DataFrame.from_records(res_list, columns=columns) - .astype(dtypes) - # aggregate x1 and x2 for the same phase for I, V, S, ... - .groupby(["branch_id", "phase", "branch_type"], observed=True) - # there are 2 values of I, V, S, ...; only one is not nan -> keep it - .mean() - # if all values are nan -> drop the row (the phase does not exist) - .dropna(how="all") - .reset_index(level="branch_type") - ) + phases = sorted(set(branch.phases1) | set(branch.phases2)) + for phase in phases: + if phase in branch.phases1: + idx1 = branch.phases2.index(phase) + i1, s1, v1 = currents1[idx1], powers1[idx1], potentials1[idx1] + else: + i1, s1, v1 = None, None, None + if phase in branch.phases2: + idx2 = branch.phases2.index(phase) + i2, s2, v2 = currents2[idx2], powers2[idx2], potentials2[idx2] + else: + i2, s2, v2 = None, None, None + res_dict["branch_id"].append(branch_id) + res_dict["phase"].append(phase) + res_dict["branch_type"].append(branch.branch_type) + res_dict["current1"].append(i1) + res_dict["current2"].append(i2) + res_dict["power1"].append(s1) + res_dict["power2"].append(s2) + res_dict["potential1"].append(v1) + res_dict["potential2"].append(v2) + return pd.DataFrame(res_dict).astype(dtypes).set_index(["branch_id", "phase"]) @property def res_transformers(self) -> pd.DataFrame: @@ -852,7 +833,19 @@ def res_transformers(self) -> pd.DataFrame: - `max_power`: The maximum power loading (in VoltAmps) of the transformer. """ self._warn_invalid_results() - res_list = [] + res_dict = { + "transformer_id": [], + "phase": [], + "current1": [], + "current2": [], + "power1": [], + "power2": [], + "potential1": [], + "potential2": [], + "max_power": [], + "violated": [], + } + dtypes = {c: _DTYPES[c] for c in res_dict} for branch in self.branches.values(): if not isinstance(branch, Transformer): continue @@ -863,65 +856,29 @@ def res_transformers(self) -> pd.DataFrame: violated = None if s_max is not None: violated = max(abs(sum(powers1)), abs(sum(powers2))) > s_max - res_list.extend( - { - "transformer_id": branch.id, - "phase": phase, - "current1": i1, - "current2": None, - "power1": s1, - "power2": None, - "potential1": v1, - "potential2": None, - "max_power": s_max, - "violated": violated, - } - for i1, s1, v1, phase in zip(currents1, powers1, potentials1, branch.phases1, strict=True) - ) - res_list.extend( - { - "transformer_id": branch.id, - "phase": phase, - "current1": None, - "current2": i2, - "power1": None, - "power2": s2, - "potential1": None, - "potential2": v2, - "max_power": s_max, - "violated": violated, - } - for i2, s2, v2, phase in zip(currents2, powers2, potentials2, branch.phases2, strict=True) - ) - - columns = [ - "transformer_id", - "phase", - "current1", - "current2", - "power1", - "power2", - "potential1", - "potential2", - "max_power", - "violated", - ] - dtypes = {c: _DTYPES[c] for c in columns} - res = ( - pd.DataFrame.from_records(res_list, columns=columns) - .astype(dtypes) - # aggregate x1 and x2 for the same phase for I, V, S, ... - .groupby(["transformer_id", "phase", "max_power", "violated"], observed=True) - # there are 2 values of I, V, S, ...; only one is not nan -> keep it - .mean() - # if all values are nan -> drop the row (the phase does not exist) - .dropna(how="all") - .reset_index(level=["max_power", "violated"]) - ) - # move the max_power and violated columns to the end - res["max_power"] = res.pop("max_power") - res["violated"] = res.pop("violated") - return res + phases = sorted(set(branch.phases1) | set(branch.phases2)) + for phase in phases: + if phase in branch.phases1: + idx1 = branch.phases2.index(phase) + i1, s1, v1 = currents1[idx1], powers1[idx1], potentials1[idx1] + else: + i1, s1, v1 = None, None, None + if phase in branch.phases2: + idx2 = branch.phases2.index(phase) + i2, s2, v2 = currents2[idx2], powers2[idx2], potentials2[idx2] + else: + i2, s2, v2 = None, None, None + res_dict["transformer_id"].append(branch.id) + res_dict["phase"].append(phase) + res_dict["current1"].append(i1) + res_dict["current2"].append(i2) + res_dict["power1"].append(s1) + res_dict["power2"].append(s2) + res_dict["potential1"].append(v1) + res_dict["potential2"].append(v2) + res_dict["max_power"].append(s_max) + res_dict["violated"].append(violated) + return pd.DataFrame(res_dict).astype(dtypes).set_index(["transformer_id", "phase"]) @property def res_lines(self) -> pd.DataFrame: @@ -1003,8 +960,7 @@ def res_lines(self) -> pd.DataFrame: res_dict["series_current"].append(i_series) res_dict["max_current"].append(i_max) res_dict["violated"].append(violated) - res = pd.DataFrame(res_dict).astype(dtypes).set_index(["line_id", "phase"]) - return res + return pd.DataFrame(res_dict).astype(dtypes).set_index(["line_id", "phase"]) @property def res_switches(self) -> pd.DataFrame: From 64e5667196ae1392fffac1ec6373abca54847dd4 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Wed, 10 Jan 2024 17:32:15 +0100 Subject: [PATCH 19/51] Remove clear_short_circuit and improve error message --- doc/usage/Short_Circuit.md | 166 +++++++++--------- roseau/load_flow/models/buses.py | 19 +- roseau/load_flow/models/tests/test_buses.py | 36 ++-- roseau/load_flow/network.py | 11 -- .../tests/test_electrical_network.py | 4 - 5 files changed, 114 insertions(+), 122 deletions(-) diff --git a/doc/usage/Short_Circuit.md b/doc/usage/Short_Circuit.md index 6e6d3760..7ce4a703 100644 --- a/doc/usage/Short_Circuit.md +++ b/doc/usage/Short_Circuit.md @@ -15,49 +15,53 @@ is impossible. >>> import numpy as np ... from roseau.load_flow import * ->>> # Create three buses -... source_bus = Bus(id="sb", phases="abcn") -... bus1 = Bus(id="b1", phases="abcn") -... bus2 = Bus(id="b2", phases="abcn") - ->>> # Define the reference of potentials -... ground = Ground(id="gnd") -... pref = PotentialRef(id="pref", element=ground) -... ground.connect(bus=source_bus) - ->>> # Create a LV source at the first bus -... un = 400 / np.sqrt(3) -... source_voltages = [un, un * np.exp(-2j * np.pi / 3), un * np.exp(2j * np.pi / 3)] -... vs = VoltageSource(id="vs", bus=source_bus, phases="abcn", voltages=source_voltages) - ->>> # Add LV lines -... lp1 = LineParameters.from_geometry( -... "U_AL_240", -... line_type=LineType.UNDERGROUND, -... conductor_type=ConductorType.AL, -... insulator_type=InsulatorType.PVC, -... section=240, -... section_neutral=240, -... height=Q_(-1.5, "m"), -... external_diameter=Q_(40, "mm"), -... ) -... line1 = Line( -... id="line1", bus1=source_bus, bus2=bus1, parameters=lp1, length=1.0, ground=ground -... ) -... lp2 = LineParameters.from_geometry( -... "U_AL_150", -... line_type=LineType.UNDERGROUND, -... conductor_type=ConductorType.AL, -... insulator_type=InsulatorType.PVC, -... section=150, -... section_neutral=150, -... height=Q_(-1.5, "m"), -... external_diameter=Q_(40, "mm"), -... ) -... line2 = Line(id="line2", bus1=bus1, bus2=bus2, parameters=lp2, length=2.0, ground=ground) +>>> def create_network(): +... # Create three buses +... source_bus = Bus(id="sb", phases="abcn") +... bus1 = Bus(id="b1", phases="abcn") +... bus2 = Bus(id="b2", phases="abcn") +... # Define the reference of potentials +... ground = Ground(id="gnd") +... pref = PotentialRef(id="pref", element=ground) +... ground.connect(bus=source_bus) +... # Create a LV source at the first bus +... un = 400 / np.sqrt(3) +... source_voltages = [un, un * np.exp(-2j * np.pi / 3), un * np.exp(2j * np.pi / 3)] +... vs = VoltageSource(id="vs", bus=source_bus, phases="abcn", voltages=source_voltages) +... # Add LV lines +... lp1 = LineParameters.from_geometry( +... "U_AL_240", +... line_type=LineType.UNDERGROUND, +... conductor_type=ConductorType.AL, +... insulator_type=InsulatorType.PVC, +... section=240, +... section_neutral=240, +... height=Q_(-1.5, "m"), +... external_diameter=Q_(40, "mm"), +... ) +... line1 = Line( +... id="line1", bus1=source_bus, bus2=bus1, parameters=lp1, length=1.0, ground=ground +... ) +... lp2 = LineParameters.from_geometry( +... "U_AL_150", +... line_type=LineType.UNDERGROUND, +... conductor_type=ConductorType.AL, +... insulator_type=InsulatorType.PVC, +... section=150, +... section_neutral=150, +... height=Q_(-1.5, "m"), +... external_diameter=Q_(40, "mm"), +... ) +... line2 = Line( +... id="line2", bus1=bus1, bus2=bus2, parameters=lp2, length=2.0, ground=ground +... ) +... # Create network +... en = ElectricalNetwork.from_element(source_bus) +... return en +... >>> # Create network -... en = ElectricalNetwork.from_element(source_bus) +... en = create_network() ``` ## Phase-to-phase @@ -65,7 +69,7 @@ is impossible. We can now add a short-circuit. Let's first create a phase-to-phase short-circuit: ```pycon ->>> bus2.add_short_circuit("a", "b") +>>> en.buses["b2"].add_short_circuit("a", "b") ``` Let's run the load flow, and get the current results. @@ -86,16 +90,16 @@ All the following tables are rounded to 2 decimals to be properly displayed. >>> en.res_branches ``` -| branch_id | phase | current1 | current2 | power1 | power2 | potential1 | potential2 | -| :-------- | :---- | -------------: | -----------------: | -----------------: | ----------------------: | --------------: | ----------------: | -| line1 | a | 376.73+75.27j | -376.51-75.17j | 87001.18-17383.7j | -69627.17+24139.23j | 230.94+0j | 190.15-26.15j | -| line1 | b | -376.14-74.96j | 376.12+74.96j | 58424.25+66571.96j | -41140.25-59810.05j | -115.47-200j | -74.72-173.91j | -| line1 | c | -0.49-0.42j | 0.49+0.21j | -26.73-147j | -14.9+126.71j | -115.47+200j | -117.06+208.26j | -| line1 | n | -0.1+0.1j | -0.1-0j | 0j | -0.15+0.85j | 0j | 1.63-8.2j | -| line2 | a | 376.51+75.17j | **-376.45-74.93j** | 69627.17-24139.23j | **-14217.89+41992.79j** | 190.15-26.15j | **57.69-100.07j** | -| line2 | b | -376.12-74.96j | **376.45+74.93j** | 41140.25+59810.05j | **14217.89-41992.79j** | -74.72-173.91j | **57.69-100.07j** | -| line2 | c | -0.49-0.21j | 0j | 14.9-126.71j | 0j | -117.06+208.26j | -120.25+224.73j | -| line2 | n | 0.1+0j | 0j | 0.15-0.85j | 0j | 1.63-8.2j | 4.88-24.6j | +| branch_id | phase | branch_type | current1 | current2 | power1 | power2 | potential1 | potential2 | +| :-------- | :---- | :---------- | -----------------: | -------------: | -----------------: | ----------------------: | --------------: | ----------------: | +| line1 | a | line | 376.73+75.27j | -376.51-75.17j | 87001.28-17383.79j | -69627.19+24139.31j | 230.94-0j | 190.15-26.15j | +| line1 | b | line | -376.14-74.96j | 376.12+74.96j | 58424.2+66571.89j | -41140.23-59809.99j | -115.47-200j | -74.72-173.91j | +| line1 | c | line | -0.49-0.42j | 0.49+0.21j | -26.77-147.2j | -14.92+126.89j | -115.47+200j | -117.06+208.26j | +| line1 | n | line | -0.1+0.1j | -0.1-0j | 0j | -0.15+0.85j | 0j | 1.63-8.2j | +| line2 | a | line | **376.51+75.17j** | -376.45-74.93j | 69627.19-24139.31j | **-14217.87+41992.82j** | 190.15-26.15j | **57.69-100.07j** | +| line2 | b | line | **-376.12-74.96j** | 376.45+74.93j | 41140.23+59809.99j | **14217.87-41992.82j** | -74.72-173.91j | **57.69-100.07j** | +| line2 | c | line | -0.49-0.21j | -0+0j | 14.92-126.89j | -0j | -117.06+208.26j | -120.25+224.73j | +| line2 | n | line | 0.1+0j | -0+0j | 0.15-0.85j | -0+0j | 1.63-8.2j | 4.88-24.6j | Looking at the line results of the second bus of the line `line2`, which is `bus2` where we added the short-circuit, one can notice that: @@ -112,23 +116,23 @@ It is possible to create short-circuits between several phases, not only two. Le short-circuit then create a new one between phases "a", "b", and "c". ```pycon ->>> bus2.clear_short_circuits() ->>> bus2.add_short_circuit("a", "b", "c") +>>> en = create_network() +>>> en.buses["b2"].add_short_circuit("a", "b", "c") >>> en.solve_load_flow() 1 >>> en.res_branches ``` -| branch_id | phase | current1 | current2 | power1 | power2 | potential1 | potential2 | -| :-------- | :---- | --------------: | --------------: | -----------------: | ------------------: | -------------: | --------------: | -| line1 | a | 371.74-146.3j | -371.55+146.39j | 85849.16+33785.8j | -63525.86-24647.08j | 230.94-0j | 170.63-0.89j | -| line1 | b | -325.13-309.42j | 325.11+309.42j | 99425.41+29296.84j | -75755.17-20038.48j | -115.47-200j | -91.49-148.71j | -| line1 | c | -46.49+455.6j | 46.51-455.8j | 96487.77+43308.67j | -75409.92-31858.5j | -115.47+200j | -85.88+156.68j | -| line1 | n | -0.12+0.12j | -0.07-0.01j | 0j | -0.4+0.53j | 0j | 6.74-7.09j | -| line2 | a | 371.55-146.39j | -371.59+146.56j | 63525.86+24647.08j | 3541.55-1646.58j | 170.63-0.89j | **-6.74+7.09j** | -| line2 | b | -325.11-309.42j | 325.28+309.3j | 75755.17+20038.48j | 1.41+4388.76j | -91.49-148.71j | **-6.74+7.09j** | -| line2 | c | -46.51+455.8j | 46.31-455.86j | 75409.92+31858.5j | -3542.97-2742.18j | -85.88+156.68j | **-6.74+7.09j** | -| line2 | n | 0.07+0.01j | 0j | 0.4-0.53j | 0j | 6.74-7.09j | 20.21-21.26j | +| branch_id | phase | branch_type | current1 | current2 | power1 | power2 | potential1 | potential2 | +| :-------- | :---- | :---------- | --------------: | --------------: | -----------------: | ------------------: | -------------: | --------------: | +| line1 | a | line | 371.74-146.3j | -371.55+146.39j | 85849.21+33785.73j | -63525.86-24647.04j | 230.94-0j | 170.63-0.89j | +| line1 | b | line | -325.13-309.42j | 325.11+309.42j | 99425.42+29296.79j | -75755.18-20038.43j | -115.47-200j | -91.49-148.71j | +| line1 | c | line | -46.49+455.59j | 46.51-455.8j | 96487.73+43308.59j | -75409.94-31858.46j | -115.47+200j | -85.88+156.68j | +| line1 | n | line | -0.12+0.12j | -0.07-0.01j | 0j | -0.4+0.53j | 0j | 6.74-7.09j | +| line2 | a | line | 371.55-146.39j | -371.59+146.56j | 63525.86+24647.04j | 3541.55-1646.58j | 170.63-0.89j | **-6.74+7.09j** | +| line2 | b | line | -325.11-309.42j | 325.28+309.3j | 75755.18+20038.43j | 1.42+4388.76j | -91.49-148.71j | **-6.74+7.09j** | +| line2 | c | line | -46.51+455.8j | 46.31-455.86j | 75409.94+31858.46j | -3542.97-2742.18j | -85.88+156.68j | **-6.74+7.09j** | +| line2 | n | line | 0.07+0.01j | -0-0j | 0.4-0.53j | 0j | 6.74-7.09j | 20.21-21.26j | Now the potentials of the three phases are equal and the currents and powers add up to zero at the bus where the short-circuit is applied. @@ -139,24 +143,24 @@ Phase-to-ground short-circuits are also possible. Let's remove the existing shor between phase "a" and ground. ```pycon ->>> bus2.clear_short_circuits() +>>> en = create_network() >>> # ground MUST be passed as a keyword argument -... bus2.add_short_circuit("a", ground=ground) +... en.buses["b2"].add_short_circuit("a", ground=en.grounds["gnd"]) >>> en.solve_load_flow() 1 >>> en.res_branches ``` -| branch_id | phase | current1 | current2 | power1 | power2 | potential1 | potential2 | -| :-------- | :---- | ------------: | ------------: | -----------------: | ------------------: | --------------: | --------------: | -| line1 | a | 96.01-188.55j | -95.8+188.65j | 22173.17+43543.72j | -16858.71-29476.66j | 230.94+0j | 160.3-7.97j | -| line1 | b | 0.53-0.42j | -0.54+0.42j | 22.57-153.79j | -3.39+192.16j | -115.47-200j | -166.27-225.68j | -| line1 | c | -0.41-0.51j | 0.43+0.28j | -54.42-141.47j | -21.19+121.75j | -115.47+200j | -162.05+176.44j | -| line1 | n | -0.04-0.07j | -0.17+0.18j | 0j | 4.19+13.62j | 0j | -50.72-25.69j | -| line2 | a | 95.8-188.65j | -95.91+188.9j | 16858.71+29476.66j | 0j | 160.3-7.97j | **0j** | -| line2 | b | 0.54-0.42j | 0j | 3.39-192.16j | 0j | -166.27-225.68j | -267.74-277.02j | -| line2 | c | -0.43-0.28j | 0j | 21.19-121.75j | 0j | -162.05+176.44j | -255.11+129.31j | -| line2 | n | 0.17-0.18j | 0j | -4.19-13.62j | 0j | -50.72-25.69j | -152.11-77.04j | +| branch_id | phase | branch_type | current1 | current2 | power1 | power2 | potential1 | potential2 | +| :-------- | :---- | :---------- | ------------: | ------------: | -----------------: | ------------------: | --------------: | --------------: | +| line1 | a | line | 96.01-188.55j | -95.8+188.65j | 22173.11+43543.54j | -16858.62-29476.54j | 230.94+0j | 160.3-7.97j | +| line1 | b | line | 0.53-0.42j | -0.55+0.42j | 22.6-154j | -3.39+192.42j | -115.47-200j | -166.27-225.68j | +| line1 | c | line | -0.41-0.51j | 0.43+0.28j | -54.5-141.67j | -21.22+121.92j | -115.47+200j | -162.05+176.44j | +| line1 | n | line | -0.04-0.07j | -0.17+0.18j | 0j | 4.2+13.63j | 0j | -50.72-25.69j | +| line2 | a | line | 95.8-188.65j | -95.91+188.9j | 16858.62+29476.54j | 0j | 160.3-7.97j | **0j** | +| line2 | b | line | 0.55-0.42j | 0j | 3.39-192.42j | -0+0j | -166.27-225.68j | -267.74-277.02j | +| line2 | c | line | -0.43-0.28j | -0+0j | 21.22-121.92j | 0j | -162.05+176.44j | -255.11+129.31j | +| line2 | n | line | 0.17-0.18j | 0j | -4.2-13.63j | -0+0j | -50.72-25.69j | -152.11-77.03j | ```pycon >>> en.res_grounds @@ -166,7 +170,7 @@ between phase "a" and ground. | :-------- | --------: | | gnd | 0j | -Here the potential at phase "a" of bus `bus2` is zero, equal to the ground potential. The sum of the currents in the +Here the potential at phase "a" of bus `b2` is zero, equal to the ground potential. The sum of the currents in the other phases is also zero indicating that the current of phase "a" went through the ground. ## Additional notes @@ -176,7 +180,7 @@ short-circuit, or when forgetting parameters. ```pycon >>> try: -... load = PowerLoad("load", bus=bus2, powers=[10, 10, 10]) +... load = PowerLoad("load", bus=en.buses["b2"], powers=[10, 10, 10]) ... except RoseauLoadFlowException as e: ... print(e) The power load 'load' is connected on bus 'b2' that already has a short-circuit. @@ -185,9 +189,9 @@ It makes the short-circuit calculation impossible. [bad_short_circuit] ```pycon >>> try: -... bus2.add_short_circuit("a") +... en.buses["b2"].add_short_circuit("a") ... except RoseauLoadFlowException as e: ... print(e) -For the short-circuit on bus 'b2', at least two phases (or a phase and a ground) -should be given (only ('a',) is given). [bad_phase] +For the short-circuit on bus 'b2', expected at least two phases or a phase and a ground. +Only phase 'a' is given. [bad_phase] ``` diff --git a/roseau/load_flow/models/buses.py b/roseau/load_flow/models/buses.py index 590f413e..5b095216 100644 --- a/roseau/load_flow/models/buses.py +++ b/roseau/load_flow/models/buses.py @@ -384,11 +384,13 @@ def add_short_circuit(self, *phases: str, ground: Optional["Ground"] = None) -> msg = f"Phase {phase!r} is not in the phases {set(self.phases)} of bus {self.id!r}." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_PHASE) - if len(phases) < 1 or (len(phases) == 1 and ground is None): - msg = ( - f"For the short-circuit on bus {self.id!r}, at least two phases (or a phase and a ground) should be " - f"given (only {phases} is given)." - ) + if not phases or (len(phases) == 1 and ground is None): + msg = f"For the short-circuit on bus {self.id!r}, expected at least two phases or a phase and a ground." + if not phases: + msg += " No phase was given." + else: + msg += f" Only phase {phases[0]!r} is given." + logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_PHASE) duplicates = [item for item in set(phases) if phases.count(item) > 1] @@ -420,10 +422,3 @@ def add_short_circuit(self, *phases: str, ground: Optional["Ground"] = None) -> def short_circuits(self) -> list[dict[str, Any]]: """Return the list of short-circuits of this bus.""" return self._short_circuits[:] # return a copy as users should not modify the list directly - - def clear_short_circuits(self) -> None: - """Remove the short-circuits of this bus.""" - # self._short_circuits = [] - msg = "Short circuits cannot be cleared for now. Please recreate the bus without the short circuits instead." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_SHORT_CIRCUIT) # TODO diff --git a/roseau/load_flow/models/tests/test_buses.py b/roseau/load_flow/models/tests/test_buses.py index 468db7be..0a445221 100644 --- a/roseau/load_flow/models/tests/test_buses.py +++ b/roseau/load_flow/models/tests/test_buses.py @@ -40,19 +40,22 @@ def test_short_circuit(): with pytest.raises(RoseauLoadFlowException) as e: bus.add_short_circuit("a", "n") assert "Phase 'n' is not in the phases" in e.value.msg - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE with pytest.raises(RoseauLoadFlowException) as e: bus.add_short_circuit("n", "a") assert "Phase 'n' is not in the phases" in e.value.msg - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE with pytest.raises(RoseauLoadFlowException) as e: bus.add_short_circuit("a", "a") assert "some phases are duplicated" in e.value.msg - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE with pytest.raises(RoseauLoadFlowException) as e: bus.add_short_circuit("a") - assert "at least two phases (or a phase and a ground) should be given" in e.value.msg - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_PHASE + assert e.value.msg == ( + "For the short-circuit on bus 'bus', expected at least two phases or a phase and a ground. " + "Only phase 'a' is given." + ) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE assert not bus._short_circuits bus.add_short_circuit("c", "a", "b") @@ -73,15 +76,20 @@ def test_short_circuit(): bus.add_short_circuit("a", ground=ground) # ok assert len(bus.short_circuits) == 2 - # With power load - # TODO: clear_short_circuits no longer works - # bus.clear_short_circuits() - # assert not bus.short_circuits - # PowerLoad(id="load", bus=bus, powers=[10, 10, 10]) - # with pytest.raises(RoseauLoadFlowException) as e: - # bus.add_short_circuit("a", "b") - # assert "is already connected on bus" in e.value.msg - # assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_SHORT_CIRCUIT + # Cannot connect a load on a short-circuited bus + with pytest.raises(RoseauLoadFlowException) as e: + PowerLoad(id="load", bus=bus, powers=[10, 10, 10]) + assert "is connected on bus" in e.value.msg + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_SHORT_CIRCUIT + + # Cannot short-circuit a bus with a power load + bus = Bus("bus", phases="abc") + assert not bus.short_circuits + _ = PowerLoad(id="load", bus=bus, powers=[10, 10, 10]) + with pytest.raises(RoseauLoadFlowException) as e: + bus.add_short_circuit("a", "b") + assert "is already connected on bus" in e.value.msg + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_SHORT_CIRCUIT def test_voltage_limits(): diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 7c38f26d..b0c71a6b 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -1181,17 +1181,6 @@ def res_potential_refs(self) -> pd.DataFrame: res_dict["current"].append(current) return pd.DataFrame(res_dict).astype(dtypes).set_index(["potential_ref_id"]) - def clear_short_circuits(self) -> None: - """Remove the short-circuits of all the buses.""" - # for bus in self.buses.values(): - # bus.clear_short_circuits() - msg = ( - "Short circuits cannot be cleared for now. Please recreate the network without the " - "short circuits instead." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_SHORT_CIRCUIT) # TODO - # # Internal methods, please do not use # diff --git a/roseau/load_flow/tests/test_electrical_network.py b/roseau/load_flow/tests/test_electrical_network.py index 393e3092..aeca38d1 100644 --- a/roseau/load_flow/tests/test_electrical_network.py +++ b/roseau/load_flow/tests/test_electrical_network.py @@ -1709,10 +1709,6 @@ def test_short_circuits(): assert bus.short_circuits - # TODO: clear_short_circuits no longer works - # en.clear_short_circuits() - # assert not bus.short_circuits - def test_catalogue_data(): # The catalogue data path exists From 645423d3a3ccbda931db568c9bd1af3466f55831 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Wed, 10 Jan 2024 18:15:03 +0100 Subject: [PATCH 20/51] Docuemnt how to modify an element in place --- doc/usage/Connecting_Elements.md | 69 ++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/doc/usage/Connecting_Elements.md b/doc/usage/Connecting_Elements.md index 2892c2d4..89a897f4 100644 --- a/doc/usage/Connecting_Elements.md +++ b/doc/usage/Connecting_Elements.md @@ -165,3 +165,72 @@ And now if you run the load flow, you can see that the new elements are taken in >>> abs(new_load.res_voltages) array([216.54956226]) ``` + +## Modifying an element + +Some properties of an element cannot be modified once the element is created. For example the phases +of an element, the buses of a branch / load / source, the winding of a transformer, and the shunt +connection of a line cannot be modified. Some other properties can be modified, like the voltage of +a voltage source. + +### Modifying a voltage source + +You can change the voltage of the voltage source: + +```pycon +>>> vs.voltages +array([ 254.03411844 +0.j, -127.01705922-220.j, -127.01705922+220.j]) +>>> vs.voltages = vs.voltages * 1.1 +>>> vs.voltages +array([ 279.43753029 +0.j, -139.71876514-242.j, -139.71876514+242.j]) +``` + +### Modifying a load + +Similarly, you can change the powers of a "constant power load", the currents of a "constant current +load", and the impedances of a "constant impedance load". + +```pycon +>>> new_load.powers +array([6000.+0.j]) +>>> new_load.powers = [3e3 + 1e3j] +array([3000.+1000.j]) +``` + +### Modifying a branch + +You can change a branch parameters by setting a new `parameters` attribute. Note that the new +parameters have to be compatible with the existing branch. This means that the number of phases +must match, and for a transformer, the windings must match. + +```pycon +>>> line.z_line +array([[0.2+0.j, 0. +0.j, 0. +0.j, 0. +0.j], + [0. +0.j, 0.2+0.j, 0. +0.j, 0. +0.j], + [0. +0.j, 0. +0.j, 0.2+0.j, 0. +0.j], + [0. +0.j, 0. +0.j, 0. +0.j, 0.2+0.j]]) +>>> line.parameters = LineParameters("lp_modified", z_line=(0.5 + 0.1j) * np.eye(4, dtype=complex)) +>>> line.z_line +array([[1.+0.2j, 0.+0.j , 0.+0.j , 0.+0.j ], + [0.+0.j , 1.+0.2j, 0.+0.j , 0.+0.j ], + [0.+0.j , 0.+0.j , 1.+0.2j, 0.+0.j ], + [0.+0.j , 0.+0.j , 0.+0.j , 1.+0.2j]]) +``` + +For a line, you can also change the length: + +```pycon +>>> line.length +2.0 +>>> line.length = 1.0 +>>> line.length +1.0 +>>> line.z_line # <-- the impedance is divided by 2 +array([[0.5+0.1j, 0. +0.j , 0. +0.j , 0. +0.j ], + [0. +0.j , 0.5+0.1j, 0. +0.j , 0. +0.j ], + [0. +0.j , 0. +0.j , 0.5+0.1j, 0. +0.j ], + [0. +0.j , 0. +0.j , 0. +0.j , 0.5+0.1j]]) +``` + +Modifying the parameters of a transformer is similar, assign a new `parameters` attribute. For a +transformer, you can also change the tap position by assigning a new `tap` attribute. From ef8dd83512f76806e7a0d795d750ae264a893255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Vinot?= Date: Thu, 11 Jan 2024 17:50:04 +0100 Subject: [PATCH 21/51] Add elements for the license checks --- roseau/load_flow/_solvers.py | 3 ++- roseau/load_flow/exceptions.py | 3 +++ roseau/load_flow/license.py | 20 +++++++++++++++++--- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/roseau/load_flow/_solvers.py b/roseau/load_flow/_solvers.py index 0061cf90..c5bc3681 100644 --- a/roseau/load_flow/_solvers.py +++ b/roseau/load_flow/_solvers.py @@ -74,7 +74,8 @@ def solve_load_flow(self, max_iterations: int, tolerance: float) -> tuple[int, f Returns: The number of iterations and the final residual """ - if get_license() is None: + lic = get_license() + if lic is None: activate_license(None) return self._cy_solver.solve_load_flow(max_iterations=max_iterations, tolerance=tolerance) diff --git a/roseau/load_flow/exceptions.py b/roseau/load_flow/exceptions.py index 8ccd1003..aee36ee0 100644 --- a/roseau/load_flow/exceptions.py +++ b/roseau/load_flow/exceptions.py @@ -112,6 +112,9 @@ class RoseauLoadFlowExceptionCode(Enum): # Import Error IMPORT_ERROR = auto() + # License errors + LICENSE_ERROR = auto() + @classmethod def package_name(cls) -> str: return "roseau.load_flow" diff --git a/roseau/load_flow/license.py b/roseau/load_flow/license.py index 10f1afaf..a0f1b6b6 100644 --- a/roseau/load_flow/license.py +++ b/roseau/load_flow/license.py @@ -1,11 +1,15 @@ import datetime as dt +import logging import os import certifi from platformdirs import user_cache_dir +from roseau.load_flow import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow_engine.cy_engine import CyLicense, cy_activate_license, cy_deactivate_license, cy_get_license +logger = logging.getLogger(__name__) + __all__ = ["activate_license", "deactivate_license", "get_license", "License"] _license = None @@ -48,6 +52,11 @@ def valid(self) -> bool: """Is the license valid?""" return self.cy_license.valid + @property + def max_nb_buses(self) -> int | None: + """The maximum allowed number of buses for a network. If `None`, the license has no limitation.""" + return self.cy_license.max_nb_buses + @staticmethod def get_machine_fingerprint() -> str: """This method retrieves your machine fingerprint for license validation.""" @@ -64,17 +73,22 @@ def get_username() -> str: return CyLicense.get_username() -def activate_license(key: str | None) -> None: +def activate_license(key: str | None = None) -> None: """Activate the license with the given key in the current process. Args: key: - The key of the license to activate. If None is provided, the environment variable + The key of the license to activate. If `None` is provided, the environment variable `ROSEAU_LOAD_FLOW_LICENSE_KEY` is read. """ if key is None: key = os.getenv("ROSEAU_LOAD_FLOW_LICENSE_KEY", "") - cy_activate_license(key=key, cacert_filepath=certifi.where(), cache_folderpath=user_cache_dir()) + try: + cy_activate_license(key=key, cacert_filepath=certifi.where(), cache_folderpath=user_cache_dir()) + except RuntimeError as e: + msg = f"The license can not be activated. The detailed error message is {e.args[0]!r}." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.LICENSE_ERROR) from e def deactivate_license() -> None: From 2d8a0bba5025ff6380c80e1ff1fe03b58976e686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Vinot?= Date: Fri, 12 Jan 2024 09:03:01 +0100 Subject: [PATCH 22/51] Change copyright year --- LICENSE.md | 2 +- doc/conf.py | 2 +- roseau/load_flow/__about__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 305daf9a..2fd48151 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2018--2024, Roseau Technologies +Copyright (c) 2018, Roseau Technologies Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/doc/conf.py b/doc/conf.py index 8df59650..c591b375 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -18,7 +18,7 @@ # -- Project information ----------------------------------------------------- project = "Roseau Load Flow" -copyright = "2018--2024, Roseau Technologies SAS" +copyright = "2018, Roseau Technologies SAS" # author = "Benoît Vinot" # The full version, including alpha/beta/rc tags diff --git a/roseau/load_flow/__about__.py b/roseau/load_flow/__about__.py index eb091007..c05651d5 100644 --- a/roseau/load_flow/__about__.py +++ b/roseau/load_flow/__about__.py @@ -7,7 +7,7 @@ "Victor Gouin", ) ) -__copyright__ = "Roseau Technologies 2018--2024" +__copyright__ = "Roseau Technologies 2018" __credits__ = "Roseau Technologies" __license__ = "Proprietary" __maintainer__ = "Ali Hamdan" From dc20790b9f9ccbf9b999f0beff91f8b9fae73fd0 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Fri, 12 Jan 2024 09:50:40 +0100 Subject: [PATCH 23/51] Better warm start handling --- roseau/load_flow/network.py | 54 ++++++++---------- .../tests/test_electrical_network.py | 57 ++++++++++++++----- 2 files changed, 68 insertions(+), 43 deletions(-) diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 5c2f77cc..7645e552 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -501,8 +501,9 @@ def solve_load_flow( Tolerance needed for the convergence. warm_start: - If true, initialize the solver with the potentials of the last successful load flow - result (if any). + If true (the default), the solver is initialized with the potentials of the last + successful load flow result (if any). Otherwise, the potentials are reset to their + initial values. solver: The name of the solver to use for the load flow. The options are: @@ -517,13 +518,14 @@ def solve_load_flow( Returns: The number of iterations taken. """ + did_warm_start = warm_start if not self._valid: self._check_validity(constructed=False) - self._create_network() + self._create_network() # <-- calls _propagate_potentials, no warm start self._solver.update_network(self) - warm_start = False + did_warm_start = False if not self.res_info: - warm_start = False + did_warm_start = False # Update solver if solver != self._solver.name: @@ -533,7 +535,7 @@ def solve_load_flow( self._solver.update_params(solver_params) if not warm_start: - self._propagate_potentials(force=True) + self._reset_inputs() start = time.perf_counter() try: @@ -578,6 +580,7 @@ def solve_load_flow( "status": "failure", "iterations": iterations, "residual": residual, + "warm_started": did_warm_start, } raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.NO_LOAD_FLOW_CONVERGENCE) @@ -594,6 +597,7 @@ def solve_load_flow( "status": "success", "iterations": iterations, "residual": residual, + "warm_started": did_warm_start, } # Lazily update the results of the elements @@ -612,11 +616,6 @@ def solve_load_flow( return iterations - def reset_inputs(self) -> None: - """Reset the input vector which is used for the first step of the newton algorithm to its initial value.""" - if self._solver is not None: - self._solver.reset_inputs() - def _results_from_dict(self, data: JsonDict) -> None: """Dispatch the results to all the elements of the network. @@ -1219,7 +1218,7 @@ def _create_network(self) -> None: for source in self.sources.values(): cy_elements.append(source._cy_element) self._elements.append(source) - self._propagate_potentials(force=False) + self._propagate_potentials() self._cy_electrical_network = CyElectricalNetwork(elements=np.array(cy_elements), nb_elements=len(cy_elements)) def _check_validity(self, constructed: bool) -> None: @@ -1280,21 +1279,17 @@ def _check_validity(self, constructed: bool) -> None: elif element.network != self: element._raise_several_network() - def _propagate_potentials(self, force: bool) -> None: - """Set the bus potentials that have not been initialized yet + def _reset_inputs(self) -> None: + """Reset the input vector used for the first step of the newton algorithm to its initial value.""" + if self._solver is not None: + self._solver.reset_inputs() - Args: - force: - If True, the `_initialized` status of the buses are ignored. If False, only uninitialized - potentials of buses will be overwritten. - """ - if force: - uninitialized = True - else: - uninitialized = False - for bus in self.buses.values(): - if not bus._initialized: - uninitialized = True + def _propagate_potentials(self) -> None: + """Set the bus potentials that have not been initialized yet.""" + uninitialized = False + for bus in self.buses.values(): + if not bus._initialized: + uninitialized = True if uninitialized: max_voltages = 0.0 @@ -1328,15 +1323,14 @@ def _propagate_potentials(self, force: bool) -> None: while elements: element, potentials = elements.pop(-1) visited.add(element) - if isinstance(element, Bus) and (force or not element._initialized): + if isinstance(element, Bus) and not element._initialized: bus_n = element._n - bus_initialized_by_the_user = element._initialized element.potentials = potentials[0:bus_n] - element._initialized_by_the_user = bus_initialized_by_the_user + element._initialized_by_the_user = False # only used for serialization for e in element._connected_elements: if e not in visited: if isinstance(element, Transformer): - k = (element.parameters.ulv / element.parameters.uhv).m_as("") + k = element.parameters._ulv / element.parameters._uhv phase_displacement = element.parameters.phase_displacement if phase_displacement is None: phase_displacement = 0 diff --git a/roseau/load_flow/tests/test_electrical_network.py b/roseau/load_flow/tests/test_electrical_network.py index aeca38d1..04a45363 100644 --- a/roseau/load_flow/tests/test_electrical_network.py +++ b/roseau/load_flow/tests/test_electrical_network.py @@ -109,6 +109,7 @@ def good_json_results() -> dict: "status": "success", "iterations": 1, "residual": 6.296829377361313e-14, + "warm_started": True, }, "buses": [ { @@ -1605,8 +1606,6 @@ def test_solver_warm_start(small_network: ElectricalNetwork, good_json_results, load: PowerLoad = small_network.loads["load"] load_bus = small_network.buses["bus1"] - original_propagate_potentials = small_network._propagate_potentials - def compare_results(expected, obtained): if isinstance(expected, dict): assert isinstance(obtained, dict) @@ -1617,6 +1616,9 @@ def compare_results(expected, obtained): assert isinstance(obtained, list | tuple) for i, item in enumerate(expected): compare_results(item, obtained[i]) + elif isinstance(expected, bool): + assert isinstance(obtained, bool) + assert expected == obtained elif isinstance(expected, complex | float | int): assert isinstance(obtained, complex | float | int) assert np.isclose(expected, obtained, atol=1e-1) @@ -1624,16 +1626,27 @@ def compare_results(expected, obtained): assert isinstance(obtained, type(expected)) assert expected == obtained - def _propagate_potentials(force): - if should_not_propagate_potentials: - raise AssertionError("Should not propagate potentials") - return original_propagate_potentials(force) + original_propagate_potentials = small_network._propagate_potentials + original_reset_inputs = small_network._reset_inputs + + def _propagate_potentials(): + nonlocal propagate_potentials_called + propagate_potentials_called = True + return original_propagate_potentials() + + def _reset_inputs(): + nonlocal reset_inputs_called + reset_inputs_called = True + return original_reset_inputs() monkeypatch.setattr(small_network, "_propagate_potentials", _propagate_potentials) + monkeypatch.setattr(small_network, "_reset_inputs", _reset_inputs) # First case: network is valid, no results yet -> no warm start - should_not_propagate_potentials = False - good_json_results["info"]["warm_start"] = False + propagate_potentials_called = False + reset_inputs_called = False + good_json_results["info"]["warm_start"] = True + good_json_results["info"]["warm_started"] = False assert small_network._valid assert not small_network.res_info # No results assert not small_network._results_valid # Results are not valid by default @@ -1641,30 +1654,42 @@ def _propagate_potentials(force): warnings.simplefilter("error") # Make sure there is no warning good_json_results["info"]["iterations"] = small_network.solve_load_flow(warm_start=True) compare_results(good_json_results, small_network.results_to_dict()) + assert not propagate_potentials_called # Is not called because it was already called in the constructor + assert not reset_inputs_called # Second case: the user requested no warm start (even though the network and results are valid) - should_not_propagate_potentials = False + propagate_potentials_called = False + reset_inputs_called = False good_json_results["info"]["warm_start"] = False + good_json_results["info"]["warm_started"] = False assert small_network._valid assert small_network._results_valid with warnings.catch_warnings(): warnings.simplefilter("error") # Make sure there is no warning good_json_results["info"]["iterations"] = small_network.solve_load_flow(warm_start=False) compare_results(good_json_results, small_network.results_to_dict()) + assert not propagate_potentials_called + assert reset_inputs_called # Third case: network is valid, results are valid -> warm start - should_not_propagate_potentials = True + propagate_potentials_called = False + reset_inputs_called = False good_json_results["info"]["warm_start"] = True + good_json_results["info"]["warm_started"] = True assert small_network._valid assert small_network._results_valid with warnings.catch_warnings(): warnings.simplefilter("error") # Make sure there is no warning good_json_results["info"]["iterations"] = small_network.solve_load_flow(warm_start=True) compare_results(good_json_results, small_network.results_to_dict()) + assert not propagate_potentials_called + assert not reset_inputs_called # Fourth case (load powers changes): network is valid, results are not valid -> warm start - should_not_propagate_potentials = True + propagate_potentials_called = False + reset_inputs_called = False good_json_results["info"]["warm_start"] = True + good_json_results["info"]["warm_started"] = True load.powers = load.powers + Q_(1 + 1j, "VA") assert small_network._valid assert not small_network._results_valid @@ -1672,10 +1697,14 @@ def _propagate_potentials(force): warnings.simplefilter("error") # Make sure there is no warning good_json_results["info"]["iterations"] = small_network.solve_load_flow(warm_start=True) compare_results(good_json_results, small_network.results_to_dict()) + assert not propagate_potentials_called + assert not reset_inputs_called # Fifth case: network is not valid -> no warm start - should_not_propagate_potentials = False - good_json_results["info"]["warm_start"] = False + propagate_potentials_called = False + reset_inputs_called = False + good_json_results["info"]["warm_start"] = True + good_json_results["info"]["warm_started"] = False new_load = PowerLoad("new_load", load_bus, powers=[100, 200, 300], phases=load.phases) new_load_result = good_json_results["loads"][0].copy() new_load_result["id"] = "new_load" @@ -1691,6 +1720,8 @@ def _propagate_potentials(force): assert not small_network._results_valid good_json_results["info"]["iterations"] = small_network.solve_load_flow(warm_start=True) compare_results(good_json_results, small_network.results_to_dict()) + assert propagate_potentials_called + assert not reset_inputs_called def test_short_circuits(): From 6dca6df9e332f25df6b4e2375251f81276d0759d Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Fri, 12 Jan 2024 12:41:19 +0100 Subject: [PATCH 24/51] Remove res_info --- doc/usage/Connecting_Elements.md | 4 +- doc/usage/Flexible_Loads.md | 6 +- doc/usage/Getting_Started.md | 37 ++---------- doc/usage/Short_Circuit.md | 6 +- roseau/load_flow/_solvers.py | 3 +- roseau/load_flow/network.py | 42 ++----------- .../tests/test_electrical_network.py | 59 ++----------------- 7 files changed, 25 insertions(+), 132 deletions(-) diff --git a/doc/usage/Connecting_Elements.md b/doc/usage/Connecting_Elements.md index 89a897f4..924f300b 100644 --- a/doc/usage/Connecting_Elements.md +++ b/doc/usage/Connecting_Elements.md @@ -53,7 +53,7 @@ The load flow can be solved: ```pycon >>> en.solve_load_flow() -2 +(2, 1.8595619621919468e-07) ``` ## Disconnecting an element @@ -161,7 +161,7 @@ And now if you run the load flow, you can see that the new elements are taken in ```pycon >>> en.solve_load_flow() -3 +(3, 1.216767654e-07) >>> abs(new_load.res_voltages) array([216.54956226]) ``` diff --git a/doc/usage/Flexible_Loads.md b/doc/usage/Flexible_Loads.md index 345e28a6..ba31fb35 100644 --- a/doc/usage/Flexible_Loads.md +++ b/doc/usage/Flexible_Loads.md @@ -129,7 +129,7 @@ Then, the load flow can be solved and the results can be retrieved. ```pycon >>> en.solve_load_flow() -2 +(2, 1.8595619621919468e-07) >>> abs(load_bus3.res_voltages) array([243.66463933, 232.20612714, 233.55093129]) ``` @@ -177,7 +177,7 @@ has been activated in this run. ```pycon >>> en.solve_load_flow() -4 +(4, 1.453686784545e-07) >>> abs(load_bus3.res_voltages) array([243.08225748, 232.46046866, 233.62854073]) ``` @@ -242,7 +242,7 @@ The load flow can be solved again. ```pycon >>> en.solve_load_flow() -6 +(6, 1.8576776876-07) >>> abs(load_bus3.res_voltages) array([239.5133208 , 230.2108052 , 237.59184615]) >>> flexible_load.res_flexible_powers diff --git a/doc/usage/Getting_Started.md b/doc/usage/Getting_Started.md index 5d97dc3f..36abb6b7 100644 --- a/doc/usage/Getting_Started.md +++ b/doc/usage/Getting_Started.md @@ -148,39 +148,12 @@ Then, the load flow can be solved by calling the `solve_load_flow` method of the ```pycon >>> en.solve_load_flow() -2 +(2, 1.8595619621919468e-07) ``` -It returns the number of iterations performed by the _Newton-Raphson_ solver, here _2_. More information about the -load flow resolution is available via the `res_info` attribute. - -```pycon ->>> en.res_info -{'solver': 'newton_goldstein', - 'solver_params': {'m1': 0.1, 'm2': 0.9}, - 'tolerance': 1e-06, - 'max_iterations': 20, - 'warm_start': True, - 'status': 'success', - 'iterations': 2, - 'residual': 1.8595619621919468e-07} -``` - -The available values are: - -- `solver`: it can be `"newton"` for the _Newton_ solver or `"newton_goldstein"` for the _Newton_ solver using the - _Goldstein and Price_ linear search; -- `solver_params`: the parameters used by the solver; -- `tolerance`: the requested tolerance for the solver. $10^{-6}$ is the default; -- `max_iterations`: the requested maximum number of iterations for the solver. 20 is the default; -- `warm_start`: if `True`, the results (potentials of each bus) from the last valid run are used - as a starting point for the solver. For large networks, using a warm start can lead to performance gains as the - solver will converge faster. `True` is the default; -- `status`: the convergence of the load flow. Two possibilities: _success_ or _failure_; -- `iterations`: the number of iterations made by the solver. -- `residual`: the precision which was reached by the solver (lower than the tolerance if successful solve). - -More details on solvers are given in the [Solvers page](../Solvers.md). +It returns the number of iterations performed by the _Newton-Raphson_ solver, and the residual +error after convergence. Here, the load flow converged in 2 iterations with a residual error of +$1.86 \times 10^{-7}$. (gs-getting-results)= @@ -503,7 +476,7 @@ unbalanced situation. ```pycon >>> load.powers = Q_([15, 0, 0], "kVA") >>> en.solve_load_flow() -3 +(3, 1.686343545e-07) >>> load_bus.res_potentials array([ 216.02252269 +0.j, -115.47005384-200.j, -115.47005384+200.j, 14.91758499 +0.j]) ``` diff --git a/doc/usage/Short_Circuit.md b/doc/usage/Short_Circuit.md index 7ce4a703..f0b309aa 100644 --- a/doc/usage/Short_Circuit.md +++ b/doc/usage/Short_Circuit.md @@ -86,7 +86,7 @@ All the following tables are rounded to 2 decimals to be properly displayed. ```pycon >>> en.solve_load_flow() -1 +(1, 1.235686457464e-07) >>> en.res_branches ``` @@ -119,7 +119,7 @@ short-circuit then create a new one between phases "a", "b", and "c". >>> en = create_network() >>> en.buses["b2"].add_short_circuit("a", "b", "c") >>> en.solve_load_flow() -1 +(1, 1.23437343475878e-07) >>> en.res_branches ``` @@ -147,7 +147,7 @@ between phase "a" and ground. >>> # ground MUST be passed as a keyword argument ... en.buses["b2"].add_short_circuit("a", ground=en.grounds["gnd"]) >>> en.solve_load_flow() -1 +(2, 1.68697431436484e-07) >>> en.res_branches ``` diff --git a/roseau/load_flow/_solvers.py b/roseau/load_flow/_solvers.py index c5bc3681..838d0e73 100644 --- a/roseau/load_flow/_solvers.py +++ b/roseau/load_flow/_solvers.py @@ -1,8 +1,9 @@ import logging from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Self +from typing import TYPE_CHECKING, Any import numpy as np +from typing_extensions import Self from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow.license import activate_license, get_license diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 7645e552..e766972d 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -157,7 +157,6 @@ def __init__( self._create_network() self._valid = True self._results_valid: bool = False - self.res_info: JsonDict = {} self._solver = AbstractSolver.from_dict(data={"name": self._DEFAULT_SOLVER, "params": {}}, network=self) def __repr__(self) -> str: @@ -516,16 +515,12 @@ def solve_load_flow( solver chosen. For more information, see the :ref:`solvers` page. Returns: - The number of iterations taken. + The number of iterations performed and the residual error at the last iteration. """ - did_warm_start = warm_start if not self._valid: self._check_validity(constructed=False) self._create_network() # <-- calls _propagate_potentials, no warm start self._solver.update_network(self) - did_warm_start = False - if not self.res_info: - did_warm_start = False # Update solver if solver != self._solver.name: @@ -568,38 +563,12 @@ def solve_load_flow( f"{residual:5n}" ) logger.error(msg=msg) - - self.res_info = { - # Input - "solver": self._solver.name, - "solver_params": self._solver.params(), - "tolerance": tolerance, - "max_iterations": max_iterations, - "warm_start": warm_start, - # Output - "status": "failure", - "iterations": iterations, - "residual": residual, - "warm_started": did_warm_start, - } - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.NO_LOAD_FLOW_CONVERGENCE) + raise RoseauLoadFlowException( + msg, RoseauLoadFlowExceptionCode.NO_LOAD_FLOW_CONVERGENCE, iterations, residual + ) logger.debug(f"The load flow converged after {iterations} iterations and {end - start:.3n} s.") - self.res_info = { - # Input - "solver": self._solver.name, - "solver_params": self._solver.params(), - "tolerance": tolerance, - "max_iterations": max_iterations, - "warm_start": warm_start, - # Output - "status": "success", - "iterations": iterations, - "residual": residual, - "warm_started": did_warm_start, - } - # Lazily update the results of the elements for element in chain( self.buses.values(), @@ -614,7 +583,7 @@ def solve_load_flow( # The results are now valid self._results_valid = True - return iterations + return iterations, residual def _results_from_dict(self, data: JsonDict) -> None: """Dispatch the results to all the elements of the network. @@ -1454,7 +1423,6 @@ def _results_to_dict(self, warning: bool) -> JsonDict: if warning: self._warn_invalid_results() # Warn only once if asked return { - "info": self.res_info, "buses": [bus._results_to_dict(False) for bus in self.buses.values()], "branches": [branch._results_to_dict(False) for branch in self.branches.values()], "loads": [load._results_to_dict(False) for load in self.loads.values()], diff --git a/roseau/load_flow/tests/test_electrical_network.py b/roseau/load_flow/tests/test_electrical_network.py index 04a45363..cde07bc2 100644 --- a/roseau/load_flow/tests/test_electrical_network.py +++ b/roseau/load_flow/tests/test_electrical_network.py @@ -100,17 +100,6 @@ def single_phase_network() -> ElectricalNetwork: @pytest.fixture() def good_json_results() -> dict: return { - "info": { - "solver": "newton_goldstein", - "solver_params": {"m1": 0.1, "m2": 0.9}, - "tolerance": 1e-06, - "max_iterations": 20, - "warm_start": True, - "status": "success", - "iterations": 1, - "residual": 6.296829377361313e-14, - "warm_started": True, - }, "buses": [ { "id": "bus0", @@ -1606,26 +1595,6 @@ def test_solver_warm_start(small_network: ElectricalNetwork, good_json_results, load: PowerLoad = small_network.loads["load"] load_bus = small_network.buses["bus1"] - def compare_results(expected, obtained): - if isinstance(expected, dict): - assert isinstance(obtained, dict) - for key, value in expected.items(): - assert key in obtained - compare_results(value, obtained[key]) - elif isinstance(expected, list | tuple): - assert isinstance(obtained, list | tuple) - for i, item in enumerate(expected): - compare_results(item, obtained[i]) - elif isinstance(expected, bool): - assert isinstance(obtained, bool) - assert expected == obtained - elif isinstance(expected, complex | float | int): - assert isinstance(obtained, complex | float | int) - assert np.isclose(expected, obtained, atol=1e-1) - else: - assert isinstance(obtained, type(expected)) - assert expected == obtained - original_propagate_potentials = small_network._propagate_potentials original_reset_inputs = small_network._reset_inputs @@ -1645,66 +1614,51 @@ def _reset_inputs(): # First case: network is valid, no results yet -> no warm start propagate_potentials_called = False reset_inputs_called = False - good_json_results["info"]["warm_start"] = True - good_json_results["info"]["warm_started"] = False assert small_network._valid - assert not small_network.res_info # No results assert not small_network._results_valid # Results are not valid by default with warnings.catch_warnings(): warnings.simplefilter("error") # Make sure there is no warning - good_json_results["info"]["iterations"] = small_network.solve_load_flow(warm_start=True) - compare_results(good_json_results, small_network.results_to_dict()) + small_network.solve_load_flow(warm_start=True) assert not propagate_potentials_called # Is not called because it was already called in the constructor assert not reset_inputs_called # Second case: the user requested no warm start (even though the network and results are valid) propagate_potentials_called = False reset_inputs_called = False - good_json_results["info"]["warm_start"] = False - good_json_results["info"]["warm_started"] = False assert small_network._valid assert small_network._results_valid with warnings.catch_warnings(): warnings.simplefilter("error") # Make sure there is no warning - good_json_results["info"]["iterations"] = small_network.solve_load_flow(warm_start=False) - compare_results(good_json_results, small_network.results_to_dict()) + small_network.solve_load_flow(warm_start=False) assert not propagate_potentials_called assert reset_inputs_called # Third case: network is valid, results are valid -> warm start propagate_potentials_called = False reset_inputs_called = False - good_json_results["info"]["warm_start"] = True - good_json_results["info"]["warm_started"] = True assert small_network._valid assert small_network._results_valid with warnings.catch_warnings(): warnings.simplefilter("error") # Make sure there is no warning - good_json_results["info"]["iterations"] = small_network.solve_load_flow(warm_start=True) - compare_results(good_json_results, small_network.results_to_dict()) + small_network.solve_load_flow(warm_start=True) assert not propagate_potentials_called assert not reset_inputs_called # Fourth case (load powers changes): network is valid, results are not valid -> warm start propagate_potentials_called = False reset_inputs_called = False - good_json_results["info"]["warm_start"] = True - good_json_results["info"]["warm_started"] = True load.powers = load.powers + Q_(1 + 1j, "VA") assert small_network._valid assert not small_network._results_valid with warnings.catch_warnings(): warnings.simplefilter("error") # Make sure there is no warning - good_json_results["info"]["iterations"] = small_network.solve_load_flow(warm_start=True) - compare_results(good_json_results, small_network.results_to_dict()) + small_network.solve_load_flow(warm_start=True) assert not propagate_potentials_called assert not reset_inputs_called # Fifth case: network is not valid -> no warm start propagate_potentials_called = False reset_inputs_called = False - good_json_results["info"]["warm_start"] = True - good_json_results["info"]["warm_started"] = False new_load = PowerLoad("new_load", load_bus, powers=[100, 200, 300], phases=load.phases) new_load_result = good_json_results["loads"][0].copy() new_load_result["id"] = "new_load" @@ -1716,10 +1670,7 @@ def _reset_inputs(): # We could warn here that the user requested warm start but the network is not valid # but this will be disruptive for the user especially that warm start is the default warnings.simplefilter("error") # Make sure there is no warning - assert not small_network._valid - assert not small_network._results_valid - good_json_results["info"]["iterations"] = small_network.solve_load_flow(warm_start=True) - compare_results(good_json_results, small_network.results_to_dict()) + small_network.solve_load_flow(warm_start=True) assert propagate_potentials_called assert not reset_inputs_called From 9a814a9521fb21d8340a12340f0c312fd0c150f4 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Fri, 12 Jan 2024 14:01:38 +0100 Subject: [PATCH 25/51] No res_info --- roseau/load_flow/network.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index e766972d..9a174e3a 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -114,21 +114,6 @@ class ElectricalNetwork(JsonMixin, CatalogueMixin[JsonDict]): potential_refs (dict[Id, roseau.load_flow.PotentialRef]): Dictionary of potential references of the network indexed by their IDs. Also available as a :attr:`DataFrame`. - - res_info (JsonDict): - Dictionary containing solver information on the last run of the load flow analysis. - Empty if the load flow analysis has not been run yet. - Example:: - - { - "solver": "newton", - "tolerance": 1e-06, - "max_iterations": 20, - "warm_start": True, - "status": "success", - "iterations": 2, - "residual": 1.8595619621919468e-07 - } """ _DEFAULT_SOLVER: Solver = "newton_goldstein" From 1a1669e58ebe66a97597614e73a8b16053de3a8d Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Fri, 12 Jan 2024 14:25:04 +0100 Subject: [PATCH 26/51] Fix res transformers and branches --- roseau/load_flow/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 9a174e3a..7c3b9922 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -724,7 +724,7 @@ def res_branches(self) -> pd.DataFrame: phases = sorted(set(branch.phases1) | set(branch.phases2)) for phase in phases: if phase in branch.phases1: - idx1 = branch.phases2.index(phase) + idx1 = branch.phases1.index(phase) i1, s1, v1 = currents1[idx1], powers1[idx1], potentials1[idx1] else: i1, s1, v1 = None, None, None @@ -799,7 +799,7 @@ def res_transformers(self) -> pd.DataFrame: phases = sorted(set(branch.phases1) | set(branch.phases2)) for phase in phases: if phase in branch.phases1: - idx1 = branch.phases2.index(phase) + idx1 = branch.phases1.index(phase) i1, s1, v1 = currents1[idx1], powers1[idx1], potentials1[idx1] else: i1, s1, v1 = None, None, None From 7d8537c49fc6d5396ba1de742b610f64098a6208 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Fri, 12 Jan 2024 14:49:20 +0100 Subject: [PATCH 27/51] Move test coverage limit to pyproject.toml --- .github/workflows/ci.yml | 4 ++-- pyproject.toml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index adda5477..a0c42c1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,8 +59,8 @@ jobs: - name: Test with pytest run: | - poetry run pytest -n=auto --durations=25 --cov=roseau --cov-report html \ - --cov-config pyproject.toml --cov-fail-under 75 roseau + poetry run pytest -vv -n=auto --durations=25 --cov-report html \ + --cov-config pyproject.toml roseau - name: Archive code coverage results uses: actions/upload-artifact@v4 diff --git a/pyproject.toml b/pyproject.toml index 2ec5ffce..efdb04cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,13 +131,14 @@ exclude_lines = [ "if TYPE_CHECKING:", ] ignore_errors = true +fail_under = 90 [tool.coverage.html] directory = "htmlcov" # Pytest [tool.pytest.ini_options] -addopts = "--color=yes -vv -n=0 --import-mode=importlib" +addopts = "--color=yes -n=0 --import-mode=importlib" testpaths = ["roseau/load_flow/"] filterwarnings = [ 'ignore:.*utcfromtimestamp:DeprecationWarning:dateutil.*', # dateutil is imported by pandas, not us From 2ea96f96b8d33c67a7371a766e6904251ef3a62d Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Fri, 12 Jan 2024 16:49:06 +0100 Subject: [PATCH 28/51] License ++ --- README.md | 7 +++-- doc/License.md | 59 +++++++++++++++++++++++++++++++++++- doc/index.md | 9 ------ roseau/load_flow/conftest.py | 5 --- roseau/load_flow/license.py | 27 +++++++++-------- 5 files changed, 77 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 407c111a..205176ac 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,16 @@ You can find the complete documentation at https://roseautechnologies.github.io/ ## License -The project is _partially_ open source but to use the solver you will need a license. Please contact -us at contact@roseautechnologies.com to obtain a license. +The project is _partially_ open source but using the solver requires a license. The license key +`A8C6DA-9405FB-E74FB9-C71C3C-207661-V3` can be used free of charge with networks containing up to 10 +buses. To obtain a personal or commercial license, please contact us at contact@roseautechnologies.com. > [!NOTE] > Licenses are given free of charge for **students and teachers**. Please contact us at > contact@roseautechnologies.com for more information. +Read more at [License](https://roseautechnologies.github.io/Roseau_Load_Flow/License.html). + ## Network data With this library, there is a sample of 20 low-voltage and 20 medium-voltage feeders included for an easy diff --git a/doc/License.md b/doc/License.md index d708f8de..9e1b7808 100644 --- a/doc/License.md +++ b/doc/License.md @@ -5,7 +5,11 @@ This project is partially open source. The source code of this repository is available under the [BSD 3-Clause License](https://github.com/RoseauTechnologies/Roseau_Load_Flow/blob/main/LICENSE.md). -The solver used in this project is not open source. A license has to be purchased to use it. To get a license, please contact us at contact@roseautechnologies.com. +The solver used in this project is not open source. A license has to be purchased to use it. To +obtain a personal or commercial license, please contact us at contact@roseautechnologies.com. + +For networks with less than 11 buses (up to 10 buses), the license key `A8C6DA-9405FB-E74FB9-C71C3C-207661-V3` +can be used free of charge. For example, this key can be used to follow the getting started guide. ```{note} Licenses are given **free of charge** for _students and teachers_. Please contact us at @@ -22,6 +26,10 @@ There are two ways to activate the license in your project: environment variable is defined, it will be automatically used by the solver to validate the license, no further action is required. **This is the recommended approach.** + ```{note} + If you need help setting an environment variable, refer to the section + [How to set an environment variable](license-environment-variable) + ``` 2. Call the function `activate_license` with the license key as argument. This function will activate the license for the current session. If you use this approach, it is recommended to store the license key in a file and read it from there to avoid hardcoding it in your code and @@ -36,3 +44,52 @@ There are two ways to activate the license in your project: # Rest of your code here ``` + + where the file `my_license_key.txt` contains `A8C6DA-9405FB-E74FB9-C71C3C-207661-V3` (replace + with your license key). + +(license-environment-variable)= + +## How to set an environment variable + +If you are not sure how to set an environment variable, [this article](https://www.bitecode.dev/p/environment-variables-for-beginners) +has instructions for Windows, MacOS and Linux. The section [Persisting an environment variable](https://www.bitecode.dev/i/121864947/persisting-an-environment-variable) +explains how to make the environment variable persistent on your machine so that you don't have to +set it every time you open a new terminal. + +### For Jupyter Notebook users + +If you are using a _Jupyter Notebook_, you can follow these instructions to set the environment +variable: + +1. Create a file named `.env` in the same directory as you notebook with the following content + (replace the key with your license key): + ```bash + ROSEAU_LOAD_FLOW_LICENSE_KEY="A8C6DA-9405FB-E74FB9-C71C3C-207661-V3" + ``` +2. Add a cell to the beginning of your notebook with the following content and execute it: + ```ipython3 + %pip install python-dotenv + %load_ext dotenv + %dotenv + ``` + The first line will install the package [python-dotenv](https://pypi.org/project/python-dotenv/) + if it is not already installed. The next lines will load the extension `dotenv` and load the + environment variables from the file `.env` in the current directory (created in step 1). + +### For VS Code users + +If you are using [Visual Studio Code](https://code.visualstudio.com/), you can create a file named +`.env` in your project directory (similar to step 1 for Jupyter) and VS Code will automatically +load the environment variables from this file when you run your code (including when using Jupyter +Notebooks in VS Code). + +### For PyCharm users + +If you are using [PyCharm](https://www.jetbrains.com/pycharm/), you can add the environment variable +to your _Python Console_ settings as indicated in the screenshot below: + +```{image} /_static/2024_01_12_Pycharm_Console_Environment_Variable.png +:alt: Pycharm Console environment variable +:align: center +``` diff --git a/doc/index.md b/doc/index.md index ba530be6..98cc6e58 100644 --- a/doc/index.md +++ b/doc/index.md @@ -99,12 +99,3 @@ caption: API Reference --- autoapi/roseau/load_flow/index ``` - - - -```{toctree} ---- -hidden: ---- -autoapi/index -``` diff --git a/roseau/load_flow/conftest.py b/roseau/load_flow/conftest.py index 2990baa2..e2181a46 100644 --- a/roseau/load_flow/conftest.py +++ b/roseau/load_flow/conftest.py @@ -1,16 +1,11 @@ -import os from pathlib import Path import numpy as np import pytest from pandas.testing import assert_frame_equal -from roseau.load_flow import activate_license from roseau.load_flow.utils import console -if "ROSEAU_LOAD_FLOW_TEST_LICENSE_KEY" in os.environ: - activate_license(os.environ["ROSEAU_LOAD_FLOW_TEST_LICENSE_KEY"]) - # Variable to test the network HERE = Path(__file__).parent.expanduser().absolute() TEST_ALL_NETWORKS_DATA_FOLDER = HERE / "tests" / "data" / "networks" diff --git a/roseau/load_flow/license.py b/roseau/load_flow/license.py index a0f1b6b6..3c28f7c9 100644 --- a/roseau/load_flow/license.py +++ b/roseau/load_flow/license.py @@ -12,8 +12,8 @@ __all__ = ["activate_license", "deactivate_license", "get_license", "License"] -_license = None -"""str|None: The Python copy of the activated license.""" +# Cache the license object. Cache cleared when the license is deactivated +_license: "License | None" = None # @@ -33,12 +33,12 @@ def __init__(self, cy_license: CyLicense) -> None: @property def key(self) -> str: - """The key of the license""" + """The key of the license. Please do not share this key.""" return self.cy_license.key @property def expiry_datetime(self) -> dt.datetime | None: - """The expiry date of the license.""" + """The expiry date of the license or ``None`` if the license has no expiry date.""" exp_dt = self.cy_license.expiry_datetime if exp_dt is None: return None @@ -57,10 +57,10 @@ def max_nb_buses(self) -> int | None: """The maximum allowed number of buses for a network. If `None`, the license has no limitation.""" return self.cy_license.max_nb_buses - @staticmethod - def get_machine_fingerprint() -> str: - """This method retrieves your machine fingerprint for license validation.""" - return CyLicense.get_machine_fingerprint() + @property + def machine_fingerprint(self) -> str: + """The anonymized machine fingerprint for license validation.""" + return self.cy_license.machine_fingerprint @staticmethod def get_hostname() -> str: @@ -74,12 +74,13 @@ def get_username() -> str: def activate_license(key: str | None = None) -> None: - """Activate the license with the given key in the current process. + """Activate a license in the current process. Args: key: - The key of the license to activate. If `None` is provided, the environment variable - `ROSEAU_LOAD_FLOW_LICENSE_KEY` is read. + The key of the license to activate. If ``None`` is provided (default), the environment + variable `ROSEAU_LOAD_FLOW_LICENSE_KEY` is used. If this variable is not set, an error + is raised. """ if key is None: key = os.getenv("ROSEAU_LOAD_FLOW_LICENSE_KEY", "") @@ -92,14 +93,14 @@ def activate_license(key: str | None = None) -> None: def deactivate_license() -> None: - """Deactivate the license in the current process.""" + """Deactivate the currently active license.""" global _license cy_deactivate_license() _license = None def get_license() -> License | None: - """A function to retrieve the currently active license.""" + """Get the currently active license or ``None`` if no license is activated.""" global _license if _license is None: cy_license = cy_get_license() From 78e289ef6d09fd687a16d6f551e4254e9bb62764 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Fri, 12 Jan 2024 17:48:45 +0100 Subject: [PATCH 29/51] No conda currently, add missing image --- .github/workflows/ci.yml | 2 ++ doc/Installation.md | 4 ++++ .../2024_01_12_Pycharm_Console_Environment_Variable.png | 3 +++ 3 files changed, 9 insertions(+) create mode 100644 doc/_static/2024_01_12_Pycharm_Console_Environment_Variable.png diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0c42c1c..b423289e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,8 @@ jobs: run: | poetry run pytest -vv -n=auto --durations=25 --cov-report html \ --cov-config pyproject.toml roseau + env: + ROSEAU_LOAD_FLOW_LICENSE_KEY: ${{ secrets.ROSEAU_LOAD_FLOW_LICENSE_KEY }} - name: Archive code coverage results uses: actions/upload-artifact@v4 diff --git a/doc/Installation.md b/doc/Installation.md index 163cf59f..e0d2816f 100644 --- a/doc/Installation.md +++ b/doc/Installation.md @@ -106,6 +106,9 @@ This installs the package in the correct environment for the active notebook ker ## 3. Using `conda` +Installations using `conda` is temporarily unavailable. Please use `pip` instead. + + diff --git a/doc/_static/2024_01_12_Pycharm_Console_Environment_Variable.png b/doc/_static/2024_01_12_Pycharm_Console_Environment_Variable.png new file mode 100644 index 00000000..2ace59ab --- /dev/null +++ b/doc/_static/2024_01_12_Pycharm_Console_Environment_Variable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e6e87a83caae20dfd721cb269926dc95e2d25170aba2425fc55f15c0cfbe244 +size 112823 From 41b57177280e9e0510602368ce0d29f8ed1983f7 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Fri, 12 Jan 2024 18:06:52 +0100 Subject: [PATCH 30/51] Skip CI tests temporarily --- .github/workflows/ci.yml | 6 +++++- .github/workflows/doc.yml | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b423289e..b6fda0fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,11 +2,15 @@ name: CI on: push: + # TODO: rerun on develop when we have the required dependencies + # branches: [main, develop] branches: [main, develop] tags: - "*" pull_request: - branches: [main, develop] + # TODO: rerun on develop when we have the required dependencies + # branches: [main, develop] + branches: [main] env: CI: true diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 4544f11e..afafea5a 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -2,7 +2,9 @@ name: Documentation on: push: - branches: ["main", "develop"] + # TODO: rerun on develop when we have the required dependencies + # branches: ["main", "develop"] + branches: ["main"] workflow_dispatch: inputs: forceDeploy: From 6acfa430c5827006fff7b97820f9c66e5bd6ed69 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Fri, 12 Jan 2024 18:09:26 +0100 Subject: [PATCH 31/51] Skip for real --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6fda0fd..3f8712ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: push: # TODO: rerun on develop when we have the required dependencies # branches: [main, develop] - branches: [main, develop] + branches: [main] tags: - "*" pull_request: From c3d492de1159deab299ce9deece6abb1f441be04 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Mon, 15 Jan 2024 10:09:53 +0100 Subject: [PATCH 32/51] Add missing deps to show_versions and simplify logging --- roseau/load_flow/utils/_versions.py | 15 +++++++- roseau/load_flow/utils/log.py | 54 ++++------------------------- 2 files changed, 20 insertions(+), 49 deletions(-) diff --git a/roseau/load_flow/utils/_versions.py b/roseau/load_flow/utils/_versions.py index a2af81e2..d3f2df42 100644 --- a/roseau/load_flow/utils/_versions.py +++ b/roseau/load_flow/utils/_versions.py @@ -18,7 +18,20 @@ def _get_sys_info() -> JsonDict: def _get_dependency_info() -> JsonDict: """Get versions of dependencies.""" - return {dist: version(dist) for dist in ("pandas", "numpy", "geopandas", "shapely", "regex", "pint")} + return { + dist: version(dist) + for dist in ( + "pandas", + "numpy", + "geopandas", + "shapely", + "regex", + "pint", + "platformdirs", + "certifi", + "roseau-load-flow-engine", + ) + } def show_versions() -> None: diff --git a/roseau/load_flow/utils/log.py b/roseau/load_flow/utils/log.py index 6f0c70c7..d4563ac8 100644 --- a/roseau/load_flow/utils/log.py +++ b/roseau/load_flow/utils/log.py @@ -1,22 +1,9 @@ -import logging -import sys +from typing import Literal from rich.console import Console -from rich.logging import RichHandler -from rich.traceback import install from roseau.load_flow_engine.cy_engine import cy_set_logging_config -# Human logging levels -log_levels = { - "trace": logging.DEBUG, # No deeper log value for Python - "debug": logging.DEBUG, - "info": logging.INFO, - "warning": logging.WARNING, - "error": logging.ERROR, - "critical": logging.CRITICAL, -} - # Rich console console = Console() @@ -43,43 +30,14 @@ """ -def set_logging_config(verbosity: str): - """A function to define the configuration of the logging module +def set_logging_config(verbosity: Literal["trace", "debug", "info", "warning", "error", "critical"]) -> None: + """Configure the logging level of the solver. Args: verbosity: - A valid verbosity level as defined in `log_levels` + A valid verbosity level to set for the solver. + Can be one of: `{"trace", "debug", "info", "warning", "error", "critical"}` """ - level = log_levels[verbosity] - rich_handler_kwargs = { - "show_time": True, - "show_level": True, - "rich_tracebacks": True, - "tracebacks_show_locals": True, - "locals_max_string": None, - } - if verbosity in ("debug", "trace"): - rich_handler_kwargs["show_path"] = True - log_time_format = "%x %X" - else: - rich_handler_kwargs["show_path"] = False - log_time_format = "%x %X" - - # Rich traceback color formatter - error_console = Console(file=sys.stderr, log_time_format=log_time_format) - install(console=error_console, width=None) - - # A first handler on the main console to have output synchronized with progress bar (which are also printed on the - # main console) - handlers = [RichHandler(level=level, console=console, **rich_handler_kwargs)] - - # Define the basic config - logging.basicConfig( - level=log_levels[verbosity], handlers=handlers, datefmt=log_time_format, format="{message}", style="{" - ) - - # Capture the warnings - logging.captureWarnings(True) - + assert verbosity in {"trace", "debug", "info", "warning", "error", "critical"} # Define the logger at C++ level cy_set_logging_config(verbosity) From a0a7924683141da7da432a17e64e2d6b0058efb2 Mon Sep 17 00:00:00 2001 From: Saelyos Date: Mon, 15 Jan 2024 10:49:53 +0100 Subject: [PATCH 33/51] Improve network creation performance --- roseau/load_flow/models/buses.py | 6 ++++-- roseau/load_flow/models/lines/lines.py | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/roseau/load_flow/models/buses.py b/roseau/load_flow/models/buses.py index 5b095216..bf5e6a8c 100644 --- a/roseau/load_flow/models/buses.py +++ b/roseau/load_flow/models/buses.py @@ -83,8 +83,10 @@ def __init__( self.geometry = geometry self._min_voltage: float | None = None self._max_voltage: float | None = None - self.min_voltage = min_voltage - self.max_voltage = max_voltage + if min_voltage is not None: + self.min_voltage = min_voltage + if max_voltage is not None: + self.max_voltage = max_voltage self._res_potentials: ComplexArray | None = None self._short_circuits: list[dict[str, Any]] = [] diff --git a/roseau/load_flow/models/lines/lines.py b/roseau/load_flow/models/lines/lines.py index 2ce149b4..1bbf3db6 100644 --- a/roseau/load_flow/models/lines/lines.py +++ b/roseau/load_flow/models/lines/lines.py @@ -237,12 +237,12 @@ def __init__( if parameters.with_shunt: self._cy_element = CyShuntLine( n=self._n, - y_shunt=(parameters.y_shunt.reshape(self._n * self._n) * self.length).m_as("S"), - z_line=(parameters.z_line.reshape(self._n * self._n) * self.length).m_as("ohm"), + y_shunt=parameters._y_shunt.reshape(self._n * self._n) * self._length, + z_line=parameters._z_line.reshape(self._n * self._n) * self._length, ) else: self._cy_element = CySimplifiedLine( - n=self._n, z_line=(parameters.z_line.reshape(self._n * self._n) * self.length).m_as("ohm") + n=self._n, z_line=parameters._z_line.reshape(self._n * self._n) * self._length ) self._cy_connect() if parameters.with_shunt: @@ -280,7 +280,7 @@ def parameters(self) -> LineParameters: @parameters.setter def parameters(self, value: LineParameters) -> None: shape = (len(self.phases),) * 2 - if value.z_line.shape != shape: + if value._z_line.shape != shape: msg = f"Incorrect z_line dimensions for line {self.id!r}: {value.z_line.shape} instead of {shape}" logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_Z_LINE_SHAPE) @@ -290,7 +290,7 @@ def parameters(self, value: LineParameters) -> None: msg = "Cannot set line parameters with a shunt to a line that does not have shunt components." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL) - if value.y_shunt.shape != shape: + if value._y_shunt.shape != shape: msg = f"Incorrect y_shunt dimensions for line {self.id!r}: {value.y_shunt.shape} instead of {shape}" logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_Y_SHUNT_SHAPE) From 63c08cc6952d6364f3b0cbff50472f05eebda12f Mon Sep 17 00:00:00 2001 From: Saelyos Date: Mon, 15 Jan 2024 10:51:57 +0100 Subject: [PATCH 34/51] Improve results performance --- roseau/load_flow/converters.py | 32 ++++++++++++++++++++--------- roseau/load_flow/models/branches.py | 7 +++++-- roseau/load_flow/network.py | 8 ++++---- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/roseau/load_flow/converters.py b/roseau/load_flow/converters.py index 12ce353b..946bb770 100644 --- a/roseau/load_flow/converters.py +++ b/roseau/load_flow/converters.py @@ -138,15 +138,33 @@ def calculate_voltages(potentials: ComplexArray, phases: str) -> ComplexArray: # we know "n" is the last phase voltages = potentials[:-1] - potentials[-1] else: # Vab, Vbc, Vca - if len(phases) == 2: # noqa: SIM108 + if len(phases) == 2: # V = potentials[0] - potentials[1] (but as array) voltages = potentials[:1] - potentials[1:] else: - # np.roll(["a", "b", "c"], -1) -> ["b", "c", "a"] - voltages = potentials - np.roll(potentials, -1) + assert phases == "abc" + voltages = np.array( + [potentials[0] - potentials[1], potentials[1] - potentials[2], potentials[2] - potentials[0]], + dtype=np.complex128, + ) return voltages +def _calculate_voltage_phases(phases: str) -> list[str]: + if "n" in phases: # "an", "bn", "cn" + return [p + "n" for p in phases[:-1]] + else: # "ab", "bc", "ca" + if len(phases) == 2: + return [phases] + else: + return [p1 + p2 for p1, p2 in zip(phases, np.roll(list(phases), -1), strict=True)] + + +_voltage_cache: dict[str, list[str]] = {} +for _phases in ("ab", "bc", "ca", "an", "bn", "cn", "abn", "bcn", "can", "abc", "abcn"): + _voltage_cache[_phases] = _calculate_voltage_phases(_phases) + + def calculate_voltage_phases(phases: str) -> list[str]: """Calculate the composite phases of the voltages given the phases of an element. @@ -167,10 +185,4 @@ def calculate_voltage_phases(phases: str) -> list[str]: >>> calculate_voltage_phases("abcn") ['an', 'bn', 'cn'] """ - if "n" in phases: # "an", "bn", "cn" - return [p + "n" for p in phases[:-1]] - else: # "ab", "bc", "ca" - if len(phases) == 2: - return [phases] - else: - return [p1 + p2 for p1, p2 in zip(phases, np.roll(list(phases), -1), strict=True)] + return _voltage_cache[phases] diff --git a/roseau/load_flow/models/branches.py b/roseau/load_flow/models/branches.py index 3aef18fd..accb2f5a 100644 --- a/roseau/load_flow/models/branches.py +++ b/roseau/load_flow/models/branches.py @@ -107,9 +107,12 @@ def res_currents(self) -> tuple[Q_[ComplexArray], Q_[ComplexArray]]: """The load flow result of the branch currents (A).""" return self._res_currents_getter(warning=True) - def _res_powers_getter(self, warning: bool) -> tuple[ComplexArray, ComplexArray]: + def _res_powers_getter( + self, warning: bool, pot1: ComplexArray | None = None, pot2: ComplexArray | None = None + ) -> tuple[ComplexArray, ComplexArray]: cur1, cur2 = self._res_currents_getter(warning) - pot1, pot2 = self._res_potentials_getter(warning=False) # we warn on the previous line + if pot1 is None or pot2 is None: + pot1, pot2 = self._res_potentials_getter(warning=False) # we warn on the previous line powers1 = pot1 * cur1.conj() powers2 = pot2 * cur2.conj() return powers1, powers2 diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 7c3b9922..3b9e91a7 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -719,8 +719,8 @@ def res_branches(self) -> pd.DataFrame: dtypes = {c: _DTYPES[c] for c in res_dict} for branch_id, branch in self.branches.items(): currents1, currents2 = branch._res_currents_getter(warning=False) - powers1, powers2 = branch._res_powers_getter(warning=False) potentials1, potentials2 = branch._res_potentials_getter(warning=False) + powers1, powers2 = branch._res_powers_getter(warning=False, pot1=potentials1, pot2=potentials2) phases = sorted(set(branch.phases1) | set(branch.phases2)) for phase in phases: if phase in branch.phases1: @@ -790,8 +790,8 @@ def res_transformers(self) -> pd.DataFrame: if not isinstance(branch, Transformer): continue currents1, currents2 = branch._res_currents_getter(warning=False) - powers1, powers2 = branch._res_powers_getter(warning=False) potentials1, potentials2 = branch._res_potentials_getter(warning=False) + powers1, powers2 = branch._res_powers_getter(warning=False, pot1=potentials1, pot2=potentials2) s_max = branch.parameters._max_power violated = None if s_max is not None: @@ -880,7 +880,7 @@ def res_lines(self) -> pd.DataFrame: continue potentials = branch._res_potentials_getter(warning=False) currents = branch._res_currents_getter(warning=False) - powers = branch._res_powers_getter(warning=False) + powers = branch._res_powers_getter(warning=False, pot1=potentials[0], pot2=potentials[1]) series_losses = branch._res_series_power_losses_getter(warning=False) series_currents = branch._res_series_currents_getter(warning=False) i_max = branch.parameters._max_current @@ -940,7 +940,7 @@ def res_switches(self) -> pd.DataFrame: continue potentials = branch._res_potentials_getter(warning=False) currents = branch._res_currents_getter(warning=False) - powers = branch._res_powers_getter(warning=False) + powers = branch._res_powers_getter(warning=False, pot1=potentials[0], pot2=potentials[1]) for i1, i2, s1, s2, v1, v2, phase in zip(*currents, *powers, *potentials, branch.phases, strict=True): res_dict["switch_id"].append(branch.id) res_dict["phase"].append(phase) From b42828028d6efcf57c704a1b0588a252a94e8299 Mon Sep 17 00:00:00 2001 From: Saelyos Date: Mon, 15 Jan 2024 13:44:57 +0100 Subject: [PATCH 35/51] Add precise error codes --- roseau/load_flow/license.py | 2 +- roseau/load_flow/network.py | 49 ++++++++++++++++++++++--------------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/roseau/load_flow/license.py b/roseau/load_flow/license.py index 3c28f7c9..deda4a4e 100644 --- a/roseau/load_flow/license.py +++ b/roseau/load_flow/license.py @@ -87,7 +87,7 @@ def activate_license(key: str | None = None) -> None: try: cy_activate_license(key=key, cacert_filepath=certifi.where(), cache_folderpath=user_cache_dir()) except RuntimeError as e: - msg = f"The license can not be activated. The detailed error message is {e.args[0]!r}." + msg = f"The license can not be activated. The detailed error message is {e.args[0][2:]!r}." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.LICENSE_ERROR) from e diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 3b9e91a7..7ddf760a 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -11,7 +11,7 @@ from importlib import resources from itertools import chain, cycle from pathlib import Path -from typing import TYPE_CHECKING, TypeVar +from typing import TYPE_CHECKING, NoReturn, TypeVar import geopandas as gpd import numpy as np @@ -465,7 +465,7 @@ def solve_load_flow( warm_start: bool = True, solver: Solver = _DEFAULT_SOLVER, solver_params: JsonDict | None = None, - ) -> int: + ) -> tuple[int, float]: """Solve the load flow for this network. To get the results of the load flow for the whole network, use the `res_` properties on the @@ -521,24 +521,7 @@ def solve_load_flow( try: iterations, residual = self._solver.solve_load_flow(max_iterations=max_iterations, tolerance=tolerance) except RuntimeError as e: - msg = e.args[0] - zero_elements_index, inf_elements_index = self._solver._cy_solver.analyse_jacobian() - if zero_elements_index: - zero_elements = [self._elements[i] for i in zero_elements_index] - printable_elements = ", ".join(f"{type(e).__name__}({e.id!r})" for e in zero_elements) - msg += ( - f"The problem seems to come from the elements [{printable_elements}] that have at least one " - f"disconnected phase. " - ) - if inf_elements_index: - inf_elements = [self._elements[i] for i in inf_elements_index] - printable_elements = ", ".join(f"{type(e).__name__}({e.id!r})" for e in inf_elements) - msg += ( - f"The problem seems to come from the elements [{printable_elements}] that induce infinite " - f"values. This might be caused by flexible loads with very high alpha." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_JACOBIAN) from e + self._handle_error(e) end = time.perf_counter() @@ -570,6 +553,32 @@ def solve_load_flow( return iterations, residual + def _handle_error(self, e: RuntimeError) -> NoReturn: + msg = e.args[0] + if msg.startswith("0 "): + msg = f"The license can not be validated. The detailed error message is {msg[2:]!r}" + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.LICENSE_ERROR) from e + else: + assert msg.startswith("1 ") + msg = msg[2:] + zero_elements_index, inf_elements_index = self._solver._cy_solver.analyse_jacobian() + if zero_elements_index: + zero_elements = [self._elements[i] for i in zero_elements_index] + printable_elements = ", ".join(f"{type(e).__name__}({e.id!r})" for e in zero_elements) + msg += ( + f"The problem seems to come from the elements [{printable_elements}] that have at least one " + f"disconnected phase. " + ) + if inf_elements_index: + inf_elements = [self._elements[i] for i in inf_elements_index] + printable_elements = ", ".join(f"{type(e).__name__}({e.id!r})" for e in inf_elements) + msg += ( + f"The problem seems to come from the elements [{printable_elements}] that induce infinite " + f"values. This might be caused by flexible loads with very high alpha." + ) + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_JACOBIAN) from e + def _results_from_dict(self, data: JsonDict) -> None: """Dispatch the results to all the elements of the network. From e0d6ad86fcad7789cea3c81f0d2522ebec48f0af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Vinot?= Date: Mon, 15 Jan 2024 17:18:02 +0100 Subject: [PATCH 36/51] Add a log --- roseau/load_flow/network.py | 1 + 1 file changed, 1 insertion(+) diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 7ddf760a..79ec0935 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -557,6 +557,7 @@ def _handle_error(self, e: RuntimeError) -> NoReturn: msg = e.args[0] if msg.startswith("0 "): msg = f"The license can not be validated. The detailed error message is {msg[2:]!r}" + logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.LICENSE_ERROR) from e else: assert msg.startswith("1 ") From 4c8589e752295ebdeb7575e1314cc6f0b134d83a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Vinot?= Date: Mon, 15 Jan 2024 18:14:39 +0100 Subject: [PATCH 37/51] Clean unused error code --- roseau/load_flow/exceptions.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/roseau/load_flow/exceptions.py b/roseau/load_flow/exceptions.py index aee36ee0..933b93de 100644 --- a/roseau/load_flow/exceptions.py +++ b/roseau/load_flow/exceptions.py @@ -22,7 +22,6 @@ class RoseauLoadFlowExceptionCode(Enum): # Buses BAD_BUS_ID = auto() - BAD_BUS_TYPE = auto() BAD_POTENTIALS_SIZE = auto() BAD_VOLTAGES = auto() BAD_VOLTAGES_SIZE = auto() @@ -80,19 +79,15 @@ class RoseauLoadFlowExceptionCode(Enum): NO_VOLTAGE_SOURCE = auto() BAD_ELEMENT_OBJECT = auto() DISCONNECTED_ELEMENT = auto() - BAD_ELEMENT_ID = auto() NO_LOAD_FLOW_CONVERGENCE = auto() - BAD_REQUEST = auto() BAD_LOAD_FLOW_RESULT = auto() LOAD_FLOW_NOT_RUN = auto() SEVERAL_NETWORKS = auto() - TOO_MANY_BUSES = auto() BAD_JACOBIAN = auto() # Solver BAD_SOLVER_NAME = auto() BAD_SOLVER_PARAMS = auto() - NETWORK_SOLVER_MISMATCH = auto() # DGS export DGS_BAD_PHASE_TECHNOLOGY = auto() From b1a0389272b507c722f33ff2e98854346f1d4034 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Tue, 16 Jan 2024 10:14:51 +0100 Subject: [PATCH 38/51] Fix minor typo --- roseau/load_flow/license.py | 2 +- roseau/load_flow/network.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/roseau/load_flow/license.py b/roseau/load_flow/license.py index deda4a4e..e5f020d0 100644 --- a/roseau/load_flow/license.py +++ b/roseau/load_flow/license.py @@ -87,7 +87,7 @@ def activate_license(key: str | None = None) -> None: try: cy_activate_license(key=key, cacert_filepath=certifi.where(), cache_folderpath=user_cache_dir()) except RuntimeError as e: - msg = f"The license can not be activated. The detailed error message is {e.args[0][2:]!r}." + msg = f"The license cannot be activated. The detailed error message is {e.args[0][2:]!r}." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.LICENSE_ERROR) from e diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 79ec0935..f4b2526f 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -556,7 +556,7 @@ def solve_load_flow( def _handle_error(self, e: RuntimeError) -> NoReturn: msg = e.args[0] if msg.startswith("0 "): - msg = f"The license can not be validated. The detailed error message is {msg[2:]!r}" + msg = f"The license cannot be validated. The detailed error message is {msg[2:]!r}" logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.LICENSE_ERROR) from e else: From 45ef8d405f8f63f178a3b31a06202d323f28c997 Mon Sep 17 00:00:00 2001 From: Saelyos Date: Mon, 22 Jan 2024 15:28:29 +0100 Subject: [PATCH 39/51] Fix potential propagation --- roseau/load_flow/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index f4b2526f..a2be6bdd 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -1292,7 +1292,7 @@ def _propagate_potentials(self) -> None: element.potentials = potentials[0:bus_n] element._initialized_by_the_user = False # only used for serialization for e in element._connected_elements: - if e not in visited: + if e not in visited and not isinstance(e, Ground): if isinstance(element, Transformer): k = element.parameters._ulv / element.parameters._uhv phase_displacement = element.parameters.phase_displacement From af1076ce602df67dc9ecccf60b2b204fdb41cb0a Mon Sep 17 00:00:00 2001 From: Saelyos Date: Mon, 22 Jan 2024 16:25:25 +0100 Subject: [PATCH 40/51] Allow more connections --- roseau/load_flow/network.py | 6 ++++- .../tests/test_electrical_network.py | 22 +++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index a2be6bdd..e1ae0370 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -1123,8 +1123,12 @@ def _connect_element(self, element: Element) -> None: self.branches[element.id] = element elif isinstance(element, VoltageSource): self.sources[element.id] = element + elif isinstance(element, Ground): + self.grounds[element.id] = element + elif isinstance(element, PotentialRef): + self.potential_refs[element.id] = element else: - msg = "Only lines, loads, buses and sources can be added to the network." + msg = f"Unknown element {element} can not be added to the network" logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_ELEMENT_OBJECT) element._network = self diff --git a/roseau/load_flow/tests/test_electrical_network.py b/roseau/load_flow/tests/test_electrical_network.py index cde07bc2..ec0b1e2b 100644 --- a/roseau/load_flow/tests/test_electrical_network.py +++ b/roseau/load_flow/tests/test_electrical_network.py @@ -196,6 +196,21 @@ def test_connect_and_disconnect(): line = Line(id="line", bus1=source_bus, bus2=load_bus, phases="abcn", parameters=lp, length=10) PotentialRef("pref", element=ground) en = ElectricalNetwork.from_element(source_bus) + + # Connection of a new connected component + load_bus2 = Bus(id="load_bus2", phases="abcn") + ground2 = Ground("ground2") + ground2.connect(bus=load_bus2) + tp = TransformerParameters.from_catalogue(id="SE_Minera_A0Ak_50kVA") + Transformer(id="transfo", bus1=load_bus, bus2=load_bus2, parameters=tp) + with pytest.raises(RoseauLoadFlowException) as e: + en._check_validity(constructed=False) + assert "does not have a potential reference" in e.value.args[0] + assert e.value.args[1] == RoseauLoadFlowExceptionCode.NO_POTENTIAL_REFERENCE + PotentialRef("pref2", element=ground2) # Add potential ref + en._check_validity(constructed=False) + + # Disconnection of a load assert load.network == en load.disconnect() assert load.network is None @@ -223,11 +238,10 @@ def test_connect_and_disconnect(): assert e.value.msg == "Ground(id='a separate ground element') is not a valid load or source." assert e.value.code == RoseauLoadFlowExceptionCode.BAD_ELEMENT_OBJECT - # Adding ground => impossible - ground2 = Ground("ground2") + # Adding unknown element with pytest.raises(RoseauLoadFlowException) as e: - en._connect_element(ground2) - assert e.value.msg == "Only lines, loads, buses and sources can be added to the network." + en._connect_element(3) + assert "Unknown element" in e.value.msg assert e.value.code == RoseauLoadFlowExceptionCode.BAD_ELEMENT_OBJECT # Remove line => impossible From cf9527ff3662b028bdd4bedc69aca9ce673e357b Mon Sep 17 00:00:00 2001 From: Saelyos Date: Mon, 22 Jan 2024 16:35:45 +0100 Subject: [PATCH 41/51] Safer check --- roseau/load_flow/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index e1ae0370..8bbbc22b 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -1128,7 +1128,7 @@ def _connect_element(self, element: Element) -> None: elif isinstance(element, PotentialRef): self.potential_refs[element.id] = element else: - msg = f"Unknown element {element} can not be added to the network" + msg = f"Unknown element {element} can not be added to the network." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_ELEMENT_OBJECT) element._network = self @@ -1296,7 +1296,7 @@ def _propagate_potentials(self) -> None: element.potentials = potentials[0:bus_n] element._initialized_by_the_user = False # only used for serialization for e in element._connected_elements: - if e not in visited and not isinstance(e, Ground): + if e not in visited and isinstance(e, AbstractBranch | Bus): if isinstance(element, Transformer): k = element.parameters._ulv / element.parameters._uhv phase_displacement = element.parameters.phase_displacement From b1fdbaaf2805df4fb0e5e0bb9f3559bfc48e0dac Mon Sep 17 00:00:00 2001 From: Saelyos Date: Mon, 22 Jan 2024 16:53:01 +0100 Subject: [PATCH 42/51] Update changelog --- doc/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/Changelog.md b/doc/Changelog.md index df1e3a36..a43db500 100644 --- a/doc/Changelog.md +++ b/doc/Changelog.md @@ -2,6 +2,7 @@ ## Unreleased +- {gh-pr}`168` {gh-issue}`166` Fix initial potentials propagation. - {gh-pr}`163` {gh-issue}`158` Fix `ElectricalNetwork.res_transformers` returning an empty dataframe when max_power is not set. - {gh-pr}`151` Require Python 3.10 or newer. From e63dd605a1347cd809567a92b160900c2d40b603 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Tue, 23 Jan 2024 17:17:23 +0100 Subject: [PATCH 43/51] Add lines catalogue and get_catalogue method (#167) Resolves #161 Fixes some of the points of #122 --- .pre-commit-config.yaml | 4 +- .vscode/cspell.json | 6 +- .vscode/settings.json | 6 +- .vscode/tasks.json | 18 +- conda/environment.yml | 1 - conda/meta.yaml | 1 - doc/conf.py | 1 - doc/models/Line/Parameters.md | 51 +- .../Transformer/Center_Tapped_Transformer.md | 4 +- doc/usage/Catalogues.md | 371 +++++++++---- poetry.lock | 40 +- pyproject.toml | 1 - roseau/load_flow/_compat.py | 41 ++ roseau/load_flow/conftest.py | 7 - roseau/load_flow/data/lines/Catalogue.csv | 356 +++++++++++++ roseau/load_flow/exceptions.py | 48 +- roseau/load_flow/io/tests/test_dict.py | 25 +- roseau/load_flow/models/core.py | 2 +- roseau/load_flow/models/lines/lines.py | 26 +- roseau/load_flow/models/lines/parameters.py | 498 +++++++++++++++--- roseau/load_flow/models/loads/loads.py | 4 +- .../models/tests/test_line_parameters.py | 178 ++++++- .../tests/test_transformer_parameters.py | 112 ++-- .../models/transformers/parameters.py | 352 +++++-------- roseau/load_flow/network.py | 282 ++++------ .../tests/test_electrical_network.py | 131 +++-- roseau/load_flow/tests/test_exceptions.py | 14 +- roseau/load_flow/utils/__init__.py | 7 +- roseau/load_flow/utils/console.py | 25 - roseau/load_flow/utils/constants.py | 88 ++-- roseau/load_flow/utils/log.py | 27 - roseau/load_flow/utils/mixins.py | 108 +++- roseau/load_flow/utils/tests/test_types.py | 43 +- roseau/load_flow/utils/types.py | 263 ++++----- 34 files changed, 1985 insertions(+), 1156 deletions(-) create mode 100644 roseau/load_flow/_compat.py create mode 100644 roseau/load_flow/data/lines/Catalogue.csv delete mode 100644 roseau/load_flow/utils/console.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d3c57805..16c4ede7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,9 @@ -exclude: ^.idea/|^conda/meta.yaml +exclude: ^.idea/|.vscode/|^conda/meta.yaml repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: check-json - exclude: ^.vscode/ - id: check-merge-conflict - id: check-toml - id: check-yaml @@ -35,4 +34,3 @@ repos: hooks: - id: prettier args: ["--print-width", "120"] - exclude: ^.vscode/ diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 62c34947..756d6369 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -7,6 +7,7 @@ "words": [ "abcn", "absolufy", + "acsr", "asarray", "astype", "bysource", @@ -34,10 +35,13 @@ "susceptance", "transfo", "ureg", + "xlpe", "yesqa" ], // flagWords - list of words to be always considered incorrect // This is useful for offensive words and common spelling errors. // For example "hte" should be "the" - "flagWords": ["hte"] + "flagWords": [ + "hte" + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index da5cade4..4ad513a2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,5 +25,9 @@ "[markdown][yaml][html][css]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, - } + }, + // Json + "[json]": { + "editor.indentSize": 2, + }, } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 48ba0e67..689358f5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,7 +5,7 @@ "options": { "cwd": "${workspaceFolder}" }, "presentation": { "showReuseMessage": true, - "clear": true + "clear": true, }, "tasks": [ { @@ -15,13 +15,13 @@ "command": "make -C doc html", "group": { "kind": "build", - "isDefault": true + "isDefault": true, }, "problemMatcher": [], "presentation": { "reveal": "silent", - "focus": true - } + "focus": true, + }, }, { "label": "Open docs", @@ -32,13 +32,13 @@ "reveal": "never", "close": true, "focus": false, - "panel": "dedicated" + "panel": "dedicated", }, "group": { "kind": "build", - "isDefault": true + "isDefault": true, }, - "isBackground": true - } - ] + "isBackground": true, + }, + ], } diff --git a/conda/environment.yml b/conda/environment.yml index efa95a45..dbac36a5 100644 --- a/conda/environment.yml +++ b/conda/environment.yml @@ -11,7 +11,6 @@ dependencies: - regex >=2022.1.18 - pint >=0.21.0 - typing_extensions >=4.6.2 - - rich >=13.5.1 - pyproj >=3.3.0 - matplotlib-base >=3.7.2 - networkx >=3.0.0 diff --git a/conda/meta.yaml b/conda/meta.yaml index 9bebd633..3bc3230d 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -33,7 +33,6 @@ requirements: - regex >=2022.1.18 - pint >=0.21.0 - typing_extensions >=4.6.2 - - rich >=13.5.1 - pyproj >=3.3.0 - matplotlib-base >=3.7.2 - networkx >=3.0.0 diff --git a/doc/conf.py b/doc/conf.py index c591b375..468be0e1 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -128,7 +128,6 @@ "geopandas": ("https://geopandas.org/en/stable/", None), "pint": ("https://pint.readthedocs.io/en/stable/", None), "typing_extensions": ("https://typing-extensions.readthedocs.io/en/stable/", None), - "rich": ("https://rich.readthedocs.io/en/stable/", None), "matplotlib": ("https://matplotlib.org/stable/", None), "networkx": ("https://networkx.org/documentation/stable/", None), } diff --git a/doc/models/Line/Parameters.md b/doc/models/Line/Parameters.md index fa3496b0..c8222e2b 100644 --- a/doc/models/Line/Parameters.md +++ b/doc/models/Line/Parameters.md @@ -2,8 +2,15 @@ # Parameters -The line parameters are briefly described [here](models-line_parameters). In this page, the alternative constructors -of `LineParameters` objects are detailed. +As described [in the previous page](models-line_parameters), a line parameters object contains the +impedance and shunt admittance matrices representing the line model. Sometimes you do not have +these matrices available but you have other data such as symmetric components or geometric +configurations and material types. + +This page describes how to build the impedance and shunt admittance matrices and thus the line +parameters object using these alternative data. This is achieved via the alternative constructors +of the `LineParameters` class. Note that only 3-phase lines are supported by the alternative +constructors. (models-line_parameters-alternative_constructors-symmetric)= @@ -11,16 +18,16 @@ of `LineParameters` objects are detailed. ### Definition -The `LineParameters` class has a class method called `from_sym` which converts zero and direct sequences of -impedance and admittance into a line parameters instance. This method requires the following data: +Line parameters can be built from a symmetric model of the line using the `LineParameters.from_sym` +class method. This method takes the following data: - The zero sequence of the impedance (in $\Omega$/km), noted $\underline{Z_0}$ and `z0` in the code. - The direct sequence of the impedance (in $\Omega$/km), noted $\underline{Z_1}$ and `z1` in the code. - The zero sequence of the admittance (in S/km), noted $\underline{Y_0}$ and `y0` in the code. - The direct sequence of the admittance (in S/km), noted $\underline{Y_1}$ and `y1` in the code. -Then, it combines them in order to build the series impedance matrix $\underline{Z}$ and the shunt admittance matrix -$\underline{Y}$ using the following equations: +The symmetric componenets are then used to build the series impedance matrix $\underline{Z}$ and +the shunt admittance matrix $\underline{Y}$ using the following equations: ```{math} \begin{aligned} @@ -51,8 +58,7 @@ defined as: \end{aligned} ``` -This class method also takes optional parameters which are used to add a neutral wire to the previously seen -three-phase matrices. These optional parameters are: +For lines with a neutral, this method also takes the following optional extra parameters: - The neutral impedance (in $\Omega$/km), noted $\underline{Z_{\mathrm{n}}}$ and `zn` in the code. - The phase-to-neutral reactance (in $\Omega$/km), noted $\left(\underline{X_{p\mathrm{n}}}\right)_{p\in\{\mathrm{a}, @@ -63,8 +69,9 @@ three-phase matrices. These optional parameters are: \mathrm{b},\mathrm{c}\}}$. As these are supposed to be the same, this unique value is noted `bpn` in the code. ```{note} -If any of those parameters is omitted, the neutral wire is omitted and a 3 phase line parameters is built. -If $\underline{Z_{\mathrm{n}}}$ and $\underline{X_{p\mathrm{n}}}$ are zeros, the same happens. +If any of those parameters is omitted or if $\underline{Z_{\mathrm{n}}}$ and +$\underline{X_{p\mathrm{n}}}$ are zeros, the neutral wire is omitted and a 3-phase line parameters +is built. ``` In this case, the following matrices are built: @@ -102,8 +109,8 @@ respectively the phase-to-neutral series impedance (in $\Omega$/km), the neutral the phase-to-neutral shunt admittance (in S/km). ````{note} -The computed impedance matrix may be non-invertible. In this case, the `from_sym` class method builds impedance and -shunt admittance matrices using the following definitions: +If the computed impedance matrix is be non-invertible, the `from_sym` class method builds impedance +and shunt admittance matrices using the following definitions: ```{math} \begin{aligned} @@ -204,7 +211,7 @@ matrices from dimensions and materials used for the insulator and the conductors proposed: the first one is for a twisted line and the second is for an underground line. Both of them include a neutral wire. -This class methods accepts the following arguments: +This class method accepts the following arguments: - the line type to choose between the twisted and the underground options. - the conductor type which defines the material of the conductors. @@ -231,15 +238,15 @@ where: The following resistivities are used by _Roseau Load Flow_: -| Material | Resistivity ($\Omega$m) | -| :------------ | :---------------------- | -| Copper | $1.72\times10^{-8}$ | -| Aluminium | $2.82\times10^{-8}$ | -| Almélec | $3.26\times10^{-8}$ | -| Alu-Acier | $4.0587\times10^{-8}$ | -| Almélec-Acier | $3.26\times10^{-8}$ | +| Material | Resistivity ($\Omega$m) | +| :------------------------- | :---------------------- | +| Copper -- Fr: Cuivre | $1.72\times10^{-8}$ | +| Aluminum -- Fr: Aluminium | $2.82\times10^{-8}$ | +| Al-Mg Alloy -- Fr: Almélec | $3.26\times10^{-8}$ | +| ACSR -- Fr: Alu-Acier | $4.0587\times10^{-8}$ | +| AACSR -- Fr: Almélec-Acier | $3.26\times10^{-8}$ | -These values are defined in the `utils` module: [](#roseau.load_flow.utils.constants.RHO). +These values are defined in the `utils` module: {data}`roseau.load_flow.utils.constants.RHO`. #### Inductance @@ -271,7 +278,7 @@ where: - $D_{ij}$ the distances between the center of the conductor $i$ and the center of the conductor $j$ - $GMR_i$ the _geometric mean radius_ of the conductor $i$. -The vacuum magnetic permeability is defined in the `utils` module [](#roseau.load_flow.utils.constants.MU_0). +The vacuum magnetic permeability is defined in the `utils` module {data}`roseau.load_flow.utils.constants.MU_0`. The geometric mean radius is defined for all $i\in \{\mathrm{a}, \mathrm{b}, \mathrm{c}, \mathrm{n}\}$ as diff --git a/doc/models/Transformer/Center_Tapped_Transformer.md b/doc/models/Transformer/Center_Tapped_Transformer.md index d8092f11..fed2ff3a 100644 --- a/doc/models/Transformer/Center_Tapped_Transformer.md +++ b/doc/models/Transformer/Center_Tapped_Transformer.md @@ -95,7 +95,9 @@ load_bus = Bus(id="load_bus", phases="abc") mv_load = PowerLoad("mv_load", load_bus, powers=[10000, 10000, 10000]) # Connect the two MV buses with a line -lp = LineParameters.from_name_mv("U_AL_150") # Underground, ALuminium, 150mm² +lp = LineParameters.from_catalogue( + id="U_AL_150", model="iec" +) # Underground, ALuminium, 150mm² line = Line("line", source_bus, load_bus, parameters=lp, length=1.0, ground=ground) # Create a low-voltage bus and a load diff --git a/doc/usage/Catalogues.md b/doc/usage/Catalogues.md index 976ee42f..65ca4c13 100644 --- a/doc/usage/Catalogues.md +++ b/doc/usage/Catalogues.md @@ -19,60 +19,57 @@ interactive map. All these networks are built from open data available in France. The entire France can be provided on demand. Please email us at [contact@roseautechnologies.com](mailto:contact@roseautechnologies.com). -### Printing the catalogue +### Inspecting the catalogue -This catalogue can be printed to the terminal: +This catalogue can be retrieved in the form of a dataframe using: ```pycon >>> from roseau.load_flow import ElectricalNetwork ->>> ElectricalNetwork.print_catalogue() +>>> ElectricalNetwork.get_catalogue() ``` | Name | Nb buses | Nb branches | Nb loads | Nb sources | Nb grounds | Nb potential refs | Available load points | -| :-------------------------------------------------------------------------------- | -------: | ----------: | -------: | ---------: | ---------: | ----------------: | --------------------: | -| LVFeeder00939 | 8 | 7 | 12 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder02639 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder04790 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder06713 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder06926 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder06975 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder18498 | 18 | 17 | 32 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder18769 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder19558 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder20256 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder23832 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder24400 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder27429 | 11 | 10 | 18 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder27681 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder30216 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder31441 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder36284 | 5 | 4 | 6 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder36360 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder37263 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder38211 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder004 | 17 | 16 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder011 | 50 | 49 | 68 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder015 | 30 | 29 | 20 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder032 | 53 | 52 | 40 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder041 | 88 | 87 | 62 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder063 | 39 | 38 | 38 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder078 | 69 | 68 | 46 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder115 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder128 | 49 | 48 | 32 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder151 | 59 | 58 | 44 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder159 | 8 | 7 | 0 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder176 | 33 | 32 | 20 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder210 | 128 | 127 | 82 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder217 | 44 | 43 | 44 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder232 | 66 | 65 | 38 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder251 | 125 | 124 | 106 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder290 | 12 | 11 | 16 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder312 | 11 | 10 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder320 | 20 | 19 | 12 | 1 | 1 | 1 | 'Summer', 'Winter' | -| MVFeeder339 | 33 | 32 | 28 | 1 | 1 | 1 | 'Summer', 'Winter' | - -The table is printed using the [Rich Python library](https://rich.readthedocs.io/en/stable/index.html). Links to the -map of each network have been added in the documentation. +| :-------------------------------------------------------------------------------- | -------: | ----------: | -------: | ---------: | ---------: | ----------------: | :-------------------- | +| LVFeeder00939 | 8 | 7 | 12 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder02639 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder04790 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder06713 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder06926 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder06975 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder18498 | 18 | 17 | 32 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder18769 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder19558 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder20256 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder23832 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder24400 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder27429 | 11 | 10 | 18 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder27681 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder30216 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder31441 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder36284 | 5 | 4 | 6 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder36360 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder37263 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder38211 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder004 | 17 | 16 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder011 | 50 | 49 | 68 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder015 | 30 | 29 | 20 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder032 | 53 | 52 | 40 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder041 | 88 | 87 | 62 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder063 | 39 | 38 | 38 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder078 | 69 | 68 | 46 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder115 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder128 | 49 | 48 | 32 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder151 | 59 | 58 | 44 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder159 | 8 | 7 | 0 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder176 | 33 | 32 | 20 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder210 | 128 | 127 | 82 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder217 | 44 | 43 | 44 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder232 | 66 | 65 | 38 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder251 | 125 | 124 | 106 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder290 | 12 | 11 | 16 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder312 | 11 | 10 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder320 | 20 | 19 | 12 | 1 | 1 | 1 | 'Summer', 'Winter' | +| MVFeeder339 | 33 | 32 | 28 | 1 | 1 | 1 | 'Summer', 'Winter' | There are MV networks whose names start with "MVFeeder" and LV networks whose names with "LVFeeder". For each network, there are two available load points: @@ -80,62 +77,62 @@ network, there are two available load points: - "Winter": it contains power loads without production. - "Summer": it contains power loads with production and 20% of the "Winter" load. -The arguments of the method `print_catalogue` can be used to filter the output. If you want to print the LV networks +The arguments of the method `get_catalogue` can be used to filter the output. If you want to get the LV networks only, you can call: ```pycon ->>> ElectricalNetwork.print_catalogue(name="LVFeeder") +>>> ElectricalNetwork.get_catalogue(name="LVFeeder") ``` | Name | Nb buses | Nb branches | Nb loads | Nb sources | Nb grounds | Nb potential refs | Available load points | -| :------------ | -------: | ----------: | -------: | ---------: | ---------: | ----------------: | --------------------: | -| LVFeeder00939 | 8 | 7 | 12 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder02639 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder04790 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder06713 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder06926 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder06975 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder18498 | 18 | 17 | 32 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder18769 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder19558 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder20256 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder23832 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder24400 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder27429 | 11 | 10 | 18 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder27681 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder30216 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder31441 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder36284 | 5 | 4 | 6 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder36360 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder37263 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | -| LVFeeder38211 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | +| :------------ | -------: | ----------: | -------: | ---------: | ---------: | ----------------: | :-------------------- | +| LVFeeder00939 | 8 | 7 | 12 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder02639 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder04790 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder06713 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder06926 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder06975 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder18498 | 18 | 17 | 32 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder18769 | 7 | 6 | 10 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder19558 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder20256 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder23832 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder24400 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder27429 | 11 | 10 | 18 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder27681 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder30216 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder31441 | 4 | 3 | 4 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder36284 | 5 | 4 | 6 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder36360 | 9 | 8 | 14 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder37263 | 3 | 2 | 2 | 1 | 1 | 1 | 'Summer', 'Winter' | +| LVFeeder38211 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | A regular expression can also be used: ```pycon ->>> ElectricalNetwork.print_catalogue(name=r"LVFeeder38[0-9]+") +>>> ElectricalNetwork.get_catalogue(name=r"LVFeeder38[0-9]+") ``` | Name | Nb buses | Nb branches | Nb loads | Nb sources | Nb grounds | Nb potential refs | Available load points | -| :------------ | -------: | ----------: | -------: | ---------: | ---------: | ----------------: | --------------------: | -| LVFeeder38211 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | +| :------------ | -------: | ----------: | -------: | ---------: | ---------: | ----------------: | :-------------------- | +| LVFeeder38211 | 6 | 5 | 8 | 1 | 1 | 1 | 'Summer', 'Winter' | ### Getting an instance -To build a network from the catalogue, the class method `from_catalogue` can be used. The name of the network and -the name of the load point must be provided: +You can build an `ElectricalNetwork` instance from the catalogue using the class method +`from_catalogue`. The name of the network and the name of the load point must be provided: ```pycon >>> en = ElectricalNetwork.from_catalogue(name="LVFeeder38211", load_point_name="Summer") ``` -In case of mistakes, an error is raised: +In case no or several results match the parameters, an error is raised: ```pycon >>> ElectricalNetwork.from_catalogue(name="LVFeeder38211", load_point_name="Unknown") -RoseauLoadFlowException: No load point matching the name 'Unknown' has been found for the network 'LVFeeder38211'. -Please look at the catalogue using the `print_catalogue` class method. [catalogue_not_found] +RoseauLoadFlowException: No load points for network 'LVFeeder38211' matching the query (load_point_name='Unknown') +have been found. Please look at the catalogue using the `get_catalogue` class method. [catalogue_not_found] ``` (catalogues-transformers)= @@ -159,13 +156,13 @@ The available transformers data come from the following data sheets: Pull requests to add some other sources are welcome! -### Printing the catalogue +### Inspecting the catalogue -This catalogue can be printed to the terminal: +This catalogue can be retrieved in the form of a dataframe using: ```pycon >>> from roseau.load_flow import TransformerParameters ->>> TransformerParameters.print_catalogue() +>>> TransformerParameters.get_catalogue() ``` | Id | Manufacturer | Product range | Efficiency | Type | Nominal power (kVA) | High voltage (kV) | Low voltage (kV) | @@ -313,11 +310,11 @@ The following data are available in this table: - the primary side phase to phase voltage, noted **uhv**. - the secondary side phase to phase volage, noted **ulv**. -The `print_catalogue` method accepts arguments (in bold above) that can be used to filter the printed table. The -following command only prints transformer parameters of transformers with an efficiency of "A0Ak": +The `get_catalogue` method accepts arguments (in bold above) that can be used to filter the returned table. The +following command only retrieves transformer parameters of transformers with an efficiency of "A0Ak": ```pycon ->>> TransformerParameters.print_catalogue(efficiency="A0Ak") +>>> TransformerParameters.get_catalogue(efficiency="A0Ak") ``` | Id | Manufacturer | Product range | Efficiency | Type | Nominal power (kVA) | High voltage (kV) | Low voltage (kV) | @@ -340,7 +337,7 @@ following command only prints transformer parameters of transformers with an eff or only transformers with a wye winding on the primary side (using a regular expression) ```pycon ->>> TransformerParameters.print_catalogue(type=r"^y.*$") +>>> TransformerParameters.get_catalogue(type=r"^y.*$") ``` | Id | Manufacturer | Product range | Efficiency | Type | Nominal power (kVA) | High voltage (kV) | Low voltage (kV) | @@ -353,22 +350,23 @@ or only transformers with a wye winding on the primary side (using a regular exp or only transformers meeting both criteria ```pycon ->>> TransformerParameters.print_catalogue(efficiency="A0Ak", type=r"^y.*$") +>>> TransformerParameters.get_catalogue(efficiency="A0Ak", type=r"^y.*$") ``` | Id | Manufacturer | Product range | Efficiency | Type | Nominal power (kVA) | High voltage (kV) | Low voltage (kV) | | :------------------- | :----------- | :------------ | :--------- | :---- | ------------------: | ----------------: | ---------------: | | SE_Minera_A0Ak_50kVA | SE | Minera | A0Ak | Yzn11 | 50.0 | 20.0 | 0.4 | -Among all the possible filters, the nominal power and voltages are expected in their default unit (VA and V). The -[Pint](https://pint.readthedocs.io/en/stable/) library can also be used. For instance, if we want to print -transformer parameters with a nominal power of 3150 kVA, the following two commands print the same table: +Among all the possible filters, the nominal power and voltages are expected in their default unit +(VA and V). You can also use the [Pint](https://pint.readthedocs.io/en/stable/) library to express +the values in different units. For instance, if you want to get transformer parameters with a +nominal power of 3150 kVA, the following two commands return the same table: ```pycon ->>> TransformerParameters.print_catalogue(sn=3150e3) # in VA by default +>>> TransformerParameters.get_catalogue(sn=3150e3) # in VA by default ->>> from roseau.load_flow.units import Q_ -... TransformerParameters.print_catalogue(sn=Q_(3150, "kVA")) +>>> from roseau.load_flow import Q_ +... TransformerParameters.get_catalogue(sn=Q_(3150, "kVA")) ``` | Id | Manufacturer | Product range | Efficiency | Type | Nominal power (kVA) | High voltage (kV) | Low voltage (kV) | @@ -379,11 +377,11 @@ transformer parameters with a nominal power of 3150 kVA, the following two comma ### Getting an instance -To build a transformer parameters from the catalogue, the class method `from_catalogue` can be used. The same filter -as the one used for the method `print_catalogue` can be used. The filter must lead to a single transformer in the -catalogue. +You can build a `TransformerParameters` instance from the catalogue using the class method `from_catalogue`. +You must filter the data to get a single transformer. You can apply the same filtering technique used for +the method `get_catalogue` to narrow down the result to a single transformer in the catalogue. -For instance, this filter leads to a single transformer parameters in the catalogue: +For instance, these parameters filter the catalogue down to a single transformer parameters: ```pycon >>> TransformerParameters.from_catalogue(efficiency="A0Ak", type=r"^y.*$") @@ -397,19 +395,188 @@ The `id` filter can be directly used: TransformerParameters(id='SE_Minera_A0Ak_50kVA') ``` -In case of mistakes, an error is raised: +In case no or several results match the parameters, an error is raised: ```pycon >>> TransformerParameters.from_catalogue(manufacturer="ft") -RoseauLoadFlowException: Several transformers matching the query ("manufacturer='ft'") -have been found. Please look at the catalogue using the `print_catalogue` class method. - [catalogue_several_found] +RoseauLoadFlowException: Several transformers matching the query (manufacturer='ft') have been found: +'FT_Standard_Standard_100kVA', 'FT_Standard_Standard_160kVA', 'FT_Standard_Standard_250kVA', +'FT_Standard_Standard_315kVA', 'FT_Standard_Standard_400kVA', 'FT_Standard_Standard_500kVA', +'FT_Standard_Standard_630kVA', 'FT_Standard_Standard_800kVA', 'FT_Standard_Standard_1000kVA', +'FT_Standard_Standard_1250kVA', 'FT_Standard_Standard_1600kVA', 'FT_Standard_Standard_2000kVA', +'FT_Standard_Standard_2500kVA', 'FT_Standard_Standard_3150kVA'. [catalogue_several_found] ``` or if no results: ```pycon >>> TransformerParameters.from_catalogue(manufacturer="unknown") -RoseauLoadFlowException: No manufacturer matching the name 'unknown' has been found. -Available manufacturers are 'FT', 'SE'. [catalogue_not_found] +RoseauLoadFlowException: No manufacturer matching 'unknown' has been found. Available manufacturers +are 'FT', 'SE'. [catalogue_not_found] +``` + +(catalogues-lines)= + +## Lines + +_Roseau Load Flow_ is provided with a catalogue of line parameters. These parameters are available +through the class `LineParameters`. + +### Source of data + +The available lines data are based on the following sources: + +- IEC standards including: IEC-60228, IEC-60287, IEC-60364 +- Technique de l'ingénieur (French technical and scientific documentation) + +### Inspecting the catalogue + +This catalogue can be retrieved in the form of a dataframe using: + +```pycon +>>> from roseau.load_flow import LineParameters +>>> LineParameters.get_catalogue() +``` + +_Truncated output_ + +| Name | Line type | Conductor material | Insulator type | Cross-section (mm²) | Resistance (ohm/km) | Reactance (ohm/km) | Susceptance (µS/km) | Maximal current (A) | +| :------- | :---------- | :----------------- | :------------- | ------------------: | ------------------: | -----------------: | ------------------: | ------------------: | +| T_AM_80 | twisted | am | | 80 | 0.457596 | 0.105575 | 3.0507e-05 | 203 | +| U_CU_19 | underground | cu | | 19 | 1.009 | 0.133054 | 2.33629e-05 | 138 | +| O_AM_33 | overhead | am | | 33 | 1.08577 | 0.375852 | 3.045e-06 | 142 | +| U_CU_150 | underground | cu | | 150 | 0.124 | 0.0960503 | 3.41234e-05 | 420 | +| O_AM_74 | overhead | am | | 74 | 0.491898 | 0.350482 | 3.2757e-06 | 232 | +| T_AM_34 | twisted | am | | 34 | 1.04719 | 0.121009 | 2.60354e-05 | 118 | +| T_AM_50 | twisted | am | | 50 | 0.744842 | 0.113705 | 2.79758e-05 | 146 | +| O_AM_95 | overhead | am | | 95 | 0.37184 | 0.342634 | 3.3543e-06 | 266 | +| U_CU_100 | underground | cu | | 100 | 0.185 | 0.102016 | 3.17647e-05 | 339 | +| T_CU_38 | twisted | cu | | 38 | 0.4966 | 0.118845 | 2.65816e-05 | 165 | +| O_AM_100 | overhead | am | | 100 | 0.356269 | 0.341022 | 3.371e-06 | 276 | +| U_AM_60 | underground | am | | 60 | 0.629804 | 0.11045 | 2.89372e-05 | 194 | +| T_AM_79 | twisted | am | | 79 | 0.463313 | 0.105781 | 3.04371e-05 | 201 | +| T_CU_60 | twisted | cu | | 60 | 0.3275 | 0.11045 | 2.89372e-05 | 219 | +| U_AM_240 | underground | am | | 240 | 0.14525 | 0.0899296 | 3.69374e-05 | 428 | +| O_AL_37 | overhead | al | | 37 | 0.837733 | 0.372257 | 3.0757e-06 | 152 | +| U_AM_93 | underground | am | | 93 | 0.383274 | 0.103152 | 3.13521e-05 | 249 | +| O_AM_28 | overhead | am | | 28 | 1.27866 | 0.381013 | 3.0019e-06 | 130 | +| T_AL_90 | twisted | al | | 90 | 0.3446 | 0.103672 | 3.11668e-05 | 219 | +| O_AM_79 | overhead | am | | 79 | 0.463313 | 0.348428 | 3.2959e-06 | 240 | + +The following data are available in this table: + +- the **name**. A name that contains the type of the line, the material of the conductor, the + cross-section area, and optionally the insulator type. It is in the form + `{line_type}_{conductor_material}_{cross_section}_{insulator_type}`. +- the **line type**. It can be `"OVERHEAD"`, `"UNDERGROUND"` or `"TWISTED"`. +- the **conductor material**. See the {class}`~roseau.load_flow.ConductorType` class. +- the **insulator type**. See the {class}`~roseau.load_flow.InsulatorType` class. +- the **cross-section** of the conductor in mm². + +in addition to the following calculated physical parameters: + +- the _resistance_ of the line in ohm/km. +- the _reactance_ of the line in ohm/km. +- the _susceptance_ of the line in µS/km. +- the _maximal current_ of the line in A. + +The `get_catalogue` method accepts arguments (in bold above) that can be used to filter the returned +table. The following command only returns line parameters made of Aluminum: + +```pycon +>>> LineParameters.get_catalogue(conductor_type="al") +``` + +_Truncated output_ + +| Name | Line type | Conductor material | Insulator type | Cross-section (mm²) | Resistance (ohm/km) | Reactance (ohm/km) | Susceptance (µS/km) | Maximal current (A) | +| :------- | :---------- | :----------------- | :------------- | ------------------: | ------------------: | -----------------: | ------------------: | ------------------: | +| U_AL_117 | underground | al | | 117 | 0.26104 | 0.0996298 | 3.2668e-05 | 286 | +| U_AL_33 | underground | al | | 33 | 0.9344 | 0.121598 | 2.58907e-05 | 144 | +| U_AL_69 | underground | al | | 69 | 0.4529 | 0.108041 | 2.96921e-05 | 212 | +| T_AL_228 | twisted | al | | 228 | 0.133509 | 0.0905569 | 3.66279e-05 | 395 | +| U_AL_150 | underground | al | | 150 | 0.206 | 0.0960503 | 3.41234e-05 | 325 | +| T_AL_69 | twisted | al | | 69 | 0.4529 | 0.108041 | 2.96921e-05 | 185 | +| O_AL_116 | overhead | al | | 116 | 0.26372 | 0.336359 | 3.42e-06 | 310 | +| U_AL_50 | underground | al | | 50 | 0.641 | 0.113705 | 2.79758e-05 | 175 | +| U_AL_93 | underground | al | | 93 | 0.32984 | 0.103152 | 3.13521e-05 | 249 | +| T_AL_59 | twisted | al | | 59 | 0.5519 | 0.110744 | 2.88474e-05 | 164 | + +or only lines with a cross section of 240 mm² (using a regular expression) + +```pycon +>>> LineParameters.get_catalogue(section=240) +``` + +| Name | Line type | Conductor material | Insulator type | Cross-section (mm²) | Resistance (ohm/km) | Reactance (ohm/km) | Susceptance (µS/km) | Maximal current (A) | +| :------- | :---------- | :----------------- | :------------- | ------------------: | ------------------: | -----------------: | ------------------: | ------------------: | +| O_AL_240 | overhead | al | | 240 | 0.125 | 0.313518 | 3.6823e-06 | 490 | +| O_CU_240 | overhead | cu | | 240 | 0.0775 | 0.313518 | 3.6823e-06 | 630 | +| O_AM_240 | overhead | am | | 240 | 0.14525 | 0.313518 | 3.6823e-06 | 490 | +| U_AL_240 | underground | al | | 240 | 0.125 | 0.0899296 | 3.69374e-05 | 428 | +| U_CU_240 | underground | cu | | 240 | 0.0775 | 0.0899296 | 3.69374e-05 | 549 | +| U_AM_240 | underground | am | | 240 | 0.14525 | 0.0899296 | 3.69374e-05 | 428 | +| T_AL_240 | twisted | al | | 240 | 0.125 | 0.0899296 | 3.69374e-05 | 409 | +| T_CU_240 | twisted | cu | | 240 | 0.0775 | 0.0899296 | 3.69374e-05 | 538 | +| T_AM_240 | twisted | am | | 240 | 0.14525 | 0.0899296 | 3.69374e-05 | 409 | + +or only lines meeting both criteria + +```pycon +>>> LineParameters.get_catalogue(conductor_type="al", section=240) +``` + +| Name | Line type | Conductor material | Insulator type | Cross-section (mm²) | Resistance (ohm/km) | Reactance (ohm/km) | Susceptance (µS/km) | Maximal current (A) | +| :------- | :---------- | :----------------- | :------------- | ------------------: | ------------------: | -----------------: | ------------------: | ------------------: | +| O_AL_240 | overhead | al | | 240 | 0.125 | 0.313518 | 3.6823e-06 | 490 | +| U_AL_240 | underground | al | | 240 | 0.125 | 0.0899296 | 3.69374e-05 | 428 | +| T_AL_240 | twisted | al | | 240 | 0.125 | 0.0899296 | 3.69374e-05 | 409 | + +When filtering by the cross-section area, it is expected to provide a numeric value in mm² or to use a pint quantity. + +### Getting an instance + +You can build a `LineParameters` instance from the catalogue using the class method `from_catalogue`. +You must filter the data to get a single line. You can apply the same filtering technique used for +the method `get_catalogue` to narrow down the result to a single line in the catalogue. + +For instance, these parameters filter the results down to a single line parameters: + +```pycon +>>> LineParameters.from_catalogue(line_type="underground", conductor_type="al", section=240) +LineParameters(id='U_AL_240') +``` + +Or you can use the `name` filter directly: + +```pycon +>>> LineParameters.from_catalogue(name="U_AL_240") +LineParameters(id='U_AL_240') +``` + +As you can see, the `id` of the created instance is the same as the name in the catalogue. You can +override this behaviour by passing the `id` parameter to `from_catalogue`. + +In case no or several results match the parameters, an error is raised: + +```pycon +>>> LineParameters.from_catalogue(name= r"^U_AL") +RoseauLoadFlowException: Several line parameters matching the query (name='^U_AL_') have been found: +'U_AL_19', 'U_AL_20', 'U_AL_22', 'U_AL_25', 'U_AL_28', 'U_AL_29', 'U_AL_33', 'U_AL_34', 'U_AL_37', +'U_AL_38', 'U_AL_40', 'U_AL_43', 'U_AL_48', 'U_AL_50', 'U_AL_54', 'U_AL_55', 'U_AL_59', 'U_AL_60', +'U_AL_69', 'U_AL_70', 'U_AL_74', 'U_AL_75', 'U_AL_79', 'U_AL_80', 'U_AL_90', 'U_AL_93', 'U_AL_95', +'U_AL_100', 'U_AL_116', 'U_AL_117', 'U_AL_120', 'U_AL_147', 'U_AL_148', 'U_AL_150', 'U_AL_228', +'U_AL_240', 'U_AL_288'. [catalogue_several_found] +``` + +or if no results: + +```pycon +>>> LineParameters.from_catalogue(name="unknown") +RoseauLoadFlowException: No name matching 'unknown' has been found. Available names are 'O_AL_12', +'O_AL_13', 'O_AL_14', 'O_AL_19', 'O_AL_20', 'O_AL_22', 'O_AL_25', 'O_AL_28', 'O_AL_29', 'O_AL_33', +'O_AL_34', 'O_AL_37', 'O_AL_38', 'O_AL_40', 'O_AL_43', 'O_AL_48', 'O_AL_50', 'O_AL_54', 'O_AL_55', +'O_AL_59', 'O_AL_60', 'O_AL_69', 'O_AL_70', 'O_AL_74', 'O_AL_75', 'O_AL_79', 'O_AL_80', 'O_AL_90', +'O_AL_93', 'O_AL_95', 'O_AL_100', 'O_AL_116', 'O_AL_117', 'O_AL_120', 'O_AL_147', 'O_AL_148', 'O_AL_150', +'O_AL_228', 'O_AL_240', 'O_AL_288', 'O_CU_3', 'O_CU_7', 'O_CU_12', 'O_CU_13', [...]. [catalogue_not_found] ``` diff --git a/poetry.lock b/poetry.lock index 5b656fed..dde1d9ae 100644 --- a/poetry.lock +++ b/poetry.lock @@ -868,16 +868,6 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1528,7 +1518,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1536,15 +1525,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1561,7 +1543,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1569,7 +1550,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1698,24 +1678,6 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "rich" -version = "13.7.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, - {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - [[package]] name = "ruff" version = "0.1.11" @@ -2184,4 +2146,4 @@ plot = ["matplotlib"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c467c96774a04a058c3b480f1a8d5c6a250bc6f82b8fe6f13dde61e2e7d765f3" +content-hash = "3d6286235226a2f20c7bf0dd6e466fb22f42972cc2deb79ea2b5cb8682862f18" diff --git a/pyproject.toml b/pyproject.toml index efdb04cf..9bcdb7d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,6 @@ shapely = ">=2.0.0" regex = ">=2022.1.18" pint = ">=0.21.0" typing-extensions = ">=4.6.2" -rich = ">=13.5.1" pyproj = ">=3.3.0" certifi = ">=2023.5.7" platformdirs = ">=4.0.0" diff --git a/roseau/load_flow/_compat.py b/roseau/load_flow/_compat.py new file mode 100644 index 00000000..634169f3 --- /dev/null +++ b/roseau/load_flow/_compat.py @@ -0,0 +1,41 @@ +import sys +from enum import Enum + +from typing_extensions import Self + +if sys.version_info >= (3, 11): + from enum import StrEnum as StrEnum +else: + + class StrEnum(str, Enum): + """ + Enum where members are also (and must be) strings. This is a backport of + `enum.StrEnum` from Python 3.11. + """ + + def __new__(cls, *values) -> Self: + "values must already be of type `str`" + if len(values) > 3: + raise TypeError(f"too many arguments for str(): {values!r}") + if len(values) == 1 and not isinstance(values[0], str): + # it must be a string + raise TypeError(f"{values[0]!r} is not a string") + if len(values) >= 2 and not isinstance(values[1], str): + # check that encoding argument is a string + raise TypeError(f"encoding must be a string, not {values[1]!r}") + if len(values) == 3 and not isinstance(values[2], str): + # check that errors argument is a string + raise TypeError(f"errors must be a string, not {values[2]!r}") + value = str(*values) + member = str.__new__(cls, value) + member._value_ = value + return member + + def __str__(self) -> str: + return str.__str__(self) + + def _generate_next_value_(name, start, count, last_values) -> str: # noqa: N805 + """ + Return the lower-cased version of the member name. + """ + return name.lower() diff --git a/roseau/load_flow/conftest.py b/roseau/load_flow/conftest.py index e2181a46..d8079b39 100644 --- a/roseau/load_flow/conftest.py +++ b/roseau/load_flow/conftest.py @@ -4,8 +4,6 @@ import pytest from pandas.testing import assert_frame_equal -from roseau.load_flow.utils import console - # Variable to test the network HERE = Path(__file__).parent.expanduser().absolute() TEST_ALL_NETWORKS_DATA_FOLDER = HERE / "tests" / "data" / "networks" @@ -80,11 +78,6 @@ def dgs_network_path(request) -> Path: return request.param -@pytest.fixture(autouse=True, scope="session") -def _set_console_width() -> None: - console.width = 210 - - # # Utils # diff --git a/roseau/load_flow/data/lines/Catalogue.csv b/roseau/load_flow/data/lines/Catalogue.csv new file mode 100644 index 00000000..76647d43 --- /dev/null +++ b/roseau/load_flow/data/lines/Catalogue.csv @@ -0,0 +1,356 @@ +name,type,material,insulator,section,r,x,b,maximal_current +O_AL_12,overhead,al,,12,2.69,0.4076321335,2.798e-06,70 +O_AL_13,overhead,al,,13,2.495,0.4051175176,2.8161e-06,76 +O_AL_14,overhead,al,,14,2.3,0.402789347,2.8331e-06,82 +O_AL_19,overhead,al,,19,1.6733333333,0.3931954996,2.9051e-06,103 +O_AL_20,overhead,al,,20,1.5944444444,0.3915840732,2.9175e-06,106 +O_AL_22,overhead,al,,22,1.4366666667,0.3885898156,2.9409e-06,113 +O_AL_25,overhead,al,,25,1.2,0.3845738118,2.973e-06,122 +O_AL_28,overhead,al,,28,1.1004,0.3810134861,3.0019e-06,130 +O_AL_29,overhead,al,,29,1.0672,0.3799110598,3.011e-06,132 +O_AL_33,overhead,al,,33,0.9344,0.3758517535,3.045e-06,142 +O_AL_34,overhead,al,,34,0.9012,0.374913895,3.0529e-06,144 +O_AL_37,overhead,al,,37,0.8377333333,0.3722574463,3.0757e-06,152 +O_AL_38,overhead,al,,38,0.8226,0.3714196387,3.0829e-06,155 +O_AL_40,overhead,al,,40,0.7923333333,0.3698082123,3.0969e-06,160 +O_AL_43,overhead,al,,43,0.7469333333,0.3675361917,3.1169e-06,167 +O_AL_48,overhead,al,,48,0.6712666667,0.3640804116,3.1478e-06,180 +O_AL_50,overhead,al,,50,0.641,0.3627979509,3.1595e-06,185 +O_AL_54,overhead,al,,54,0.6014,0.3603801484,3.1816e-06,193 +O_AL_55,overhead,al,,55,0.5915,0.3598036933,3.187e-06,195 +O_AL_59,overhead,al,,59,0.5519,0.3575981614,3.2075e-06,203 +O_AL_60,overhead,al,,60,0.542,0.3570701502,3.2125e-06,206 +O_AL_69,overhead,al,,69,0.4529,0.3526793993,3.2543e-06,224 +O_AL_70,overhead,al,,70,0.443,0.3522273638,3.2587e-06,226 +O_AL_74,overhead,al,,74,0.42332,0.3504815854,3.2757e-06,232 +O_AL_75,overhead,al,,75,0.4184,0.3500598888,3.2798e-06,234 +O_AL_79,overhead,al,,79,0.39872,0.3484275255,3.2959e-06,240 +O_AL_80,overhead,al,,80,0.3938,0.3480323514,3.2999e-06,242 +O_AL_90,overhead,al,,90,0.3446,0.3443320882,3.337e-06,258 +O_AL_93,overhead,al,,93,0.32984,0.3433019655,3.3475e-06,263 +O_AL_95,overhead,al,,95,0.32,0.3426335163,3.3543e-06,266 +O_AL_100,overhead,al,,100,0.3066,0.3410220899,3.371e-06,276 +O_AL_116,overhead,al,,116,0.26372,0.336359338,3.42e-06,310 +O_AL_117,overhead,al,,117,0.26104,0.3360896717,3.4229e-06,312 +O_AL_120,overhead,al,,120,0.253,0.3352942893,3.4314e-06,318 +O_AL_147,overhead,al,,147,0.2107,0.3289187147,3.5012e-06,356 +O_AL_148,overhead,al,,148,0.2091333333,0.3287057245,3.5036e-06,357 +O_AL_150,overhead,al,,150,0.206,0.3282840279,3.5083e-06,360 +O_AL_228,overhead,al,,228,0.1335090909,0.3151298548,3.6625e-06,474 +O_AL_240,overhead,al,,240,0.125,0.3135184284,3.6823e-06,490 +O_AL_288,overhead,al,,288,0.105,0.3077906278,3.7545e-06,552 +O_CU_3,overhead,cu,,3,6.4766666667,0.4511838553,2.5182e-06,35 +O_CU_7,overhead,cu,,7,2.7675,0.424565208,2.6822e-06,59 +O_CU_12,overhead,cu,,12,1.6033333333,0.4076321335,2.798e-06,90 +O_CU_13,overhead,cu,,13,1.49,0.4051175176,2.8161e-06,98 +O_CU_14,overhead,cu,,14,1.3766666667,0.402789347,2.8331e-06,105 +O_CU_19,overhead,cu,,19,1.009,0.3931954996,2.9051e-06,132 +O_CU_20,overhead,cu,,20,0.962,0.3915840732,2.9175e-06,136 +O_CU_22,overhead,cu,,22,0.868,0.3885898156,2.9409e-06,145 +O_CU_25,overhead,cu,,25,0.727,0.3845738118,2.973e-06,157 +O_CU_28,overhead,cu,,28,0.6661,0.3810134861,3.0019e-06,167 +O_CU_29,overhead,cu,,29,0.6458,0.3799110598,3.011e-06,170 +O_CU_33,overhead,cu,,33,0.5646,0.3758517535,3.045e-06,183 +O_CU_34,overhead,cu,,34,0.5443,0.374913895,3.0529e-06,187 +O_CU_37,overhead,cu,,37,0.5057333333,0.3722574463,3.0757e-06,196 +O_CU_38,overhead,cu,,38,0.4966,0.3714196387,3.0829e-06,199 +O_CU_40,overhead,cu,,40,0.4783333333,0.3698082123,3.0969e-06,204 +O_CU_43,overhead,cu,,43,0.4509333333,0.3675361917,3.1169e-06,213 +O_CU_48,overhead,cu,,48,0.4052666667,0.3640804116,3.1478e-06,227 +O_CU_50,overhead,cu,,50,0.387,0.3627979509,3.1595e-06,233 +O_CU_54,overhead,cu,,54,0.3632,0.3603801484,3.1816e-06,245 +O_CU_55,overhead,cu,,55,0.35725,0.3598036933,3.187e-06,248 +O_CU_59,overhead,cu,,59,0.33345,0.3575981614,3.2075e-06,260 +O_CU_60,overhead,cu,,60,0.3275,0.3570701502,3.2125e-06,262 +O_CU_69,overhead,cu,,69,0.27395,0.3526793993,3.2543e-06,289 +O_CU_70,overhead,cu,,70,0.268,0.3522273638,3.2587e-06,292 +O_CU_74,overhead,cu,,74,0.256,0.3504815854,3.2757e-06,302 +O_CU_75,overhead,cu,,75,0.253,0.3500598888,3.2798e-06,305 +O_CU_79,overhead,cu,,79,0.241,0.3484275255,3.2959e-06,315 +O_CU_80,overhead,cu,,80,0.238,0.3480323514,3.2999e-06,318 +O_CU_90,overhead,cu,,90,0.208,0.3443320882,3.337e-06,343 +O_CU_93,overhead,cu,,93,0.199,0.3433019655,3.3475e-06,351 +O_CU_95,overhead,cu,,95,0.193,0.3426335163,3.3543e-06,356 +O_CU_100,overhead,cu,,100,0.185,0.3410220899,3.371e-06,367 +O_CU_116,overhead,cu,,116,0.1594,0.336359338,3.42e-06,401 +O_CU_117,overhead,cu,,117,0.1578,0.3360896717,3.4229e-06,403 +O_CU_120,overhead,cu,,120,0.153,0.3352942893,3.4314e-06,409 +O_CU_147,overhead,cu,,147,0.1269,0.3289187147,3.5012e-06,459 +O_CU_148,overhead,cu,,148,0.1259333333,0.3287057245,3.5036e-06,461 +O_CU_150,overhead,cu,,150,0.124,0.3282840279,3.5083e-06,465 +O_CU_228,overhead,cu,,228,0.0826272727,0.3151298548,3.6625e-06,609 +O_CU_240,overhead,cu,,240,0.0775,0.3135184284,3.6823e-06,630 +O_CU_288,overhead,cu,,288,0.0651,0.3077906278,3.7545e-06,705 +O_AM_12,overhead,am,,12,3.12578,0.4076321335,2.798e-06,70 +O_AM_13,overhead,am,,13,2.89919,0.4051175176,2.8161e-06,76 +O_AM_14,overhead,am,,14,2.6726,0.402789347,2.8331e-06,82 +O_AM_19,overhead,am,,19,1.9444133333,0.3931954996,2.9051e-06,103 +O_AM_20,overhead,am,,20,1.8527444444,0.3915840732,2.9175e-06,106 +O_AM_22,overhead,am,,22,1.6694066667,0.3885898156,2.9409e-06,113 +O_AM_25,overhead,am,,25,1.3944,0.3845738118,2.973e-06,122 +O_AM_28,overhead,am,,28,1.2786648,0.3810134861,3.0019e-06,130 +O_AM_29,overhead,am,,29,1.2400864,0.3799110598,3.011e-06,132 +O_AM_33,overhead,am,,33,1.0857728,0.3758517535,3.045e-06,142 +O_AM_34,overhead,am,,34,1.0471944,0.374913895,3.0529e-06,144 +O_AM_37,overhead,am,,37,0.9734461333,0.3722574463,3.0757e-06,152 +O_AM_38,overhead,am,,38,0.9558612,0.3714196387,3.0829e-06,155 +O_AM_40,overhead,am,,40,0.9206913333,0.3698082123,3.0969e-06,160 +O_AM_43,overhead,am,,43,0.8679365333,0.3675361917,3.1169e-06,167 +O_AM_48,overhead,am,,48,0.7800118667,0.3640804116,3.1478e-06,180 +O_AM_50,overhead,am,,50,0.744842,0.3627979509,3.1595e-06,185 +O_AM_54,overhead,am,,54,0.6988268,0.3603801484,3.1816e-06,193 +O_AM_55,overhead,am,,55,0.687323,0.3598036933,3.187e-06,195 +O_AM_59,overhead,am,,59,0.6413078,0.3575981614,3.2075e-06,203 +O_AM_60,overhead,am,,60,0.629804,0.3570701502,3.2125e-06,206 +O_AM_69,overhead,am,,69,0.5262698,0.3526793993,3.2543e-06,224 +O_AM_70,overhead,am,,70,0.514766,0.3522273638,3.2587e-06,226 +O_AM_74,overhead,am,,74,0.49189784,0.3504815854,3.2757e-06,232 +O_AM_75,overhead,am,,75,0.4861808,0.3500598888,3.2798e-06,234 +O_AM_79,overhead,am,,79,0.46331264,0.3484275255,3.2959e-06,240 +O_AM_80,overhead,am,,80,0.4575956,0.3480323514,3.2999e-06,242 +O_AM_90,overhead,am,,90,0.4004252,0.3443320882,3.337e-06,258 +O_AM_93,overhead,am,,93,0.38327408,0.3433019655,3.3475e-06,263 +O_AM_95,overhead,am,,95,0.37184,0.3426335163,3.3543e-06,266 +O_AM_100,overhead,am,,100,0.3562692,0.3410220899,3.371e-06,276 +O_AM_116,overhead,am,,116,0.30644264,0.336359338,3.42e-06,310 +O_AM_117,overhead,am,,117,0.30332848,0.3360896717,3.4229e-06,312 +O_AM_120,overhead,am,,120,0.293986,0.3352942893,3.4314e-06,318 +O_AM_147,overhead,am,,147,0.2448334,0.3289187147,3.5012e-06,356 +O_AM_148,overhead,am,,148,0.2430129333,0.3287057245,3.5036e-06,357 +O_AM_150,overhead,am,,150,0.239372,0.3282840279,3.5083e-06,360 +O_AM_228,overhead,am,,228,0.1551375636,0.3151298548,3.6625e-06,474 +O_AM_240,overhead,am,,240,0.14525,0.3135184284,3.6823e-06,490 +O_AM_288,overhead,am,,288,0.12201,0.3077906278,3.7545e-06,552 +U_AL_19,underground,al,,19,1.6733333333,0.1330544178,2.33629e-05,107 +U_AL_20,underground,al,,20,1.5944444444,0.1319453158,2.35859e-05,110 +U_AL_22,underground,al,,22,1.4366666667,0.1299081697,2.40066e-05,116 +U_AL_25,underground,al,,25,1.2,0.1272251024,2.45842e-05,125 +U_AL_28,underground,al,,28,1.1004,0.124894529,2.51089e-05,132 +U_AL_29,underground,al,,29,1.0672,0.1241821772,2.52738e-05,135 +U_AL_33,underground,al,,33,0.9344,0.1215975746,2.58907e-05,144 +U_AL_34,underground,al,,34,0.9012,0.1210090932,2.60354e-05,147 +U_AL_37,underground,al,,37,0.8377333333,0.119360076,2.64496e-05,152 +U_AL_38,underground,al,,38,0.8226,0.1188454977,2.65816e-05,154 +U_AL_40,underground,al,,40,0.7923333333,0.1178632261,2.68372e-05,158 +U_AL_43,underground,al,,43,0.7469333333,0.116495047,2.72015e-05,163 +U_AL_48,underground,al,,48,0.6712666667,0.1144519385,2.77643e-05,172 +U_AL_50,underground,al,,50,0.641,0.113705448,2.79758e-05,175 +U_AL_54,underground,al,,54,0.6014,0.112315465,2.83783e-05,183 +U_AL_55,underground,al,,55,0.5915,0.1119874251,2.8475e-05,185 +U_AL_59,underground,al,,59,0.5519,0.1107443314,2.88474e-05,193 +U_AL_60,underground,al,,60,0.542,0.1104495588,2.89372e-05,194 +U_AL_69,underground,al,,69,0.4529,0.1080408311,2.96921e-05,212 +U_AL_70,underground,al,,70,0.443,0.1077971677,2.97707e-05,214 +U_AL_74,underground,al,,74,0.42332,0.1068637229,3.00755e-05,220 +U_AL_75,underground,al,,75,0.4184,0.1066400577,3.01495e-05,222 +U_AL_79,underground,al,,79,0.39872,0.1057809122,3.04371e-05,228 +U_AL_80,underground,al,,80,0.3938,0.1055745142,3.0507e-05,229 +U_AL_90,underground,al,,90,0.3446,0.1036719918,3.11668e-05,244 +U_AL_93,underground,al,,93,0.32984,0.1031520348,3.13521e-05,249 +U_AL_95,underground,al,,95,0.32,0.1028168921,3.14727e-05,252 +U_AL_100,underground,al,,100,0.3066,0.1020162744,3.17647e-05,260 +U_AL_116,underground,al,,116,0.26372,0.0997578116,3.26182e-05,285 +U_AL_117,underground,al,,117,0.26104,0.0996298374,3.2668e-05,286 +U_AL_120,underground,al,,120,0.253,0.0992540572,3.28149e-05,291 +U_AL_147,underground,al,,147,0.2107,0.0963323591,3.40041e-05,322 +U_AL_148,underground,al,,148,0.2091333333,0.0962375209,3.40441e-05,323 +U_AL_150,underground,al,,150,0.206,0.0960502777,3.41234e-05,325 +U_AL_228,underground,al,,228,0.1335090909,0.0905569282,3.66279e-05,415 +U_AL_240,underground,al,,240,0.125,0.0899296465,3.69374e-05,428 +U_AL_288,underground,al,,288,0.105,0.0877788677,3.80397e-05,474 +U_CU_19,underground,cu,,19,1.009,0.1330544178,2.33629e-05,138 +U_CU_20,underground,cu,,20,0.962,0.1319453158,2.35859e-05,142 +U_CU_22,underground,cu,,22,0.868,0.1299081697,2.40066e-05,149 +U_CU_25,underground,cu,,25,0.727,0.1272251024,2.45842e-05,161 +U_CU_28,underground,cu,,28,0.6661,0.124894529,2.51089e-05,170 +U_CU_29,underground,cu,,29,0.6458,0.1241821772,2.52738e-05,173 +U_CU_33,underground,cu,,33,0.5646,0.1215975746,2.58907e-05,186 +U_CU_34,underground,cu,,34,0.5443,0.1210090932,2.60354e-05,189 +U_CU_37,underground,cu,,37,0.5057333333,0.119360076,2.64496e-05,196 +U_CU_38,underground,cu,,38,0.4966,0.1188454977,2.65816e-05,199 +U_CU_40,underground,cu,,40,0.4783333333,0.1178632261,2.68372e-05,203 +U_CU_43,underground,cu,,43,0.4509333333,0.116495047,2.72015e-05,210 +U_CU_48,underground,cu,,48,0.4052666667,0.1144519385,2.77643e-05,221 +U_CU_50,underground,cu,,50,0.387,0.113705448,2.79758e-05,225 +U_CU_54,underground,cu,,54,0.3632,0.112315465,2.83783e-05,235 +U_CU_55,underground,cu,,55,0.35725,0.1119874251,2.8475e-05,238 +U_CU_59,underground,cu,,59,0.33345,0.1107443314,2.88474e-05,248 +U_CU_60,underground,cu,,60,0.3275,0.1104495588,2.89372e-05,250 +U_CU_69,underground,cu,,69,0.27395,0.1080408311,2.96921e-05,273 +U_CU_70,underground,cu,,70,0.268,0.1077971677,2.97707e-05,276 +U_CU_74,underground,cu,,74,0.256,0.1068637229,3.00755e-05,285 +U_CU_75,underground,cu,,75,0.253,0.1066400577,3.01495e-05,287 +U_CU_79,underground,cu,,79,0.241,0.1057809122,3.04371e-05,295 +U_CU_80,underground,cu,,80,0.238,0.1055745142,3.0507e-05,298 +U_CU_90,underground,cu,,90,0.208,0.1036719918,3.11668e-05,319 +U_CU_93,underground,cu,,93,0.199,0.1031520348,3.13521e-05,326 +U_CU_95,underground,cu,,95,0.193,0.1028168921,3.14727e-05,330 +U_CU_100,underground,cu,,100,0.185,0.1020162744,3.17647e-05,339 +U_CU_116,underground,cu,,116,0.1594,0.0997578116,3.26182e-05,368 +U_CU_117,underground,cu,,117,0.1578,0.0996298374,3.2668e-05,370 +U_CU_120,underground,cu,,120,0.153,0.0992540572,3.28149e-05,375 +U_CU_147,underground,cu,,147,0.1269,0.0963323591,3.40041e-05,416 +U_CU_148,underground,cu,,148,0.1259333333,0.0962375209,3.40441e-05,417 +U_CU_150,underground,cu,,150,0.124,0.0960502777,3.41234e-05,420 +U_CU_228,underground,cu,,228,0.0826272727,0.0905569282,3.66279e-05,533 +U_CU_240,underground,cu,,240,0.0775,0.0899296465,3.69374e-05,549 +U_CU_288,underground,cu,,288,0.0651,0.0877788677,3.80397e-05,605 +U_AM_19,underground,am,,19,1.9444133333,0.1330544178,2.33629e-05,107 +U_AM_20,underground,am,,20,1.8527444444,0.1319453158,2.35859e-05,110 +U_AM_22,underground,am,,22,1.6694066667,0.1299081697,2.40066e-05,116 +U_AM_25,underground,am,,25,1.3944,0.1272251024,2.45842e-05,125 +U_AM_28,underground,am,,28,1.2786648,0.124894529,2.51089e-05,132 +U_AM_29,underground,am,,29,1.2400864,0.1241821772,2.52738e-05,135 +U_AM_33,underground,am,,33,1.0857728,0.1215975746,2.58907e-05,144 +U_AM_34,underground,am,,34,1.0471944,0.1210090932,2.60354e-05,147 +U_AM_37,underground,am,,37,0.9734461333,0.119360076,2.64496e-05,152 +U_AM_38,underground,am,,38,0.9558612,0.1188454977,2.65816e-05,154 +U_AM_40,underground,am,,40,0.9206913333,0.1178632261,2.68372e-05,158 +U_AM_43,underground,am,,43,0.8679365333,0.116495047,2.72015e-05,163 +U_AM_48,underground,am,,48,0.7800118667,0.1144519385,2.77643e-05,172 +U_AM_50,underground,am,,50,0.744842,0.113705448,2.79758e-05,175 +U_AM_54,underground,am,,54,0.6988268,0.112315465,2.83783e-05,183 +U_AM_55,underground,am,,55,0.687323,0.1119874251,2.8475e-05,185 +U_AM_59,underground,am,,59,0.6413078,0.1107443314,2.88474e-05,193 +U_AM_60,underground,am,,60,0.629804,0.1104495588,2.89372e-05,194 +U_AM_69,underground,am,,69,0.5262698,0.1080408311,2.96921e-05,212 +U_AM_70,underground,am,,70,0.514766,0.1077971677,2.97707e-05,214 +U_AM_74,underground,am,,74,0.49189784,0.1068637229,3.00755e-05,220 +U_AM_75,underground,am,,75,0.4861808,0.1066400577,3.01495e-05,222 +U_AM_79,underground,am,,79,0.46331264,0.1057809122,3.04371e-05,228 +U_AM_80,underground,am,,80,0.4575956,0.1055745142,3.0507e-05,229 +U_AM_90,underground,am,,90,0.4004252,0.1036719918,3.11668e-05,244 +U_AM_93,underground,am,,93,0.38327408,0.1031520348,3.13521e-05,249 +U_AM_95,underground,am,,95,0.37184,0.1028168921,3.14727e-05,252 +U_AM_100,underground,am,,100,0.3562692,0.1020162744,3.17647e-05,260 +U_AM_116,underground,am,,116,0.30644264,0.0997578116,3.26182e-05,285 +U_AM_117,underground,am,,117,0.30332848,0.0996298374,3.2668e-05,286 +U_AM_120,underground,am,,120,0.293986,0.0992540572,3.28149e-05,291 +U_AM_147,underground,am,,147,0.2448334,0.0963323591,3.40041e-05,322 +U_AM_148,underground,am,,148,0.2430129333,0.0962375209,3.40441e-05,323 +U_AM_150,underground,am,,150,0.239372,0.0960502777,3.41234e-05,325 +U_AM_228,underground,am,,228,0.1551375636,0.0905569282,3.66279e-05,415 +U_AM_240,underground,am,,240,0.14525,0.0899296465,3.69374e-05,428 +U_AM_288,underground,am,,288,0.12201,0.0877788677,3.80397e-05,474 +T_AL_12,twisted,al,,12,2.69,0.1433737819,2.14745e-05,64 +T_AL_13,twisted,al,,13,2.495,0.1415282478,2.17895e-05,68 +T_AL_14,twisted,al,,14,2.3,0.1398372258,2.20863e-05,71 +T_AL_19,twisted,al,,19,1.6733333333,0.1330544178,2.33629e-05,84 +T_AL_20,twisted,al,,20,1.5944444444,0.1319453158,2.35859e-05,86 +T_AL_22,twisted,al,,22,1.4366666667,0.1299081697,2.40066e-05,90 +T_AL_25,twisted,al,,25,1.2,0.1272251024,2.45842e-05,97 +T_AL_28,twisted,al,,28,1.1004,0.124894529,2.51089e-05,104 +T_AL_29,twisted,al,,29,1.0672,0.1241821772,2.52738e-05,106 +T_AL_33,twisted,al,,33,0.9344,0.1215975746,2.58907e-05,115 +T_AL_34,twisted,al,,34,0.9012,0.1210090932,2.60354e-05,118 +T_AL_37,twisted,al,,37,0.8377333333,0.119360076,2.64496e-05,123 +T_AL_38,twisted,al,,38,0.8226,0.1188454977,2.65816e-05,125 +T_AL_40,twisted,al,,40,0.7923333333,0.1178632261,2.68372e-05,129 +T_AL_43,twisted,al,,43,0.7469333333,0.116495047,2.72015e-05,134 +T_AL_48,twisted,al,,48,0.6712666667,0.1144519385,2.77643e-05,143 +T_AL_50,twisted,al,,50,0.641,0.113705448,2.79758e-05,146 +T_AL_54,twisted,al,,54,0.6014,0.112315465,2.83783e-05,154 +T_AL_55,twisted,al,,55,0.5915,0.1119874251,2.8475e-05,156 +T_AL_59,twisted,al,,59,0.5519,0.1107443314,2.88474e-05,164 +T_AL_60,twisted,al,,60,0.542,0.1104495588,2.89372e-05,166 +T_AL_69,twisted,al,,69,0.4529,0.1080408311,2.96921e-05,185 +T_AL_70,twisted,al,,70,0.443,0.1077971677,2.97707e-05,187 +T_AL_74,twisted,al,,74,0.42332,0.1068637229,3.00755e-05,193 +T_AL_75,twisted,al,,75,0.4184,0.1066400577,3.01495e-05,195 +T_AL_79,twisted,al,,79,0.39872,0.1057809122,3.04371e-05,201 +T_AL_80,twisted,al,,80,0.3938,0.1055745142,3.0507e-05,203 +T_AL_90,twisted,al,,90,0.3446,0.1036719918,3.11668e-05,219 +T_AL_93,twisted,al,,93,0.32984,0.1031520348,3.13521e-05,224 +T_AL_95,twisted,al,,95,0.32,0.1028168921,3.14727e-05,227 +T_AL_100,twisted,al,,100,0.3066,0.1020162744,3.17647e-05,234 +T_AL_116,twisted,al,,116,0.26372,0.0997578116,3.26182e-05,257 +T_AL_117,twisted,al,,117,0.26104,0.0996298374,3.2668e-05,259 +T_AL_120,twisted,al,,120,0.253,0.0992540572,3.28149e-05,263 +T_AL_147,twisted,al,,147,0.2107,0.0963323591,3.40041e-05,300 +T_AL_148,twisted,al,,148,0.2091333333,0.0962375209,3.40441e-05,301 +T_AL_150,twisted,al,,150,0.206,0.0960502777,3.41234e-05,304 +T_AL_228,twisted,al,,228,0.1335090909,0.0905569282,3.66279e-05,395 +T_AL_240,twisted,al,,240,0.125,0.0899296465,3.69374e-05,409 +T_AL_288,twisted,al,,288,0.105,0.0877788677,3.80397e-05,459 +T_CU_3,twisted,cu,,3,6.4766666667,0.1780965781,1.68827e-05,35 +T_CU_7,twisted,cu,,7,2.7675,0.1562894945,1.95015e-05,59 +T_CU_12,twisted,cu,,12,1.6033333333,0.1433737819,2.14745e-05,83 +T_CU_13,twisted,cu,,13,1.49,0.1415282478,2.17895e-05,88 +T_CU_14,twisted,cu,,14,1.3766666667,0.1398372258,2.20863e-05,92 +T_CU_19,twisted,cu,,19,1.009,0.1330544178,2.33629e-05,109 +T_CU_20,twisted,cu,,20,0.962,0.1319453158,2.35859e-05,112 +T_CU_22,twisted,cu,,22,0.868,0.1299081697,2.40066e-05,118 +T_CU_25,twisted,cu,,25,0.727,0.1272251024,2.45842e-05,127 +T_CU_28,twisted,cu,,28,0.6661,0.124894529,2.51089e-05,136 +T_CU_29,twisted,cu,,29,0.6458,0.1241821772,2.52738e-05,139 +T_CU_33,twisted,cu,,33,0.5646,0.1215975746,2.58907e-05,152 +T_CU_34,twisted,cu,,34,0.5443,0.1210090932,2.60354e-05,155 +T_CU_37,twisted,cu,,37,0.5057333333,0.119360076,2.64496e-05,163 +T_CU_38,twisted,cu,,38,0.4966,0.1188454977,2.65816e-05,165 +T_CU_40,twisted,cu,,40,0.4783333333,0.1178632261,2.68372e-05,169 +T_CU_43,twisted,cu,,43,0.4509333333,0.116495047,2.72015e-05,176 +T_CU_48,twisted,cu,,48,0.4052666667,0.1144519385,2.77643e-05,187 +T_CU_50,twisted,cu,,50,0.387,0.113705448,2.79758e-05,192 +T_CU_54,twisted,cu,,54,0.3632,0.112315465,2.83783e-05,203 +T_CU_55,twisted,cu,,55,0.35725,0.1119874251,2.8475e-05,206 +T_CU_59,twisted,cu,,59,0.33345,0.1107443314,2.88474e-05,216 +T_CU_60,twisted,cu,,60,0.3275,0.1104495588,2.89372e-05,219 +T_CU_69,twisted,cu,,69,0.27395,0.1080408311,2.96921e-05,243 +T_CU_70,twisted,cu,,70,0.268,0.1077971677,2.97707e-05,246 +T_CU_74,twisted,cu,,74,0.256,0.1068637229,3.00755e-05,254 +T_CU_75,twisted,cu,,75,0.253,0.1066400577,3.01495e-05,256 +T_CU_79,twisted,cu,,79,0.241,0.1057809122,3.04371e-05,265 +T_CU_80,twisted,cu,,80,0.238,0.1055745142,3.0507e-05,267 +T_CU_90,twisted,cu,,90,0.208,0.1036719918,3.11668e-05,288 +T_CU_93,twisted,cu,,93,0.199,0.1031520348,3.13521e-05,294 +T_CU_95,twisted,cu,,95,0.193,0.1028168921,3.14727e-05,298 +T_CU_100,twisted,cu,,100,0.185,0.1020162744,3.17647e-05,308 +T_CU_116,twisted,cu,,116,0.1594,0.0997578116,3.26182e-05,338 +T_CU_117,twisted,cu,,117,0.1578,0.0996298374,3.2668e-05,340 +T_CU_120,twisted,cu,,120,0.153,0.0992540572,3.28149e-05,346 +T_CU_147,twisted,cu,,147,0.1269,0.0963323591,3.40041e-05,394 +T_CU_148,twisted,cu,,148,0.1259333333,0.0962375209,3.40441e-05,395 +T_CU_150,twisted,cu,,150,0.124,0.0960502777,3.41234e-05,399 +T_CU_228,twisted,cu,,228,0.0826272727,0.0905569282,3.66279e-05,520 +T_CU_240,twisted,cu,,240,0.0775,0.0899296465,3.69374e-05,538 +T_CU_288,twisted,cu,,288,0.0651,0.0877788677,3.80397e-05,604 +T_AM_12,twisted,am,,12,3.12578,0.1433737819,2.14745e-05,64 +T_AM_13,twisted,am,,13,2.89919,0.1415282478,2.17895e-05,68 +T_AM_14,twisted,am,,14,2.6726,0.1398372258,2.20863e-05,71 +T_AM_19,twisted,am,,19,1.9444133333,0.1330544178,2.33629e-05,84 +T_AM_20,twisted,am,,20,1.8527444444,0.1319453158,2.35859e-05,86 +T_AM_22,twisted,am,,22,1.6694066667,0.1299081697,2.40066e-05,90 +T_AM_25,twisted,am,,25,1.3944,0.1272251024,2.45842e-05,97 +T_AM_28,twisted,am,,28,1.2786648,0.124894529,2.51089e-05,104 +T_AM_29,twisted,am,,29,1.2400864,0.1241821772,2.52738e-05,106 +T_AM_33,twisted,am,,33,1.0857728,0.1215975746,2.58907e-05,115 +T_AM_34,twisted,am,,34,1.0471944,0.1210090932,2.60354e-05,118 +T_AM_37,twisted,am,,37,0.9734461333,0.119360076,2.64496e-05,123 +T_AM_38,twisted,am,,38,0.9558612,0.1188454977,2.65816e-05,125 +T_AM_40,twisted,am,,40,0.9206913333,0.1178632261,2.68372e-05,129 +T_AM_43,twisted,am,,43,0.8679365333,0.116495047,2.72015e-05,134 +T_AM_48,twisted,am,,48,0.7800118667,0.1144519385,2.77643e-05,143 +T_AM_50,twisted,am,,50,0.744842,0.113705448,2.79758e-05,146 +T_AM_54,twisted,am,,54,0.6988268,0.112315465,2.83783e-05,154 +T_AM_55,twisted,am,,55,0.687323,0.1119874251,2.8475e-05,156 +T_AM_59,twisted,am,,59,0.6413078,0.1107443314,2.88474e-05,164 +T_AM_60,twisted,am,,60,0.629804,0.1104495588,2.89372e-05,166 +T_AM_69,twisted,am,,69,0.5262698,0.1080408311,2.96921e-05,185 +T_AM_70,twisted,am,,70,0.514766,0.1077971677,2.97707e-05,187 +T_AM_74,twisted,am,,74,0.49189784,0.1068637229,3.00755e-05,193 +T_AM_75,twisted,am,,75,0.4861808,0.1066400577,3.01495e-05,195 +T_AM_79,twisted,am,,79,0.46331264,0.1057809122,3.04371e-05,201 +T_AM_80,twisted,am,,80,0.4575956,0.1055745142,3.0507e-05,203 +T_AM_90,twisted,am,,90,0.4004252,0.1036719918,3.11668e-05,219 +T_AM_93,twisted,am,,93,0.38327408,0.1031520348,3.13521e-05,224 +T_AM_95,twisted,am,,95,0.37184,0.1028168921,3.14727e-05,227 +T_AM_100,twisted,am,,100,0.3562692,0.1020162744,3.17647e-05,234 +T_AM_116,twisted,am,,116,0.30644264,0.0997578116,3.26182e-05,257 +T_AM_117,twisted,am,,117,0.30332848,0.0996298374,3.2668e-05,259 +T_AM_120,twisted,am,,120,0.293986,0.0992540572,3.28149e-05,263 +T_AM_147,twisted,am,,147,0.2448334,0.0963323591,3.40041e-05,300 +T_AM_148,twisted,am,,148,0.2430129333,0.0962375209,3.40441e-05,301 +T_AM_150,twisted,am,,150,0.239372,0.0960502777,3.41234e-05,304 +T_AM_228,twisted,am,,228,0.1551375636,0.0905569282,3.66279e-05,395 +T_AM_240,twisted,am,,240,0.14525,0.0899296465,3.69374e-05,409 +T_AM_288,twisted,am,,288,0.12201,0.0877788677,3.80397e-05,459 diff --git a/roseau/load_flow/exceptions.py b/roseau/load_flow/exceptions.py index 933b93de..41fe31c3 100644 --- a/roseau/load_flow/exceptions.py +++ b/roseau/load_flow/exceptions.py @@ -1,14 +1,12 @@ """ This module contains the exceptions used by Roseau Load Flow. """ -import unicodedata -from enum import Enum, auto -from typing import Union +from enum import auto -from typing_extensions import Self +from roseau.load_flow._compat import StrEnum -class RoseauLoadFlowExceptionCode(Enum): +class RoseauLoadFlowExceptionCode(StrEnum): """Error codes used by Roseau Load Flow.""" # Generic @@ -110,44 +108,18 @@ class RoseauLoadFlowExceptionCode(Enum): # License errors LICENSE_ERROR = auto() - @classmethod - def package_name(cls) -> str: - return "roseau.load_flow" - - def __str__(self) -> str: - return f"{self.package_name()}.{self.name}".lower() - def __eq__(self, other) -> bool: if isinstance(other, str): - return other.lower() == str(self).lower() + return other.lower() == self.lower() return super().__eq__(other) @classmethod - def from_string(cls, string: Union[str, "RoseauLoadFlowExceptionCode"]) -> Self: - """A method to convert a string into an error code enumerated type. - - Args: - string: - The string depicted the error code. If a good element is given - - Returns: - The enumerated type value corresponding with `string`. - """ - if isinstance(string, cls): - return string - elif isinstance(string, str): - pass - else: - string = str(string) - - # Withdraw accents and make lowercase - string = unicodedata.normalize("NFKD", string.lower()).encode("ASCII", "ignore").decode() - - # Withdraw the package prefix (e.g. roseau.core) - error_str = string.removeprefix(f"{cls.package_name()}.") - - # Get the value of this string - return cls[error_str.upper()] + def _missing_(cls, value: object) -> "RoseauLoadFlowExceptionCode | None": + if isinstance(value, str): + try: + return cls[value.upper().replace(" ", "_").replace("-", "_")] + except KeyError: + return None class RoseauLoadFlowException(Exception): diff --git a/roseau/load_flow/io/tests/test_dict.py b/roseau/load_flow/io/tests/test_dict.py index 2420586f..91331f8b 100644 --- a/roseau/load_flow/io/tests/test_dict.py +++ b/roseau/load_flow/io/tests/test_dict.py @@ -17,6 +17,7 @@ VoltageSource, ) from roseau.load_flow.network import ElectricalNetwork +from roseau.load_flow.utils import ConductorType, InsulatorType, LineType def test_to_dict(): @@ -30,7 +31,15 @@ def test_to_dict(): vs = VoltageSource("vs", source_bus, phases="abcn", voltages=voltages) # Same id, different line parameters -> fail - lp1 = LineParameters("test", z_line=np.eye(4, dtype=complex), y_shunt=np.eye(4, dtype=complex)) + lp1 = LineParameters( + "test", + z_line=np.eye(4, dtype=complex), + y_shunt=np.eye(4, dtype=complex), + line_type=LineType.UNDERGROUND, + conductor_type=ConductorType.AA, + insulator_type=InsulatorType.PVC, + section=120, + ) lp2 = LineParameters("test", z_line=np.eye(4, dtype=complex), y_shunt=np.eye(4, dtype=complex) * 1.1) geom = LineString([(0.0, 0.0), (0.0, 1.0)]) @@ -66,7 +75,12 @@ def test_to_dict(): assert "geometry" in res["branches"][1] assert np.isclose(res["buses"][0]["min_voltage"], 0.9 * vn) assert np.isclose(res["buses"][1]["max_voltage"], 1.1 * vn) - assert np.isclose(res["lines_params"][0]["max_current"], 1000) + lp_dict = res["lines_params"][0] + assert np.isclose(lp_dict["max_current"], 1000) + assert lp_dict["line_type"] == "UNDERGROUND" + assert lp_dict["conductor_type"] == "AA" + assert lp_dict["insulator_type"] == "PVC" + assert np.isclose(lp_dict["section"], 120) res = en.to_dict(_lf_only=True) assert "geometry" not in res["buses"][0] @@ -75,7 +89,12 @@ def test_to_dict(): assert "geometry" not in res["branches"][1] assert "min_voltage" not in res["buses"][0] assert "max_voltage" not in res["buses"][1] - assert "max_current" not in res["lines_params"][0] + lp_dict = res["lines_params"][0] + assert "max_current" not in lp_dict + assert "line_type" not in lp_dict + assert "conductor_type" not in lp_dict + assert "insulator_type" not in lp_dict + assert "section" not in lp_dict # Same id, different transformer parameters -> fail ground = Ground("ground") diff --git a/roseau/load_flow/models/core.py b/roseau/load_flow/models/core.py index f6762aa2..66d3335c 100644 --- a/roseau/load_flow/models/core.py +++ b/roseau/load_flow/models/core.py @@ -170,7 +170,7 @@ def _res_getter(self, value: _T | None, warning: bool) -> _T: return value @staticmethod - def _parse_geometry(geometry: str | None | Any) -> BaseGeometry | None: + def _parse_geometry(geometry: str | dict[str, Any] | None) -> BaseGeometry | None: if geometry is None: return None elif isinstance(geometry, str): diff --git a/roseau/load_flow/models/lines/lines.py b/roseau/load_flow/models/lines/lines.py index 1bbf3db6..a2de8314 100644 --- a/roseau/load_flow/models/lines/lines.py +++ b/roseau/load_flow/models/lines/lines.py @@ -172,11 +172,12 @@ def __init__( The second bus (aka `"to_bus"`) to connect to the line. parameters: - Parameters defining the electrical model of the line. This is an instance of the - :class:`LineParameters` class and can be used by multiple lines. + Parameters defining the electric model of the line using its impedance and shunt + admittance matrices. This is an instance of the :class:`LineParameters` class and + can be used by multiple lines. length: - The length of the line in km. + The length of the line (in km). phases: The phases of the line. A string like ``"abc"`` or ``"an"`` etc. The order of the @@ -256,6 +257,7 @@ def phases(self) -> str: @property @ureg_wraps("km", (None,)) def length(self) -> Q_[float]: + """The length of the line (in km).""" return self._length @length.setter @@ -274,7 +276,7 @@ def length(self, value: float | Q_[float]) -> None: @property def parameters(self) -> LineParameters: - """The parameters of the line.""" + """The parameters defining the impedance and shunt admittance matrices of line model.""" return self._parameters @parameters.setter @@ -320,18 +322,18 @@ def parameters(self, value: LineParameters) -> None: @property @ureg_wraps("ohm", (None,)) def z_line(self) -> Q_[ComplexArray]: - """Impedance of the line in Ohm""" + """Impedance of the line (in Ohm).""" return self.parameters._z_line * self._length @property @ureg_wraps("S", (None,)) def y_shunt(self) -> Q_[ComplexArray]: - """Shunt admittance of the line in Siemens""" + """Shunt admittance of the line (in Siemens).""" return self.parameters._y_shunt * self._length @property def max_current(self) -> Q_[float] | None: - """The maximum current loading of the line in A.""" + """The maximum current loading of the line (in A).""" # Do not add a setter. The user must know that if they change the max_current, it changes # for all lines that share the parameters. It is better to set it on the parameters. return self.parameters.max_current @@ -353,7 +355,7 @@ def _res_series_currents_getter(self, warning: bool) -> ComplexArray: @property @ureg_wraps("A", (None,)) def res_series_currents(self) -> Q_[ComplexArray]: - """Get the current in the series elements of the line (A).""" + """Get the current in the series elements of the line (in A).""" return self._res_series_currents_getter(warning=True) def _res_series_power_losses_getter(self, warning: bool) -> ComplexArray: @@ -363,7 +365,7 @@ def _res_series_power_losses_getter(self, warning: bool) -> ComplexArray: @property @ureg_wraps("VA", (None,)) def res_series_power_losses(self) -> Q_[ComplexArray]: - """Get the power losses in the series elements of the line (VA).""" + """Get the power losses in the series elements of the line (in VA).""" return self._res_series_power_losses_getter(warning=True) def _res_shunt_values_getter(self, warning: bool) -> tuple[ComplexArray, ComplexArray, ComplexArray, ComplexArray]: @@ -387,7 +389,7 @@ def _res_shunt_currents_getter(self, warning: bool) -> tuple[ComplexArray, Compl @property @ureg_wraps(("A", "A"), (None,)) def res_shunt_currents(self) -> tuple[Q_[ComplexArray], Q_[ComplexArray]]: - """Get the currents in the shunt elements of the line (A).""" + """Get the currents in the shunt elements of the line (in A).""" return self._res_shunt_currents_getter(warning=True) def _res_shunt_power_losses_getter(self, warning: bool) -> ComplexArray: @@ -399,7 +401,7 @@ def _res_shunt_power_losses_getter(self, warning: bool) -> ComplexArray: @property @ureg_wraps("VA", (None,)) def res_shunt_power_losses(self) -> Q_[ComplexArray]: - """Get the power losses in the shunt elements of the line (VA).""" + """Get the power losses in the shunt elements of the line (in VA).""" return self._res_shunt_power_losses_getter(warning=True) def _res_power_losses_getter(self, warning: bool) -> ComplexArray: @@ -410,7 +412,7 @@ def _res_power_losses_getter(self, warning: bool) -> ComplexArray: @property @ureg_wraps("VA", (None,)) def res_power_losses(self) -> Q_[ComplexArray]: - """Get the power losses in the line (VA).""" + """Get the power losses in the line (in VA).""" return self._res_power_losses_getter(warning=True) @property diff --git a/roseau/load_flow/models/lines/parameters.py b/roseau/load_flow/models/lines/parameters.py index 887cb885..f5c60061 100644 --- a/roseau/load_flow/models/lines/parameters.py +++ b/roseau/load_flow/models/lines/parameters.py @@ -1,5 +1,7 @@ import logging import re +from importlib import resources +from pathlib import Path from typing import NoReturn import numpy as np @@ -11,7 +13,6 @@ from roseau.load_flow.typing import ComplexArray, ComplexArrayLike2D, Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps from roseau.load_flow.utils import ( - CX, EPSILON_0, EPSILON_R, MU_0, @@ -19,6 +20,7 @@ PI, RHO, TAN_D, + CatalogueMixin, ConductorType, Identifiable, InsulatorType, @@ -28,24 +30,38 @@ logger = logging.getLogger(__name__) +_DEFAULT_CONDUCTOR_TYPE = { + LineType.OVERHEAD: ConductorType.ACSR, + LineType.TWISTED: ConductorType.AL, + LineType.UNDERGROUND: ConductorType.AL, +} -class LineParameters(Identifiable, JsonMixin): +_DEFAULT_INSULATION_TYPE = { + LineType.OVERHEAD: InsulatorType.UNKNOWN, # Not used for overhead lines + LineType.TWISTED: InsulatorType.XLPE, + LineType.UNDERGROUND: InsulatorType.PVC, +} + + +class LineParameters(Identifiable, JsonMixin, CatalogueMixin[pd.DataFrame]): """Parameters that define electrical models of lines.""" - _type_re = "|".join("|".join(x) for x in LineType.CODES.values()) + _type_re = "|".join(x.code() for x in LineType) _material_re = "|".join(x.code() for x in ConductorType) _section_re = r"[1-9][0-9]*" - _REGEXP_LINE_TYPE_NAME: re.Pattern = re.compile( - rf"^({_type_re})_({_material_re})_{_section_re}$", flags=re.IGNORECASE - ) + _REGEXP_LINE_TYPE_NAME = re.compile(rf"^({_type_re})_({_material_re})_{_section_re}$", flags=re.IGNORECASE) - @ureg_wraps(None, (None, None, "ohm/km", "S/km", "A")) + @ureg_wraps(None, (None, None, "ohm/km", "S/km", "A", None, None, None, "mm²")) def __init__( self, id: Id, z_line: ComplexArrayLike2D, y_shunt: ComplexArrayLike2D | None = None, max_current: float | None = None, + line_type: LineType | None = None, + conductor_type: ConductorType | None = None, + insulator_type: InsulatorType | None = None, + section: float | Q_[float] | None = None, ) -> None: """LineParameters constructor. @@ -60,7 +76,27 @@ def __init__( The Y matrix of the line (Siemens/km). This field is optional if the line has no shunt part. max_current: - An optional maximum current loading of the line (A). It is not used in the load flow. + The maximum current loading of the line (A). The maximum current is optional, it is + not used in the load flow but can be used to check for overloading. + See also :meth:`Line.res_violated `. + + line_type: + The type of the line (overhead, underground, twisted). The line type is optional, + it is informative only and is not used in the load flow. This field gets + automatically filled when the line parameters are created from a geometric model or + from the catalogue. + + conductor_type: + The type of the conductor material (Aluminum, Copper, ...). The conductor type is + optional, it is informative only and is not used in the load flow. This field gets + automatically filled when the line parameters are created from a geometric model or + from the catalogue. + + insulator_type: + The type of the cable insulator (PVC, XLPE, ...). The insulator type is optional, + it is informative only and is not used in the load flow. This field gets + automatically filled when the line parameters are created from a geometric model or + from the catalogue. """ super().__init__(id) self._z_line = np.array(z_line, dtype=np.complex128) @@ -71,6 +107,10 @@ def __init__( self._with_shunt = not np.allclose(y_shunt, 0) self._y_shunt = np.array(y_shunt, dtype=np.complex128) self.max_current = max_current + self._line_type = line_type + self._conductor_type = conductor_type + self._insulator_type = insulator_type + self._section: float = section self._check_matrix() def __eq__(self, other: object) -> bool: @@ -110,6 +150,26 @@ def max_current(self) -> Q_[float] | None: """The maximum current loading of the line (A) if it is set.""" return None if self._max_current is None else Q_(self._max_current, "A") + @property + def line_type(self) -> LineType | None: + """The type of the line. Informative only, it has no impact on the load flow.""" + return self._line_type + + @property + def conductor_type(self) -> ConductorType | None: + """The type of the conductor material. Informative only, it has no impact on the load flow.""" + return self._conductor_type + + @property + def insulator_type(self) -> InsulatorType | None: + """The type of the cable insulator. Informative only, it has no impact on the load flow.""" + return self._insulator_type + + @property + def section(self) -> Q_[float] | None: + """The cross section area of the cable (in mm²). Informative only, it has no impact on the load flow.""" + return None if self._section is None else Q_(self._section, "mm**2") + @max_current.setter @ureg_wraps(None, (None, "A")) def max_current(self, value: float | Q_[float] | None) -> None: @@ -299,11 +359,12 @@ def _sym_to_zy( def from_geometry( cls, id: Id, + *, line_type: LineType, - conductor_type: ConductorType, - insulator_type: InsulatorType, + conductor_type: ConductorType | None = None, + insulator_type: InsulatorType | None = None, section: float | Q_[float], - section_neutral: float | Q_[float], + section_neutral: float | Q_[float] | None = None, height: float | Q_[float], external_diameter: float | Q_[float], max_current: float | Q_[float] | None = None, @@ -315,25 +376,29 @@ def from_geometry( The id of the line parameters type. line_type: - Overhead or underground. + Overhead or underground. See also :class:`~roseau.load_flow.LineType`. conductor_type: - Type of the conductor + Type of the conductor. If ``None``, ``ACSR`` is used for overhead lines and ``AL`` + for underground or twisted lines. See also :class:`~roseau.load_flow.ConductorType`. insulator_type: - Type of insulator. + Type of insulator. If ``None``, ``XLPE`` is used for twisted lines and ``PVC`` for + underground lines. See also :class:`~roseau.load_flow.InsulatorType`. section: - Surface of the phases (mm²). + Cross-section surface area of the phases (mm²). section_neutral: - Surface of the neutral (mm²). + Cross-section surface area of the neutral (mm²). If None it will be the same as the + section of the other phases. height: - Height of the line (m). + Height of the line (m). It must be positive for overhead lines and negative for + underground lines. external_diameter: - External diameter of the wire (m). + External diameter of the cable (m). max_current: An optional maximum current loading of the line (A). It is not used in the load flow. @@ -344,7 +409,7 @@ def from_geometry( See Also: :ref:`Line parameters alternative constructor documentation ` """ - z_line, y_shunt = cls._geometry_to_zy( + z_line, y_shunt, line_type, conductor_type, insulator_type, section = cls._from_geometry( id=id, line_type=line_type, conductor_type=conductor_type, @@ -354,30 +419,39 @@ def from_geometry( height=height, external_diameter=external_diameter, ) - return cls(id=id, z_line=z_line, y_shunt=y_shunt, max_current=max_current) + return cls( + id=id, + z_line=z_line, + y_shunt=y_shunt, + max_current=max_current, + line_type=line_type, + conductor_type=conductor_type, + insulator_type=insulator_type, + section=section, + ) @staticmethod - def _geometry_to_zy( + def _from_geometry( id: Id, line_type: LineType, - conductor_type: ConductorType, - insulator_type: InsulatorType, + conductor_type: ConductorType | None, + insulator_type: InsulatorType | None, section: float, - section_neutral: float, + section_neutral: float | None, height: float, external_diameter: float, - ) -> tuple[ComplexArray, ComplexArray]: - """Create impedance and admittance matrix using a geometric model. + ) -> tuple[ComplexArray, ComplexArray, LineType, ConductorType, InsulatorType, float]: + """Create impedance and admittance matrices using a geometric model. Args: id: The id of the line parameters. line_type: - Overhead or underground. + Overhead, twisted overhead, or underground. conductor_type: - Type of the conductor + Type of the conductor material (Aluminum, Copper, ...). insulator_type: Type of insulator. @@ -386,10 +460,12 @@ def _geometry_to_zy( Surface of the phases (mm²). section_neutral: - Surface of the neutral (mm²). + Surface of the neutral (mm²). If None it will be the same as the section of the + other phases. height: - Height of the line (m). + Height of the line (m). Positive for overhead lines and negative for underground + lines. external_diameter: External diameter of the wire (m). @@ -401,53 +477,87 @@ def _geometry_to_zy( # dpn = data["dpn"] # Distance phase to neutral (m) # dsh = data["dsh"] # Diameter of the sheath (mm) + if conductor_type is None: + conductor_type = _DEFAULT_CONDUCTOR_TYPE[line_type] + if insulator_type is None: + insulator_type = _DEFAULT_INSULATION_TYPE[line_type] + if section_neutral is None: + section_neutral = section + line_type = LineType(line_type) + conductor_type = ConductorType(conductor_type) + insulator_type = InsulatorType(insulator_type) + # Geometric configuration if line_type in (LineType.OVERHEAD, LineType.TWISTED): # TODO This configuration is for twisted lines... Create a overhead configuration. - # TODO Add some checks on provided geometric values... + if height <= 0: + msg = f"The height of a '{line_type}' line must be a positive number." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL) + x = np.sqrt(3) * external_diameter / 8 coord = np.array( [ - [-np.sqrt(3) / 8 * external_diameter, height + external_diameter / 8], - [np.sqrt(3) / 8 * external_diameter, height + external_diameter / 8], + [-x, height + external_diameter / 8], + [x, height + external_diameter / 8], [0, height - external_diameter / 4], [0, height], ] ) # m coord_prim = np.array( [ - [-np.sqrt(3) / 8 * external_diameter, -height - external_diameter / 8], - [np.sqrt(3) / 8 * external_diameter, -height - external_diameter / 8], + [-x, -height - external_diameter / 8], + [x, -height - external_diameter / 8], [0, -height + external_diameter / 4], [0, -height], ] ) # m epsilon = EPSILON_0.m_as("F/m") elif line_type == LineType.UNDERGROUND: - coord = np.array( - [ - [-np.sqrt(2) / 8 * external_diameter, height - np.sqrt(2) / 8 * external_diameter], - [np.sqrt(2) / 8 * external_diameter, height - np.sqrt(2) / 8 * external_diameter], - [np.sqrt(2) / 8 * external_diameter, height + np.sqrt(2) / 8 * external_diameter], - [-np.sqrt(2) / 8 * external_diameter, height + np.sqrt(2) / 8 * external_diameter], - ] - ) # m - coord_prim = np.array( - [ - [-np.sqrt(2) * 3 / 8 * external_diameter, height - np.sqrt(2) * 3 / 8 * external_diameter], - [np.sqrt(2) * 3 / 8 * external_diameter, height - np.sqrt(2) * 3 / 8 * external_diameter], - [np.sqrt(2) * 3 / 8 * external_diameter, height + np.sqrt(2) * 3 / 8 * external_diameter], - [-np.sqrt(2) * 3 / 8 * external_diameter, height + np.sqrt(2) * 3 / 8 * external_diameter], - ] - ) # m + if height >= 0: + msg = f"The height of a '{line_type}' line must be a negative number." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL) + x = np.sqrt(2) * external_diameter / 8 + coord = np.array([[-x, height - x], [x, height - x], [x, height + x], [-x, height + x]]) # m + xp = x * 3 + coord_prim = np.array([[-xp, height - xp], [xp, height - xp], [xp, height + xp], [-xp, height + xp]]) # m epsilon = (EPSILON_0 * EPSILON_R[insulator_type]).m_as("F/m") else: - msg = f"The line type of the line {id!r} is unknown. It should have been filled in the reading." + msg = f"The line type {line_type!r} of the line {id!r} is unknown." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_TYPE) # Distance computation sections = np.array([section, section, section, section_neutral], dtype=np.float64) * 1e-6 # surfaces (m2) radius = np.sqrt(sections / PI) # radius (m) + phase_radius, neutral_radius = radius[0], radius[3] + if line_type == LineType.TWISTED: + max_radii = external_diameter / 4 + if phase_radius + neutral_radius > max_radii: + msg = ( + f"Conductors too big for 'twisted' line parameter of id {id!r}. Inequality " + f"`neutral_radius + phase_radius <= external_diameter / 4` is not satisfied." + ) + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL) + elif line_type == LineType.UNDERGROUND: + max_radii = external_diameter / 4 * np.sqrt(2) + if phase_radius + neutral_radius > max_radii: + msg = ( + f"Conductors too big for 'underground' line parameter of id {id!r}. Inequality " + f"`neutral_radius + phase_radius <= external_diameter * sqrt(2) / 4` is not satisfied." + ) + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL) + if phase_radius * 2 > max_radii: + msg = ( + f"Conductors too big for 'underground' line parameter of id {id!r}. Inequality " + f"`phase_radius*2 <= external_diameter * sqrt(2) / 4` is not satisfied." + ) + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL) + else: + pass # TODO Overhead lines check gmr = radius * np.exp(-0.25) # geometric mean radius (m) # distance between two wires (m) coord_new_dim = coord[:, None, :] @@ -488,7 +598,7 @@ def _geometry_to_zy( y_shunt[mask_diagonal] = np.einsum("ij->i", y) y_shunt[mask_off_diagonal] = -y[mask_off_diagonal] - return z_line, y_shunt + return z_line, y_shunt, line_type, conductor_type, insulator_type, section @classmethod @deprecated( @@ -539,8 +649,8 @@ def from_name_lv( # Check the user input and retrieve enumerated types line_type, conductor_type, section = name.split("_") - line_type = LineType.from_string(line_type) - conductor_type = ConductorType.from_string(conductor_type) + line_type = LineType(line_type) + conductor_type = ConductorType(conductor_type) insulator_type = InsulatorType.PVC section = float(section) @@ -565,13 +675,19 @@ def from_name_lv( ) @classmethod + # @deprecated( + # "The method LineParameters.from_name_mv() is deprecated and will be removed in a future " + # "version. Use LineParameters.from_catalogue() instead.", + # category=FutureWarning, + # ) @ureg_wraps(None, (None, None, "A")) def from_name_mv(cls, name: str, max_current: float | Q_[float] | None = None) -> Self: - """Method to get the electrical parameters of a MV line from its canonical name. + """Get the electrical parameters of a MV line from its canonical name (France specific model) Args: name: - The name of the line the parameters must be computed. E.g. "U_AL_150". + The canonical name of the line parameters. It must be in the format + `lineType_conductorType_crossSection`. E.g. "U_AL_150". max_current: An optional maximum current loading of the line (A). It is not used in the load flow. @@ -587,26 +703,31 @@ def from_name_mv(cls, name: str, max_current: float | Q_[float] | None = None) - # Check the user input and retrieve enumerated types line_type, conductor_type, section = name.split("_") - line_type = LineType.from_string(string=line_type) - conductor_type = ConductorType.from_string(conductor_type) + line_type = LineType(line_type) + conductor_type = ConductorType(conductor_type) section = Q_(float(section), "mm**2") r = RHO[conductor_type] / section - x = CX[line_type] - if type == LineType.OVERHEAD: + if line_type == LineType.OVERHEAD: c_b1 = Q_(50, "µF/km") c_b2 = Q_(0, "µF/(km*mm**2)") - elif type == LineType.TWISTED: - # Twisted line + x = Q_(0.35, "ohm/km") + elif line_type == LineType.TWISTED: c_b1 = Q_(1750, "µF/km") c_b2 = Q_(5, "µF/(km*mm**2)") - else: + x = Q_(0.1, "ohm/km") + elif line_type == LineType.UNDERGROUND: if section <= Q_(50, "mm**2"): c_b1 = Q_(1120, "µF/km") c_b2 = Q_(33, "µF/(km*mm**2)") else: c_b1 = Q_(2240, "µF/km") c_b2 = Q_(15, "µF/(km*mm**2)") + x = Q_(0.1, "ohm/km") + else: + msg = f"The line type {line_type!r} of the line {name!r} is unknown." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_TYPE) b = (c_b1 + c_b2 * section) * 1e-4 * OMEGA b = b.to("S/km") @@ -614,6 +735,233 @@ def from_name_mv(cls, name: str, max_current: float | Q_[float] | None = None) - y_shunt = b * 1j * np.eye(3, dtype=np.float64) # in siemens/km return cls(name, z_line=z_line, y_shunt=y_shunt, max_current=max_current) + # + # Catalogue Mixin + # + @classmethod + def catalogue_path(cls) -> Path: + return Path(resources.files("roseau.load_flow") / "data" / "lines").expanduser().absolute() + + @classmethod + def catalogue_data(cls) -> pd.DataFrame: + file = cls.catalogue_path() / "Catalogue.csv" + return pd.read_csv(file, parse_dates=False).fillna({"insulator": ""}) + + @classmethod + def _get_catalogue( + cls, + name: str | re.Pattern[str] | None, + line_type: str | None, + conductor_type: str | None, + insulator_type: str | None, + section: float | None, + raise_if_not_found: bool, + ) -> tuple[pd.DataFrame, str]: + catalogue_data = cls.catalogue_data() + + # Filter on strings/regular expressions + query_msg_list = [] + for value, column_name, display_name, display_name_plural in [ + (name, "name", "name", "names"), + ]: + if value is None: + continue + + mask = cls._filter_catalogue_str(value, strings=catalogue_data[column_name]) + if raise_if_not_found and mask.sum() == 0: + cls._raise_not_found_in_catalogue( + value=repr(value), + name=display_name, + name_plural=display_name_plural, + strings=catalogue_data[column_name], + query_msg_list=query_msg_list, + ) + catalogue_data = catalogue_data.loc[mask, :] + query_msg_list.append(f"{display_name}={value!r}") + + # Filter on enumerated types + for value, column_name, display_name, enum_class in ( + (line_type, "type", "line_type", LineType), + (conductor_type, "material", "conductor_type", ConductorType), + (insulator_type, "insulator", "insulator_type", InsulatorType), + ): + if value is None: + continue + + enum_series = catalogue_data[column_name].apply(enum_class) + try: + mask = enum_series == enum_class(value) + except RoseauLoadFlowException: + mask = pd.Series(False, index=catalogue_data.index) + if raise_if_not_found and mask.sum() == 0: + cls._raise_not_found_in_catalogue( + value=repr(value), + name=display_name, + name_plural=display_name + "s", + strings=enum_series, + query_msg_list=query_msg_list, + ) + catalogue_data = catalogue_data.loc[mask, :] + query_msg_list.append(f"{display_name}={value!r}") + + # Filter on floats + for value, column_name, display_name, display_name_plural, unit in [ + (section, "section", "cross-section", "cross-sections", "mm²"), + ]: + if value is None: + continue + + mask = np.isclose(catalogue_data[column_name], value) + if raise_if_not_found and mask.sum() == 0: + cls._raise_not_found_in_catalogue( + value=f"{value:.1f} {unit}", + name=display_name, + name_plural=display_name_plural, + strings=catalogue_data[column_name].apply(lambda x: f"{x:.1f} {unit}"), # noqa: B023 + query_msg_list=query_msg_list, + ) + catalogue_data = catalogue_data.loc[mask, :] + query_msg_list.append(f"{display_name}={value!r} {unit}") + + return catalogue_data, ", ".join(query_msg_list) + + @classmethod + @ureg_wraps(None, (None, None, None, None, None, "mm²", None)) + def from_catalogue( + cls, + name: str | re.Pattern[str] | None = None, + line_type: str | None = None, + conductor_type: str | None = None, + insulator_type: str | None = None, + section: float | Q_[float] | None = None, + id: Id | None = None, + ) -> Self: + """Create line parameters from a catalogue. + + Args: + name: + The name of the line parameters to get from the catalogue. It can be a regular + expression. + + line_type: + The type of the line parameters to get. It can be ``"overhead"``, ``"twisted"``, or + ``"underground"``. See also :class:`~roseau.load_flow.LineType`. + + conductor_type: + The type of the conductor material (Al, Cu, ...). See also + :class:`~roseau.load_flow.ConductorType`. + + insulator_type: + The type of insulator. See also :class:`~roseau.load_flow.InsulatorType`. + + section: + The cross-section surface area of the phases (mm²). + + id: + A unique ID for the created line parameters object (optional). If ``None`` + (default), the id of the created object will be its name in the catalogue. + + Returns: + The created line parameters. + """ + catalogue_data, query_info = cls._get_catalogue( + name=name, + line_type=line_type, + conductor_type=conductor_type, + insulator_type=insulator_type, + section=section, + raise_if_not_found=True, + ) + + cls._assert_one_found( + found_data=catalogue_data["name"].tolist(), display_name="line parameters", query_info=query_info + ) + idx = catalogue_data.index[0] + name = str(catalogue_data.at[idx, "name"]) + r = catalogue_data.at[idx, "r"] + x = catalogue_data.at[idx, "x"] + b = catalogue_data.at[idx, "b"] + line_type = LineType(catalogue_data.at[idx, "type"]) + conductor_type = ConductorType(catalogue_data.at[idx, "material"]) + insulator_type = InsulatorType(catalogue_data.at[idx, "insulator"]) + section = catalogue_data.at[idx, "section"] + max_current = catalogue_data.at[idx, "maximal_current"] + if pd.isna(max_current): + max_current = None + z_line = (r + x * 1j) * np.eye(3, dtype=np.complex128) + y_shunt = (b * 1j) * np.eye(3, dtype=np.complex128) + if id is None: + id = name + return cls( + id=id, + z_line=z_line, + y_shunt=y_shunt, + max_current=max_current, + line_type=line_type, + conductor_type=conductor_type, + insulator_type=insulator_type, + section=section, + ) + + @classmethod + @ureg_wraps(None, (None, None, None, None, None, "mm²")) + def get_catalogue( + cls, + name: str | re.Pattern[str] | None = None, + line_type: str | None = None, + conductor_type: str | None = None, + insulator_type: str | None = None, + section: float | Q_[float] | None = None, + ) -> pd.DataFrame: + """Get the catalogue of available lines. + + You can use the parameters below to filter the catalogue. If you do not specify any + parameter, all the catalogue will be returned. + + Args: + name: + The name of the line parameters to get from the catalogue. It can be a regular + expression. + + line_type: + The type of the line parameters to get. It can be ``"overhead"``, ``"twisted"``, or + ``"underground"``. See also :class:`~roseau.load_flow.LineType`. + + conductor_type: + The type of the conductor material (Al, Cu, ...). See also + :class:`~roseau.load_flow.ConductorType`. + + insulator_type: + The type of insulator. See also :class:`~roseau.load_flow.InsulatorType`. + + section: + The cross-section surface area of the phases (mm²). + + Returns: + The catalogue data as a dataframe. + """ + catalogue_data, _ = cls._get_catalogue( + name=name, + line_type=line_type, + conductor_type=conductor_type, + insulator_type=insulator_type, + section=section, + raise_if_not_found=False, + ) + return catalogue_data.rename( + columns={ + "name": "Name", + "r": "Resistance (ohm/km)", + "x": "Reactance (ohm/km)", + "b": "Susceptance (µS/km)", + "maximal_current": "Maximal current (A)", + "type": "Line type", + "material": "Conductor material", + "insulator": "Insulator type", + "section": "Cross-section (mm²)", + } + ).set_index("Name") + # # Json Mixin interface # @@ -630,7 +978,19 @@ def from_dict(cls, data: JsonDict) -> Self: """ z_line = np.array(data["z_line"][0]) + 1j * np.array(data["z_line"][1]) y_shunt = np.array(data["y_shunt"][0]) + 1j * np.array(data["y_shunt"][1]) if "y_shunt" in data else None - return cls(id=data["id"], z_line=z_line, y_shunt=y_shunt, max_current=data.get("max_current")) + line_type = LineType(data["line_type"]) if "line_type" in data else None + conductor_type = ConductorType(data["conductor_type"]) if "conductor_type" in data else None + insulator_type = InsulatorType(data["insulator_type"]) if "insulator_type" in data else None + return cls( + id=data["id"], + z_line=z_line, + y_shunt=y_shunt, + max_current=data.get("max_current"), + line_type=line_type, + conductor_type=conductor_type, + insulator_type=insulator_type, + section=data.get("section"), + ) def to_dict(self, *, _lf_only: bool = False) -> JsonDict: """Return the line parameters information as a dictionary format.""" @@ -639,6 +999,14 @@ def to_dict(self, *, _lf_only: bool = False) -> JsonDict: res["y_shunt"] = [self._y_shunt.real.tolist(), self._y_shunt.imag.tolist()] if not _lf_only and self.max_current is not None: res["max_current"] = self.max_current.magnitude + if not _lf_only and self._line_type is not None: + res["line_type"] = self._line_type.name + if not _lf_only and self._conductor_type is not None: + res["conductor_type"] = self._conductor_type.name + if not _lf_only and self._insulator_type is not None: + res["insulator_type"] = self._insulator_type.name + if not _lf_only and self._section is not None: + res["section"] = self._section return res def _results_to_dict(self, warning: bool) -> NoReturn: diff --git a/roseau/load_flow/models/loads/loads.py b/roseau/load_flow/models/loads/loads.py index 4ad0b40c..9178347a 100644 --- a/roseau/load_flow/models/loads/loads.py +++ b/roseau/load_flow/models/loads/loads.py @@ -125,9 +125,7 @@ def _validate_value(self, value: ComplexArrayLike1D) -> ComplexArray: if len(value) != self._size: msg = f"Incorrect number of {self._type}s: {len(value)} instead of {self._size}" logger.error(msg) - raise RoseauLoadFlowException( - msg=msg, code=RoseauLoadFlowExceptionCode.from_string(f"BAD_{self._symbol}_SIZE") - ) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode[f"BAD_{self._symbol}_SIZE"]) # A load cannot have any zero impedance if self._type == "impedance" and np.isclose(value, 0).any(): msg = f"An impedance of the load {self.id!r} is null" diff --git a/roseau/load_flow/models/tests/test_line_parameters.py b/roseau/load_flow/models/tests/test_line_parameters.py index 85b517b7..fc5c77c6 100644 --- a/roseau/load_flow/models/tests/test_line_parameters.py +++ b/roseau/load_flow/models/tests/test_line_parameters.py @@ -1,6 +1,9 @@ +import re + import numpy as np import numpy.linalg as nplin import numpy.testing as npt +import pandas as pd import pytest from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode @@ -112,7 +115,7 @@ def test_geometry(): # line_data = {"dpp": 0, "dpn": 0, "dsh": 0.04} # Working example - z_line, y_shunt = LineParameters._geometry_to_zy( + z_line, y_shunt, line_type, conductor_type, insulator_type, section = LineParameters._from_geometry( "test", line_type=LineType.OVERHEAD, conductor_type=ConductorType.AL, @@ -123,6 +126,7 @@ def test_geometry(): external_diameter=0.04, ) + # TODO regenerate all expected values with the IEC constants and update this test y_line_expected = np.array( [ [3.3915102901533754, -1.2233003903972888, -1.2233003903972615, -0.7121721195595286], @@ -139,7 +143,7 @@ def test_geometry(): ] ) - npt.assert_allclose(z_line, nplin.inv(y_line_expected)) + npt.assert_allclose(z_line, nplin.inv(y_line_expected), rtol=0.04, atol=0.02) y_shunt_expected = np.array( [ [ @@ -168,12 +172,17 @@ def test_geometry(): ], ] ) - npt.assert_allclose(y_shunt, y_shunt_expected) + npt.assert_allclose(y_shunt, y_shunt_expected, rtol=0.001) + + assert line_type == LineType.OVERHEAD + assert conductor_type == ConductorType.AL + assert insulator_type == InsulatorType.PEX + assert section == 150 # line_data = {"dpp": 0, "dpn": 0, "dsh": 0.04} # Working example - z_line, y_shunt = LineParameters._geometry_to_zy( + z_line, y_shunt, line_type, conductor_type, insulator_type, section = LineParameters._from_geometry( "test", line_type=LineType.UNDERGROUND, conductor_type=ConductorType.AL, @@ -198,7 +207,7 @@ def test_geometry(): [-0.03859093131793137, 0.20837873067712717, -0.03859093131792582, -0.6182914857776997], ] ) - npt.assert_allclose(z_line, nplin.inv(y_line_expected)) + npt.assert_allclose(z_line, nplin.inv(y_line_expected), rtol=0.04, atol=0.02) y_shunt_expected = np.array( [ [ @@ -228,7 +237,12 @@ def test_geometry(): ] ) - npt.assert_allclose(y_shunt, y_shunt_expected) + npt.assert_allclose(y_shunt, y_shunt_expected, rtol=0.3) + + assert line_type == LineType.UNDERGROUND + assert conductor_type == ConductorType.AL + assert insulator_type == InsulatorType.PVC + assert section == 150 def test_sym(): @@ -315,39 +329,155 @@ def test_sym(): def test_from_name_lv(): with pytest.raises(RoseauLoadFlowException) as e, pytest.warns(FutureWarning): - LineParameters.from_name_lv("totoS_Al_150") + LineParameters.from_name_lv("totoU_Al_150") assert "The line type name does not follow the syntax rule." in e.value.msg assert e.value.code == RoseauLoadFlowExceptionCode.BAD_TYPE_NAME_SYNTAX with pytest.warns(FutureWarning): - lp = LineParameters.from_name_lv("S_AL_150") + lp = LineParameters.from_name_lv("U_AL_150") assert lp.z_line.shape == (4, 4) assert lp.y_shunt.shape == (4, 4) assert (lp.z_line.real >= 0).all().all() - with pytest.warns(FutureWarning): - lp2 = LineParameters.from_name_lv("U_AL_150") - npt.assert_allclose(lp2.z_line.m_as("ohm/km"), lp.z_line.m_as("ohm/km")) - npt.assert_allclose(lp2.y_shunt.m_as("S/km"), lp.y_shunt.m_as("S/km"), rtol=1e-4) - def test_from_name_mv(): - with pytest.raises(RoseauLoadFlowException) as e: - LineParameters.from_name_mv("totoS_Al_150") + with pytest.raises(RoseauLoadFlowException) as e, pytest.warns(FutureWarning): + LineParameters.from_name_mv("totoU_Al_150") assert "The line type name does not follow the syntax rule." in e.value.msg assert e.value.code == RoseauLoadFlowExceptionCode.BAD_TYPE_NAME_SYNTAX - lp = LineParameters.from_name_mv("S_AL_150") - z_line_expected = (0.188 + 0.1j) * np.eye(3) + lp = LineParameters.from_name_mv("U_AL_150") + z_line_expected = (0.1767 + 0.1j) * np.eye(3) y_shunt_expected = 0.00014106j * np.eye(3) - npt.assert_allclose(lp.z_line.m_as("ohm/km"), z_line_expected) - npt.assert_allclose(lp.y_shunt.m_as("S/km"), y_shunt_expected, rtol=1e-4) - - # The same with "underground" - lp = LineParameters.from_name_mv("U_AL_150") - npt.assert_allclose(lp.z_line.m_as("ohm/km"), z_line_expected) - npt.assert_allclose(lp.y_shunt.m_as("S/km"), y_shunt_expected, rtol=1e-4) + npt.assert_allclose(lp.z_line.m_as("ohm/km"), z_line_expected, rtol=0.01, atol=0.01) + npt.assert_allclose(lp.y_shunt.m_as("S/km"), y_shunt_expected, rtol=0.01, atol=0.01) + + +def test_catalogue_data(): + # The catalogue data path exists + catalogue_path = LineParameters.catalogue_path() + assert catalogue_path.exists() + + catalogue_data = LineParameters.catalogue_data() + + # Check that the name is unique + assert catalogue_data["name"].is_unique, "Regenerate catalogue." + + for row in catalogue_data.itertuples(): + assert re.match(r"^(?:U|O|T)_[A-Z]+_\d+(?:_\w+)?$", row.name) + assert isinstance(row.r, float) + assert isinstance(row.x, float) + assert isinstance(row.b, float) + assert isinstance(row.maximal_current, int | float) + LineType(row.type) # Check that the type is valid + ConductorType(row.material) # Check that the material is valid + InsulatorType(row.insulator) # Check that the insulator is valid + assert isinstance(row.section, int | float) + + +def test_from_catalogue(): + # Unknown strings + for field_name in ("name",): + # String + with pytest.raises(RoseauLoadFlowException) as e: + LineParameters.from_catalogue(**{field_name: "unknown"}) + assert e.value.msg.startswith(f"No {field_name} matching 'unknown' has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + + # Regexp + with pytest.raises(RoseauLoadFlowException) as e: + LineParameters.from_catalogue(**{field_name: r"unknown[a-z]+"}) + assert e.value.msg.startswith(f"No {field_name} matching 'unknown[a-z]+' has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + + # Unknown enums + for field_name in ("line_type", "conductor_type", "insulator_type"): + # String + with pytest.raises(RoseauLoadFlowException) as e: + LineParameters.from_catalogue(**{field_name: "invalid"}) + assert e.value.msg.startswith(f"No {field_name} matching 'invalid' has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + + # Regexp + with pytest.raises(RoseauLoadFlowException) as e: + LineParameters.from_catalogue(**{field_name: r"invalid[a-z]+"}) + assert e.value.msg.startswith(f"No {field_name} matching 'invalid[a-z]+' has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + + # Unknown floats + for field_name, display_name, display_unit in (("section", "cross-section", "mm²"),): + # Without unit + with pytest.raises(RoseauLoadFlowException) as e: + LineParameters.from_catalogue(**{field_name: 3.1415}) + assert e.value.msg.startswith(f"No {display_name} matching 3.1 {display_unit} has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + + # With unit + with pytest.raises(RoseauLoadFlowException) as e: + LineParameters.from_catalogue(**{field_name: Q_(0.031415, "cm²")}) + assert e.value.msg.startswith(f"No {display_name} matching 3.1 {display_unit} has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + + # Several line parameters + with pytest.raises(RoseauLoadFlowException) as e: + LineParameters.from_catalogue(name=r"U_AL_") + assert e.value.msg == ( + "Several line parameters matching the query (name='U_AL_') have been found: " + "'U_AL_19', 'U_AL_20', 'U_AL_22', 'U_AL_25', 'U_AL_28', 'U_AL_29', 'U_AL_33', " + "'U_AL_34', 'U_AL_37', 'U_AL_38', 'U_AL_40', 'U_AL_43', 'U_AL_48', 'U_AL_50', " + "'U_AL_54', 'U_AL_55', 'U_AL_59', 'U_AL_60', 'U_AL_69', 'U_AL_70', 'U_AL_74', " + "'U_AL_75', 'U_AL_79', 'U_AL_80', 'U_AL_90', 'U_AL_93', 'U_AL_95', 'U_AL_100', " + "'U_AL_116', 'U_AL_117', 'U_AL_120', 'U_AL_147', 'U_AL_148', 'U_AL_150', 'U_AL_228', " + "'U_AL_240', 'U_AL_288'." + ) + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND + + # Success + lp = LineParameters.from_catalogue(name="U_AL_150") + assert lp.id == "U_AL_150" + assert lp.z_line.shape == (3, 3) + assert lp.y_shunt.shape == (3, 3) + assert lp.max_current > 0 + assert lp.line_type == LineType.UNDERGROUND + assert lp.conductor_type == ConductorType.AL + assert lp.insulator_type == InsulatorType.UNKNOWN + assert lp.section.m == 150 + + lp = LineParameters.from_catalogue(name="U_AL_150", id="lp1") + assert lp.id == "lp1" + + +def test_get_catalogue(): + # Get the entire catalogue + catalogue = LineParameters.get_catalogue() + assert isinstance(catalogue, pd.DataFrame) + assert catalogue.shape == (355, 8) + + # Filter on a single attribute + for field_name, value, expected_size in ( + ("name", r"U_AL_150.*", 1), + ("line_type", "OvErHeAd", 122), + ("conductor_type", "Cu", 121), + # ("insulator_type", InsulatorType.SE, 240), + ("section", 150, 9), + ("section", Q_(1.5, "cm²"), 9), + ): + filtered_catalogue = LineParameters.get_catalogue(**{field_name: value}) + assert filtered_catalogue.shape == (expected_size, 8) + + # Filter on two attributes + for field_name, value, expected_size in ( + ("name", r"U_AL_150.*", 1), + ("line_type", "OvErHeAd", 122), + ("section", 150, 9), + ): + filtered_catalogue = LineParameters.get_catalogue(**{field_name: value}) + assert filtered_catalogue.shape == (expected_size, 8) + + # No results + empty_catalogue = LineParameters.get_catalogue(section=15000) + assert empty_catalogue.shape == (0, 8) def test_max_current(): diff --git a/roseau/load_flow/models/tests/test_transformer_parameters.py b/roseau/load_flow/models/tests/test_transformer_parameters.py index 3b259f6d..8f1bf8ea 100644 --- a/roseau/load_flow/models/tests/test_transformer_parameters.py +++ b/roseau/load_flow/models/tests/test_transformer_parameters.py @@ -1,13 +1,13 @@ import numbers import numpy as np +import pandas as pd import pytest from pint import DimensionalityError from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow.models import TransformerParameters from roseau.load_flow.units import Q_ -from roseau.load_flow.utils import console def test_transformer_parameters(): @@ -302,7 +302,7 @@ def test_transformer_type(): else: with pytest.raises(RoseauLoadFlowException) as e: TransformerParameters.extract_windings(t) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_WINDINGS + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_WINDINGS else: with pytest.raises(RoseauLoadFlowException) as e: TransformerParameters.extract_windings(t) @@ -368,16 +368,14 @@ def test_from_catalogue(): # String with pytest.raises(RoseauLoadFlowException) as e: TransformerParameters.from_catalogue(**{field_name: "unknown"}) - assert e.value.args[0].startswith(f"No {field_name} matching the name 'unknown' has been found. Available ") - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + assert e.value.msg.startswith(f"No {field_name} matching 'unknown' has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND # Regexp with pytest.raises(RoseauLoadFlowException) as e: TransformerParameters.from_catalogue(**{field_name: r"unknown[a-z]+"}) - assert e.value.args[0].startswith( - f"No {field_name} matching the name 'unknown[a-z]+' has been found. " f"Available " - ) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + assert e.value.msg.startswith(f"No {field_name} matching 'unknown[a-z]+' has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND # Unknown floats for field_name, display_name, display_unit in ( @@ -388,78 +386,76 @@ def test_from_catalogue(): # Without unit with pytest.raises(RoseauLoadFlowException) as e: TransformerParameters.from_catalogue(**{field_name: 3141.5}) - assert e.value.args[0].startswith(f"No {display_name} matching 3.1 {display_unit} has been found. Available ") - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + assert e.value.msg.startswith(f"No {display_name} matching 3.1 {display_unit} has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND # With unit with pytest.raises(RoseauLoadFlowException) as e: TransformerParameters.from_catalogue(**{field_name: Q_(3141.5, display_unit.removeprefix("k"))}) - assert e.value.args[0].startswith(f"No {display_name} matching 3.1 {display_unit} has been found. Available ") - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + assert e.value.msg.startswith(f"No {display_name} matching 3.1 {display_unit} has been found. Available ") + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND # Several transformers with pytest.raises(RoseauLoadFlowException) as e: TransformerParameters.from_catalogue(type="yzn", sn=50e3) - assert ( - e.value.args[0] - == "Several transformers matching the query (\"type='yzn', nominal power=50.0 kVA\") have been found. Please " - "look at the catalogue using the `print_catalogue` class method." + assert e.value.msg == ( + "Several transformers matching the query (type='yzn', nominal power=50.0 kVA) have been " + "found: 'SE_Minera_A0Ak_50kVA', 'SE_Minera_B0Bk_50kVA', 'SE_Minera_C0Bk_50kVA', " + "'SE_Minera_Standard_50kVA'." ) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND -def test_print_catalogue(): - # Print the entire catalogue - with console.capture() as capture: - TransformerParameters.print_catalogue() - assert len(capture.get().split("\n")) == 136 +def test_get_catalogue(): + # Get the entire catalogue + catalogue = TransformerParameters.get_catalogue() + assert isinstance(catalogue, pd.DataFrame) + assert catalogue.shape == (130, 7) # Filter on a single attribute - for field_name, value, expected_lines in ( - ("id", "SE_Minera_A0Ak_50kVA", 7), - ("manufacturer", "SE", 122), - ("range", r"min.*", 62), - ("efficiency", "c0", 35), - ("type", "dy", 132), - ("sn", Q_(160, "kVA"), 16), - ("uhv", Q_(20, "kV"), 136), - ("ulv", 400, 136), + for field_name, value, expected_size in ( + ("id", "SE_Minera_A0Ak_50kVA", 1), + ("manufacturer", "SE", 116), + ("range", r"min.*", 56), + ("efficiency", "c0", 29), + ("type", "dy", 126), + ("sn", Q_(160, "kVA"), 10), + ("uhv", Q_(20, "kV"), 130), + ("ulv", 400, 130), ): - with console.capture() as capture: - TransformerParameters.print_catalogue(**{field_name: value}) - assert len(capture.get().split("\n")) == expected_lines + filtered_catalogue = TransformerParameters.get_catalogue(**{field_name: value}) + assert filtered_catalogue.shape == (expected_size, 7) # Filter on two attributes - for field_name, value, expected_lines in ( - ("id", "SE_Minera_A0Ak_50kVA", 7), - ("range", "minera", 62), - ("efficiency", "c0", 35), - ("type", r"^d.*11$", 118), - ("sn", Q_(160, "kVA"), 15), - ("uhv", Q_(20, "kV"), 122), - ("ulv", 400, 122), + for field_name, value, expected_size in ( + ("id", "SE_Minera_A0Ak_50kVA", 1), + ("range", "minera", 56), + ("efficiency", "c0", 29), + ("type", r"^d.*11$", 112), + ("sn", Q_(160, "kVA"), 9), + ("uhv", Q_(20, "kV"), 116), + ("ulv", 400, 116), ): - with console.capture() as capture: - TransformerParameters.print_catalogue(**{field_name: value}, manufacturer="se") - assert len(capture.get().split("\n")) == expected_lines + filtered_catalogue = TransformerParameters.get_catalogue(**{field_name: value}, manufacturer="se") + assert filtered_catalogue.shape == (expected_size, 7) # Filter on three attributes - for field_name, value, expected_lines in ( - ("id", "se_VEGETA_C0BK_3150kva", 7), - ("efficiency", r"c0[abc]k", 21), - ("type", "dyn", 36), - ("sn", Q_(160, "kVA"), 8), - ("uhv", Q_(20, "kV"), 36), - ("ulv", 400, 36), + for field_name, value, expected_size in ( + ("id", "se_VEGETA_C0BK_3150kva", 1), + ("efficiency", r"c0[abc]k", 15), + ("type", "dyn", 30), + ("sn", Q_(160, "kVA"), 2), + ("uhv", Q_(20, "kV"), 30), + ("ulv", 400, 30), ): - with console.capture() as capture: - TransformerParameters.print_catalogue(**{field_name: value}, manufacturer="se", range=r"^vegeta$") - assert len(capture.get().split("\n")) == expected_lines + filtered_catalogue = TransformerParameters.get_catalogue( + **{field_name: value}, manufacturer="se", range=r"^vegeta$" + ) + assert filtered_catalogue.shape == (expected_size, 7) # No results - with console.capture() as capture: - TransformerParameters.print_catalogue(ulv=250) - assert len(capture.get().split("\n")) == 2 + empty_catalogue = TransformerParameters.get_catalogue(ulv=250) + assert empty_catalogue.shape == (0, 7) def test_max_power(): diff --git a/roseau/load_flow/models/transformers/parameters.py b/roseau/load_flow/models/transformers/parameters.py index aa596ee5..9ee332e2 100644 --- a/roseau/load_flow/models/transformers/parameters.py +++ b/roseau/load_flow/models/transformers/parameters.py @@ -1,21 +1,19 @@ +import json import logging import re -import textwrap from importlib import resources -from itertools import cycle from pathlib import Path from typing import NoReturn import numpy as np import pandas as pd import regex -from rich.table import Table from typing_extensions import Self from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow.typing import Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps -from roseau.load_flow.utils import CatalogueMixin, Identifiable, JsonMixin, console, palette +from roseau.load_flow.utils import CatalogueMixin, Identifiable, JsonMixin logger = logging.getLogger(__name__) @@ -312,7 +310,71 @@ def catalogue_path(cls) -> Path: @classmethod def catalogue_data(cls) -> pd.DataFrame: - return pd.read_csv(cls.catalogue_path() / "Catalogue.csv") + file = cls.catalogue_path() / "Catalogue.csv" + return pd.read_csv(file, parse_dates=False) + + @classmethod + def _get_catalogue( + cls, + id: str | re.Pattern[str] | None, + manufacturer: str | re.Pattern[str] | None, + range: str | re.Pattern[str] | None, + efficiency: str | re.Pattern[str] | None, + type: str | re.Pattern[str] | None, + sn: float | None, + uhv: float | None, + ulv: float | None, + raise_if_not_found: bool, + ) -> tuple[pd.DataFrame, str]: + # Get the catalogue data + catalogue_data = cls.catalogue_data() + + # Filter on string/regular expressions + query_msg_list = [] + for value, column_name, display_name, display_name_plural in ( + (id, "id", "id", "ids"), + (manufacturer, "manufacturer", "manufacturer", "manufacturers"), + (range, "range", "range", "ranges"), + (efficiency, "efficiency", "efficiency", "efficiencies"), + (type, "type", "type", "types"), + ): + if pd.isna(value): + continue + + mask = cls._filter_catalogue_str(value=value, strings=catalogue_data[column_name]) + if raise_if_not_found and mask.sum() == 0: + cls._raise_not_found_in_catalogue( + value=repr(value), + name=display_name, + name_plural=display_name_plural, + strings=catalogue_data[column_name], + query_msg_list=query_msg_list, + ) + catalogue_data = catalogue_data.loc[mask, :] + query_msg_list.append(f"{display_name}={value!r}") + + # Filter on float + for value, column_name, display_name, display_name_plural, display_unit in ( + (sn, "sn", "nominal power", "nominal powers", "kVA"), + (uhv, "uhv", "primary side voltage", "primary side voltages", "kV"), + (ulv, "ulv", "secondary side voltage", "secondary side voltages", "kV"), + ): + if pd.isna(value): + continue + + mask = np.isclose(catalogue_data[column_name], value) + if raise_if_not_found and mask.sum() == 0: + cls._raise_not_found_in_catalogue( + value=f"{value / 1000:.1f} {display_unit}", + name=display_name, + name_plural=display_name_plural, + strings=catalogue_data[column_name].apply(lambda x: f"{x/1000:.1f} {display_unit}"), # noqa: B023 + query_msg_list=query_msg_list, + ) + catalogue_data = catalogue_data.loc[mask, :] + query_msg_list.append(f"{display_name}={value/1000:.1f} {display_unit}") + + return catalogue_data, ", ".join(query_msg_list) @classmethod @ureg_wraps(None, (None, None, None, None, None, None, "VA", "V", "V")) @@ -323,9 +385,9 @@ def from_catalogue( range: str | re.Pattern[str] | None = None, efficiency: str | re.Pattern[str] | None = None, type: str | re.Pattern[str] | None = None, - sn: float | None = None, - uhv: float | None = None, - ulv: float | None = None, + sn: float | Q_[float] | None = None, + uhv: float | Q_[float] | None = None, + ulv: float | Q_[float] | None = None, ) -> Self: """Build a transformer parameters from one in the catalogue. @@ -359,120 +421,57 @@ def from_catalogue( raised. """ # Get the catalogue data - catalogue_data = cls.catalogue_data() - - # Filter on string/regular expressions - query_msg_list = [] - for value, column_name, display_name, display_name_plural in ( - (id, "id", "id", "ids"), - (manufacturer, "manufacturer", "manufacturer", "manufacturers"), - (range, "range", "range", "ranges"), - (efficiency, "efficiency", "efficiency", "efficiencies"), - (type, "type", "type", "types"), - ): - if pd.isna(value): - continue - - mask = cls._filter_catalogue_str(value=value, catalogue_data=catalogue_data, column_name=column_name) - if mask.sum() == 0: - available_values = catalogue_data[column_name].unique().tolist() - msg_part = textwrap.shorten(", ".join(repr(x) for x in available_values), width=500) - if query_msg_list: - query_msg_part = ", ".join(query_msg_list) - msg = ( - f"No {display_name} matching the name {value!r} has been found for the query {query_msg_part}. " - f"Available {display_name_plural} are {msg_part}." - ) - else: - msg = ( - f"No {display_name} matching the name {value!r} has been found. " - f"Available {display_name_plural} are {msg_part}." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND) - catalogue_data = catalogue_data.loc[mask, :] - query_msg_list.append(f"{display_name}={value!r}") - - # Filter on float - for value, column_name, display_name, display_name_plural, display_unit in ( - (sn, "sn", "nominal power", "nominal powers", "kVA"), - (uhv, "uhv", "primary side voltage", "primary side voltages", "kV"), - (ulv, "ulv", "secondary side voltage", "secondary side voltages", "kV"), - ): - if pd.isna(value): - continue - - mask = cls._filter_catalogue_float(value=value, catalogue_data=catalogue_data, column_name=column_name) - if mask.sum() == 0: - available_values = catalogue_data[column_name].unique().tolist() - msg_part = textwrap.shorten( - ", ".join(f"{x/1000:.1f} {display_unit}" for x in available_values), width=500 - ) - if query_msg_list: - query_msg_part = ", ".join(query_msg_list) - msg = ( - f"No {display_name} matching {value/1000:.1f} {display_unit} has been found for the query" - f" {query_msg_part}. Available {display_name_plural} are {msg_part}." - ) - else: - msg = ( - f"No {display_name} matching {value/1000:.1f} {display_unit} has been found. " - f"Available {display_name_plural} are {msg_part}." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND) - catalogue_data = catalogue_data.loc[mask, :] - query_msg_list.append(f"{display_name}={value/1000:.1f} {display_unit}") + catalogue_data, query_info = cls._get_catalogue( + id=id, + manufacturer=manufacturer, + range=range, + efficiency=efficiency, + type=type, + sn=sn, + uhv=uhv, + ulv=ulv, + raise_if_not_found=True, + ) - # Final check - if len(catalogue_data) == 0: # pragma: no cover - # This option should never happen as an error is raised when a filter is empty - query_msg_part = ", ".join(query_msg_list) - msg = ( - f"No transformers matching the query ({query_msg_part!r}) have been found. Please look at the " - f"catalogue using the `print_catalogue` class method." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND) - elif len(catalogue_data) > 1: - query_msg_part = ", ".join(query_msg_list) - msg = ( - f"Several transformers matching the query ({query_msg_part!r}) have been found. Please look at the " - f"catalogue using the `print_catalogue` class method." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND) + cls._assert_one_found( + found_data=catalogue_data["id"].tolist(), display_name="transformers", query_info=query_info + ) # A single one has been chosen idx = catalogue_data.index[0] - manufacturer = catalogue_data.at[idx, "manufacturer"] - range = catalogue_data.at[idx, "range"] - efficiency = catalogue_data.at[idx, "efficiency"] + manufacturer = str(catalogue_data.at[idx, "manufacturer"]) + range = str(catalogue_data.at[idx, "range"]) + efficiency = str(catalogue_data.at[idx, "efficiency"]) nominal_power = int(catalogue_data.at[idx, "sn"] / 1000) # Get the data from the Json file path = cls.catalogue_path() / manufacturer / range / efficiency / f"{nominal_power}.json" - if not path.exists(): # pragma: no cover + try: + json_dict = json.loads(path.read_text()) + except FileNotFoundError: msg = f"The file {path} has not been found while it should exist. Please post an issue on GitHub." logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_MISSING) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_MISSING) from None - return cls.from_json(path=path) + return cls.from_dict(json_dict) @classmethod @ureg_wraps(None, (None, None, None, None, None, None, "VA", "V", "V")) - def print_catalogue( + def get_catalogue( cls, id: str | re.Pattern[str] | None = None, manufacturer: str | re.Pattern[str] | None = None, range: str | re.Pattern[str] | None = None, efficiency: str | re.Pattern[str] | None = None, type: str | re.Pattern[str] | None = None, - sn: float | None = None, - uhv: float | None = None, - ulv: float | None = None, - ) -> None: - """Print the catalogue of available transformers. + sn: float | Q_[float] | None = None, + uhv: float | Q_[float] | None = None, + ulv: float | Q_[float] | None = None, + ) -> pd.DataFrame: + """Get the catalogue of available transformers. + + You can use the parameters below to filter the catalogue. If you do not specify any + parameter, all the catalogue will be returned. Args: id: @@ -498,122 +497,45 @@ def print_catalogue( ulv: An optional secondary side voltage to filter the output. - """ - # Get the catalogue data - catalogue_data = cls.catalogue_data() - - # Start creating a table to display the results - table = Table(title="Available Transformer Parameters") - table.add_column("Id", overflow="fold") - table.add_column("Manufacturer", overflow="fold") - table.add_column("Product range", overflow="fold") - table.add_column("Efficiency", overflow="fold") - table.add_column("Type", overflow="fold") - table.add_column("Nominal power (kVA)", justify="right", overflow="fold") - table.add_column("High voltage (kV)", justify="right", overflow="fold") - table.add_column("Low voltage (kV)", justify="right", overflow="fold") - empty_table = True - - # Match on the manufacturer, range, efficiency and type - catalogue_mask = pd.Series(True, index=catalogue_data.index) - query_msg_list = [] - for value, column_name in ( - (id, "id"), - (manufacturer, "manufacturer"), - (range, "range"), - (efficiency, "efficiency"), - (type, "type"), - ): - if pd.isna(value): - continue - catalogue_mask &= cls._filter_catalogue_str( - value=value, catalogue_data=catalogue_data, column_name=column_name - ) - query_msg_list.append(f"{column_name}={value!r}") - - # Mask on nominal power, primary and secondary voltages - for value, column_name, display_unit in ((uhv, "uhv", "kV"), (ulv, "ulv", "kV"), (sn, "sn", "kVA")): - if pd.isna(value): - continue - catalogue_mask &= cls._filter_catalogue_float( - value=value, catalogue_data=catalogue_data, column_name=column_name - ) - query_msg_list.append(f"{column_name}={value/1000:.1f} {display_unit}") - - # Iterate over the transformers - selected_index = catalogue_mask[catalogue_mask].index - cycler = cycle(palette) - for idx in selected_index: - empty_table = False - table.add_row( - catalogue_data.at[idx, "id"], - catalogue_data.at[idx, "manufacturer"], - catalogue_data.at[idx, "range"], - catalogue_data.at[idx, "efficiency"], - catalogue_data.at[idx, "type"], - f"{catalogue_data.at[idx, 'sn']/1000:.1f}", # VA to kVA - f"{catalogue_data.at[idx, 'uhv']/1000:.1f}", # V to kV - f"{catalogue_data.at[idx, 'ulv']/1000:.1f}", # V to kV - style=next(cycler), - ) - - # Handle the case of an empty table - if empty_table: - query_msg_part = ", ".join(query_msg_list) - msg = f"No transformers can be found in the catalogue matching your query: {query_msg_part}." - console.print(msg) - else: - console.print(table) - - @staticmethod - def _filter_catalogue_str( - value: str | re.Pattern[str], catalogue_data: pd.DataFrame, column_name: str - ) -> pd.Series: - """Filter the catalogue using a string/regexp value. - - Args: - value: - The string or regular expression to use as a filter. - - catalogue_data: - The catalogue data to use. - - column_name: - The name of the column to use for the filter. Returns: - The mask of matching results. + The catalogue data as a dataframe. """ - if isinstance(value, re.Pattern): - return catalogue_data[column_name].str.match(value) - else: - try: - pattern = re.compile(pattern=value, flags=re.IGNORECASE) - return catalogue_data[column_name].str.match(pattern) - except re.error: - return catalogue_data[column_name].str.lower() == value.lower() - - @staticmethod - def _filter_catalogue_float(value: float, catalogue_data: pd.DataFrame, column_name: str) -> pd.Series: - """Filter the catalogue using a float/int value. - - Args: - value: - The float or integer to use as a filter. - - catalogue_data: - The catalogue data to use. - - column_name: - The name of the column to use for the filter. - - Returns: - The mask of matching results. - """ - if isinstance(value, int): - return catalogue_data[column_name] == value - else: - return np.isclose(catalogue_data[column_name], value) + catalogue_data, _ = cls._get_catalogue( + id=id, + manufacturer=manufacturer, + range=range, + efficiency=efficiency, + type=type, + sn=sn, + uhv=uhv, + ulv=ulv, + raise_if_not_found=False, + ) + catalogue_data["sn"] /= 1000 # kVA + catalogue_data["uhv"] /= 1000 # kV + catalogue_data["ulv"] /= 1000 # kV + return ( + catalogue_data.drop(columns=["i0", "p0", "psc", "vsc"]) + .rename( + columns={ + "id": "Id", + "manufacturer": "Manufacturer", + "range": "Product range", + "efficiency": "Efficiency", + "type": "Type", + "sn": "Nominal power (kVA)", + "uhv": "High voltage (kV)", + "ulv": "Low voltage (kV)", + # # If we ever want to display these columns + # "i0": "No-load current (%)", + # "p0": "No-load losses (W)", + # "psc": "Load Losses at 75°C (W)", + # "vsc": "Impedance voltage (%)", + } + ) + .set_index("Id") + ) # # Utils diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index 8bbbc22b..ca53f1e5 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -4,12 +4,11 @@ import json import logging import re -import textwrap import time import warnings -from collections.abc import Mapping, Sized +from collections.abc import Iterable, Mapping, Sized from importlib import resources -from itertools import chain, cycle +from itertools import chain from pathlib import Path from typing import TYPE_CHECKING, NoReturn, TypeVar @@ -17,7 +16,6 @@ import numpy as np import pandas as pd from pyproj import CRS -from rich.table import Table from typing_extensions import Self from roseau.load_flow._solvers import AbstractSolver @@ -37,7 +35,7 @@ VoltageSource, ) from roseau.load_flow.typing import Id, JsonDict, MapOrSeq, Solver, StrPath -from roseau.load_flow.utils import CatalogueMixin, JsonMixin, _optional_deps, console, palette +from roseau.load_flow.utils import CatalogueMixin, JsonMixin, _optional_deps from roseau.load_flow.utils.types import _DTYPES, VoltagePhaseDtype from roseau.load_flow_engine.cy_engine import CyElectricalNetwork @@ -1307,7 +1305,7 @@ def _propagate_potentials(self) -> None: elements.append((e, potentials)) @staticmethod - def _check_ref(elements: list[Element]) -> None: + def _check_ref(elements: Iterable[Element]) -> None: """Check the number of potential references to avoid having a singular jacobian matrix.""" visited_elements: set[Element] = set() for initial_element in elements: @@ -1465,6 +1463,68 @@ def catalogue_path(cls) -> Path: def catalogue_data(cls) -> JsonDict: return json.loads((cls.catalogue_path() / "Catalogue.json").read_text()) + @classmethod + def _get_catalogue( + cls, name: str | re.Pattern[str] | None, load_point_name: str | re.Pattern[str] | None, raise_if_not_found: bool + ) -> tuple[pd.DataFrame, str]: + # Get the catalogue data + catalogue_data = cls.catalogue_data() + + catalogue_dict = { + "name": [], + "nb_buses": [], + "nb_branches": [], + "nb_loads": [], + "nb_sources": [], + "nb_grounds": [], + "nb_potential_refs": [], + "load_points": [], + } + query_msg_list = [] + + # Match on the name + available_names = list(catalogue_data) + match_names_list = available_names + if name is not None: + match_names_list = cls._filter_catalogue_str(name, strings=available_names) + if isinstance(name, re.Pattern): + name = name.pattern + query_msg_list.append(f"{name=!r}") + if raise_if_not_found: + cls._assert_one_found(found_data=match_names_list, display_name="networks", query_info=f"{name=!r}") + + if load_point_name is not None: + load_point_name_str = load_point_name if isinstance(load_point_name, str) else load_point_name.pattern + query_msg_list.append(f"load_point_name={load_point_name_str!r}") + + for name in match_names_list: + network_data = catalogue_data[name] + + # Match on the load point + available_load_points: list[str] = network_data["load_points"] + match_load_point_names_list = available_load_points + if load_point_name is not None: + match_load_point_names_list = cls._filter_catalogue_str(load_point_name, strings=available_load_points) + if raise_if_not_found: + cls._assert_one_found( + found_data=match_load_point_names_list, + display_name=f"load points for network {name!r}", + query_info=query_msg_list[-1], + ) + elif not match_load_point_names_list: + continue + + catalogue_dict["name"].append(name) + catalogue_dict["nb_buses"].append(network_data["nb_buses"]) + catalogue_dict["nb_branches"].append(network_data["nb_branches"]) + catalogue_dict["nb_loads"].append(network_data["nb_loads"]) + catalogue_dict["nb_sources"].append(network_data["nb_sources"]) + catalogue_dict["nb_grounds"].append(network_data["nb_grounds"]) + catalogue_dict["nb_potential_refs"].append(network_data["nb_potential_refs"]) + catalogue_dict["load_points"].append(match_load_point_names_list) + + return pd.DataFrame(catalogue_dict), ", ".join(query_msg_list) + @classmethod def from_catalogue(cls, name: str | re.Pattern[str], load_point_name: str | re.Pattern[str]) -> Self: """Build a network from one in the catalogue. @@ -1481,188 +1541,62 @@ def from_catalogue(cls, name: str | re.Pattern[str], load_point_name: str | re.P The selected network """ # Get the catalogue data - catalogue_data = cls.catalogue_data() + catalogue_data, _ = cls._get_catalogue( + name=name, + load_point_name=load_point_name, + raise_if_not_found=True, + ) - # Match on the name - if isinstance(name, re.Pattern): - name_pattern = name - name = name.pattern - match_names_list = [k for k in catalogue_data if name_pattern.match(k)] - else: - try: - name_pattern = re.compile(pattern=name, flags=re.IGNORECASE) - match_names_list = [k for k in catalogue_data if name_pattern.match(k)] - except re.error: - name_pattern = name.lower() - match_names_list = [k for k in catalogue_data if k.lower() == name_pattern] - if not match_names_list: - msg = ( - f"No network matching the name {name!r} has been found. " - f"Please look at the catalogue using the `print_catalogue` class method." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND) - elif len(match_names_list) > 1: - msg_part = textwrap.shorten(", ".join(repr(x) for x in sorted(match_names_list)), width=500) - msg = f"Several networks matching the name {name!r} have been found: {msg_part}." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND) - name = match_names_list[0] - - # Match on the load point - c_data = catalogue_data[name] - available_load_points = c_data["load_points"] - if isinstance(load_point_name, re.Pattern): - load_point_name_pattern = load_point_name - load_point_name = load_point_name.pattern - match_load_point_names_list = [k for k in available_load_points if load_point_name_pattern.match(k)] - else: - try: - load_point_name_pattern = re.compile(pattern=load_point_name, flags=re.IGNORECASE) - match_load_point_names_list = [k for k in available_load_points if load_point_name_pattern.match(k)] - except re.error: - load_point_name_pattern = load_point_name.lower() - match_load_point_names_list = [k for k in available_load_points if k.lower() == load_point_name_pattern] - if not match_load_point_names_list: - msg_part = textwrap.shorten(", ".join(repr(x) for x in sorted(available_load_points)), width=500) - msg = ( - f"No load point matching the name {load_point_name!r} has been found for the network {name!r}. " - f"Available load points are {msg_part}." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND) - elif len(match_load_point_names_list) > 1: - msg_part = textwrap.shorten(", ".join(repr(x) for x in sorted(match_load_point_names_list)), width=500) - msg = ( - f"Several load points matching the name {load_point_name!r} have been found for the network " - f"{name!r}: {msg_part}." - ) - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND) - load_point_name = match_load_point_names_list[0] + name = catalogue_data["name"].item() + load_point_name = catalogue_data["load_points"].item()[0] # Get the data from the Json file path = cls.catalogue_path() / f"{name}_{load_point_name}.json" - if not path.exists(): # pragma: no cover + try: + json_dict = json.loads(path.read_text()) + except FileNotFoundError: msg = f"The file {path} has not been found while it should exist. Please post an issue on GitHub." logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_MISSING) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_MISSING) from None - return cls.from_json(path=path) + return cls.from_dict(json_dict) @classmethod - def print_catalogue( - cls, - name: str | re.Pattern[str] | None = None, - load_point_name: str | re.Pattern[str] | None = None, - ) -> None: - """Print the catalogue of available networks. + def get_catalogue( + cls, name: str | re.Pattern[str] | None = None, load_point_name: str | re.Pattern[str] | None = None + ) -> pd.DataFrame: + """Read a network dictionary from the catalogue. Args: name: - The name of the networks to display. It can be a regular expression. For instance, `name="lv"` will - match all the network name starting with "lv" (ignoring case). + The name of the network to get from the catalogue. It can be a regular expression. load_point_name: - Only networks having a load point matching this string or regular expression will be displayed. - """ - # Get the catalogue data - catalogue_data = cls.catalogue_data() - - # Start creating a table to display the results - table = Table(title="Available Networks") - table.add_column("Name", overflow="fold") - table.add_column("Nb buses", justify="right", overflow="fold") - table.add_column("Nb branches", justify="right", overflow="fold") - table.add_column("Nb loads", justify="right", overflow="fold") - table.add_column("Nb sources", justify="right", overflow="fold") - table.add_column("Nb grounds", justify="right", overflow="fold") - table.add_column("Nb potential refs", justify="right", overflow="fold") - table.add_column("Available load points", overflow="fold") - empty_table = True - - # Match on the name - match_names_list = cls._filter_name(name=name, catalogue_data=catalogue_data) - - # Match on load point name - if load_point_name is None: - load_point_name_pattern = None - - def match_load_point_function(x: str) -> bool: - return True - - elif isinstance(load_point_name, re.Pattern): - load_point_name_pattern = load_point_name - load_point_name = load_point_name.pattern - match_load_point_function = load_point_name_pattern.match - else: - try: - load_point_name_pattern = re.compile(pattern=load_point_name, flags=re.IGNORECASE) - match_load_point_function = load_point_name_pattern.match - except re.error: - load_point_name_pattern = name.lower() - - def match_load_point_function(x: str) -> bool: - nonlocal load_point_name_pattern - return x.lower() == load_point_name_pattern - - # Iterate over the networks - cycler = cycle(palette) - for c_name in match_names_list: - c_data = catalogue_data[c_name] - available_load_points = c_data["load_points"] - if any(match_load_point_function(x) for x in available_load_points): - empty_table = False - table.add_row( - c_name, - str(c_data["nb_buses"]), - str(c_data["nb_branches"]), - str(c_data["nb_loads"]), - str(c_data["nb_sources"]), - str(c_data["nb_grounds"]), - str(c_data["nb_potential_refs"]), - ", ".join(repr(x) for x in sorted(c_data["load_points"])), - style=next(cycler), - ) - - # Handle the case of an empty table - if empty_table: - msg = "No networks can be found in the catalogue" - if name is not None and load_point_name is not None: - msg += f" with the name {name!r} and having a load point named {load_point_name!r}" - elif name is not None: - msg += f" with the name {name!r}" - elif load_point_name is not None: - msg += f" having a load point named {load_point_name!r}" - msg += "!" - console.print(msg) - else: - console.print(table) - - @staticmethod - def _filter_name(name: str | re.Pattern[str] | None, catalogue_data: JsonDict) -> list[str]: - """Filter the catalogue using the network name. - - Args: - name: - The optional name to use as a filter. - - catalogue_data: - The catalogue of available networks. It avoids an additional read. + The name of the load point to get. For each network, several load points may be available. It can be + a regular expression. Returns: - The list of network names matching the provided one. + The dictionary containing the network data. """ - if name is None: - match_names_list = list(catalogue_data) - elif isinstance(name, re.Pattern): - match_names_list = [k for k in catalogue_data if name.match(k)] - else: - try: - name_pattern = re.compile(pattern=name, flags=re.IGNORECASE) - match_names_list = [k for k in catalogue_data if name_pattern.match(k)] - except re.error: - name_pattern = name.lower() - match_names_list = [k for k in catalogue_data if k.lower() == name_pattern] - - return match_names_list + + catalogue_data, _ = cls._get_catalogue( + name=name, + load_point_name=load_point_name, + raise_if_not_found=False, + ) + return ( + catalogue_data.reset_index(drop=True) + .rename( + columns={ + "name": "Name", + "nb_buses": "Nb buses", + "nb_branches": "Nb branches", + "nb_loads": "Nb loads", + "nb_sources": "Nb sources", + "nb_grounds": "Nb grounds", + "nb_potential_refs": "Nb potential refs", + "load_points": "Available load points", + } + ) + .set_index("Name") + ) diff --git a/roseau/load_flow/tests/test_electrical_network.py b/roseau/load_flow/tests/test_electrical_network.py index ec0b1e2b..29a21e92 100644 --- a/roseau/load_flow/tests/test_electrical_network.py +++ b/roseau/load_flow/tests/test_electrical_network.py @@ -27,7 +27,7 @@ ) from roseau.load_flow.network import ElectricalNetwork from roseau.load_flow.units import Q_ -from roseau.load_flow.utils import BranchTypeDtype, PhaseDtype, VoltagePhaseDtype, console +from roseau.load_flow.utils import BranchTypeDtype, PhaseDtype, VoltagePhaseDtype @pytest.fixture() @@ -217,8 +217,8 @@ def test_connect_and_disconnect(): assert load.bus is None with pytest.raises(RoseauLoadFlowException) as e: load.to_dict() - assert e.value.args[0] == "The load 'power load' is disconnected and cannot be used anymore." - assert e.value.args[1] == RoseauLoadFlowExceptionCode.DISCONNECTED_ELEMENT + assert e.value.msg == "The load 'power load' is disconnected and cannot be used anymore." + assert e.value.code == RoseauLoadFlowExceptionCode.DISCONNECTED_ELEMENT new_load = PowerLoad(id="power load", phases="abcn", bus=load_bus, powers=[100 + 0j, 100 + 0j, 100 + 0j]) assert new_load.network == en @@ -229,8 +229,8 @@ def test_connect_and_disconnect(): assert vs.bus is None with pytest.raises(RoseauLoadFlowException) as e: vs.to_dict() - assert e.value.args[0] == "The voltage source 'vs' is disconnected and cannot be used anymore." - assert e.value.args[1] == RoseauLoadFlowExceptionCode.DISCONNECTED_ELEMENT + assert e.value.msg == "The voltage source 'vs' is disconnected and cannot be used anymore." + assert e.value.code == RoseauLoadFlowExceptionCode.DISCONNECTED_ELEMENT # Bad key with pytest.raises(RoseauLoadFlowException) as e: @@ -275,7 +275,7 @@ def test_recursive_connect_disconnect(): new_load2 = PowerLoad(id="new_load2", bus=new_bus2, phases="abcn", powers=Q_([100, 0, 0], "VA")) new_bus = Bus(id="new_bus", phases="abcn") new_load = PowerLoad(id="new_load", bus=new_bus, phases="abcn", powers=Q_([100, 0, 0], "VA")) - lp = LineParameters("S_AL_240_without_shunt", z_line=Q_(0.1 * np.eye(4), "ohm/km"), y_shunt=None) + lp = LineParameters("U_AL_240_without_shunt", z_line=Q_(0.1 * np.eye(4), "ohm/km"), y_shunt=None) new_line2 = Line( id="new_line2", bus1=new_bus2, @@ -380,7 +380,7 @@ def test_recursive_connect_disconnect_ground(): assert new_load2.id not in en.loads lp = LineParameters( - "S_AL_240_with_shunt", z_line=Q_(0.1 * np.eye(4), "ohm/km"), y_shunt=Q_(0.1 * np.eye(4), "S/km") + "U_AL_240_with_shunt", z_line=Q_(0.1 * np.eye(4), "ohm/km"), y_shunt=Q_(0.1 * np.eye(4), "S/km") ) new_line2 = Line( id="new_line2", @@ -976,8 +976,8 @@ def test_network_elements(small_network: ElectricalNetwork): # Connect the two networks with pytest.raises(RoseauLoadFlowException) as e: Switch("switch2", bus1=bus2, bus2=bus_vs) - assert e.value.args[0] == "The Bus 'bus_vs' is already assigned to another network." - assert e.value.args[1] == RoseauLoadFlowExceptionCode.SEVERAL_NETWORKS + assert e.value.msg == "The Bus 'bus_vs' is already assigned to another network." + assert e.value.code == RoseauLoadFlowExceptionCode.SEVERAL_NETWORKS # Every object have their good network after this failure for element in it.chain( @@ -1017,34 +1017,34 @@ def test_network_results_warning(small_network: ElectricalNetwork, recwarn): # for bus in small_network.buses.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = bus.res_potentials - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN with pytest.raises(RoseauLoadFlowException) as e: _ = bus.res_voltages - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN for branch in small_network.branches.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = branch.res_currents - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN for load in small_network.loads.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = load.res_currents - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN if load.is_flexible and isinstance(load, PowerLoad): with pytest.raises(RoseauLoadFlowException) as e: _ = load.res_flexible_powers - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN for source in small_network.sources.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = source.res_currents - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN for ground in small_network.grounds.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = ground.res_potential - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN for p_ref in small_network.potential_refs.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = p_ref.res_current - assert e.value.args[1] == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN + assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN # Solve a load flow small_network.solve_load_flow() @@ -1753,89 +1753,78 @@ def test_from_catalogue(): # Unknown network name with pytest.raises(RoseauLoadFlowException) as e: ElectricalNetwork.from_catalogue(name="unknown", load_point_name="winter") - assert ( - e.value.args[0] - == "No network matching the name 'unknown' has been found. Please look at the catalogue using the " - "`print_catalogue` class method." + assert e.value.msg == ( + "No networks matching the query (name='unknown') have been found. Please look at the " + "catalogue using the `get_catalogue` class method." ) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND # Unknown load point name with pytest.raises(RoseauLoadFlowException) as e: ElectricalNetwork.from_catalogue(name="MVFeeder004", load_point_name="unknown") - assert ( - e.value.args[0] - == "No load point matching the name 'unknown' has been found for the network 'MVFeeder004'. Available " - "load points are 'Summer', 'Winter'." + assert e.value.msg == ( + "No load points for network 'MVFeeder004' matching the query (load_point_name='unknown') have " + "been found. Please look at the catalogue using the `get_catalogue` class method." ) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND # Several network name matched with pytest.raises(RoseauLoadFlowException) as e: ElectricalNetwork.from_catalogue(name="MVFeeder", load_point_name="winter") - assert e.value.args[0] == ( - "Several networks matching the name 'MVFeeder' have been found: 'MVFeeder004', " - "'MVFeeder011', 'MVFeeder015', 'MVFeeder032', 'MVFeeder041', 'MVFeeder063', 'MVFeeder078', 'MVFeeder115', " - "'MVFeeder128', 'MVFeeder151', 'MVFeeder159', 'MVFeeder176', 'MVFeeder210', 'MVFeeder217', 'MVFeeder232'," - " 'MVFeeder251', 'MVFeeder290', 'MVFeeder312', 'MVFeeder320', 'MVFeeder339'." + assert e.value.msg == ( + "Several networks matching the query (name='MVFeeder') have been found: 'MVFeeder004', " + "'MVFeeder011', 'MVFeeder015', 'MVFeeder032', 'MVFeeder041', 'MVFeeder063', 'MVFeeder078', " + "'MVFeeder115', 'MVFeeder128', 'MVFeeder151', 'MVFeeder159', 'MVFeeder176', 'MVFeeder210', " + "'MVFeeder217', 'MVFeeder232', 'MVFeeder251', 'MVFeeder290', 'MVFeeder312', 'MVFeeder320', " + "'MVFeeder339'." ) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND # Several load point name matched with pytest.raises(RoseauLoadFlowException) as e: ElectricalNetwork.from_catalogue(name="MVFeeder004", load_point_name=r".*") - assert e.value.args[0] == ( - "Several load points matching the name '.*' have been found for the network 'MVFeeder004': 'Summer', 'Winter'." + assert e.value.msg == ( + "Several load points for network 'MVFeeder004' matching the query (load_point_name='.*') have " + "been found: 'Summer', 'Winter'." ) - assert e.value.args[1] == RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND + assert e.value.code == RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND # Both known ElectricalNetwork.from_catalogue(name="MVFeeder004", load_point_name="winter") -def test_print_catalogue(): - # Print the entire catalogue - with console.capture() as capture: - ElectricalNetwork.print_catalogue() - assert len(capture.get().split("\n")) == 46 +def test_get_catalogue(): + # Get the entire catalogue + catalogue = ElectricalNetwork.get_catalogue() + assert catalogue.shape == (40, 7) # Filter on the network name - with console.capture() as capture: - ElectricalNetwork.print_catalogue(name="MV") - assert len(capture.get().split("\n")) == 26 - with console.capture() as capture: - ElectricalNetwork.print_catalogue(name=re.compile(r"^MV")) - assert len(capture.get().split("\n")) == 26 + catalogue = ElectricalNetwork.get_catalogue(name="MV") + assert catalogue.shape == (20, 7) + catalogue = ElectricalNetwork.get_catalogue(name=re.compile(r"^MV")) + assert catalogue.shape == (20, 7) # Filter on the load point name - with console.capture() as capture: - ElectricalNetwork.print_catalogue(load_point_name="winter") - assert len(capture.get().split("\n")) == 46 - with console.capture() as capture: - ElectricalNetwork.print_catalogue(load_point_name=re.compile(r"^Winter")) - assert len(capture.get().split("\n")) == 46 + catalogue = ElectricalNetwork.get_catalogue(load_point_name="winter") + assert catalogue.shape == (40, 7) + catalogue = ElectricalNetwork.get_catalogue(load_point_name=re.compile(r"^Winter")) + assert catalogue.shape == (40, 7) # Filter on both - with console.capture() as capture: - ElectricalNetwork.print_catalogue(name="MV", load_point_name="winter") - assert len(capture.get().split("\n")) == 26 - with console.capture() as capture: - ElectricalNetwork.print_catalogue(name="MV", load_point_name=re.compile(r"^Winter")) - assert len(capture.get().split("\n")) == 26 - with console.capture() as capture: - ElectricalNetwork.print_catalogue(name=re.compile(r"^MV"), load_point_name="winter") - assert len(capture.get().split("\n")) == 26 - with console.capture() as capture: - ElectricalNetwork.print_catalogue(name=re.compile(r"^MV"), load_point_name=re.compile(r"^Winter")) - assert len(capture.get().split("\n")) == 26 + catalogue = ElectricalNetwork.get_catalogue(name="MV", load_point_name="winter") + assert catalogue.shape == (20, 7) + catalogue = ElectricalNetwork.get_catalogue(name="MV", load_point_name=re.compile(r"^Winter")) + assert catalogue.shape == (20, 7) + catalogue = ElectricalNetwork.get_catalogue(name=re.compile(r"^MV"), load_point_name="winter") + assert catalogue.shape == (20, 7) + catalogue = ElectricalNetwork.get_catalogue(name=re.compile(r"^MV"), load_point_name=re.compile(r"^Winter")) + assert catalogue.shape == (20, 7) # Regexp error - with console.capture() as capture: - ElectricalNetwork.print_catalogue(name=r"^MV[0-") - assert len(capture.get().split("\n")) == 2 - with console.capture() as capture: - ElectricalNetwork.print_catalogue(load_point_name=r"^winter[0-]") - assert len(capture.get().split("\n")) == 2 + catalogue = ElectricalNetwork.get_catalogue(name=r"^MV[0-") + assert catalogue.empty + catalogue = ElectricalNetwork.get_catalogue(load_point_name=r"^winter[0-]") + assert catalogue.empty def test_to_graph(small_network: ElectricalNetwork): diff --git a/roseau/load_flow/tests/test_exceptions.py b/roseau/load_flow/tests/test_exceptions.py index 4b54635b..fcc2e3db 100644 --- a/roseau/load_flow/tests/test_exceptions.py +++ b/roseau/load_flow/tests/test_exceptions.py @@ -3,17 +3,13 @@ def test_exceptions(): for x in RoseauLoadFlowExceptionCode: - # String starts with the package name - assert str(x).startswith("roseau.load_flow.") - - # String equality - assert str(x) == x - - # No equality without the prefix - assert str(x).removeprefix("roseau.load_flow.") != x - # Case-insensitive assert str(x).upper() == x + assert str(x).lower() == x + # Case-insensitive constructor (with or without spaces or dashes) + assert RoseauLoadFlowExceptionCode("BaD_bus_ID") == RoseauLoadFlowExceptionCode.BAD_BUS_ID + assert RoseauLoadFlowExceptionCode("bad bus id") == RoseauLoadFlowExceptionCode.BAD_BUS_ID + assert RoseauLoadFlowExceptionCode("BAD-BUS-ID") == RoseauLoadFlowExceptionCode.BAD_BUS_ID r = RoseauLoadFlowException(msg="toto", code=RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_WINDINGS) assert r.msg == "toto" diff --git a/roseau/load_flow/utils/__init__.py b/roseau/load_flow/utils/__init__.py index e774d8d0..898b322c 100644 --- a/roseau/load_flow/utils/__init__.py +++ b/roseau/load_flow/utils/__init__.py @@ -1,8 +1,7 @@ """ This module contains utility classes and functions for Roseau Load Flow. """ -from roseau.load_flow.utils.console import console, palette -from roseau.load_flow.utils.constants import CX, DELTA_P, EPSILON_0, EPSILON_R, MU_0, MU_R, OMEGA, PI, RHO, TAN_D, F +from roseau.load_flow.utils.constants import DELTA_P, EPSILON_0, EPSILON_R, MU_0, MU_R, OMEGA, PI, RHO, TAN_D, F from roseau.load_flow.utils.mixins import CatalogueMixin, Identifiable, JsonMixin from roseau.load_flow.utils.types import ( BranchTypeDtype, @@ -15,7 +14,6 @@ __all__ = [ # Constants - "CX", "DELTA_P", "EPSILON_0", "EPSILON_R", @@ -38,7 +36,4 @@ "PhaseDtype", "VoltagePhaseDtype", "BranchTypeDtype", - # Console - "console", - "palette", ] diff --git a/roseau/load_flow/utils/console.py b/roseau/load_flow/utils/console.py deleted file mode 100644 index c16e9c7f..00000000 --- a/roseau/load_flow/utils/console.py +++ /dev/null @@ -1,25 +0,0 @@ -from rich.console import Console - -console = Console() - -palette = [ - "#4c72b0", - "#dd8452", - "#55a868", - "#c44e52", - "#8172b3", - "#937860", - "#da8bc3", - "#8c8c8c", - "#ccb974", - "#64b5cd", -] -"""Color palette for the catalogue tables. - -This is seaborn's default color palette. Generated with: -```python -import seaborn as sns -sns.set_theme() -list(sns.color_palette().as_hex()) -``` -""" diff --git a/roseau/load_flow/utils/constants.py b/roseau/load_flow/utils/constants.py index c1d7b586..dc59b3c6 100644 --- a/roseau/load_flow/utils/constants.py +++ b/roseau/load_flow/utils/constants.py @@ -1,71 +1,77 @@ import numpy as np from roseau.load_flow.units import Q_ -from roseau.load_flow.utils.types import ConductorType, InsulatorType, LineType +from roseau.load_flow.utils.types import ConductorType, InsulatorType PI = np.pi -"""The famous constant :math:`\\pi`.""" +"""The famous mathematical constant :math:`\\pi = 3.141592\\ldots`.""" MU_0 = Q_(1.25663706212e-6, "H/m") -"""Magnetic permeability of the vacuum (H/m).""" +"""Magnetic permeability of the vacuum :math:`\\mu_0 = 4 \\pi \\times 10^{-7}` (H/m).""" EPSILON_0 = Q_(8.8541878128e-12, "F/m") -"""Permittivity of the vacuum (F/m).""" +"""Vacuum permittivity :math:`\\varepsilon_0 = 8.8541878128 \\times 10^{-12}` (F/m).""" F = Q_(50.0, "Hz") -"""Network frequency :math:`=50` (Hz).""" +"""Network frequency :math:`f = 50` (Hz).""" OMEGA = Q_(2 * PI * F, "rad/s") -"""Pulsation :math:`\\omega = 2 \\pi f` (rad/s).""" +"""Angular frequency :math:`\\omega = 2 \\pi f` (rad/s).""" RHO = { - ConductorType.CU: Q_(1.72e-8, "ohm*m"), - ConductorType.AL: Q_(2.82e-8, "ohm*m"), - ConductorType.AM: Q_(3.26e-8, "ohm*m"), - ConductorType.AA: Q_(4.0587e-8, "ohm*m"), + ConductorType.CU: Q_(1.7241e-8, "ohm*m"), # IEC 60287-1-1 Table 1 + ConductorType.AL: Q_(2.8264e-8, "ohm*m"), # IEC 60287-1-1 Table 1 + ConductorType.AM: Q_(3.26e-8, "ohm*m"), # verified + ConductorType.AA: Q_(4.0587e-8, "ohm*m"), # verified (approx. AS 3607 ACSR/GZ) ConductorType.LA: Q_(3.26e-8, "ohm*m"), } -"""Resistivity of common conductor materials (ohm.m).""" - -CX = { - LineType.OVERHEAD: Q_(0.35, "ohm/km"), - LineType.UNDERGROUND: Q_(0.1, "ohm/km"), - LineType.TWISTED: Q_(0.1, "ohm/km"), -} -"""Reactance parameter for a typical line in France (Ohm/km).""" +"""Resistivity of common conductor materials (Ohm.m).""" MU_R = { - ConductorType.CU: Q_(1.2566e-8, "H/m"), - ConductorType.AL: Q_(1.2566e-8, "H/m"), - ConductorType.AM: Q_(1.2566e-8, "H/m"), - ConductorType.AA: Q_(np.nan, "H/m"), # TODO - ConductorType.LA: Q_(np.nan, "H/m"), # TODO + ConductorType.CU: Q_(0.9999935849131266), + ConductorType.AL: Q_(1.0000222328028834), + ConductorType.AM: Q_(0.9999705074463784), + ConductorType.AA: Q_(1.0000222328028834), # ==AL + ConductorType.LA: Q_(0.9999705074463784), # ==AM } -"""Magnetic permeability of common conductor materials (H/m).""" +"""Relative magnetic permeability of common conductor materials.""" DELTA_P = { - ConductorType.CU: Q_(9.3, "mm"), - ConductorType.AL: Q_(112, "mm"), - ConductorType.AM: Q_(12.9, "mm"), - ConductorType.AA: Q_(np.nan, "mm"), # TODO - ConductorType.LA: Q_(np.nan, "mm"), # TODO + ConductorType.CU: Q_(9.33, "mm"), + ConductorType.AL: Q_(11.95, "mm"), + ConductorType.AM: Q_(12.85, "mm"), + ConductorType.AA: Q_(14.34, "mm"), + ConductorType.LA: Q_(12.85, "mm"), } -"""Skin effect of common conductor materials (mm).""" +"""Skin depth of common conductor materials :math:`\\sqrt{\\dfrac{\\rho}{\\pi f \\mu_r \\mu_0}}` (mm).""" +# Skin depth is the depth at which the current density is reduced to 1/e (~37%) of the surface value. +# Generated with: +# --------------- +# def delta_p(rho, mu_r): +# return np.sqrt(rho / (PI * F * mu_r * MU_0)) +# for material in ConductorType: +# print(material, delta_p(RHO[material], MU_R[material]).m_as("mm")) TAN_D = { - InsulatorType.PVC: Q_(600e-4), - InsulatorType.HDPE: Q_(6e-4), - InsulatorType.LDPE: Q_(6e-4), - InsulatorType.PEX: Q_(30e-4), - InsulatorType.EPR: Q_(125e-4), + InsulatorType.PVC: Q_(1000e-4), + InsulatorType.HDPE: Q_(10e-4), + InsulatorType.MDPE: Q_(10e-4), + InsulatorType.LDPE: Q_(10e-4), + InsulatorType.XLPE: Q_(40e-4), + InsulatorType.EPR: Q_(200e-4), + InsulatorType.IP: Q_(100e-4), } -"""Loss angles of common insulator materials.""" +"""Loss angles of common insulator materials according to the IEC 60287 standard.""" +# IEC 60287-1-1 Table 3. We only include the MV values. EPSILON_R = { - InsulatorType.PVC: Q_(6.5), + InsulatorType.PVC: Q_(8), InsulatorType.HDPE: Q_(2.3), - InsulatorType.LDPE: Q_(2.2), - InsulatorType.PEX: Q_(2.5), - InsulatorType.EPR: Q_(3.1), + InsulatorType.MDPE: Q_(2.3), + InsulatorType.LDPE: Q_(2.3), + InsulatorType.XLPE: Q_(2.5), + InsulatorType.EPR: Q_(3), + InsulatorType.IP: Q_(4), } -"""Relative permittivity of common insulator materials.""" +"""Relative permittivity of common insulator materials according to the IEC 60287 standard.""" +# IEC 60287-1-1 Table 3. We only include the MV values. diff --git a/roseau/load_flow/utils/log.py b/roseau/load_flow/utils/log.py index d4563ac8..624207c2 100644 --- a/roseau/load_flow/utils/log.py +++ b/roseau/load_flow/utils/log.py @@ -1,34 +1,7 @@ from typing import Literal -from rich.console import Console - from roseau.load_flow_engine.cy_engine import cy_set_logging_config -# Rich console -console = Console() - -palette = [ - "#4c72b0", - "#dd8452", - "#55a868", - "#c44e52", - "#8172b3", - "#937860", - "#da8bc3", - "#8c8c8c", - "#ccb974", - "#64b5cd", -] -"""Color palette for the catalogue tables. - -This is seaborn's default color palette. Generated with: -```python -import seaborn as sns -sns.set_theme() -list(sns.color_palette().as_hex()) -``` -""" - def set_logging_config(verbosity: Literal["trace", "debug", "info", "warning", "error", "critical"]) -> None: """Configure the logging level of the solver. diff --git a/roseau/load_flow/utils/mixins.py b/roseau/load_flow/utils/mixins.py index af01846c..99afd138 100644 --- a/roseau/load_flow/utils/mixins.py +++ b/roseau/load_flow/utils/mixins.py @@ -1,10 +1,13 @@ import json import logging import re +import textwrap from abc import ABCMeta, abstractmethod +from collections.abc import Sequence from pathlib import Path -from typing import Generic, TypeVar +from typing import Generic, NoReturn, TypeVar, overload +import pandas as pd from typing_extensions import Self from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode @@ -173,12 +176,101 @@ def from_catalogue(cls, **kwargs) -> Self: """ raise NotImplementedError - @classmethod - @abstractmethod - def print_catalogue(cls, **kwargs) -> None: - """Print the catalogue. + @overload + @staticmethod + def _filter_catalogue_str(value: str | re.Pattern[str], strings: pd.Series) -> "pd.Series[bool]": + ... - Keyword Args: - Arguments that can be used to filter the printed part of the catalogue. + @overload + @staticmethod + def _filter_catalogue_str(value: str | re.Pattern[str], strings: list[str]) -> list[str]: + ... + + @staticmethod + def _filter_catalogue_str( + value: str | re.Pattern[str], strings: list[str] | pd.Series + ) -> "pd.Series[bool] | list[str]": + """Filter the catalogue using a string/regexp value. + + Args: + value: + The string or regular expression to use as a filter. + + strings: + The catalogue data to filter. Either a :class:`pandas.Series` or a list of strings. + + Returns: + The mask of matching results if `strings` is a :class:`pandas.Series`, otherwise + the list of matching results. """ - raise NotImplementedError + vector = pd.Series(strings) + if isinstance(value, re.Pattern): + result = vector.str.match(value) + else: + try: + pattern = re.compile(pattern=value, flags=re.IGNORECASE) + result = vector.str.match(pattern) + except re.error: + # fallback to string comparison + result = vector.str.lower() == value.lower() + if isinstance(strings, pd.Series): + return result + else: + return vector[result].tolist() + + @staticmethod + def _raise_not_found_in_catalogue( + value: object, name: str, name_plural: str, strings: pd.Series, query_msg_list: list[str] + ) -> NoReturn: + """Raise an exception when no element has been found in the catalogue. + + Args: + value: + The value that has been searched in the catalogue. + + name: + The name of the element to display in the error message. + + name_plural: + The plural form of the name of the element to display in the error message. + + strings: + The catalogue data to filter. + + query_msg_list: + The query information to display in the error message. + """ + available_values = textwrap.shorten(", ".join(map(repr, strings.unique().tolist())), width=500) + msg = f"No {name} matching {value} has been found" + if query_msg_list: + msg += f" for the query {', '.join(query_msg_list)}" + msg += f". Available {name_plural} are {available_values}." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND) + + @staticmethod + def _assert_one_found(found_data: Sequence[object], display_name: str, query_info: str) -> None: + """Assert that only one element has been found in the catalogue. + + Args: + found_data: + The data found in the catalogue. If multiple elements have been found, they are + displayed in the error message. + + display_name: + The name of the element to display in the error message. + + query_info: + The query information to display in the error message. + """ + if len(found_data) == 1: + return + msg_middle = f"{display_name} matching the query ({query_info}) have been found" + if len(found_data) == 0: + msg = f"No {msg_middle}. Please look at the catalogue using the `get_catalogue` class method." + code = RoseauLoadFlowExceptionCode.CATALOGUE_NOT_FOUND + else: + msg = f"Several {msg_middle}: {textwrap.shorten(', '.join(map(repr, found_data)), width=500)}." + code = RoseauLoadFlowExceptionCode.CATALOGUE_SEVERAL_FOUND + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=code) diff --git a/roseau/load_flow/utils/tests/test_types.py b/roseau/load_flow/utils/tests/test_types.py index 5e0b7f31..e38591a3 100644 --- a/roseau/load_flow/utils/tests/test_types.py +++ b/roseau/load_flow/utils/tests/test_types.py @@ -10,39 +10,40 @@ @pytest.mark.parametrize(scope="module", argnames="t", argvalues=TYPES, ids=TYPES_IDS) def test_types_basic(t): for x in t: - assert t.from_string(str(x)) == x + assert t(str(x)) == x assert "." not in str(x) def test_line_type(): with pytest.raises(RoseauLoadFlowException) as e: - LineType.from_string("") - assert "cannot be converted into a LineType" in e.value.args[0] - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_LINE_TYPE + LineType("") + assert "cannot be converted into a LineType" in e.value.msg + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_LINE_TYPE with pytest.raises(RoseauLoadFlowException) as e: - LineType.from_string("nan") - assert "cannot be converted into a LineType" in e.value.args[0] - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_LINE_TYPE + LineType("nan") + assert "cannot be converted into a LineType" in e.value.msg + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_LINE_TYPE - assert LineType.from_string("Aérien") == LineType.OVERHEAD - assert LineType.from_string("Aerien") == LineType.OVERHEAD - assert LineType.from_string("galerie") == LineType.OVERHEAD - assert LineType.from_string("Souterrain") == LineType.UNDERGROUND - assert LineType.from_string("torsadé") == LineType.TWISTED - assert LineType.from_string("Torsade") == LineType.TWISTED + assert LineType("oVeRhEaD") == LineType.OVERHEAD + assert LineType("o") == LineType.OVERHEAD + assert LineType("uNdErGrOuNd") == LineType.UNDERGROUND + assert LineType("u") == LineType.UNDERGROUND + assert LineType("tWiStEd") == LineType.TWISTED + assert LineType("T") == LineType.TWISTED def test_insulator_type(): - assert InsulatorType.from_string("") == InsulatorType.UNKNOWN - assert InsulatorType.from_string("nan") == InsulatorType.UNKNOWN + assert InsulatorType("") == InsulatorType.UNKNOWN + assert InsulatorType("nan") == InsulatorType.UNKNOWN + assert InsulatorType("pex") == InsulatorType.XLPE def test_conductor_type(): with pytest.raises(RoseauLoadFlowException) as e: - ConductorType.from_string("") - assert "cannot be converted into a ConductorType" in e.value.args[0] - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_CONDUCTOR_TYPE + ConductorType("") + assert "cannot be converted into a ConductorType" in e.value.msg + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_CONDUCTOR_TYPE with pytest.raises(RoseauLoadFlowException) as e: - ConductorType.from_string("nan") - assert "cannot be converted into a ConductorType" in e.value.args[0] - assert e.value.args[1] == RoseauLoadFlowExceptionCode.BAD_CONDUCTOR_TYPE + ConductorType("nan") + assert "cannot be converted into a ConductorType" in e.value.msg + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_CONDUCTOR_TYPE diff --git a/roseau/load_flow/utils/types.py b/roseau/load_flow/utils/types.py index e792f510..e1c63440 100644 --- a/roseau/load_flow/utils/types.py +++ b/roseau/load_flow/utils/types.py @@ -1,9 +1,9 @@ import logging -from enum import Enum, auto, unique +from enum import auto import pandas as pd -from typing_extensions import Self +from roseau.load_flow._compat import StrEnum from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode # The local logger @@ -52,198 +52,129 @@ } -@unique -class LineType(Enum): +class LineType(StrEnum): """The type of a line.""" OVERHEAD = auto() - """The line is an overhead line.""" + """An overhead line that can be vertically or horizontally configured -- Fr = Aérien.""" UNDERGROUND = auto() - """The line is an underground line.""" + """An underground or a submarine cable -- Fr = Souterrain/Sous-Marin.""" TWISTED = auto() - """The line is a twisted line.""" + """A twisted line commonly known as Aerial Cable or Aerial Bundled Conductor (ABC) -- Fr = Torsadé.""" - def __str__(self) -> str: - """Print a `LineType` - - Returns: - A printable string of the line type. - """ - return self.name.lower() + # aliases + O = OVERHEAD # noqa: E741 + U = UNDERGROUND + T = TWISTED @classmethod - def from_string(cls, string: str) -> Self: - """Convert a string into a LineType - - Args: - string: - The string to convert - - Returns: - The corresponding LineType. - """ - string = string.lower() - if string in ("overhead", "aérien", "aerien", "galerie", "a", "o"): - return cls.OVERHEAD - elif string in ("underground", "souterrain", "sous-marin", "s", "u"): - return cls.UNDERGROUND - elif string in ("twisted", "torsadé", "torsade", "t"): - return cls.TWISTED - else: - msg = f"The string {string!r} cannot be converted into a LineType." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_TYPE) - - # - # WordingCodeMixin - # - def code(self) -> str: - """The code method is modified to retrieve a code that can be used in line type names. - - Returns: - The code of the enumerated value. - """ - if self == LineType.OVERHEAD: - return "O" - elif self == LineType.UNDERGROUND: - return "U" - elif self == LineType.TWISTED: - return "T" - else: # pragma: no cover - msg = f"There is code missing here. I do not know the LineType {self!r}." - logger.error(msg) - raise NotImplementedError(msg) + def _missing_(cls, value: object) -> "LineType | None": + if isinstance(value, str): + try: + return cls[value.upper()] + except KeyError: + pass + msg = f"{value!r} cannot be converted into a LineType." + logger.error(msg) + raise RoseauLoadFlowException(msg, RoseauLoadFlowExceptionCode.BAD_LINE_TYPE) - -# Add the list of codes for each line type -LineType.CODES = {LineType.OVERHEAD: {"A", "O"}, LineType.UNDERGROUND: {"U", "S"}, LineType.TWISTED: {"T"}} + def code(self) -> str: + """A code that can be used in line type names.""" + return self.name[0] -@unique -class ConductorType(Enum): - """The type of conductor.""" +class ConductorType(StrEnum): + """The type of the material of the conductor.""" - AL = auto() - """The conductor is in Aluminium.""" CU = auto() - """The conductor is in Copper.""" + """Copper -- Fr = Cuivre.""" + AL = auto() + """All Aluminum Conductor (AAC) -- Fr = Aluminium.""" AM = auto() - """The conductor is in Almélec.""" + """All Aluminum Alloy Conductor (AAAC) -- Fr = Almélec.""" AA = auto() - """The conductor is in Alu-Acier.""" + """Aluminum Conductor Steel Reinforced (ACSR) -- Fr = Alu-Acier.""" LA = auto() - """The conductor is in Almélec-Acier.""" - - def __str__(self) -> str: - """Print a `ConductorType` - - Returns: - A printable string of the conductor type. - """ - if self == ConductorType.AL: - return "Al" - elif self == ConductorType.CU: - return "Cu" - elif self == ConductorType.AM: - return "AM" - elif self == ConductorType.AA: - return "AA" - elif self == ConductorType.LA: - return "LA" - else: - s = super().__str__() - msg = f"The ConductorType {s} is not known..." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_CONDUCTOR_TYPE) + """Aluminum Alloy Conductor Steel Reinforced (AACSR) -- Fr = Almélec-Acier.""" + + # Aliases + AAC = AL # 1350-H19 (Standard Round of Compact Round) + """All Aluminum Conductor (AAC) -- Fr = Aluminium.""" + # AAC/TW # 1380-H19 (Trapezoidal Wire) + + AAAC = AM + """All Aluminum Alloy Conductor (AAAC) -- Fr = Almélec.""" + # Aluminum alloy 6201-T81. + # Concentric-lay-stranded + # conforms to ASTM Specification B-399 + # Applications: Overhead + + ACSR = AA + """Aluminum Conductor Steel Reinforced (ACSR) -- Fr = Alu-Acier.""" + # Aluminum alloy 1350-H-19 + # Applications: Bare overhead transmission cable and primary and secondary distribution cable + + AACSR = LA + """Aluminum Alloy Conductor Steel Reinforced (AACSR) -- Fr = Almélec-Acier.""" @classmethod - def from_string(cls, string: str) -> Self: - """Convert a string into a ConductorType - - Args: - string: - The string to convert - - Returns: - The corresponding ConductorType. - """ - string = string.lower() - if string == "al": - return cls.AL - elif string == "cu": - return cls.CU - elif string == "am": - return cls.AM - elif string == "aa": - return cls.AA - elif string == "la": - return cls.LA - else: - msg = f"The string {string!r} cannot be converted into a ConductorType." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_CONDUCTOR_TYPE) - - # - # WordingCodeMixin - # - def code(self) -> str: - """The code method is modified to retrieve a code that can be used in line type names. + def _missing_(cls, value: object) -> "ConductorType | None": + if isinstance(value, str): + try: + return cls[value.upper()] + except KeyError: + pass + msg = f"{value!r} cannot be converted into a ConductorType." + logger.error(msg) + raise RoseauLoadFlowException(msg, RoseauLoadFlowExceptionCode.BAD_CONDUCTOR_TYPE) - Returns: - The code of the enumerated value. - """ - return self.name.upper() + def code(self) -> str: + """A code that can be used in conductor type names.""" + return self.name -@unique -class InsulatorType(Enum): +class InsulatorType(StrEnum): """The type of the insulator for a wire.""" UNKNOWN = auto() - """The insulator of the conductor is made with unknown material.""" + """The material of the insulator is unknown.""" + + # General insulators (IEC 60287) HDPE = auto() - """The insulator of the conductor is made with High-Density PolyEthylene.""" + """High-Density PolyEthylene (HDPE) insulation.""" + MDPE = auto() + """Medium-Density PolyEthylene (MDPE) insulation.""" LDPE = auto() - """The insulator of the conductor is made with Low-Density PolyEthylene.""" - PEX = auto() - """The insulator of the conductor is made with Cross-linked polyethylene.""" + """Low-Density PolyEthylene (LDPE) insulation.""" + XLPE = auto() + """Cross-linked polyethylene (XLPE) insulation.""" EPR = auto() - """The insulator of the conductor is made with Ethylene-Propylene Rubber.""" + """Ethylene-Propylene Rubber (EPR) insulation.""" PVC = auto() - """The insulator of the conductor is made with PolyVinyl Chloride.""" - - def __str__(self) -> str: - """Print a `InsulatorType` + """PolyVinyl Chloride (PVC) insulation.""" + IP = auto() + """Impregnated Paper (IP) insulation.""" - Returns: - A printable string of the insulator type. - """ - return self.name.upper() + # Aliases + PEX = XLPE + """Alias -- Cross-linked polyethylene (XLPE) insulation.""" + PE = MDPE + """Alias -- Medium-Density PolyEthylene (MDPE) insulation.""" @classmethod - def from_string(cls, string: str) -> Self: - """Convert a string into a InsulatorType - - Args: - string: - The string to convert - - Returns: - The corresponding InsulatorType. - """ - if string.lower() in ("", "unknown", "nan"): - return cls.UNKNOWN - elif string == "HDPE": - return cls.HDPE - elif string == "LDPE": - return cls.LDPE - elif string == "PEX": - return cls.PEX - elif string == "EPR": - return cls.EPR - elif string == "PVC": - return cls.PVC - else: - msg = f"The string {string!r} cannot be converted into a InsulatorType." - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_INSULATOR_TYPE) + def _missing_(cls, value: object) -> "InsulatorType | None": + if isinstance(value, str): + string = value.upper() + if string in {"", "NAN"}: + return cls.UNKNOWN + try: + return cls[string] + except KeyError: + pass + msg = f"{value!r} cannot be converted into a InsulatorType." + logger.error(msg) + raise RoseauLoadFlowException(msg, RoseauLoadFlowExceptionCode.BAD_INSULATOR_TYPE) + + def code(self) -> str: + """A code that can be used in insulator type names.""" + return self.name From e4b50731d186621d6a31cb0e97affd81df09857b Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Tue, 23 Jan 2024 18:06:21 +0100 Subject: [PATCH 44/51] Add missing changelog entries (#170) --- doc/Changelog.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/doc/Changelog.md b/doc/Changelog.md index a43db500..9522c9c8 100644 --- a/doc/Changelog.md +++ b/doc/Changelog.md @@ -3,8 +3,42 @@ ## Unreleased - {gh-pr}`168` {gh-issue}`166` Fix initial potentials propagation. +- {gh-pr}`167` {gh-issue}`161` Add a catalogue of lines using the IEC standards. You can use the method + `LineParameters.get_catalogue()` to get a data frame of the available lines and the method + `LineParameters.from_catalogue()` to create a line from the catalogue. Several line types, conductor + material, and insulation types have been updated. Physical constants have been updated to match the + IEC standards where applicable. +- {gh-pr}`167` The class `LineParameters` now takes optional arguments `line_type`, `conductor_type`, + `insulator_type` and `section`. These parameters are accessible as properties. They are filled + automatically when creating a line from the catalogue or from a geometry. +- {gh-pr}`167` Replace all `print_catalogue()` methods by `get_catalogue()` methods that return a + data frame instead of printing the catalogue to the console. +- {gh-pr}`167` Enumeration classes no longer have a `from_string` method, you can call the enumeration + class directly with the string value to get the corresponding enumeration member. Case insensitive + behavior is preserved. +- {gh-pr}`167` {gh-issue}`122` Add checks on line height and diameter in the `LineParameters.from_geometry()` + alternative constructor. This method will try to guess a default conductor and insulation type if + none is provided. +- {gh-pr}`163` **BREAKING CHANGE:** roseau-load-flow is no longer a SaaS project. Starting with version + 0.7.0, the software is distributed as a standalone Python package. You need a license to use it for + commercial purposes. See the documentation for more details. This comes with a huge performance + improvement but requires a breaking change to the API: + - The `ElectricalNetwork.solve_load_flow()` method no longer takes an `auth` argument. + - To activate the license, you need to call `roseau.load_flow.activate_license("MY LICENSE KEY")` + or set the environment variable `ROSEAU_LOAD_FLOW_LICENSE_KEY` (preferred) before calling + `ElectricalNetwork.solve_load_flow()`. More information in the documentation. + - Several methods on the `FlexibleParameter` class that previously required `auth` are changed. Make + sure to follow the documentation to update your code. - {gh-pr}`163` {gh-issue}`158` Fix `ElectricalNetwork.res_transformers` returning an empty dataframe when max_power is not set. +- {gh-pr}`163` Several unused exception codes were removed. An `EMPTY_NETWORK` code was added to indicate + that a network is being created with no elements. +- {gh-pr}`163` Remove the `ElectricalNetwork.res_info` attribute. `ElectricalNetwork.solve_load_flow()` now + returns the tuple (number of iterations, residual). +- {gh-pr}`163` Remove the `Bus.clear_short_circuits()` and `ElectricalNetwork.clear_short_circuits()` + methods. It is currently not possible to clear short-circuits from the network. +- {gh-pr}`163` Improve performance of network creation and results access. +- {gh-pr}`163` Attributes `phases` and `bus` are now read-only on all elements. - {gh-pr}`151` Require Python 3.10 or newer. ## Version 0.6.0 From ff35a33ecfdee6479735bdce1a0b5a487fb4d366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Vinot?= Date: Wed, 24 Jan 2024 09:51:05 +0100 Subject: [PATCH 45/51] Update dependencies --- .github/workflows/doc.yml | 103 -------- .pre-commit-config.yaml | 4 +- conda/meta.yaml | 2 +- doc/Changelog.md | 11 +- doc/License.md | 4 +- doc/conf.py | 4 +- poetry.lock | 427 ++++++++++++++++++---------------- pyproject.toml | 18 +- roseau/load_flow/__about__.py | 2 +- 9 files changed, 254 insertions(+), 321 deletions(-) delete mode 100644 .github/workflows/doc.yml diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml deleted file mode 100644 index afafea5a..00000000 --- a/.github/workflows/doc.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: Documentation - -on: - push: - # TODO: rerun on develop when we have the required dependencies - # branches: ["main", "develop"] - branches: ["main"] - workflow_dispatch: - inputs: - forceDeploy: - description: "Deploy?" - required: true - default: false - type: boolean - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Install debian dependencies - run: | - sudo apt update - sudo apt -yq --no-install-suggests --no-install-recommends install pandoc make - - - uses: actions/checkout@v4 - with: - lfs: false - - - name: Create LFS file list - run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id - - - name: Cache git LFS - uses: actions/cache@v3 - with: - path: .git/lfs - key: git-lfs-v1-${{ hashFiles('.lfs-assets-id') }} - restore-keys: | - git-lfs-v1 - git-lfs - - - name: Git LFS - run: | - git lfs checkout - git lfs pull - git lfs prune --verify-remote - - - name: Setup Pages - uses: actions/configure-pages@v4 - - - name: Install poetry - run: pipx install poetry - - - name: Set up Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: "3.12" - cache: "poetry" - - - name: Install dependencies - run: | - poetry env use "3.12" - poetry install --only doc - - - name: Build with Sphinx - run: | - poetry env use "3.12" - cd doc && make html - env: - SPHINXBUILD: poetry run sphinx-build - - - name: Upload pages artifact - if: ${{ github.ref == 'refs/heads/main' || inputs.forceDeploy == true }} - uses: actions/upload-pages-artifact@v3 - with: - path: "build/html/" - - - name: Upload artifact - uses: actions/upload-artifact@v4 - if: ${{ !(github.ref == 'refs/heads/main' || inputs.forceDeploy == true) }} - with: - path: "build/html/" - - # Deployment job - deploy: - if: ${{ github.ref == 'refs/heads/main' || inputs.forceDeploy == true }} - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 16c4ede7..e7f68b1a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: hooks: - id: poetry-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.11 # keep in sync with pyproject.toml + rev: v0.1.14 # keep in sync with pyproject.toml hooks: - id: ruff types_or: [python, pyi, jupyter] @@ -30,7 +30,7 @@ repos: args: [-l 90] additional_dependencies: [black==23.12.1] - repo: https://github.com/pre-commit/mirrors-prettier - rev: v4.0.0-alpha.8 + rev: v3.1.0 hooks: - id: prettier args: ["--print-width", "120"] diff --git a/conda/meta.yaml b/conda/meta.yaml index 3bc3230d..db6714df 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -1,6 +1,6 @@ # prettier-ignore {% set name = "roseau-load-flow" %} -{% set version = "0.6.0" %} +{% set version = "0.7.0-alpha" %} package: name: "{{ name|lower }}" diff --git a/doc/Changelog.md b/doc/Changelog.md index 9522c9c8..e3be1bc6 100644 --- a/doc/Changelog.md +++ b/doc/Changelog.md @@ -1,8 +1,13 @@ # Changelog -## Unreleased +## Version 0.7.0-alpha -- {gh-pr}`168` {gh-issue}`166` Fix initial potentials propagation. +```{important} +Starting with version 0.7.0, Roseau Load Flow will no longer be supplied as a SaaS. The software will be available as +a standalone Python library. +``` + +- {gh-pr}`168` {gh-issue}`166` Fix initial potentials' propagation. - {gh-pr}`167` {gh-issue}`161` Add a catalogue of lines using the IEC standards. You can use the method `LineParameters.get_catalogue()` to get a data frame of the available lines and the method `LineParameters.from_catalogue()` to create a line from the catalogue. Several line types, conductor @@ -14,7 +19,7 @@ - {gh-pr}`167` Replace all `print_catalogue()` methods by `get_catalogue()` methods that return a data frame instead of printing the catalogue to the console. - {gh-pr}`167` Enumeration classes no longer have a `from_string` method, you can call the enumeration - class directly with the string value to get the corresponding enumeration member. Case insensitive + class directly with the string value to get the corresponding enumeration member. Case-insensitive behavior is preserved. - {gh-pr}`167` {gh-issue}`122` Add checks on line height and diameter in the `LineParameters.from_geometry()` alternative constructor. This method will try to guess a default conductor and insulation type if diff --git a/doc/License.md b/doc/License.md index 9e1b7808..d1d41ced 100644 --- a/doc/License.md +++ b/doc/License.md @@ -13,12 +13,12 @@ can be used free of charge. For example, this key can be used to follow the gett ```{note} Licenses are given **free of charge** for _students and teachers_. Please contact us at -contact@roseautechnologies.com to get a license key. +[contact@roseautechnologies.com](mailto:contact@roseautechnologies.com) to get a license key. ``` (license-activation)= -## How to activate the license in your project +## How to activate the license in your project? There are two ways to activate the license in your project: diff --git a/doc/conf.py b/doc/conf.py index 468be0e1..195fcc87 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -22,8 +22,8 @@ # author = "Benoît Vinot" # The full version, including alpha/beta/rc tags -version = "0.6" -release = "0.6.0" +version = "0.7" +release = "0.7.0-alpha" # -- General configuration --------------------------------------------------- diff --git a/poetry.lock b/poetry.lock index dde1d9ae..78962dce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -71,19 +71,22 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] [[package]] name = "beautifulsoup4" -version = "4.12.2" +version = "4.12.3" description = "Screen-scraping library" optional = false python-versions = ">=3.6.0" files = [ - {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, - {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, ] [package.dependencies] soupsieve = ">1.2" [package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] @@ -528,53 +531,53 @@ test = ["Fiona[s3]", "pytest (>=7)", "pytest-cov", "pytz"] [[package]] name = "fonttools" -version = "4.47.0" +version = "4.47.2" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.47.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2d2404107626f97a221dc1a65b05396d2bb2ce38e435f64f26ed2369f68675d9"}, - {file = "fonttools-4.47.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01f409be619a9a0f5590389e37ccb58b47264939f0e8d58bfa1f3ba07d22671"}, - {file = "fonttools-4.47.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d986b66ff722ef675b7ee22fbe5947a41f60a61a4da15579d5e276d897fbc7fa"}, - {file = "fonttools-4.47.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8acf6dd0434b211b3bd30d572d9e019831aae17a54016629fa8224783b22df8"}, - {file = "fonttools-4.47.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:495369c660e0c27233e3c572269cbe520f7f4978be675f990f4005937337d391"}, - {file = "fonttools-4.47.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c59227d7ba5b232281c26ae04fac2c73a79ad0e236bca5c44aae904a18f14faf"}, - {file = "fonttools-4.47.0-cp310-cp310-win32.whl", hash = "sha256:59a6c8b71a245800e923cb684a2dc0eac19c56493e2f896218fcf2571ed28984"}, - {file = "fonttools-4.47.0-cp310-cp310-win_amd64.whl", hash = "sha256:52c82df66201f3a90db438d9d7b337c7c98139de598d0728fb99dab9fd0495ca"}, - {file = "fonttools-4.47.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:854421e328d47d70aa5abceacbe8eef231961b162c71cbe7ff3f47e235e2e5c5"}, - {file = "fonttools-4.47.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:511482df31cfea9f697930f61520f6541185fa5eeba2fa760fe72e8eee5af88b"}, - {file = "fonttools-4.47.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0e2c88c8c985b7b9a7efcd06511fb0a1fe3ddd9a6cd2895ef1dbf9059719d7"}, - {file = "fonttools-4.47.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7a0a8848726956e9d9fb18c977a279013daadf0cbb6725d2015a6dd57527992"}, - {file = "fonttools-4.47.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e869da810ae35afb3019baa0d0306cdbab4760a54909c89ad8904fa629991812"}, - {file = "fonttools-4.47.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dd23848f877c3754f53a4903fb7a593ed100924f9b4bff7d5a4e2e8a7001ae11"}, - {file = "fonttools-4.47.0-cp311-cp311-win32.whl", hash = "sha256:bf1810635c00f7c45d93085611c995fc130009cec5abdc35b327156aa191f982"}, - {file = "fonttools-4.47.0-cp311-cp311-win_amd64.whl", hash = "sha256:61df4dee5d38ab65b26da8efd62d859a1eef7a34dcbc331299a28e24d04c59a7"}, - {file = "fonttools-4.47.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e3f4d61f3a8195eac784f1d0c16c0a3105382c1b9a74d99ac4ba421da39a8826"}, - {file = "fonttools-4.47.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:174995f7b057e799355b393e97f4f93ef1f2197cbfa945e988d49b2a09ecbce8"}, - {file = "fonttools-4.47.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea592e6a09b71cb7a7661dd93ac0b877a6228e2d677ebacbad0a4d118494c86d"}, - {file = "fonttools-4.47.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40bdbe90b33897d9cc4a39f8e415b0fcdeae4c40a99374b8a4982f127ff5c767"}, - {file = "fonttools-4.47.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:843509ae9b93db5aaf1a6302085e30bddc1111d31e11d724584818f5b698f500"}, - {file = "fonttools-4.47.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9acfa1cdc479e0dde528b61423855913d949a7f7fe09e276228298fef4589540"}, - {file = "fonttools-4.47.0-cp312-cp312-win32.whl", hash = "sha256:66c92ec7f95fd9732550ebedefcd190a8d81beaa97e89d523a0d17198a8bda4d"}, - {file = "fonttools-4.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8fa20748de55d0021f83754b371432dca0439e02847962fc4c42a0e444c2d78"}, - {file = "fonttools-4.47.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c75e19971209fbbce891ebfd1b10c37320a5a28e8d438861c21d35305aedb81c"}, - {file = "fonttools-4.47.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e79f1a3970d25f692bbb8c8c2637e621a66c0d60c109ab48d4a160f50856deff"}, - {file = "fonttools-4.47.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:562681188c62c024fe2c611b32e08b8de2afa00c0c4e72bed47c47c318e16d5c"}, - {file = "fonttools-4.47.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a77a60315c33393b2bd29d538d1ef026060a63d3a49a9233b779261bad9c3f71"}, - {file = "fonttools-4.47.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4fabb8cc9422efae1a925160083fdcbab8fdc96a8483441eb7457235df625bd"}, - {file = "fonttools-4.47.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2a78dba8c2a1e9d53a0fb5382979f024200dc86adc46a56cbb668a2249862fda"}, - {file = "fonttools-4.47.0-cp38-cp38-win32.whl", hash = "sha256:e6b968543fde4119231c12c2a953dcf83349590ca631ba8216a8edf9cd4d36a9"}, - {file = "fonttools-4.47.0-cp38-cp38-win_amd64.whl", hash = "sha256:4a9a51745c0439516d947480d4d884fa18bd1458e05b829e482b9269afa655bc"}, - {file = "fonttools-4.47.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:62d8ddb058b8e87018e5dc26f3258e2c30daad4c87262dfeb0e2617dd84750e6"}, - {file = "fonttools-4.47.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5dde0eab40faaa5476133123f6a622a1cc3ac9b7af45d65690870620323308b4"}, - {file = "fonttools-4.47.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4da089f6dfdb822293bde576916492cd708c37c2501c3651adde39804630538"}, - {file = "fonttools-4.47.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:253bb46bab970e8aae254cebf2ae3db98a4ef6bd034707aa68a239027d2b198d"}, - {file = "fonttools-4.47.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1193fb090061efa2f9e2d8d743ae9850c77b66746a3b32792324cdce65784154"}, - {file = "fonttools-4.47.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:084511482dd265bce6dca24c509894062f0117e4e6869384d853f46c0e6d43be"}, - {file = "fonttools-4.47.0-cp39-cp39-win32.whl", hash = "sha256:97620c4af36e4c849e52661492e31dc36916df12571cb900d16960ab8e92a980"}, - {file = "fonttools-4.47.0-cp39-cp39-win_amd64.whl", hash = "sha256:e77bdf52185bdaf63d39f3e1ac3212e6cfa3ab07d509b94557a8902ce9c13c82"}, - {file = "fonttools-4.47.0-py3-none-any.whl", hash = "sha256:d6477ba902dd2d7adda7f0fd3bfaeb92885d45993c9e1928c9f28fc3961415f7"}, - {file = "fonttools-4.47.0.tar.gz", hash = "sha256:ec13a10715eef0e031858c1c23bfaee6cba02b97558e4a7bfa089dba4a8c2ebf"}, + {file = "fonttools-4.47.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b629108351d25512d4ea1a8393a2dba325b7b7d7308116b605ea3f8e1be88df"}, + {file = "fonttools-4.47.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c19044256c44fe299d9a73456aabee4b4d06c6b930287be93b533b4737d70aa1"}, + {file = "fonttools-4.47.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8be28c036b9f186e8c7eaf8a11b42373e7e4949f9e9f370202b9da4c4c3f56c"}, + {file = "fonttools-4.47.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f83a4daef6d2a202acb9bf572958f91cfde5b10c8ee7fb1d09a4c81e5d851fd8"}, + {file = "fonttools-4.47.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a5a5318ba5365d992666ac4fe35365f93004109d18858a3e18ae46f67907670"}, + {file = "fonttools-4.47.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8f57ecd742545362a0f7186774b2d1c53423ed9ece67689c93a1055b236f638c"}, + {file = "fonttools-4.47.2-cp310-cp310-win32.whl", hash = "sha256:a1c154bb85dc9a4cf145250c88d112d88eb414bad81d4cb524d06258dea1bdc0"}, + {file = "fonttools-4.47.2-cp310-cp310-win_amd64.whl", hash = "sha256:3e2b95dce2ead58fb12524d0ca7d63a63459dd489e7e5838c3cd53557f8933e1"}, + {file = "fonttools-4.47.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:29495d6d109cdbabe73cfb6f419ce67080c3ef9ea1e08d5750240fd4b0c4763b"}, + {file = "fonttools-4.47.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0a1d313a415eaaba2b35d6cd33536560deeebd2ed758b9bfb89ab5d97dc5deac"}, + {file = "fonttools-4.47.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90f898cdd67f52f18049250a6474185ef6544c91f27a7bee70d87d77a8daf89c"}, + {file = "fonttools-4.47.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3480eeb52770ff75140fe7d9a2ec33fb67b07efea0ab5129c7e0c6a639c40c70"}, + {file = "fonttools-4.47.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0255dbc128fee75fb9be364806b940ed450dd6838672a150d501ee86523ac61e"}, + {file = "fonttools-4.47.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f791446ff297fd5f1e2247c188de53c1bfb9dd7f0549eba55b73a3c2087a2703"}, + {file = "fonttools-4.47.2-cp311-cp311-win32.whl", hash = "sha256:740947906590a878a4bde7dd748e85fefa4d470a268b964748403b3ab2aeed6c"}, + {file = "fonttools-4.47.2-cp311-cp311-win_amd64.whl", hash = "sha256:63fbed184979f09a65aa9c88b395ca539c94287ba3a364517698462e13e457c9"}, + {file = "fonttools-4.47.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4ec558c543609e71b2275c4894e93493f65d2f41c15fe1d089080c1d0bb4d635"}, + {file = "fonttools-4.47.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e040f905d542362e07e72e03612a6270c33d38281fd573160e1003e43718d68d"}, + {file = "fonttools-4.47.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dd58cc03016b281bd2c74c84cdaa6bd3ce54c5a7f47478b7657b930ac3ed8eb"}, + {file = "fonttools-4.47.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32ab2e9702dff0dd4510c7bb958f265a8d3dd5c0e2547e7b5f7a3df4979abb07"}, + {file = "fonttools-4.47.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a808f3c1d1df1f5bf39be869b6e0c263570cdafb5bdb2df66087733f566ea71"}, + {file = "fonttools-4.47.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ac71e2e201df041a2891067dc36256755b1229ae167edbdc419b16da78732c2f"}, + {file = "fonttools-4.47.2-cp312-cp312-win32.whl", hash = "sha256:69731e8bea0578b3c28fdb43dbf95b9386e2d49a399e9a4ad736b8e479b08085"}, + {file = "fonttools-4.47.2-cp312-cp312-win_amd64.whl", hash = "sha256:b3e1304e5f19ca861d86a72218ecce68f391646d85c851742d265787f55457a4"}, + {file = "fonttools-4.47.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:254d9a6f7be00212bf0c3159e0a420eb19c63793b2c05e049eb337f3023c5ecc"}, + {file = "fonttools-4.47.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eabae77a07c41ae0b35184894202305c3ad211a93b2eb53837c2a1143c8bc952"}, + {file = "fonttools-4.47.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86a5ab2873ed2575d0fcdf1828143cfc6b977ac448e3dc616bb1e3d20efbafa"}, + {file = "fonttools-4.47.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13819db8445a0cec8c3ff5f243af6418ab19175072a9a92f6cc8ca7d1452754b"}, + {file = "fonttools-4.47.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4e743935139aa485fe3253fc33fe467eab6ea42583fa681223ea3f1a93dd01e6"}, + {file = "fonttools-4.47.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d49ce3ea7b7173faebc5664872243b40cf88814ca3eb135c4a3cdff66af71946"}, + {file = "fonttools-4.47.2-cp38-cp38-win32.whl", hash = "sha256:94208ea750e3f96e267f394d5588579bb64cc628e321dbb1d4243ffbc291b18b"}, + {file = "fonttools-4.47.2-cp38-cp38-win_amd64.whl", hash = "sha256:0f750037e02beb8b3569fbff701a572e62a685d2a0e840d75816592280e5feae"}, + {file = "fonttools-4.47.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d71606c9321f6701642bd4746f99b6089e53d7e9817fc6b964e90d9c5f0ecc6"}, + {file = "fonttools-4.47.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:86e0427864c6c91cf77f16d1fb9bf1bbf7453e824589e8fb8461b6ee1144f506"}, + {file = "fonttools-4.47.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a00bd0e68e88987dcc047ea31c26d40a3c61185153b03457956a87e39d43c37"}, + {file = "fonttools-4.47.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5d77479fb885ef38a16a253a2f4096bc3d14e63a56d6246bfdb56365a12b20c"}, + {file = "fonttools-4.47.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5465df494f20a7d01712b072ae3ee9ad2887004701b95cb2cc6dcb9c2c97a899"}, + {file = "fonttools-4.47.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4c811d3c73b6abac275babb8aa439206288f56fdb2c6f8835e3d7b70de8937a7"}, + {file = "fonttools-4.47.2-cp39-cp39-win32.whl", hash = "sha256:5b60e3afa9635e3dfd3ace2757039593e3bd3cf128be0ddb7a1ff4ac45fa5a50"}, + {file = "fonttools-4.47.2-cp39-cp39-win_amd64.whl", hash = "sha256:7ee48bd9d6b7e8f66866c9090807e3a4a56cf43ffad48962725a190e0dd774c8"}, + {file = "fonttools-4.47.2-py3-none-any.whl", hash = "sha256:7eb7ad665258fba68fd22228a09f347469d95a97fb88198e133595947a20a184"}, + {file = "fonttools-4.47.2.tar.gz", hash = "sha256:7df26dd3650e98ca45f1e29883c96a0b9f5bb6af8d632a6a108bc744fa0bd9b3"}, ] [package.extras] @@ -675,13 +678,13 @@ files = [ [[package]] name = "jinja2" -version = "3.1.2" +version = "3.1.3" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [package.dependencies] @@ -843,61 +846,71 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "2.1.3" +version = "2.1.4" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-win32.whl", hash = "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-win32.whl", hash = "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-win32.whl", hash = "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-win32.whl", hash = "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-win_amd64.whl", hash = "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-win32.whl", hash = "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-win_amd64.whl", hash = "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-win32.whl", hash = "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959"}, + {file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"}, ] [[package]] @@ -1094,36 +1107,40 @@ files = [ [[package]] name = "pandas" -version = "2.1.4" +version = "2.2.0" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" files = [ - {file = "pandas-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bdec823dc6ec53f7a6339a0e34c68b144a7a1fd28d80c260534c39c62c5bf8c9"}, - {file = "pandas-2.1.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:294d96cfaf28d688f30c918a765ea2ae2e0e71d3536754f4b6de0ea4a496d034"}, - {file = "pandas-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b728fb8deba8905b319f96447a27033969f3ea1fea09d07d296c9030ab2ed1d"}, - {file = "pandas-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00028e6737c594feac3c2df15636d73ace46b8314d236100b57ed7e4b9ebe8d9"}, - {file = "pandas-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:426dc0f1b187523c4db06f96fb5c8d1a845e259c99bda74f7de97bd8a3bb3139"}, - {file = "pandas-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:f237e6ca6421265643608813ce9793610ad09b40154a3344a088159590469e46"}, - {file = "pandas-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b7d852d16c270e4331f6f59b3e9aa23f935f5c4b0ed2d0bc77637a8890a5d092"}, - {file = "pandas-2.1.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd7d5f2f54f78164b3d7a40f33bf79a74cdee72c31affec86bfcabe7e0789821"}, - {file = "pandas-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa6e92e639da0d6e2017d9ccff563222f4eb31e4b2c3cf32a2a392fc3103c0d"}, - {file = "pandas-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d797591b6846b9db79e65dc2d0d48e61f7db8d10b2a9480b4e3faaddc421a171"}, - {file = "pandas-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2d3e7b00f703aea3945995ee63375c61b2e6aa5aa7871c5d622870e5e137623"}, - {file = "pandas-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:dc9bf7ade01143cddc0074aa6995edd05323974e6e40d9dbde081021ded8510e"}, - {file = "pandas-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:482d5076e1791777e1571f2e2d789e940dedd927325cc3cb6d0800c6304082f6"}, - {file = "pandas-2.1.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8a706cfe7955c4ca59af8c7a0517370eafbd98593155b48f10f9811da440248b"}, - {file = "pandas-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0513a132a15977b4a5b89aabd304647919bc2169eac4c8536afb29c07c23540"}, - {file = "pandas-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9f17f2b6fc076b2a0078862547595d66244db0f41bf79fc5f64a5c4d635bead"}, - {file = "pandas-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:45d63d2a9b1b37fa6c84a68ba2422dc9ed018bdaa668c7f47566a01188ceeec1"}, - {file = "pandas-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:f69b0c9bb174a2342818d3e2778584e18c740d56857fc5cdb944ec8bbe4082cf"}, - {file = "pandas-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3f06bda01a143020bad20f7a85dd5f4a1600112145f126bc9e3e42077c24ef34"}, - {file = "pandas-2.1.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab5796839eb1fd62a39eec2916d3e979ec3130509930fea17fe6f81e18108f6a"}, - {file = "pandas-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edbaf9e8d3a63a9276d707b4d25930a262341bca9874fcb22eff5e3da5394732"}, - {file = "pandas-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ebfd771110b50055712b3b711b51bee5d50135429364d0498e1213a7adc2be8"}, - {file = "pandas-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8ea107e0be2aba1da619cc6ba3f999b2bfc9669a83554b1904ce3dd9507f0860"}, - {file = "pandas-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:d65148b14788b3758daf57bf42725caa536575da2b64df9964c563b015230984"}, - {file = "pandas-2.1.4.tar.gz", hash = "sha256:fcb68203c833cc735321512e13861358079a96c174a61f5116a1de89c58c0ef7"}, + {file = "pandas-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8108ee1712bb4fa2c16981fba7e68b3f6ea330277f5ca34fa8d557e986a11670"}, + {file = "pandas-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:736da9ad4033aeab51d067fc3bd69a0ba36f5a60f66a527b3d72e2030e63280a"}, + {file = "pandas-2.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38e0b4fc3ddceb56ec8a287313bc22abe17ab0eb184069f08fc6a9352a769b18"}, + {file = "pandas-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20404d2adefe92aed3b38da41d0847a143a09be982a31b85bc7dd565bdba0f4e"}, + {file = "pandas-2.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ea3ee3f125032bfcade3a4cf85131ed064b4f8dd23e5ce6fa16473e48ebcaf5"}, + {file = "pandas-2.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9670b3ac00a387620489dfc1bca66db47a787f4e55911f1293063a78b108df1"}, + {file = "pandas-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a946f210383c7e6d16312d30b238fd508d80d927014f3b33fb5b15c2f895430"}, + {file = "pandas-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a1b438fa26b208005c997e78672f1aa8138f67002e833312e6230f3e57fa87d5"}, + {file = "pandas-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ce2fbc8d9bf303ce54a476116165220a1fedf15985b09656b4b4275300e920b"}, + {file = "pandas-2.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2707514a7bec41a4ab81f2ccce8b382961a29fbe9492eab1305bb075b2b1ff4f"}, + {file = "pandas-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85793cbdc2d5bc32620dc8ffa715423f0c680dacacf55056ba13454a5be5de88"}, + {file = "pandas-2.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cfd6c2491dc821b10c716ad6776e7ab311f7df5d16038d0b7458bc0b67dc10f3"}, + {file = "pandas-2.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a146b9dcacc3123aa2b399df1a284de5f46287a4ab4fbfc237eac98a92ebcb71"}, + {file = "pandas-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbc1b53c0e1fdf16388c33c3cca160f798d38aea2978004dd3f4d3dec56454c9"}, + {file = "pandas-2.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a41d06f308a024981dcaa6c41f2f2be46a6b186b902c94c2674e8cb5c42985bc"}, + {file = "pandas-2.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:159205c99d7a5ce89ecfc37cb08ed179de7783737cea403b295b5eda8e9c56d1"}, + {file = "pandas-2.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1e1f3861ea9132b32f2133788f3b14911b68102d562715d71bd0013bc45440"}, + {file = "pandas-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:761cb99b42a69005dec2b08854fb1d4888fdf7b05db23a8c5a099e4b886a2106"}, + {file = "pandas-2.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a20628faaf444da122b2a64b1e5360cde100ee6283ae8effa0d8745153809a2e"}, + {file = "pandas-2.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f5be5d03ea2073627e7111f61b9f1f0d9625dc3c4d8dda72cc827b0c58a1d042"}, + {file = "pandas-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:a626795722d893ed6aacb64d2401d017ddc8a2341b49e0384ab9bf7112bdec30"}, + {file = "pandas-2.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9f66419d4a41132eb7e9a73dcec9486cf5019f52d90dd35547af11bc58f8637d"}, + {file = "pandas-2.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57abcaeda83fb80d447f28ab0cc7b32b13978f6f733875ebd1ed14f8fbc0f4ab"}, + {file = "pandas-2.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e60f1f7dba3c2d5ca159e18c46a34e7ca7247a73b5dd1a22b6d59707ed6b899a"}, + {file = "pandas-2.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb61dc8567b798b969bcc1fc964788f5a68214d333cade8319c7ab33e2b5d88a"}, + {file = "pandas-2.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:52826b5f4ed658fa2b729264d63f6732b8b29949c7fd234510d57c61dbeadfcd"}, + {file = "pandas-2.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bde2bc699dbd80d7bc7f9cab1e23a95c4375de615860ca089f34e7c64f4a8de7"}, + {file = "pandas-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:3de918a754bbf2da2381e8a3dcc45eede8cd7775b047b923f9006d5f876802ae"}, + {file = "pandas-2.2.0.tar.gz", hash = "sha256:30b83f7c3eb217fb4d1b494a57a2fda5444f17834f5df2de6b2ffff68dc3c8e2"}, ] [package.dependencies] @@ -1134,31 +1151,31 @@ numpy = [ ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" -tzdata = ">=2022.1" +tzdata = ">=2022.7" [package.extras] -all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"] -aws = ["s3fs (>=2022.05.0)"] -clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"] -compression = ["zstandard (>=0.17.0)"] -computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] consortium-standard = ["dataframe-api-compat (>=0.1.7)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"] -feather = ["pyarrow (>=7.0.0)"] -fss = ["fsspec (>=2022.05.0)"] -gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"] -hdf5 = ["tables (>=3.7.0)"] -html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"] -mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"] -parquet = ["pyarrow (>=7.0.0)"] -performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"] -plot = ["matplotlib (>=3.6.1)"] -postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"] -spss = ["pyreadstat (>=1.1.5)"] -sql-other = ["SQLAlchemy (>=1.4.36)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.8.0)"] +xml = ["lxml (>=4.9.2)"] [[package]] name = "pillow" @@ -1518,6 +1535,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1525,8 +1543,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1543,6 +1568,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1550,6 +1576,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1678,30 +1705,48 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "roseau-load-flow-engine" +version = "0.12.0a0" +description = "Highly capable three-phase load flow solver" +optional = false +python-versions = ">=3.10,<4.0" +files = [ + {file = "roseau_load_flow_engine-0.12.0a0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:48d0cce802fb068b80086ef1e451878df47cd40f7e68b0e8163fe18121b3915b"}, + {file = "roseau_load_flow_engine-0.12.0a0-cp310-cp310-win_amd64.whl", hash = "sha256:e8d62eeea95743eb20c40feadb243a8ea816bb63a4d2bcd34ac477e3daf534fa"}, + {file = "roseau_load_flow_engine-0.12.0a0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:0952a00a0e5a91dbbf5e62de8a6b9b447363c18794fd9878c0625664183917c5"}, + {file = "roseau_load_flow_engine-0.12.0a0-cp311-cp311-win_amd64.whl", hash = "sha256:791e327ddc0224d35040783b4bf32f83a91ca8620d876de6cd80b5776c735dd0"}, + {file = "roseau_load_flow_engine-0.12.0a0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:70f162550f86f57e727f4815b79ab9aa37e693dc699af598c6e1845ad3e42b77"}, + {file = "roseau_load_flow_engine-0.12.0a0-cp312-cp312-win_amd64.whl", hash = "sha256:41a4f773153dc7d79f8dd7b3ac00c37d07c99435bd040918eb1c47a3b2275a87"}, +] + +[package.dependencies] +numpy = ">=1.21.5" + [[package]] name = "ruff" -version = "0.1.11" +version = "0.1.14" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a7f772696b4cdc0a3b2e527fc3c7ccc41cdcb98f5c80fdd4f2b8c50eb1458196"}, - {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:934832f6ed9b34a7d5feea58972635c2039c7a3b434fe5ba2ce015064cb6e955"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea0d3e950e394c4b332bcdd112aa566010a9f9c95814844a7468325290aabfd9"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bd4025b9c5b429a48280785a2b71d479798a69f5c2919e7d274c5f4b32c3607"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ad00662305dcb1e987f5ec214d31f7d6a062cae3e74c1cbccef15afd96611d"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4b077ce83f47dd6bea1991af08b140e8b8339f0ba8cb9b7a484c30ebab18a23f"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a88efecec23c37b11076fe676e15c6cdb1271a38f2b415e381e87fe4517f18"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b25093dad3b055667730a9b491129c42d45e11cdb7043b702e97125bcec48a1"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231d8fb11b2cc7c0366a326a66dafc6ad449d7fcdbc268497ee47e1334f66f77"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:09c415716884950080921dd6237767e52e227e397e2008e2bed410117679975b"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f58948c6d212a6b8d41cd59e349751018797ce1727f961c2fa755ad6208ba45"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:190a566c8f766c37074d99640cd9ca3da11d8deae2deae7c9505e68a4a30f740"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6464289bd67b2344d2a5d9158d5eb81025258f169e69a46b741b396ffb0cda95"}, - {file = "ruff-0.1.11-py3-none-win32.whl", hash = "sha256:9b8f397902f92bc2e70fb6bebfa2139008dc72ae5177e66c383fa5426cb0bf2c"}, - {file = "ruff-0.1.11-py3-none-win_amd64.whl", hash = "sha256:eb85ee287b11f901037a6683b2374bb0ec82928c5cbc984f575d0437979c521a"}, - {file = "ruff-0.1.11-py3-none-win_arm64.whl", hash = "sha256:97ce4d752f964ba559c7023a86e5f8e97f026d511e48013987623915431c7ea9"}, - {file = "ruff-0.1.11.tar.gz", hash = "sha256:f9d4d88cb6eeb4dfe20f9f0519bd2eaba8119bde87c3d5065c541dbae2b5a2cb"}, + {file = "ruff-0.1.14-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:96f76536df9b26622755c12ed8680f159817be2f725c17ed9305b472a757cdbb"}, + {file = "ruff-0.1.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab3f71f64498c7241123bb5a768544cf42821d2a537f894b22457a543d3ca7a9"}, + {file = "ruff-0.1.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7060156ecc572b8f984fd20fd8b0fcb692dd5d837b7606e968334ab7ff0090ab"}, + {file = "ruff-0.1.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a53d8e35313d7b67eb3db15a66c08434809107659226a90dcd7acb2afa55faea"}, + {file = "ruff-0.1.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bea9be712b8f5b4ebed40e1949379cfb2a7d907f42921cf9ab3aae07e6fba9eb"}, + {file = "ruff-0.1.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2270504d629a0b064247983cbc495bed277f372fb9eaba41e5cf51f7ba705a6a"}, + {file = "ruff-0.1.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80258bb3b8909b1700610dfabef7876423eed1bc930fe177c71c414921898efa"}, + {file = "ruff-0.1.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:653230dd00aaf449eb5ff25d10a6e03bc3006813e2cb99799e568f55482e5cae"}, + {file = "ruff-0.1.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b3acc6c4e6928459ba9eb7459dd4f0c4bf266a053c863d72a44c33246bfdbf"}, + {file = "ruff-0.1.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6b3dadc9522d0eccc060699a9816e8127b27addbb4697fc0c08611e4e6aeb8b5"}, + {file = "ruff-0.1.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1c8eca1a47b4150dc0fbec7fe68fc91c695aed798532a18dbb1424e61e9b721f"}, + {file = "ruff-0.1.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:62ce2ae46303ee896fc6811f63d6dabf8d9c389da0f3e3f2bce8bc7f15ef5488"}, + {file = "ruff-0.1.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b2027dde79d217b211d725fc833e8965dc90a16d0d3213f1298f97465956661b"}, + {file = "ruff-0.1.14-py3-none-win32.whl", hash = "sha256:722bafc299145575a63bbd6b5069cb643eaa62546a5b6398f82b3e4403329cab"}, + {file = "ruff-0.1.14-py3-none-win_amd64.whl", hash = "sha256:e3d241aa61f92b0805a7082bd89a9990826448e4d0398f0e2bc8f05c75c63d99"}, + {file = "ruff-0.1.14-py3-none-win_arm64.whl", hash = "sha256:269302b31ade4cde6cf6f9dd58ea593773a37ed3f7b97e793c8594b262466b67"}, + {file = "ruff-0.1.14.tar.gz", hash = "sha256:ad3f8088b2dfd884820289a06ab718cde7d38b94972212cc4ba90d5fbc9955f3"}, ] [[package]] @@ -1937,20 +1982,18 @@ sphinx = "*" [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.7" +version = "1.0.8" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.9" files = [ - {file = "sphinxcontrib_applehelp-1.0.7-py3-none-any.whl", hash = "sha256:094c4d56209d1734e7d252f6e0b3ccc090bd52ee56807a5d9315b19c122ab15d"}, - {file = "sphinxcontrib_applehelp-1.0.7.tar.gz", hash = "sha256:39fdc8d762d33b01a7d8f026a3b7d71563ea3b72787d5f00ad8465bd9d6dfbfa"}, + {file = "sphinxcontrib_applehelp-1.0.8-py3-none-any.whl", hash = "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4"}, + {file = "sphinxcontrib_applehelp-1.0.8.tar.gz", hash = "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619"}, ] -[package.dependencies] -Sphinx = ">=5" - [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] @@ -1972,20 +2015,18 @@ Sphinx = ">=3.5" [[package]] name = "sphinxcontrib-devhelp" -version = "1.0.5" +version = "1.0.6" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false python-versions = ">=3.9" files = [ - {file = "sphinxcontrib_devhelp-1.0.5-py3-none-any.whl", hash = "sha256:fe8009aed765188f08fcaadbb3ea0d90ce8ae2d76710b7e29ea7d047177dae2f"}, - {file = "sphinxcontrib_devhelp-1.0.5.tar.gz", hash = "sha256:63b41e0d38207ca40ebbeabcf4d8e51f76c03e78cd61abe118cf4435c73d4212"}, + {file = "sphinxcontrib_devhelp-1.0.6-py3-none-any.whl", hash = "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f"}, + {file = "sphinxcontrib_devhelp-1.0.6.tar.gz", hash = "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3"}, ] -[package.dependencies] -Sphinx = ">=5" - [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] @@ -2004,20 +2045,18 @@ Sphinx = ">=0.6" [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.4" +version = "2.0.5" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.9" files = [ - {file = "sphinxcontrib_htmlhelp-2.0.4-py3-none-any.whl", hash = "sha256:8001661c077a73c29beaf4a79968d0726103c5605e27db92b9ebed8bab1359e9"}, - {file = "sphinxcontrib_htmlhelp-2.0.4.tar.gz", hash = "sha256:6c26a118a05b76000738429b724a0568dbde5b72391a688577da08f11891092a"}, + {file = "sphinxcontrib_htmlhelp-2.0.5-py3-none-any.whl", hash = "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"}, + {file = "sphinxcontrib_htmlhelp-2.0.5.tar.gz", hash = "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015"}, ] -[package.dependencies] -Sphinx = ">=5" - [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] [[package]] @@ -2036,38 +2075,34 @@ test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" -version = "1.0.6" +version = "1.0.7" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false python-versions = ">=3.9" files = [ - {file = "sphinxcontrib_qthelp-1.0.6-py3-none-any.whl", hash = "sha256:bf76886ee7470b934e363da7a954ea2825650013d367728588732c7350f49ea4"}, - {file = "sphinxcontrib_qthelp-1.0.6.tar.gz", hash = "sha256:62b9d1a186ab7f5ee3356d906f648cacb7a6bdb94d201ee7adf26db55092982d"}, + {file = "sphinxcontrib_qthelp-1.0.7-py3-none-any.whl", hash = "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182"}, + {file = "sphinxcontrib_qthelp-1.0.7.tar.gz", hash = "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6"}, ] -[package.dependencies] -Sphinx = ">=5" - [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-serializinghtml" -version = "1.1.9" +version = "1.1.10" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false python-versions = ">=3.9" files = [ - {file = "sphinxcontrib_serializinghtml-1.1.9-py3-none-any.whl", hash = "sha256:9b36e503703ff04f20e9675771df105e58aa029cfcbc23b8ed716019b7416ae1"}, - {file = "sphinxcontrib_serializinghtml-1.1.9.tar.gz", hash = "sha256:0c64ff898339e1fac29abd2bf5f11078f3ec413cfe9c046d3120d7ca65530b54"}, + {file = "sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl", hash = "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7"}, + {file = "sphinxcontrib_serializinghtml-1.1.10.tar.gz", hash = "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f"}, ] -[package.dependencies] -Sphinx = ">=5" - [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] @@ -2146,4 +2181,4 @@ plot = ["matplotlib"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "3d6286235226a2f20c7bf0dd6e466fb22f42972cc2deb79ea2b5cb8682862f18" +content-hash = "8a04ff082cf90ef142a2e820cb9977688d7a669f8b9ee14ad7d851b78daecf9e" diff --git a/pyproject.toml b/pyproject.toml index 9bcdb7d2..3b86c9fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "roseau-load-flow" -version = "0.6.0" +version = "0.7.0-alpha" description = "Highly capable three-phase load flow solver" authors = [ "Ali Hamdan ", @@ -26,6 +26,7 @@ packages = [ ] classifiers = [ "Development Status :: 3 - Alpha", + # "License :: OSI Approved :: The 3-Clause BSD License (BSD-3-Clause)", # https://github.com/pypa/trove-classifiers/issues/70 "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -48,6 +49,7 @@ typing-extensions = ">=4.6.2" pyproj = ">=3.3.0" certifi = ">=2023.5.7" platformdirs = ">=4.0.0" +roseau-load-flow-engine = "==0.12.0-alpha" # Optional dependencies matplotlib = { version = ">=3.7.2", optional = true } @@ -68,7 +70,7 @@ networkx = ">=3.0.0" [tool.poetry.group.dev.dependencies] pre-commit = "^3.0.0" -ruff = "==0.1.11" # keep in sync with .pre-commit-config.yaml +ruff = "==0.1.14" # keep in sync with .pre-commit-config.yaml [tool.poetry.group.doc.dependencies] sphinx = "^7.0.1" @@ -90,15 +92,9 @@ extend-include = ["*.ipynb"] select = ["E", "F", "C90", "W", "B", "UP", "I", "RUF100", "TID", "SIM", "PT", "PIE", "N", "C4", "NPY", "T10"] unfixable = ["B"] ignore = ["E501", "B024", "N818"] - -[tool.ruff.flake8-tidy-imports] -ban-relative-imports = "all" - -[tool.ruff.flake8-pytest-style] -parametrize-values-type = "tuple" - -[tool.ruff.mccabe] -max-complexity = 15 +flake8-tidy-imports.ban-relative-imports = "all" +flake8-pytest-style.parametrize-values-type = "tuple" +mccabe.max-complexity = 15 [tool.ruff.per-file-ignores] "*.ipynb" = ["E402", "F403", "F405", "B018"] diff --git a/roseau/load_flow/__about__.py b/roseau/load_flow/__about__.py index c05651d5..dd7b9c7b 100644 --- a/roseau/load_flow/__about__.py +++ b/roseau/load_flow/__about__.py @@ -9,7 +9,7 @@ ) __copyright__ = "Roseau Technologies 2018" __credits__ = "Roseau Technologies" -__license__ = "Proprietary" +__license__ = "BSD-3-Clause" __maintainer__ = "Ali Hamdan" __email__ = "ali.hamdan@roseautechnologies.com" __status__ = "In development" From 0e38318e4f9cd1885b9515fbc69a0b1754b3cba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Vinot?= Date: Wed, 24 Jan 2024 09:51:53 +0100 Subject: [PATCH 46/51] Update tests for pandas 2.2.0 --- roseau/load_flow/tests/test_converters.py | 2 +- roseau/load_flow/tests/test_electrical_network.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/roseau/load_flow/tests/test_converters.py b/roseau/load_flow/tests/test_converters.py index 06b262c6..62167302 100644 --- a/roseau/load_flow/tests/test_converters.py +++ b/roseau/load_flow/tests/test_converters.py @@ -96,4 +96,4 @@ def test_series_phasor_to_sym(): sym_index = sym_index.set_names("sequence", level=-1).set_levels(sym_index.levels[-1].astype(seq_dtype), level=-1) expected = pd.Series([0, va, 0, 0, va / 2, 0], index=sym_index, name="voltage") - assert_series_equal(series_phasor_to_sym(voltage), expected) + assert_series_equal(series_phasor_to_sym(voltage), expected, check_exact=False) diff --git a/roseau/load_flow/tests/test_electrical_network.py b/roseau/load_flow/tests/test_electrical_network.py index 29a21e92..16a210ea 100644 --- a/roseau/load_flow/tests/test_electrical_network.py +++ b/roseau/load_flow/tests/test_electrical_network.py @@ -666,7 +666,7 @@ def test_buses_voltages(small_network: ElectricalNetwork, good_json_results): assert buses_voltages.shape == (6, 4) assert buses_voltages.index.names == ["bus_id", "phase"] assert list(buses_voltages.columns) == ["voltage", "min_voltage", "max_voltage", "violated"] - assert_frame_equal(buses_voltages, expected_buses_voltages) + assert_frame_equal(buses_voltages, expected_buses_voltages, check_exact=False) def test_to_from_dict_roundtrip(small_network: ElectricalNetwork): @@ -892,6 +892,7 @@ def test_single_phase_network(single_phase_network: ElectricalNetwork): } ) .set_index(["line_id", "phase"]), + check_exact=False, ) # Switches results pd.testing.assert_frame_equal( @@ -1562,7 +1563,7 @@ def test_load_flow_results_frames(small_network: ElectricalNetwork, good_json_re .astype({"potential_ref_id": object, "current": complex}) .set_index(["potential_ref_id"]) ) - assert_frame_equal(small_network.res_potential_refs, expected_res_potential_refs) + assert_frame_equal(small_network.res_potential_refs, expected_res_potential_refs, check_exact=False) # No flexible loads assert small_network.res_loads_flexible_powers.empty From b9b760cf1653f2f18947b5339c5183174f3edc39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Vinot?= Date: Wed, 24 Jan 2024 09:53:10 +0100 Subject: [PATCH 47/51] Update documentation URL --- README.md | 11 ++++++----- conda/meta.yaml | 2 +- doc/License.md | 11 +++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 205176ac..3a9ee7f2 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ _Roseau Load Flow_ is a highly capable three-phase load flow solver with an ergo for unbalanced power flow analysis. This project is compatible with Python version 3.10 and newer. The -[installation instructions](https://roseautechnologies.github.io/Roseau_Load_Flow/Installation.html) +[installation instructions](https://roseau-load-flow.roseautechnologies.com/Installation.html) will guide you through the installation process. If you are new to _Roseau Load Flow_, we recommend you start with the -[getting started tutorial](https://roseautechnologies.github.io/Roseau_Load_Flow/usage/Getting_Started.html). -You can find the complete documentation at https://roseautechnologies.github.io/Roseau_Load_Flow. +[getting started tutorial](https://roseau-load-flow.roseautechnologies.com/usage/Getting_Started.html). +You can find the complete documentation at https://roseau-load-flow.roseautechnologies.com/. > [!IMPORTANT] > Starting with version 0.7.0, Roseau Load Flow will no longer be supplied as a SaaS. The software will @@ -21,13 +21,14 @@ You can find the complete documentation at https://roseautechnologies.github.io/ The project is _partially_ open source but using the solver requires a license. The license key `A8C6DA-9405FB-E74FB9-C71C3C-207661-V3` can be used free of charge with networks containing up to 10 -buses. To obtain a personal or commercial license, please contact us at contact@roseautechnologies.com. +buses. To obtain a personal or commercial license, please contact us +at [contact@roseautechnologies.com](mailto:contact@roseautechnologies.com). > [!NOTE] > Licenses are given free of charge for **students and teachers**. Please contact us at > contact@roseautechnologies.com for more information. -Read more at [License](https://roseautechnologies.github.io/Roseau_Load_Flow/License.html). +Read more at [License](https://roseau-load-flow.roseautechnologies.com/License.html). ## Network data diff --git a/conda/meta.yaml b/conda/meta.yaml index db6714df..4bda245d 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -56,7 +56,7 @@ about: license: BSD-3-Clause license_file: LICENSE.md summary: Highly capable three-phase load flow solver of Roseau Technologies. - doc_url: https://roseautechnologies.github.io/Roseau_Load_Flow/ + doc_url: https://roseau-load-flow.roseautechnologies.com/ extra: recipe-maintainers: diff --git a/doc/License.md b/doc/License.md index d1d41ced..56af9c2e 100644 --- a/doc/License.md +++ b/doc/License.md @@ -28,19 +28,18 @@ There are two ways to activate the license in your project: **This is the recommended approach.** ```{note} If you need help setting an environment variable, refer to the section - [How to set an environment variable](license-environment-variable) + [How to set an environment variable?](license-environment-variable) ``` 2. Call the function `activate_license` with the license key as argument. This function will activate the license for the current session. If you use this approach, it is recommended to - store the license key in a file and read it from there to avoid hardcoding it in your code and + store the license key in a file and read it from there to avoid hard coding it in your code and accidentally committing it to your repository. Example: ```python + from pathlib import Path import roseau.load_flow as lf - with open("my_license_key.txt", "r") as f: - license_key = f.read().strip() - lf.activate_license(license_key) + lf.activate_license(Path("my_license_key.txt").read_text().strip()) # Rest of your code here ``` @@ -50,7 +49,7 @@ There are two ways to activate the license in your project: (license-environment-variable)= -## How to set an environment variable +## How to set an environment variable? If you are not sure how to set an environment variable, [this article](https://www.bitecode.dev/p/environment-variables-for-beginners) has instructions for Windows, MacOS and Linux. The section [Persisting an environment variable](https://www.bitecode.dev/i/121864947/persisting-an-environment-variable) From bf61cf5de44e2e6435e35e955c63daa4cba38af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Vinot?= Date: Wed, 24 Jan 2024 09:53:49 +0100 Subject: [PATCH 48/51] Update documentation --- doc/conf.py | 1 + doc/usage/Connecting_Elements.md | 8 ++-- doc/usage/Short_Circuit.md | 68 ++++++++++++++++---------------- 3 files changed, 39 insertions(+), 38 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 195fcc87..57003592 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -69,6 +69,7 @@ autodoc_typehints = "signature" autodoc_inherit_docstrings = True autoclass_content = "both" # show both class and __init__ docstrings +autodoc_mock_imports = ["roseau.load_flow_engine"] # Ignore missing dependencies when building the documentation # -- Options for HTML output ------------------------------------------------- diff --git a/doc/usage/Connecting_Elements.md b/doc/usage/Connecting_Elements.md index 924f300b..9492821b 100644 --- a/doc/usage/Connecting_Elements.md +++ b/doc/usage/Connecting_Elements.md @@ -123,9 +123,9 @@ belong to a network) will propagate the network to the new elements. ... conductor_type=ConductorType.AL, ... insulator_type=InsulatorType.PVC, ... section=240, -... section_neutral=240, +... section_neutral=120, ... height=Q_(-1.5, "m"), -... external_diameter=Q_(40, "mm"), +... external_diameter=Q_(50, "mm"), ... ) >>> new_line = Line( ... id="new_line", @@ -161,9 +161,9 @@ And now if you run the load flow, you can see that the new elements are taken in ```pycon >>> en.solve_load_flow() -(3, 1.216767654e-07) +(3, 5.209166431541234e-13) >>> abs(new_load.res_voltages) -array([216.54956226]) +array([214.8358114]) ``` ## Modifying an element diff --git a/doc/usage/Short_Circuit.md b/doc/usage/Short_Circuit.md index f0b309aa..ae4e482b 100644 --- a/doc/usage/Short_Circuit.md +++ b/doc/usage/Short_Circuit.md @@ -35,9 +35,9 @@ is impossible. ... conductor_type=ConductorType.AL, ... insulator_type=InsulatorType.PVC, ... section=240, -... section_neutral=240, +... section_neutral=120, ... height=Q_(-1.5, "m"), -... external_diameter=Q_(40, "mm"), +... external_diameter=Q_(50, "mm"), ... ) ... line1 = Line( ... id="line1", bus1=source_bus, bus2=bus1, parameters=lp1, length=1.0, ground=ground @@ -86,20 +86,20 @@ All the following tables are rounded to 2 decimals to be properly displayed. ```pycon >>> en.solve_load_flow() -(1, 1.235686457464e-07) +(1, 3.339550858072471e-13) >>> en.res_branches ``` -| branch_id | phase | branch_type | current1 | current2 | power1 | power2 | potential1 | potential2 | -| :-------- | :---- | :---------- | -----------------: | -------------: | -----------------: | ----------------------: | --------------: | ----------------: | -| line1 | a | line | 376.73+75.27j | -376.51-75.17j | 87001.28-17383.79j | -69627.19+24139.31j | 230.94-0j | 190.15-26.15j | -| line1 | b | line | -376.14-74.96j | 376.12+74.96j | 58424.2+66571.89j | -41140.23-59809.99j | -115.47-200j | -74.72-173.91j | -| line1 | c | line | -0.49-0.42j | 0.49+0.21j | -26.77-147.2j | -14.92+126.89j | -115.47+200j | -117.06+208.26j | -| line1 | n | line | -0.1+0.1j | -0.1-0j | 0j | -0.15+0.85j | 0j | 1.63-8.2j | -| line2 | a | line | **376.51+75.17j** | -376.45-74.93j | 69627.19-24139.31j | **-14217.87+41992.82j** | 190.15-26.15j | **57.69-100.07j** | -| line2 | b | line | **-376.12-74.96j** | 376.45+74.93j | 41140.23+59809.99j | **14217.87-41992.82j** | -74.72-173.91j | **57.69-100.07j** | -| line2 | c | line | -0.49-0.21j | -0+0j | 14.92-126.89j | -0j | -117.06+208.26j | -120.25+224.73j | -| line2 | n | line | 0.1+0j | -0+0j | 0.15-0.85j | -0+0j | 1.63-8.2j | 4.88-24.6j | +| branch_id | phase | branch_type | current1 | current2 | power1 | power2 | potential1 | potential2 | +| :-------- | :---- | :---------- | -----------------: | --------------: | -----------------: | ----------------------: | --------------: | ----------------: | +| line1 | a | line | 374.19+65.47j | -374.2-65.22j) | 86414.44-15119.6j | -69427.92+23726.69j | 230.94-0j | 190.79-30.15j | +| line1 | b | line | -373.43-65.15j | 373.71+64.99j) | 56149.99+67164.05j | -39212.61-58608.72j | -115.47-200j | -75.38-169.94j | +| line1 | c | line | -0.88-0.32j | 0.61+0.24j) | 37.17-214.38j | -22.32+155.56j | -115.47+200j | -116.82+208.22j | +| line1 | n | line | 0.16-0.01j | -0.13-0j) | 0j | -0.17+1.03j | 0j | 1.38-8.15j | +| line2 | a | line | **374.2+65.22j** | -374.11-64.94j) | 69427.92-23726.69j | **-15076.23+41188.79j** | 190.79-30.15j | **57.67-100.09j** | +| line2 | b | line | **-373.71-64.99j** | 374.11+64.94j) | 39212.61+58608.72j | **15076.23-41188.79j** | -75.38-169.94j | **57.67-100.09j** | +| line2 | c | line | -0.61-0.24j | -0j | 22.32-155.56j | -0-0j | -116.82+208.22j | -119.55+224.61j | +| line2 | n | line | 0.13+0j | -0j | 0.17-1.03j | -0j | 1.38-8.15j | 4.18-24.45j | Looking at the line results of the second bus of the line `line2`, which is `bus2` where we added the short-circuit, one can notice that: @@ -119,20 +119,20 @@ short-circuit then create a new one between phases "a", "b", and "c". >>> en = create_network() >>> en.buses["b2"].add_short_circuit("a", "b", "c") >>> en.solve_load_flow() -(1, 1.23437343475878e-07) +(1, 6.572520305780927e-13) >>> en.res_branches ``` | branch_id | phase | branch_type | current1 | current2 | power1 | power2 | potential1 | potential2 | | :-------- | :---- | :---------- | --------------: | --------------: | -----------------: | ------------------: | -------------: | --------------: | -| line1 | a | line | 371.74-146.3j | -371.55+146.39j | 85849.21+33785.73j | -63525.86-24647.04j | 230.94-0j | 170.63-0.89j | -| line1 | b | line | -325.13-309.42j | 325.11+309.42j | 99425.42+29296.79j | -75755.18-20038.43j | -115.47-200j | -91.49-148.71j | -| line1 | c | line | -46.49+455.59j | 46.51-455.8j | 96487.73+43308.59j | -75409.94-31858.46j | -115.47+200j | -85.88+156.68j | -| line1 | n | line | -0.12+0.12j | -0.07-0.01j | 0j | -0.4+0.53j | 0j | 6.74-7.09j | -| line2 | a | line | 371.55-146.39j | -371.59+146.56j | 63525.86+24647.04j | 3541.55-1646.58j | 170.63-0.89j | **-6.74+7.09j** | -| line2 | b | line | -325.11-309.42j | 325.28+309.3j | 75755.18+20038.43j | 1.42+4388.76j | -91.49-148.71j | **-6.74+7.09j** | -| line2 | c | line | -46.51+455.8j | 46.31-455.86j | 75409.94+31858.46j | -3542.97-2742.18j | -85.88+156.68j | **-6.74+7.09j** | -| line2 | n | line | 0.07+0.01j | -0-0j | 0.4-0.53j | 0j | 6.74-7.09j | 20.21-21.26j | +| line1 | a | line | 364.42-152.4j | -364.45+152.64j | 84159.75+35195.32j | -62323.26-24107.78j | 230.94-0j | 169.06-4.66j | +| line1 | b | line | -329.25-298.27j | 329.5+298.09j | 97671.94+31407.98j | -74421.29-19633.88j | -115.47-200j | -94.56-145.13j | +| line1 | c | line | -35.27+450.66j | 35.03-450.73j | 94203.88+44984.19j | -73584.22-31005.25j | -115.47+200j | -80.99+156.96j | +| line1 | n | line | 0.11-0.01j | -0.08-0.01j | 0j | -0.5+0.64j | 0j | 6.47-7.18j | +| line2 | a | line | 364.45-152.64j | -364.48+152.85j | 62323.26+24107.78j | 3461.67-1626.3j | 169.06-4.66j | **-6.49+7.18j** | +| line2 | b | line | -329.5-298.09j | 329.7+297.94j | 74421.29+19633.88j | 1.41+4300.23j | -94.56-145.13j | **-6.49+7.18j** | +| line2 | c | line | -35.03+450.73j | 34.78-450.79j | 73584.22+31005.25j | -3463.08-2673.93j | -80.99+156.96j | **-6.49+7.18j** | +| line2 | n | line | 0.08+0.01j | -0j | 0.5-0.64j | -0j | 6.47-7.18j | 19.44-21.56j | Now the potentials of the three phases are equal and the currents and powers add up to zero at the bus where the short-circuit is applied. @@ -147,20 +147,20 @@ between phase "a" and ground. >>> # ground MUST be passed as a keyword argument ... en.buses["b2"].add_short_circuit("a", ground=en.grounds["gnd"]) >>> en.solve_load_flow() -(2, 1.68697431436484e-07) +(1, 2.464140003155535e-13) >>> en.res_branches ``` -| branch_id | phase | branch_type | current1 | current2 | power1 | power2 | potential1 | potential2 | -| :-------- | :---- | :---------- | ------------: | ------------: | -----------------: | ------------------: | --------------: | --------------: | -| line1 | a | line | 96.01-188.55j | -95.8+188.65j | 22173.11+43543.54j | -16858.62-29476.54j | 230.94+0j | 160.3-7.97j | -| line1 | b | line | 0.53-0.42j | -0.55+0.42j | 22.6-154j | -3.39+192.42j | -115.47-200j | -166.27-225.68j | -| line1 | c | line | -0.41-0.51j | 0.43+0.28j | -54.5-141.67j | -21.22+121.92j | -115.47+200j | -162.05+176.44j | -| line1 | n | line | -0.04-0.07j | -0.17+0.18j | 0j | 4.2+13.63j | 0j | -50.72-25.69j | -| line2 | a | line | 95.8-188.65j | -95.91+188.9j | 16858.62+29476.54j | 0j | 160.3-7.97j | **0j** | -| line2 | b | line | 0.55-0.42j | 0j | 3.39-192.42j | -0+0j | -166.27-225.68j | -267.74-277.02j | -| line2 | c | line | -0.43-0.28j | -0+0j | 21.22-121.92j | 0j | -162.05+176.44j | -255.11+129.31j | -| line2 | n | line | 0.17-0.18j | 0j | -4.2-13.63j | -0+0j | -50.72-25.69j | -152.11-77.03j | +| branch_id | phase | branch_type | current1 | current2 | power1 | power2 | potential1 | potential2 | +| :-------- | :---- | :---------- | ------------: | -------------: | -----------------: | ----------------: | --------------: | --------------: | +| line1 | a | line | 95.83-188.13j | -95.86+188.37j | 22130.38+43446.19j | -16871.5-29433.8j | 230.94+0j | 160.32-7.98j | +| line1 | b | line | 0.96-0.74j | -0.65+0.52j | 36.74-277.43j | -10.48+232.63j | -115.47-200j | -163.66-224.36j | +| line1 | c | line | -0.81-0.43j | 0.55+0.33j | 8.47-212.03j | -29.32+150.27j | -115.47+200j | -159.37+177.78j | +| line1 | n | line | 0.24-0.25j | -0.21+0.22j | 0j | 4.52+15.58j | 0j | -48.11-24.34j | +| line2 | a | line | 95.86-188.37j | -95.99+188.69j | 16871.5+29433.8j | -0j | 160.32-7.98j | **0j** | +| line2 | b | line | 0.65-0.52j | 0j | 10.48-232.63j | -0-0j | -163.66-224.36j | -265.1-275.72j | +| line2 | c | line | -0.55-0.33j | -0j | 29.32-150.27j | -0-0j | -159.37+177.78j | -252.37+130.63j | +| line2 | n | line | 0.21-0.22j | -0j | -4.52-15.58j | -0-0j | -48.11-24.34j | -149.45-75.72j | ```pycon >>> en.res_grounds @@ -168,7 +168,7 @@ between phase "a" and ground. | ground_id | potential | | :-------- | --------: | -| gnd | 0j | +| gnd | 0+0j | Here the potential at phase "a" of bus `b2` is zero, equal to the ground potential. The sum of the currents in the other phases is also zero indicating that the current of phase "a" went through the ground. From dd0a9f832583449bc9b72ebbcb85073f5a68cda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Vinot?= Date: Wed, 24 Jan 2024 09:54:49 +0100 Subject: [PATCH 49/51] Update bug template report --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 83c26ded..63f1ab67 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -16,7 +16,7 @@ Minimal Working Example to understand the problem from roseau.load_flow import * # Your code here -# Please do not add username/or password here +# Please do not add username, password or API keys here ``` **Expected behavior** From b6267830bd8ec27e3d88a1669b985c6cf999f895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Vinot?= Date: Wed, 24 Jan 2024 09:55:29 +0100 Subject: [PATCH 50/51] Remove conda build for now --- .github/workflows/conda.yml | 68 ------------------------------------- 1 file changed, 68 deletions(-) delete mode 100644 .github/workflows/conda.yml diff --git a/.github/workflows/conda.yml b/.github/workflows/conda.yml deleted file mode 100644 index 30a5d483..00000000 --- a/.github/workflows/conda.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Conda - -on: - push: - branches: [main] - tags: - - "*" - pull_request: - branches: [main] - -env: - CI: true - -jobs: - build: - runs-on: "ubuntu-latest" - strategy: - fail-fast: false - matrix: - python-version: ["3.12"] - - steps: - - uses: actions/checkout@v4 - with: - lfs: false - - - name: Create LFS file list - run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id - - - name: Cache git LFS - uses: actions/cache@v3 - with: - path: .git/lfs - key: git-lfs-v1-${{ matrix.python-version }}-${{ hashFiles('.lfs-assets-id') }} - restore-keys: | - git-lfs-v1-${{ matrix.python-version }} - git-lfs-v1 - git-lfs - - - name: Git LFS - run: | - git lfs checkout - git lfs pull - git lfs prune --verify-remote - - - uses: conda-incubator/setup-miniconda@v3 - with: - auto-update-conda: true - python-version: ${{ matrix.python-version }} - miniforge-variant: Mambaforge - use-mamba: true - - - name: Conda Build - id: conda-build - shell: bash -l {0} - run: | - mamba config --add channels conda-forge - mamba config --set channel_priority strict - mamba install --channel conda-forge conda-build conda-verify - mkdir -p dist/ - mamba build --output-folder dist/ conda/ - echo "CONDA_ARCHIVE=$(mamba build --output-folder dist/ --output conda/)" >> $GITHUB_OUTPUT - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: package-python-${{ matrix.python-version }} - path: ${{ steps.conda-build.outputs.CONDA_ARCHIVE }} From 5569374673fc366adb26cc4804496d7f33de1a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Vinot?= Date: Wed, 24 Jan 2024 09:57:22 +0100 Subject: [PATCH 51/51] Update ci workflow --- .github/workflows/ci.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f8712ad..b423289e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,15 +2,11 @@ name: CI on: push: - # TODO: rerun on develop when we have the required dependencies - # branches: [main, develop] - branches: [main] + branches: [main, develop] tags: - "*" pull_request: - # TODO: rerun on develop when we have the required dependencies - # branches: [main, develop] - branches: [main] + branches: [main, develop] env: CI: true