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** diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a6e9cdc..b423289e 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 @@ -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" @@ -59,11 +59,13 @@ 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 + env: + ROSEAU_LOAD_FLOW_LICENSE_KEY: ${{ secrets.ROSEAU_LOAD_FLOW_LICENSE_KEY }} - 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 deleted file mode 100644 index 7fcd9e14..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.9"] - - 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@v2 - 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@v3 - 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 deleted file mode 100644 index 65d867c6..00000000 --- a/.github/workflows/doc.yml +++ /dev/null @@ -1,101 +0,0 @@ -name: Documentation - -on: - push: - branches: ["main", "develop"] - 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@v3 - - - name: Install poetry - run: pipx install poetry - - - name: Set up Python 3.12 - uses: actions/setup-python@v4 - 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@v2 - with: - path: "build/html/" - - - name: Upload artifact - uses: actions/upload-artifact@v3 - 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@v2 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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7a8257c..e7f68b1a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +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-builtin-literals - id: check-json - exclude: ^.vscode/ - id: check-merge-conflict - id: check-toml - id: check-yaml @@ -17,7 +15,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.14 # keep in sync with pyproject.toml hooks: - id: ruff types_or: [python, pyi, jupyter] @@ -28,12 +26,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.10.1] # keep in sync with black above + additional_dependencies: [black==23.12.1] - repo: https://github.com/pre-commit/mirrors-prettier rev: v3.1.0 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/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..4ad513a2 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", @@ -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/LICENSE.md b/LICENSE.md index aed1a24a..2fd48151 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2023, 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/README.md b/README.md index b878d1d3..3a9ee7f2 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,33 @@ [![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 -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://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://roseau-load-flow.roseautechnologies.com/usage/Getting_Started.html). +You can find the complete documentation at https://roseau-load-flow.roseautechnologies.com/. -- [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 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](mailto:contact@roseautechnologies.com). -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. + +Read more at [License](https://roseau-load-flow.roseautechnologies.com/License.html). -# 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 +39,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 1285b8e3..dbac36a5 100644 --- a/conda/environment.yml +++ b/conda/environment.yml @@ -10,8 +10,9 @@ 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 - - matplotlib >=3.7.2 + - typing_extensions >=4.6.2 + - 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 3ce83ce7..4bda245d 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 }}" @@ -32,11 +32,12 @@ 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 - - matplotlib >=3.7.2 + - typing_extensions >=4.6.2 + - pyproj >=3.3.0 + - matplotlib-base >=3.7.2 - networkx >=3.0.0 + - certifi >=2023.5.7 + - platformdirs >=4.0.0 test: imports: @@ -45,13 +46,17 @@ 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/ 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/Changelog.md b/doc/Changelog.md index 6eea5d84..e3be1bc6 100644 --- a/doc/Changelog.md +++ b/doc/Changelog.md @@ -1,5 +1,51 @@ # Changelog +## Version 0.7.0-alpha + +```{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 + 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 - {gh-pr}`149` {gh-issue}`145` Add custom pint wrapper for better handling of pint arrays. 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/License.md b/doc/License.md new file mode 100644 index 00000000..56af9c2e --- /dev/null +++ b/doc/License.md @@ -0,0 +1,94 @@ +(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 +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 +[contact@roseautechnologies.com](mailto: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.** + ```{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 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 + + lf.activate_license(Path("my_license_key.txt").read_text().strip()) + + # 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/_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 diff --git a/doc/conf.py b/doc/conf.py index 098cb6b9..57003592 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -18,12 +18,12 @@ # -- Project information ----------------------------------------------------- project = "Roseau Load Flow" -copyright = "2022--2023, Roseau Technologies SAS" +copyright = "2018, Roseau Technologies SAS" # 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 --------------------------------------------------- @@ -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 ------------------------------------------------- @@ -126,10 +127,8 @@ "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), "matplotlib": ("https://matplotlib.org/stable/", None), "networkx": ("https://networkx.org/documentation/stable/", None), } diff --git a/doc/index.md b/doc/index.md index 49e1c4c7..98cc6e58 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} @@ -65,6 +65,18 @@ caption: Solvers Solvers ``` +## License + +Read more about the license of this project: + +```{toctree} +--- +maxdepth: 2 +caption: License +--- +License +``` + ## Changelog ```{toctree} 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/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/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/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/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/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/doc/usage/Connecting_Elements.md b/doc/usage/Connecting_Elements.md index 4c27df69..9492821b 100644 --- a/doc/usage/Connecting_Elements.md +++ b/doc/usage/Connecting_Elements.md @@ -52,9 +52,8 @@ 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) -2 +>>> en.solve_load_flow() +(2, 1.8595619621919468e-07) ``` ## Disconnecting an element @@ -124,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,8 +160,77 @@ 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) -3 +>>> en.solve_load_flow() +(3, 5.209166431541234e-13) >>> abs(new_load.res_voltages) -array([216.54956226]) +array([214.8358114]) ``` + +## 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. diff --git a/doc/usage/Flexible_Loads.md b/doc/usage/Flexible_Loads.md index 1b4a11ef..ba31fb35 100644 --- a/doc/usage/Flexible_Loads.md +++ b/doc/usage/Flexible_Loads.md @@ -128,9 +128,8 @@ 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) -2 +>>> en.solve_load_flow() +(2, 1.8595619621919468e-07) >>> abs(load_bus3.res_voltages) array([243.66463933, 232.20612714, 233.55093129]) ``` @@ -177,8 +176,8 @@ 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) -4 +>>> en.solve_load_flow() +(4, 1.453686784545e-07) >>> abs(load_bus3.res_voltages) array([243.08225748, 232.46046866, 233.62854073]) ``` @@ -242,8 +241,8 @@ production is totally shut down. The load flow can be solved again. ```pycon ->>> en.solve_load_flow(auth=auth) -6 +>>> en.solve_load_flow() +(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 6d8de653..36abb6b7 100644 --- a/doc/usage/Getting_Started.md +++ b/doc/usage/Getting_Started.md @@ -140,49 +140,20 @@ 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. -``` - -```pycon ->>> auth = ("username", "password") ->>> en.solve_load_flow(auth=auth) -2 -``` - -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. +Then, the load flow can be solved by calling the `solve_load_flow` method of the `ElectricalNetwork` ```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} +>>> en.solve_load_flow() +(2, 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)= @@ -504,8 +475,8 @@ unbalanced situation. ```pycon >>> load.powers = Q_([15, 0, 0], "kVA") ->>> en.solve_load_flow(auth=auth) -3 +>>> en.solve_load_flow() +(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 8c8a8e5b..ae4e482b 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=120, +... height=Q_(-1.5, "m"), +... external_diameter=Q_(50, "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,32 +69,37 @@ 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. +```{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) -1 +>>> en.solve_load_flow() +(1, 3.339550858072471e-13) >>> 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 | 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: @@ -107,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.solve_load_flow(auth=auth) -1 +>>> en = create_network() +>>> en.buses["b2"].add_short_circuit("a", "b", "c") +>>> en.solve_load_flow() +(1, 6.572520305780927e-13) >>> 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 | 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. @@ -134,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.solve_load_flow(auth=auth) -1 +... en.buses["b2"].add_short_circuit("a", ground=en.grounds["gnd"]) +>>> en.solve_load_flow() +(1, 2.464140003155535e-13) >>> 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 | 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 @@ -159,9 +168,9 @@ between phase "a" and ground. | ground_id | potential | | :-------- | --------: | -| gnd | 0j | +| gnd | 0+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 @@ -171,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. @@ -180,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/poetry.lock b/poetry.lock index 2d058cdc..78962dce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,14 +1,14 @@ -# 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" -version = "0.7.13" -description = "A configurable sidebar-enabled Sphinx theme" +version = "0.7.16" +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.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] [[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,54 +38,55 @@ 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"] [[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"] @@ -334,63 +335,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 +417,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]] @@ -519,7 +520,6 @@ certifi = "*" click = ">=8.0,<9.0" click-plugins = ">=1.0" cligj = ">=0.5" -importlib-metadata = {version = "*", markers = "python_version < \"3.10\""} setuptools = "*" six = "*" @@ -531,59 +531,59 @@ test = ["Fiona[s3]", "pytest (>=7)", "pytest-cov", "pytz"] [[package]] name = "fonttools" -version = "4.45.1" +version = "4.47.2" 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.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] -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"] @@ -613,13 +613,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] @@ -631,13 +631,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] @@ -665,43 +665,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" @@ -715,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] @@ -883,71 +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-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"}, - {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]] @@ -991,7 +954,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" @@ -1089,47 +1051,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]] @@ -1145,36 +1107,40 @@ files = [ [[package]] name = "pandas" -version = "2.1.3" +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.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.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] @@ -1185,108 +1151,126 @@ 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" -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] @@ -1294,23 +1278,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] @@ -1334,13 +1320,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] @@ -1454,13 +1440,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] @@ -1598,99 +1584,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]] @@ -1715,77 +1706,58 @@ 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" +name = "roseau-load-flow-engine" +version = "0.12.0a0" +description = "Highly capable three-phase load flow solver" optional = false -python-versions = "*" +python-versions = ">=3.10,<4.0" 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"}, + {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] -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" -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)"] +numpy = ">=1.21.5" [[package]] name = "ruff" -version = "0.1.6" +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.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.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]] 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] @@ -1900,7 +1872,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" @@ -2011,56 +1982,51 @@ 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]] 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] 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" [[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]] @@ -2079,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]] @@ -2111,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]] @@ -2158,24 +2118,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]] @@ -2196,13 +2156,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] @@ -2214,26 +2174,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 = "ed57186be9cfa88d048a30d9d37120cd0ecbbebd74197bc207235f1cb88b05ab" +python-versions = "^3.10" +content-hash = "8a04ff082cf90ef142a2e820cb9977688d7a669f8b9ee14ad7d851b78daecf9e" diff --git a/pyproject.toml b/pyproject.toml index 9004aaad..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 ", @@ -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 = [ @@ -26,7 +26,7 @@ packages = [ ] classifiers = [ "Development Status :: 3 - Alpha", - "Programming Language :: Python :: 3.9", + # "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", @@ -38,20 +38,18 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry.dependencies] -python = "^3.9" -numpy = [ - { version = ">=1.21.5", python = "<3.12" }, - { version = ">=1.26.0", python = ">=3.12,<3.13" }, - { version = "*", python = ">=3.13" }, -] +python = "^3.10" +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" +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 } @@ -66,14 +64,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.14" # keep in sync with .pre-commit-config.yaml [tool.poetry.group.doc.dependencies] sphinx = "^7.0.1" @@ -88,22 +85,16 @@ 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"] - -[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"] @@ -135,13 +126,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" +addopts = "--color=yes -n=0 --import-mode=importlib" testpaths = ["roseau/load_flow/"] filterwarnings = [ 'ignore:.*utcfromtimestamp:DeprecationWarning:dateutil.*', # dateutil is imported by pandas, not us diff --git a/roseau/load_flow/__about__.py b/roseau/load_flow/__about__.py index ce864993..dd7b9c7b 100644 --- a/roseau/load_flow/__about__.py +++ b/roseau/load_flow/__about__.py @@ -7,9 +7,9 @@ "Victor Gouin", ) ) -__copyright__ = "Roseau Technologies 2018--2023" +__copyright__ = "Roseau Technologies 2018" __credits__ = "Roseau Technologies" -__license__ = "Proprietary" +__license__ = "BSD-3-Clause" __maintainer__ = "Ali Hamdan" __email__ = "ali.hamdan@roseautechnologies.com" __status__ = "In development" 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/_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/_solvers.py b/roseau/load_flow/_solvers.py new file mode 100644 index 00000000..838d0e73 --- /dev/null +++ b/roseau/load_flow/_solvers.py @@ -0,0 +1,231 @@ +import logging +from abc import ABC, abstractmethod +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 +from roseau.load_flow.typing import JsonDict, Solver +from roseau.load_flow_engine.cy_engine import CyAbstractSolver, CyNewton, CyNewtonGoldstein + +logger = logging.getLogger(__name__) + +_SOLVERS_PARAMS: dict[Solver, list[str]] = { + "newton": [], + "newton_goldstein": ["m1", "m2"], +} +SOLVERS = list(_SOLVERS_PARAMS) + +if TYPE_CHECKING: + from roseau.load_flow.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 + """ + lic = get_license() + if lic is None: + activate_license(None) + 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 + + @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()} + + 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) + + 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 + 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 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} 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/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/converters.py b/roseau/load_flow/converters.py index 1ebeafe3..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))] + return _voltage_cache[phases] 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 31d8ed0d..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 @@ -22,7 +20,6 @@ class RoseauLoadFlowExceptionCode(Enum): # Buses BAD_BUS_ID = auto() - BAD_BUS_TYPE = auto() BAD_POTENTIALS_SIZE = auto() BAD_VOLTAGES = auto() BAD_VOLTAGES_SIZE = auto() @@ -75,23 +72,20 @@ 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() 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() @@ -111,44 +105,21 @@ class RoseauLoadFlowExceptionCode(Enum): # Import Error IMPORT_ERROR = auto() - @classmethod - def package_name(cls) -> str: - return "roseau.load_flow" - - def __str__(self) -> str: - return f"{self.package_name()}.{self.name}".lower() + # License errors + LICENSE_ERROR = auto() 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/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/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/license.py b/roseau/load_flow/license.py new file mode 100644 index 00000000..e5f020d0 --- /dev/null +++ b/roseau/load_flow/license.py @@ -0,0 +1,111 @@ +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"] + +# Cache the license object. Cache cleared when the license is deactivated +_license: "License | None" = None + + +# +# License class accessor +# +class License: + """A class to access the main data of the License.""" + + def __init__(self, cy_license: CyLicense) -> None: + """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. 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 or ``None`` if the license has no expiry date.""" + 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 + + @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 + + @property + def machine_fingerprint(self) -> str: + """The anonymized machine fingerprint for license validation.""" + return self.cy_license.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 = None) -> None: + """Activate a license in the current process. + + Args: + key: + 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", "") + try: + cy_activate_license(key=key, cacert_filepath=certifi.where(), cache_folderpath=user_cache_dir()) + except RuntimeError as e: + 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 + + +def deactivate_license() -> None: + """Deactivate the currently active license.""" + global _license + cy_deactivate_license() + _license = None + + +def get_license() -> License | None: + """Get the currently active license or ``None`` if no license is activated.""" + 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 diff --git a/roseau/load_flow/models/branches.py b/roseau/load_flow/models/branches.py index ea5fb888..accb2f5a 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. @@ -60,13 +60,13 @@ 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: 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}" @@ -76,7 +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]: + 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 @@ -85,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 @@ -119,6 +144,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 # @@ -143,6 +186,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 b84b256b..bf5e6a8c 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 @@ -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.cy_engine import CyBus logger = logging.getLogger(__name__) @@ -35,10 +36,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. @@ -74,22 +75,35 @@ 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) self.potentials = potentials self.geometry = geometry - self._min_voltage: Optional[float] = None - self._max_voltage: Optional[float] = None - self.min_voltage = min_voltage - self.max_voltage = max_voltage - - self._res_potentials: Optional[ComplexArray] = None + self._min_voltage: float | None = None + self._max_voltage: float | None = None + 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]] = [] + self._n = len(self._phases) + self._initialized = initialized + 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]: @@ -105,8 +119,14 @@ 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 + 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: + 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 @@ -141,13 +161,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 +180,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 +199,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 +241,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 +294,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: @@ -327,7 +347,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: @@ -340,6 +360,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 { @@ -365,11 +386,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] @@ -391,11 +414,13 @@ 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.""" 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 = [] diff --git a/roseau/load_flow/models/core.py b/roseau/load_flow/models/core.py index b117105d..66d3335c 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 @@ -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.cy_engine import CyElement if TYPE_CHECKING: from roseau.load_flow.network import ElectricalNetwork @@ -39,7 +40,9 @@ 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 + self._cy_element: CyElement | None = None + self._fetch_results = False @property def network(self) -> Optional["ElectricalNetwork"]: @@ -47,7 +50,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 @@ -125,13 +128,16 @@ 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""" 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: @@ -160,10 +166,11 @@ def _res_getter(self, value: Optional[_T], warning: bool) -> _T: category=UserWarning, stacklevel=2, ) + self._fetch_results = False return value @staticmethod - def _parse_geometry(geometry: Union[str, None, Any]) -> Optional[BaseGeometry]: + 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/grounds.py b/roseau/load_flow/models/grounds.py index 083534af..95d554e0 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 @@ -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.cy_engine import CyGround if TYPE_CHECKING: from roseau.load_flow.models.buses import Bus @@ -44,12 +45,15 @@ 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 + 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: + if self._fetch_results: + self._res_potential = self._cy_element.get_potentials(1)[0] return self._res_getter(self._res_potential, warning) @property @@ -85,6 +89,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 @@ -104,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 6a0a4d3e..a2de8314 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 @@ -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.cy_engine import CyShuntLine, CySimplifiedLine, CySwitch logger = logging.getLogger(__name__) @@ -38,8 +39,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. @@ -83,9 +84,17 @@ 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._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""" @@ -95,7 +104,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 +112,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 +153,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. @@ -163,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 @@ -203,7 +213,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 @@ -224,14 +234,35 @@ 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, + 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 + ) + self._cy_connect() + if parameters.with_shunt: + 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,)) def length(self) -> Q_[float]: + """The length of the line (in km).""" return self._length @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) @@ -239,15 +270,19 @@ def length(self, value: Union[float, Q_[float]]) -> None: self._length = value self._invalidate_network_results() + if self._cy_element is not None: + # Reassign the same parameters with the new length + self.parameters = self.parameters + @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 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) @@ -257,7 +292,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) @@ -273,21 +308,32 @@ def parameters(self, value: LineParameters) -> None: self._parameters = value self._invalidate_network_results() + 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"), + ) + 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]: - """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) -> Optional[Q_[float]]: - """The maximum current loading of the line in A.""" + 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. return self.parameters.max_current @@ -309,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: @@ -319,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]: @@ -343,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: @@ -355,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: @@ -366,11 +412,11 @@ 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 - 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..f5c60061 100644 --- a/roseau/load_flow/models/lines/parameters.py +++ b/roseau/load_flow/models/lines/parameters.py @@ -1,6 +1,8 @@ import logging import re -from typing import NoReturn, Optional, Union +from importlib import resources +from pathlib import Path +from typing import NoReturn import numpy as np import numpy.linalg as nplin @@ -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: Optional[ComplexArrayLike2D] = None, - max_current: Optional[float] = None, + 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: @@ -106,13 +146,33 @@ 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") + @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: Optional[Union[float, Q_[float]]]) -> None: + def max_current(self, value: float | Q_[float] | None) -> None: self._max_current = value @classmethod @@ -120,15 +180,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 +241,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. @@ -299,14 +359,15 @@ def _sym_to_zy( def from_geometry( cls, id: Id, + *, 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, + conductor_type: ConductorType | None = None, + insulator_type: InsulatorType | None = None, + section: 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, ) -> Self: """Create line parameters from its geometry. @@ -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( @@ -500,10 +610,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 @@ -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: Optional[Union[float, Q_[float]]] = None) -> Self: - """Method to get the electrical parameters of a MV line from its canonical name. + def from_name_mv(cls, name: str, max_current: float | Q_[float] | None = None) -> Self: + """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: Optional[Union[float, Q_[float]]] # 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: Optional[Union[float, Q_[float]]] 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/flexible_parameters.py b/roseau/load_flow/models/loads/flexible_parameters.py index d687fbae..76050ad0 100644 --- a/roseau/load_flow/models/loads/flexible_parameters.py +++ b/roseau/load_flow/models/loads/flexible_parameters.py @@ -1,22 +1,16 @@ 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 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 logger = logging.getLogger(__name__) @@ -48,11 +42,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. @@ -89,6 +83,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.""" @@ -189,7 +186,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 +217,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 +249,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"``. @@ -380,6 +377,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.""" @@ -455,18 +453,15 @@ 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, 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. @@ -494,11 +489,20 @@ 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, + 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,)) @@ -508,7 +512,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." @@ -521,6 +525,8 @@ def s_max(self, value: Union[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 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,)) @@ -530,7 +536,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." @@ -542,6 +548,8 @@ def q_min(self, value: Optional[Union[float, Q_[float]]]) -> None: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_FLEXIBLE_PARAMETER_VALUE) self._q_min = value + 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,)) @@ -551,7 +559,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." @@ -563,6 +571,8 @@ def q_max(self, value: Optional[Union[float, Q_[float]]]) -> None: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_FLEXIBLE_PARAMETER_VALUE) self._q_max = value + 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: @@ -572,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, ) @@ -582,9 +592,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, @@ -626,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, ) @@ -638,9 +648,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, @@ -679,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, ) @@ -691,13 +701,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, @@ -751,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, @@ -765,15 +775,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, @@ -837,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, @@ -852,16 +862,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, @@ -924,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, @@ -940,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( @@ -980,79 +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: Union[complex, Q_[complex]], - solve_kwargs: Optional[JsonDict] = 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) - - def _compute_powers( - self, auth: Authentication, voltages: ComplexArrayLike1D, power: complex, solve_kwargs: Optional[JsonDict] - ) -> ComplexArray: - from roseau.load_flow import Bus, ElectricalNetwork, PotentialRef, PowerLoad, VoltageSource + return self._compute_powers(voltages=voltages, power=power) + def _compute_powers(self, voltages: ComplexArrayLike1D, power: complex) -> ComplexArray: # 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]) - - return np.array(res_flexible_powers, dtype=np.complex128) + 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: 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, + voltages_labels_mask: NDArray[np.bool_] | 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. @@ -1062,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). @@ -1092,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) @@ -1114,7 +1078,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( @@ -1148,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: 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, ) -> 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. @@ -1173,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). @@ -1195,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 @@ -1214,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: 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, ) -> 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. @@ -1239,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). @@ -1261,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 dd92dc4d..9178347a 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 @@ -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.cy_engine import ( + CyAdmittanceLoad, + CyCurrentLoad, + CyDeltaAdmittanceLoad, + CyDeltaCurrentLoad, + CyDeltaFlexibleLoad, + CyDeltaPowerLoad, + CyFlexibleLoad, + CyPowerLoad, +) logger = logging.getLogger(__name__) @@ -23,18 +33,13 @@ 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 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: @@ -68,8 +73,9 @@ def __init__(self, id: Id, bus: Bus, *, phases: Optional[str] = None, **kwargs: raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_PHASE) self._connect(bus) - self.phases = phases - self.bus = bus + 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 @@ -78,12 +84,22 @@ 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 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.""" @@ -95,6 +111,8 @@ def voltage_phases(self) -> list[str]: return calculate_voltage_phases(self.phases) def _res_currents_getter(self, warning: bool) -> ComplexArray: + 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 @@ -107,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" @@ -148,13 +164,21 @@ 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 # 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.""" @@ -171,18 +195,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) @@ -210,8 +232,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 +276,27 @@ def __init__( self._flexible_params = flexible_params self.powers = powers - self._res_flexible_powers: Optional[ComplexArray] = None + 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) -> Optional[list[FlexibleParameter]]: + def flexible_params(self) -> list[FlexibleParameter] | None: return self._flexible_params @property @@ -275,7 +314,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"): @@ -304,8 +343,12 @@ def powers(self, value: ComplexArrayLike1D) -> None: raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_S_VALUE) self._powers = value self._invalidate_network_results() + if self._cy_element is not None: + self._cy_element.update_powers(self._powers) def _res_flexible_powers_getter(self, warning: bool) -> ComplexArray: + 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 @@ -350,7 +393,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. @@ -373,6 +416,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 +433,8 @@ def currents(self) -> Q_[ComplexArray]: def currents(self, value: ComplexArrayLike1D) -> None: self._currents = self._validate_value(value) self._invalidate_network_results() + 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() @@ -402,7 +452,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. @@ -425,6 +475,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 +492,8 @@ def impedances(self) -> Q_[ComplexArray]: def impedances(self, impedances: ComplexArrayLike1D) -> None: self._impedances = self._validate_value(impedances) self._invalidate_network_results() + 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() @@ -446,8 +503,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 daba1a7f..3a01ee0e 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 @@ -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.cy_engine import CyDeltaPotentialRef, CyPotentialRef logger = logging.getLogger(__name__) @@ -25,7 +26,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: @@ -56,15 +57,34 @@ def __init__(self, id: Id, element: Union[Bus, Ground], *, phase: Optional[str] 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: Optional[complex] = 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) + 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})" + @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: + if self._fetch_results: + self._res_current = self._cy_element.get_current() return self._res_getter(self._res_current, warning) @property @@ -97,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 49a5e7bc..a7f003dd 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 @@ -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.cy_engine import CyDeltaVoltageSource, CyVoltageSource logger = logging.getLogger(__name__) @@ -22,7 +23,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. @@ -69,12 +70,19 @@ 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) + 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: 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 @@ -83,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]: @@ -98,6 +116,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 self._cy_element is not None: + self._cy_element.update_voltages(self._voltages) @property def voltage_phases(self) -> list[str]: @@ -105,6 +125,8 @@ def voltage_phases(self) -> list[str]: return calculate_voltage_phases(self.phases) def _res_currents_getter(self, warning: bool) -> ComplexArray: + 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 @@ -134,13 +156,21 @@ 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 # 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.""" @@ -168,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..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,14 +76,20 @@ def test_short_circuit(): bus.add_short_circuit("a", ground=ground) # ok assert len(bus.short_circuits) == 2 - # With power load - bus.clear_short_circuits() + # 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]) + _ = 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 + assert e.value.code == 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_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_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/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 31ec8d07..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, Optional, Union +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__) @@ -42,14 +40,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 +196,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,)) @@ -312,20 +310,84 @@ 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")) 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 | 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: 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, - ) -> None: - """Print the catalogue of available transformers. + 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 | 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: Union[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/models/transformers/transformers.py b/roseau/load_flow/models/transformers/transformers.py index 3d73ccda..dca30bad 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 @@ -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.cy_engine import ( + CyCenterTransformer, + CyExtendedTransformer, + CyReducedTransformer, + CySingleTransformer, +) logger = logging.getLogger(__name__) @@ -41,9 +47,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. @@ -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 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) @property def parameters(self) -> TransformerParameters: @@ -128,9 +175,12 @@ def parameters(self, value: TransformerParameters) -> None: raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_TRANSFORMER_TYPE) self._parameters = value self._invalidate_network_results() + 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) @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 +195,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 +237,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 +264,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 +303,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..ca53f1e5 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -4,23 +4,21 @@ 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 cycle +from itertools import chain from pathlib import Path -from typing import TYPE_CHECKING, NoReturn, Optional, TypeVar, Union -from urllib.parse import urljoin +from typing import TYPE_CHECKING, NoReturn, TypeVar 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 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,10 +34,10 @@ 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.utils import CatalogueMixin, JsonMixin, _optional_deps, console, palette +from roseau.load_flow.typing import Id, JsonDict, MapOrSeq, Solver, StrPath +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 if TYPE_CHECKING: from networkx import Graph @@ -114,40 +112,10 @@ 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_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 # @@ -159,7 +127,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) @@ -168,14 +135,15 @@ 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.from_dict(data={"name": self._DEFAULT_SOLVER, "params": {}}, network=self) 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: @@ -490,27 +458,24 @@ 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, + max_iterations: int = 20, + tolerance: float = 1e-6, + warm_start: bool = True, solver: Solver = _DEFAULT_SOLVER, - solver_params: Optional[JsonDict] = None, - ) -> int: - """Solve the load flow for this network (Requires internet access). + solver_params: JsonDict | None = None, + ) -> 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 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. + 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. @@ -518,8 +483,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: @@ -532,107 +498,85 @@ 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. """ - 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._create_network() - - # 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__}, - ) + self._check_validity(constructed=False) + self._create_network() # <-- calls _propagate_potentials, no warm start + self._solver.update_network(self) - # 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, - ) + # 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._reset_inputs() + + start = time.perf_counter() + try: + iterations, residual = self._solver.solve_load_flow(max_iterations=max_iterations, tolerance=tolerance) + except RuntimeError as e: + self._handle_error(e) - # 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) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.NO_LOAD_FLOW_CONVERGENCE) + raise RoseauLoadFlowException( + msg, RoseauLoadFlowExceptionCode.NO_LOAD_FLOW_CONVERGENCE, iterations, residual + ) - 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.") - # Dispatch the results - self._results_from_dict(data=results) + # 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 - return self.res_info["iterations"] + # The results are now valid + self._results_valid = True - @staticmethod - def _parse_error(response: Response) -> NoReturn: - """Parse a response when its status is not "ok". + return iterations, residual - 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." + def _handle_error(self, e: RuntimeError) -> NoReturn: + msg = e.args[0] + if msg.startswith("0 "): + 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: - 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) + 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. @@ -692,7 +636,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 +681,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) @@ -769,63 +713,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) - ) - 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) - ) - - 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") - ) + 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: + idx1 = branch.phases1.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: @@ -850,78 +775,58 @@ 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 = [] + 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 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: 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) - ) - 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) - ) - - 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.phases1.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: @@ -983,12 +888,12 @@ 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 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) @@ -1003,8 +908,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: @@ -1044,8 +948,8 @@ 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) - for i1, i2, s1, s2, v1, v2, phase in zip(*currents, *powers, *potentials, branch.phases): + 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) res_dict["current1"].append(i1) @@ -1075,7 +979,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 +1002,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 +1028,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 +1053,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) @@ -1194,11 +1098,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() - # # Internal methods, please do not use # @@ -1222,8 +1121,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 @@ -1242,7 +1145,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) @@ -1261,6 +1164,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() + 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` @@ -1279,6 +1204,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 @@ -1315,8 +1245,67 @@ def _check_validity(self, constructed: bool) -> None: elif element.network != self: element._raise_several_network() + 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() + + 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 + 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 not element._initialized: + bus_n = element._n + 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 isinstance(e, AbstractBranch | Bus): + if isinstance(element, Transformer): + k = element.parameters._ulv / element.parameters._uhv + 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: + 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: @@ -1367,7 +1356,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, @@ -1431,7 +1420,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()], @@ -1454,7 +1442,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, @@ -1476,7 +1464,69 @@ 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 _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. Args: @@ -1491,188 +1541,62 @@ def from_catalogue(cls, name: Union[str, re.Pattern[str]], load_point_name: Unio 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: Optional[Union[str, re.Pattern[str]]] = None, - load_point_name: Optional[Union[str, re.Pattern[str]]] = 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: Optional[Union[str, re.Pattern[str]]], 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/solvers.py b/roseau/load_flow/solvers.py deleted file mode 100644 index 4ef18260..00000000 --- a/roseau/load_flow/solvers.py +++ /dev/null @@ -1,56 +0,0 @@ -import logging -from typing import Optional - -from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode -from roseau.load_flow.typing import JsonDict, Solver - -logger = logging.getLogger(__name__) - -_SOLVERS_PARAMS: dict[Solver, list[str]] = { - "newton": [], - "newton_goldstein": ["m1", "m2"], -} -SOLVERS = list(_SOLVERS_PARAMS) - - -def check_solver_params(solver: Solver, params: Optional[JsonDict]) -> 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_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 0d8ef238..16a210ea 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 @@ -29,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() @@ -102,15 +100,6 @@ def single_phase_network() -> ElectricalNetwork: @pytest.fixture() def good_json_results() -> dict: return { - "info": { - "solver": "newton", - "tolerance": 1e-06, - "max_iterations": 20, - "warm_start": True, - "status": "success", - "iterations": 1, - "residual": 6.296829377361313e-14, - }, "buses": [ { "id": "bus0", @@ -169,10 +158,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], ], } ], @@ -207,14 +196,29 @@ 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 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 @@ -225,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: @@ -234,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 @@ -272,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, @@ -377,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", @@ -515,139 +518,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 @@ -705,90 +575,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): @@ -868,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): @@ -900,58 +698,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(auth=("", "")) + single_phase_network.solve_load_flow() # Test results of elements # ------------------------ @@ -1145,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( @@ -1229,8 +977,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( @@ -1251,7 +999,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 @@ -1270,40 +1018,37 @@ def test_network_results_warning(small_network: ElectricalNetwork, good_json_res 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 - 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() @@ -1818,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 @@ -1861,72 +1606,74 @@ 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 - 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 + + 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_warm_start = False + propagate_potentials_called = False + reset_inputs_called = 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 + 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) - should_warm_start = False + propagate_potentials_called = False + reset_inputs_called = 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 + 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 - should_warm_start = True + propagate_potentials_called = False + reset_inputs_called = 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=True) - assert small_network.results_to_dict() == good_json_results + 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 - should_warm_start = True + propagate_potentials_called = False + reset_inputs_called = False 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 + 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 - should_warm_start = False + propagate_potentials_called = False + reset_inputs_called = 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" @@ -1934,15 +1681,13 @@ 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 + small_network.solve_load_flow(warm_start=True) + assert propagate_potentials_called + assert not reset_inputs_called def test_short_circuits(): @@ -1960,8 +1705,6 @@ 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 def test_catalogue_data(): @@ -2011,89 +1754,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/tests/test_solvers.py b/roseau/load_flow/tests/test_solvers.py index ac03fa59..a9eadff4 100644 --- a/roseau/load_flow/tests/test_solvers.py +++ b/roseau/load_flow/tests/test_solvers.py @@ -1,29 +1,76 @@ +import contextlib + 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) + + with contextlib.suppress(RoseauLoadFlowException): # No valid license + en.solve_load_flow() + solver = en._solver + assert isinstance(solver, NewtonGoldstein) + + 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 + + with contextlib.suppress(RoseauLoadFlowException): # No valid license + en.solve_load_flow(solver="newton") + assert solver != en._solver + assert isinstance(en._solver, Newton) + + 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/typing.py b/roseau/load_flow/typing.py index fc202af2..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. @@ -54,39 +50,30 @@ """ 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]] +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__ = [ @@ -96,7 +83,6 @@ "ControlType", "ProjectionType", "Solver", - "Authentication", "MapOrSeq", "ComplexArray", "ComplexArrayLike1D", 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/__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/_versions.py b/roseau/load_flow/utils/_versions.py index 8b1cce24..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", "requests")} + 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/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 new file mode 100644 index 00000000..624207c2 --- /dev/null +++ b/roseau/load_flow/utils/log.py @@ -0,0 +1,16 @@ +from typing import Literal + +from roseau.load_flow_engine.cy_engine import cy_set_logging_config + + +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 to set for the solver. + Can be one of: `{"trace", "debug", "info", "warning", "error", "critical"}` + """ + assert verbosity in {"trace", "debug", "info", "warning", "error", "critical"} + # Define the logger at C++ level + cy_set_logging_config(verbosity) diff --git a/roseau/load_flow/utils/mixins.py b/roseau/load_flow/utils/mixins.py index 9f3c3f7d..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 @@ -19,7 +22,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) @@ -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