diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b423289e..75972c68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,8 +5,14 @@ on: branches: [main, develop] tags: - "*" + paths-ignore: + - "doc/**" + - ".vscode/**" pull_request: branches: [main, develop] + paths-ignore: + - "doc/**" + - ".vscode/**" env: CI: true @@ -28,7 +34,7 @@ jobs: run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id - name: Cache git LFS - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: .git/lfs key: git-lfs-v1-${{ matrix.python-version }}-${{ hashFiles('.lfs-assets-id') }} diff --git a/.gitignore b/.gitignore index bd430e34..97ea5f58 100644 --- a/.gitignore +++ b/.gitignore @@ -249,3 +249,4 @@ doc/images/*.pdf roseau/* !roseau/load_flow +node_modules/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e7f68b1a..bc2e50ae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: hooks: - id: poetry-check - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.14 # keep in sync with pyproject.toml + rev: v0.2.1 hooks: - id: ruff types_or: [python, pyi, jupyter] @@ -30,7 +30,8 @@ repos: args: [-l 90] additional_dependencies: [black==23.12.1] - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.1.0 + rev: v4.0.0-alpha.8 hooks: - id: prettier args: ["--print-width", "120"] + require_serial: true diff --git a/README.md b/README.md index 3a9ee7f2..fec8d9f4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Roseau Load Flow ![CI](https://github.com/RoseauTechnologies/Roseau_Load_Flow/workflows/CI/badge.svg) -[![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 with an ergonomic Python API diff --git a/conda/environment.yml b/conda/environment.yml deleted file mode 100644 index dbac36a5..00000000 --- a/conda/environment.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: roseau-load-flow -channels: - - conda-forge - - defaults -dependencies: - - python - - numpy >=1.21.5 - - pandas >=1.4.0 - - geopandas >=0.10.2 - - shapely >=2.0.0 - - regex >=2022.1.18 - - pint >=0.21.0 - - 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 deleted file mode 100644 index 4bda245d..00000000 --- a/conda/meta.yaml +++ /dev/null @@ -1,69 +0,0 @@ -# prettier-ignore -{% set name = "roseau-load-flow" %} -{% set version = "0.7.0-alpha" %} - -package: - name: "{{ name|lower }}" - version: "{{ version }}" - -source: - path: ../ -# url: ../dist/{{ name }}-{{ version }}.tar.gz -# url: https://pypi.io/packages/source/{{ name[0] }}/{{ name }}/{{ name }}-{{ version }}.tar.gz -# sha256: 5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7 - - -build: - number: 0 - script: "{{ PYTHON }} -m pip install . --no-deps --ignore-installed -vv " - noarch: python - -requirements: - host: - - pip - - python - - numpy >=1.21.5 - - poetry - run: - - python - - {{ pin_compatible('numpy') }} - - pandas >=1.4.0 - - geopandas >=0.10.2 - - shapely >=2.0.0 - - regex >=2022.1.18 - - pint >=0.21.0 - - 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: - - roseau - - roseau.load_flow - - 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://roseau-load-flow.roseautechnologies.com/ - -extra: - recipe-maintainers: - - benoit9126 - -# https://www.underworldcode.org/articles/build-conda-packages/ -# conda config --add channels conda-forge -# conda config --set channel_priority strict -# conda build conda -# conda install --use-local roseau_load_flow diff --git a/doc/Changelog.md b/doc/Changelog.md index e3be1bc6..9cc4d342 100644 --- a/doc/Changelog.md +++ b/doc/Changelog.md @@ -1,12 +1,24 @@ # Changelog -## Version 0.7.0-alpha +## Version 0.7.0 ```{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. ``` +- The documentation is moved from GitHub Pages to https://www.roseau-load-flow.roseautechnologies.com/. +- Fix a bug in the engine: it was impossible to change the parameters of center-tapped and single phase transformers. +- {gh-pr}`179` Fix a bug in the propagation of potentials when a center-tapped transformer is used without neutral at + the primary side. +- {gh-pr}`178` {gh-issue}`176` Merge the `results_to_json`, `results_from_json`, `results_to_dict` + and `results_from_dict` methods of the `ElectricalNetwork` and `Element`s classes into the methods + `to_json`, `from_json`, `to_dict` and `from_dict` respectively. The old `results_` methods are + **deprecated** and will be removed in a future release. The new methods will include the results by + default, but you can pass `include_results=False` to exclude them. +- {gh-pr}`175` {gh-issue}`174` Fix JSON serialization of network with line parameters created from the + catalogue. +- {gh-pr}`173` Remove the conda installation option. - {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 diff --git a/doc/Installation.md b/doc/Installation.md index e0d2816f..aadb159e 100644 --- a/doc/Installation.md +++ b/doc/Installation.md @@ -104,38 +104,6 @@ In [1]: %pip install roseau-load-flow This installs the package in the correct environment for the active notebook kernel. -## 3. Using `conda` - -Installations using `conda` is temporarily unavailable. Please use `pip` instead. - - - diff --git a/doc/License.md b/doc/License.md index 56af9c2e..fdaa357c 100644 --- a/doc/License.md +++ b/doc/License.md @@ -23,7 +23,7 @@ Licenses are given **free of charge** for _students and teachers_. Please contac 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 + environment variable is defined, it will automatically be used by the solver to validate the license, no further action is required. **This is the recommended approach.** ```{note} diff --git a/doc/conf.py b/doc/conf.py index 57003592..20276c41 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -23,7 +23,7 @@ # The full version, including alpha/beta/rc tags version = "0.7" -release = "0.7.0-alpha" +release = "0.7.0" # -- General configuration --------------------------------------------------- diff --git a/doc/usage/Connecting_Elements.md b/doc/usage/Connecting_Elements.md index 9492821b..e16f9070 100644 --- a/doc/usage/Connecting_Elements.md +++ b/doc/usage/Connecting_Elements.md @@ -1,8 +1,10 @@ -# (Dis)Connecting elements +(usage-modifying-network)= + +# Modifying a network ## Creating a network -Let's take the electrical network of the [Getting started page](usage-getting-started). +Let's take the electrical network of the [Getting started page](usage-getting-started) as an example. ```pycon >>> import numpy as np @@ -89,15 +91,14 @@ UserWarning: The results of this element may be outdated. Please re-run a load f ``` ```{danger} -The load element `load` doesn't belong to a network and a part of its results is not accessible any more. `res_` -methods may raise errors. +The load element `load` doesn't belong to a network and a some of its results are not accessible +anymore. Accessing `res_` properties may raise errors. ``` ## Connecting an element -Let's create a new line and a new load at the end of this line. - -The new bus and the new load are created first. +Let's extend the network with a new line and add a load at its end. First, we create a new bus and +the new load. ```pycon >>> new_bus = Bus(id="new_bus", phases="abcn") @@ -175,7 +176,7 @@ a voltage source. ### Modifying a voltage source -You can change the voltage of the voltage source: +You can change the voltage of the voltage source using the `voltages` attribute: ```pycon >>> vs.voltages @@ -222,10 +223,10 @@ For a line, you can also change the length: ```pycon >>> line.length 2.0 ->>> line.length = 1.0 +>>> line.length = 1.0 # <-- shorten the line by half >>> line.length 1.0 ->>> line.z_line # <-- the impedance is divided by 2 +>>> line.z_line # <-- the impedance gets 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 ], diff --git a/doc/usage/Getting_Started.md b/doc/usage/Getting_Started.md index 36abb6b7..8c962428 100644 --- a/doc/usage/Getting_Started.md +++ b/doc/usage/Getting_Started.md @@ -11,8 +11,7 @@ In this tutorial you will learn how to: 3. [Get the results of the load flow](gs-getting-results); 4. [Analyze the results](gs-analysis-and-violations); 5. [Update the elements of the network](gs-updating-elements); -6. [Save the network and the results to the disk for later analysis](gs-saving-network); -7. [Load the saved network and the results from the disk](gs-loading-network). +6. [Save the network to a file and load a saved network](gs-network-json); (gs-creating-network)= @@ -23,18 +22,18 @@ The following is a summary of the available elements: - Buses: - - `Bus`: An electrical bus. + - `Bus`: A basic multi-phase electrical bus. - Branches: - - `Line`: A line connects two buses. The parameters of the line are defined by a `LineParameters` object. - - `LineParameters`: This object defines the parameters of a line (model, impedance, etc.) + - `Line`: A line connects two buses. The physical parameters of the line like its impedance are + defined by a `LineParameters` object. - `Switch`: A basic switch element. - - `Transformer`: A generic transformer. The parameters of the transformer are defined by a `TransformerParameters` - object. - - `TransformerParameters`: This object defines the parameters of a transformer (model, windings, etc.) + - `Transformer`: A generic transformer. The physical parameters of the transformer like its + impedance and winding configuration are defined by a `TransformerParameters` object. - Loads: + The ZIP load model is available via the following classes: - `ImpedanceLoad`: A constant impedance (Z) load: $S = |V|^2 \times \overline{Z}$, $|S|$ is proportional to $|V|^2$. @@ -77,7 +76,7 @@ It leads to the following code ... u_max = 1.1 * un # V ... i_max = 500.0 # A ->>> # Create two buses +>>> # Create two buses (notice the phases of LV buses contain the neutral) ... source_bus = Bus(id="sb", phases="abcn", min_voltage=u_min, max_voltage=u_max) ... load_bus = Bus(id="lb", phases="abcn", min_voltage=u_min, max_voltage=u_max) @@ -90,9 +89,7 @@ It leads to the following code >>> # Create a LV source at the first bus ... # (phase-to-neutral voltage because the source is connected to the neutral) ... source_voltages = un * np.exp([0, -2j * np.pi / 3, 2j * np.pi / 3]) -... vs = VoltageSource( -... id="vs", bus=source_bus, voltages=source_voltages -... ) # phases="abcn" inferred from the bus +... vs = VoltageSource(id="vs", bus=source_bus, voltages=source_voltages) >>> # Add a load at the second bus ... load = PowerLoad(id="load", bus=load_bus, powers=[10e3 + 0j, 10e3, 10e3]) # VA @@ -105,11 +102,30 @@ It leads to the following code ... line = Line(id="line", bus1=source_bus, bus2=load_bus, parameters=lp, length=2.0) ``` +Notice how the phases of the load, line and source are not explicitly given. They are inferred to be +`"abcn"` from the buses they are connected to. You can also explicitly declare the phases of these +elements. For example, to create a delta-connected source instead, you can use the following code: + +```pycon +>>> source_voltages = un * np.sqrt(3) * np.exp([0, -2j * np.pi / 3, 2j * np.pi / 3]) +... vs = VoltageSource( +... id="vs", bus=source_bus, voltages=source_voltages, phases="abc" +... ) +``` + +Note the use of `un * np.sqrt(3)` in the source voltage as it now represents the phase-to-phase +voltage. This is because everywhere in `roseau-load-flow`, the `voltages` of an element depend on +the element's `phases`. Voltages of elements connected in a _Star (wye)_ configuration (elements +that have a neutral connection indicated by the presence of the `'n'` char in their `phases` +attribute) are the **phase-to-neutral** voltages. Voltages of elements connected in a _Delta_ +configuration (elements that do not have a neutral connection indicated by the absence of the +`'n'` char from their `phases` attribute) are the **phase-to-phase** voltages. + At this point, all the basic elements of the network have been defined and connected. Now, everything can be encapsulated in an `ElectricalNetwork` object, but first, some important notes on the `Ground` and `PotentialRef` elements: -```{important} +````{important} The `Ground` element does not have a fixed potential as one would expect from a real ground connection. The potential reference (0 Volts) is defined by the `PotentialRef` element that itself can be connected to any bus or ground in the network. This is to give more flexibility @@ -118,15 +134,15 @@ for the user to define the potential reference of their network. A `PotentialRef` defines the potential reference for the network. It is a mandatory reference for the load flow resolution to be well-defined. A network MUST have one and only one potential reference per a galvanically isolated section. -``` -```{tip} +```{note} The `Ground` element is not required in this simple network as it is connected to a single element. No current will flow through the ground and no two points in the network will be forced to have the same potential. In this scenario you are allowed to define the potential reference directly on the bus element: `pref = PotentialRef(id="pref", element=source_bus, phase="n")` and not bother with creating the ground element at all. ``` +```` An `ElectricalNetwork` object can now be created using the `from_element` constructor. The source bus `source_bus` is given to this constructor. All the elements connected to this bus are @@ -167,26 +183,26 @@ are returned as complex numbers. Calling `abs(load_bus.res_potentials)` gives yo of the load's potentials (in Volts) and `np.angle(load_bus.res_potentials)` gives their angle (phase shift) in radians. -````{note} -Roseau Load Flow uses the [Pint](https://pint.readthedocs.io/en/stable/) `Quantity` objects to -present the data in unit-agnostic way for the user. All input data (load powers, source voltages, +Roseau Load Flow uses [Pint](https://pint.readthedocs.io/en/stable/) `Quantity` objects to +present the data in unit-agnostic way for the user. Most input data (load powers, source voltages, etc.) are expected to be either given in SI units or using the pint Quantity interface for non-SI -units (example below). The `length` parameter of the `Line` class is an exception where the -default unit is Kilometers. -Example, create a load with powers expressed in kVA: +units (example below). Exceptions to this rule include the `length` parameter of the `Line` class +with the default unit being Kilometers (km) and the `section` parameter of the `LineParameters` +class with the default unit being Square Millimeters (mm²). + +In the following example, we create a load with powers expressed in kVA: ```python from roseau.load_flow import Q_ load = PowerLoad(id="load", bus=load_bus, phases="abcn", powers=Q_([10, 10, 10], "kVA")) ``` -```` The results returned by the `res_` properties are also `Quantity` objects. ### Available results -The available results depend on the type of element. The following table summarizes the available +The available results depend on the type of element. The following table lists the available results for each element type: | Element type | Available results | @@ -201,7 +217,7 @@ results for each element type: | `PotentialRef` | `res_current` _(Always zero for a successful load flow)_ | ⁎: `res_flexible_powers` is only available for flexible loads (`PowerLoad`s with `flexible_params`). You'll see -an example on the usage of flexible loads in the _Flexible Loads_ section. +an example on the usage of flexible loads in the _Flexible Loads_ page. ### Getting results per object @@ -214,22 +230,15 @@ array([ 2.21928183e+02-2.60536682e-18j, -1.10964092e+02-1.92195445e+02j, -1.10964092e+02+1.92195445e+02j, 2.68637675e-15-6.67652444e-17j]) ``` -As the results are _pint quantities_, they can be converted to different units. Here, the magnitudes -of the voltages of the same bus are displayed in kilovolts. +As the results are _pint quantities_, they can be converted to different units easily. ```pycon ->>> abs(load_bus.res_voltages).to("kV") +>>> abs(load_bus.res_voltages).to("kV") # Get a Quantity in kV array([0.22192818, 0.22192818, 0.22192818]) -``` - -```{important} -Everywhere in `roseau-load-flow`, the `voltages` of an element depend on the element's `phases`. -Voltages of elements connected in a *Star (wye)* configuration (elements that have a neutral -connection indicated by the presence of the `'n'` char in their `phases` attribute) are the -**phase-to-neutral** voltages. Voltages of elements connected in a *Delta* configuration (elements -that do not have a neutral connection indicated by the absence of the `'n'` char from their -`phases` attribute) are the **phase-to-phase** voltages. This is true for *input* voltages, such -as the `voltages` parameter to a `VoltageSource`, as well as for the results such as the `res_voltages` property of a `Bus`. +>>> abs(load_bus.res_voltages).m_as("kV") # Get the magnitude in kV +array([0.22192818, 0.22192818, 0.22192818]) +>>> abs(load_bus.res_voltages).m # Get the default magnitude (Volts) +array([221.928183, 221.928183, 221.928183]) ``` The currents of the line are available using the `res_currents` property of the `line` object. @@ -254,7 +263,7 @@ would have been non-zero. ### Dataframe network results The results can also be retrieved for the entire network using `res_` properties of the -`ElectricalNetwork` instance as [pandas](https://pandas.pydata.org/docs/) {doc}` DataFrames `. +`ElectricalNetwork` instance as pandas {doc}` DataFrames `. Available results for the network are: @@ -481,34 +490,44 @@ unbalanced situation. array([ 216.02252269 +0.j, -115.47005384-200.j, -115.47005384+200.j, 14.91758499 +0.j]) ``` -One can notice that the neutral's potential of the bus is no longer close to 0 V. +Notice how the change was taken into account where the introduced unbalance manifested in the neutral's +potential of the bus no longer being close to 0 V. -(gs-saving-network)= +More information on modifying the network elements can be found in the [Modifying a network](usage-modifying-network) page. -## Saving the network to a file +(gs-network-json)= -The network can be saved to a JSON file using the `en.to_json` method. Note that this method -does not save the results of the load flow. It only saves the network elements. +## Saving/loading the network -To save the results of the load flow, use the `en.results_to_json` method. +An electrical network can be written to a JSON file, for later analysis or for sharing with others, +using the {meth}`~roseau.load_flow.ElectricalNetwork.to_json` method. ```pycon >>> en.to_json("my_network.json") ->>> en.results_to_json("my_network_results.json") ``` ```{warning} -The `to_json` and `results_to_json` methods will overwrite the file if it already exists. +The `to_json` method will overwrite the file if it already exists. ``` -(gs-loading-network)= - -## Loading a network from a file - -A saved network can be loaded using the `ElectricalNetwork.from_json` method. The results of -the load flow can then be loaded using the `ElectricalNetwork.results_from_json` method. +To load the network from a JSON file, use the {meth}`~roseau.load_flow.ElectricalNetwork.from_json` method. ```pycon >>> en = ElectricalNetwork.from_json("my_network.json") ->>> en.results_from_json("my_network_results.json") +``` + +By default, the `to_json` and `from_json` methods will include the load flow results if they are +available and are valid. If you want to save/load the network without the results, you can pass +`include_results=False` to these methods. + +Note that calling the `to_json()` method on a network with invalid results (say after an element +has been modified) will raise an exception. In this case, you can use the `include_results=False` +option to save the network without the results or you can call the `solve_load_flow()` method to +update the results before saving the network. + +```{important} +We do not recommend modifying the JSON file manually. The content of the JSON file is not +guaranteed to be stable across different versions of the library and should be considered an +internal representation of the network. Any changes to the JSON file should be done through the +`ElectricalNetwork` object otherwise it may lead to unexpected behavior. ``` diff --git a/poetry.lock b/poetry.lock index 78962dce..ff50192e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -24,13 +24,13 @@ files = [ [[package]] name = "astroid" -version = "3.0.2" +version = "3.0.3" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.8.0" files = [ - {file = "astroid-3.0.2-py3-none-any.whl", hash = "sha256:d6e62862355f60e716164082d6b4b041d38e2a8cf1c7cd953ded5108bac8ff5c"}, - {file = "astroid-3.0.2.tar.gz", hash = "sha256:4a61cf0a59097c7bb52689b0fd63717cd2a8a14dc9f1eee97b82d814881c8c91"}, + {file = "astroid-3.0.3-py3-none-any.whl", hash = "sha256:92fcf218b89f449cdf9f7b39a269f8d5d617b27be68434912e11e79203963a17"}, + {file = "astroid-3.0.3.tar.gz", hash = "sha256:4148645659b08b70d72460ed1921158027a9e53ae8b7234149b1400eddacbb93"}, ] [package.dependencies] @@ -92,13 +92,13 @@ lxml = ["lxml"] [[package]] name = "certifi" -version = "2023.11.17" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] @@ -335,63 +335,63 @@ test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] [[package]] name = "coverage" -version = "7.4.0" +version = "7.4.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {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"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, + {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, + {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, + {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, + {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, + {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, + {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, + {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, + {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, + {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, + {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, + {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, + {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, ] [package.dependencies] @@ -531,60 +531,60 @@ test = ["Fiona[s3]", "pytest (>=7)", "pytest-cov", "pytz"] [[package]] name = "fonttools" -version = "4.47.2" +version = "4.48.1" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {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"}, + {file = "fonttools-4.48.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:702ae93058c81f46461dc4b2c79f11d3c3d8fd7296eaf8f75b4ba5bbf813cd5f"}, + {file = "fonttools-4.48.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97f0a49fa6aa2d6205c6f72f4f98b74ef4b9bfdcb06fd78e6fe6c7af4989b63e"}, + {file = "fonttools-4.48.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3260db55f1843e57115256e91247ad9f68cb02a434b51262fe0019e95a98738"}, + {file = "fonttools-4.48.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e740a7602c2bb71e1091269b5dbe89549749a8817dc294b34628ffd8b2bf7124"}, + {file = "fonttools-4.48.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4108b1d247953dd7c90ec8f457a2dec5fceb373485973cc852b14200118a51ee"}, + {file = "fonttools-4.48.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56339ec557f0c342bddd7c175f5e41c45fc21282bee58a86bd9aa322bec715f2"}, + {file = "fonttools-4.48.1-cp310-cp310-win32.whl", hash = "sha256:bff5b38d0e76eb18e0b8abbf35d384e60b3371be92f7be36128ee3e67483b3ec"}, + {file = "fonttools-4.48.1-cp310-cp310-win_amd64.whl", hash = "sha256:f7449493886da6a17472004d3818cc050ba3f4a0aa03fb47972e4fa5578e6703"}, + {file = "fonttools-4.48.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:18b35fd1a850ed7233a99bbd6774485271756f717dac8b594958224b54118b61"}, + {file = "fonttools-4.48.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cad5cfd044ea2e306fda44482b3dd32ee47830fa82dfa4679374b41baa294f5f"}, + {file = "fonttools-4.48.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f30e605c7565d0da6f0aec75a30ec372072d016957cd8fc4469721a36ea59b7"}, + {file = "fonttools-4.48.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aee76fd81a8571c68841d6ef0da750d5ff08ff2c5f025576473016f16ac3bcf7"}, + {file = "fonttools-4.48.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5057ade278e67923000041e2b195c9ea53e87f227690d499b6a4edd3702f7f01"}, + {file = "fonttools-4.48.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b10633aafc5932995a391ec07eba5e79f52af0003a1735b2306b3dab8a056d48"}, + {file = "fonttools-4.48.1-cp311-cp311-win32.whl", hash = "sha256:0d533f89819f9b3ee2dbedf0fed3825c425850e32bdda24c558563c71be0064e"}, + {file = "fonttools-4.48.1-cp311-cp311-win_amd64.whl", hash = "sha256:d20588466367f05025bb1efdf4e5d498ca6d14bde07b6928b79199c588800f0a"}, + {file = "fonttools-4.48.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0a2417547462e468edf35b32e3dd06a6215ac26aa6316b41e03b8eeaf9f079ea"}, + {file = "fonttools-4.48.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cf5a0cd974f85a80b74785db2d5c3c1fd6cc09a2ba3c837359b2b5da629ee1b0"}, + {file = "fonttools-4.48.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0452fcbfbce752ba596737a7c5ec5cf76bc5f83847ce1781f4f90eab14ece252"}, + {file = "fonttools-4.48.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578c00f93868f64a4102ecc5aa600a03b49162c654676c3fadc33de2ddb88a81"}, + {file = "fonttools-4.48.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:63dc592a16cd08388d8c4c7502b59ac74190b23e16dfc863c69fe1ea74605b68"}, + {file = "fonttools-4.48.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9b58638d8a85e3a1b32ec0a91d9f8171a877b4b81c408d4cb3257d0dee63e092"}, + {file = "fonttools-4.48.1-cp312-cp312-win32.whl", hash = "sha256:d10979ef14a8beaaa32f613bb698743f7241d92f437a3b5e32356dfb9769c65d"}, + {file = "fonttools-4.48.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdfd7557d1bd294a200bd211aa665ca3b02998dcc18f8211a5532da5b8fad5c5"}, + {file = "fonttools-4.48.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3cdb9a92521b81bf717ebccf592bd0292e853244d84115bfb4db0c426de58348"}, + {file = "fonttools-4.48.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b4ec6d42a7555f5ae35f3b805482f0aad0f1baeeef54859492ea3b782959d4a"}, + {file = "fonttools-4.48.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:902e9c4e9928301912f34a6638741b8ae0b64824112b42aaf240e06b735774b1"}, + {file = "fonttools-4.48.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8c8b54bd1420c184a995f980f1a8076f87363e2bb24239ef8c171a369d85a31"}, + {file = "fonttools-4.48.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:12ee86abca46193359ea69216b3a724e90c66ab05ab220d39e3fc068c1eb72ac"}, + {file = "fonttools-4.48.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6978bade7b6c0335095bdd0bd97f8f3d590d2877b370f17e03e0865241694eb5"}, + {file = "fonttools-4.48.1-cp38-cp38-win32.whl", hash = "sha256:bcd77f89fc1a6b18428e7a55dde8ef56dae95640293bfb8f4e929929eba5e2a2"}, + {file = "fonttools-4.48.1-cp38-cp38-win_amd64.whl", hash = "sha256:f40441437b039930428e04fb05ac3a132e77458fb57666c808d74a556779e784"}, + {file = "fonttools-4.48.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0d2b01428f7da26f229a5656defc824427b741e454b4e210ad2b25ed6ea2aed4"}, + {file = "fonttools-4.48.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:df48798f9a4fc4c315ab46e17873436c8746f5df6eddd02fad91299b2af7af95"}, + {file = "fonttools-4.48.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2eb4167bde04e172a93cf22c875d8b0cff76a2491f67f5eb069566215302d45d"}, + {file = "fonttools-4.48.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c900508c46274d32d308ae8e82335117f11aaee1f7d369ac16502c9a78930b0a"}, + {file = "fonttools-4.48.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:594206b31c95fcfa65f484385171fabb4ec69f7d2d7f56d27f17db26b7a31814"}, + {file = "fonttools-4.48.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:292922dc356d7f11f5063b4111a8b719efb8faea92a2a88ed296408d449d8c2e"}, + {file = "fonttools-4.48.1-cp39-cp39-win32.whl", hash = "sha256:4709c5bf123ba10eac210d2d5c9027d3f472591d9f1a04262122710fa3d23199"}, + {file = "fonttools-4.48.1-cp39-cp39-win_amd64.whl", hash = "sha256:63c73b9dd56a94a3cbd2f90544b5fca83666948a9e03370888994143b8d7c070"}, + {file = "fonttools-4.48.1-py3-none-any.whl", hash = "sha256:e3e33862fc5261d46d9aae3544acb36203b1a337d00bdb5d3753aae50dac860e"}, + {file = "fonttools-4.48.1.tar.gz", hash = "sha256:8b8a45254218679c7f1127812761e7854ed5c8e34349aebf581e8c9204e7495a"}, ] [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", "pycairo", "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)", "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", "pycairo", "scipy"] -lxml = ["lxml (>=4.0,<5)"] +lxml = ["lxml (>=4.0)"] pathops = ["skia-pathops (>=0.5.0)"] plot = ["matplotlib"] repacker = ["uharfbuzz (>=0.23.0)"] @@ -596,13 +596,13 @@ woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] [[package]] name = "furo" -version = "2023.9.10" +version = "2024.1.29" description = "A clean customisable Sphinx documentation theme." optional = false python-versions = ">=3.8" files = [ - {file = "furo-2023.9.10-py3-none-any.whl", hash = "sha256:513092538537dc5c596691da06e3c370714ec99bc438680edc1debffb73e5bfc"}, - {file = "furo-2023.9.10.tar.gz", hash = "sha256:5707530a476d2a63b8cad83b4f961f3739a69f4b058bcf38a03a39fa537195b2"}, + {file = "furo-2024.1.29-py3-none-any.whl", hash = "sha256:3548be2cef45a32f8cdc0272d415fcb3e5fa6a0eb4ddfe21df3ecf1fe45a13cf"}, + {file = "furo-2024.1.29.tar.gz", hash = "sha256:4d6b2fe3f10a6e36eb9cc24c1e7beb38d7a23fc7b3c382867503b7fcac8a1e02"}, ] [package.dependencies] @@ -613,13 +613,13 @@ sphinx-basic-ng = "*" [[package]] name = "geopandas" -version = "0.14.2" +version = "0.14.3" description = "Geographic pandas extensions" optional = false python-versions = ">=3.9" files = [ - {file = "geopandas-0.14.2-py3-none-any.whl", hash = "sha256:0efa61235a68862c1c6be89fc3707cdeba67667d5676bb19e24f3c57a8c2f723"}, - {file = "geopandas-0.14.2.tar.gz", hash = "sha256:6e71d57b8376f9fdc9f1c3aa3170e7e420e91778de854f51013ae66fd371ccdb"}, + {file = "geopandas-0.14.3-py3-none-any.whl", hash = "sha256:41b31ad39e21bc9e8c4254f78f8dc4ce3d33d144e22e630a00bb336c83160204"}, + {file = "geopandas-0.14.3.tar.gz", hash = "sha256:748af035d4a068a4ae00cab384acb61d387685c833b0022e0729aa45216b23ac"}, ] [package.dependencies] @@ -846,71 +846,71 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "2.1.4" +version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" files = [ - {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"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] @@ -1051,47 +1051,47 @@ setuptools = "*" [[package]] name = "numpy" -version = "1.26.3" +version = "1.26.4" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {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"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] [[package]] @@ -1290,28 +1290,28 @@ xarray = ["xarray"] [[package]] name = "platformdirs" -version = "4.1.0" +version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, - {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] [[package]] name = "pluggy" -version = "1.3.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] @@ -1440,13 +1440,13 @@ certifi = "*" [[package]] name = "pytest" -version = "7.4.4" +version = "8.0.0" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"}, + {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"}, ] [package.dependencies] @@ -1454,7 +1454,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" +pluggy = ">=1.3.0,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] @@ -1514,13 +1514,13 @@ six = ">=1.5" [[package]] name = "pytz" -version = "2023.3.post1" +version = "2024.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, - {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] [[package]] @@ -1707,17 +1707,17 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "roseau-load-flow-engine" -version = "0.12.0a0" +version = "0.12.0" description = "Highly capable three-phase load flow solver" optional = false python-versions = ">=3.10,<4.0" files = [ - {file = "roseau_load_flow_engine-0.12.0a0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:48d0cce802fb068b80086ef1e451878df47cd40f7e68b0e8163fe18121b3915b"}, - {file = "roseau_load_flow_engine-0.12.0a0-cp310-cp310-win_amd64.whl", hash = "sha256:e8d62eeea95743eb20c40feadb243a8ea816bb63a4d2bcd34ac477e3daf534fa"}, - {file = "roseau_load_flow_engine-0.12.0a0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:0952a00a0e5a91dbbf5e62de8a6b9b447363c18794fd9878c0625664183917c5"}, - {file = "roseau_load_flow_engine-0.12.0a0-cp311-cp311-win_amd64.whl", hash = "sha256:791e327ddc0224d35040783b4bf32f83a91ca8620d876de6cd80b5776c735dd0"}, - {file = "roseau_load_flow_engine-0.12.0a0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:70f162550f86f57e727f4815b79ab9aa37e693dc699af598c6e1845ad3e42b77"}, - {file = "roseau_load_flow_engine-0.12.0a0-cp312-cp312-win_amd64.whl", hash = "sha256:41a4f773153dc7d79f8dd7b3ac00c37d07c99435bd040918eb1c47a3b2275a87"}, + {file = "roseau_load_flow_engine-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:f5154e3851c07b9014e3c3204ae833dd84c61b2d91ff1dd5d80455c32c02fa4a"}, + {file = "roseau_load_flow_engine-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:68788cbe9e9c7876baeef45deea93b9cec902bf1fab62132aae64158bc8aecfc"}, + {file = "roseau_load_flow_engine-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:9c3207368b0477ba4ec64ee0e491eadb9acb8112d36d37274670a3c3d1c14f7a"}, + {file = "roseau_load_flow_engine-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:c04d4299674c8b697dd0327e9978f7704518144e8a734aaa5f1b79c967cb1fc0"}, + {file = "roseau_load_flow_engine-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b0b98cc515e3c5164b83e509579d73a3b477c20cd030f85bd19d09e7ae1a38c8"}, + {file = "roseau_load_flow_engine-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:9e0a26b0192d71d38b7224cc06eb03494ef7679e8656c04eb6afc70c557b9362"}, ] [package.dependencies] @@ -1725,28 +1725,28 @@ numpy = ">=1.21.5" [[package]] name = "ruff" -version = "0.1.14" +version = "0.2.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {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"}, + {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"}, + {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"}, + {file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"}, + {file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"}, + {file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"}, + {file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"}, + {file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"}, + {file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"}, + {file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"}, + {file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"}, + {file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"}, ] [[package]] @@ -2140,17 +2140,18 @@ files = [ [[package]] name = "urllib3" -version = "2.1.0" +version = "2.2.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, - {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, + {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, + {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -2181,4 +2182,4 @@ plot = ["matplotlib"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "8a04ff082cf90ef142a2e820cb9977688d7a669f8b9ee14ad7d851b78daecf9e" +content-hash = "7bd36b1aa48c2053fc8c31dd373454dfcd0a6142ad471c9c738e4a20cbc1d245" diff --git a/pyproject.toml b/pyproject.toml index 3b86c9fb..22784fe0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "roseau-load-flow" -version = "0.7.0-alpha" +version = "0.7.0" description = "Highly capable three-phase load flow solver" authors = [ "Ali Hamdan ", @@ -49,7 +49,7 @@ typing-extensions = ">=4.6.2" pyproj = ">=3.3.0" certifi = ">=2023.5.7" platformdirs = ">=4.0.0" -roseau-load-flow-engine = "==0.12.0-alpha" +roseau-load-flow-engine = "==0.12.0" # Optional dependencies matplotlib = { version = ">=3.7.2", optional = true } @@ -61,7 +61,7 @@ plot = ["matplotlib"] graph = ["networkx"] [tool.poetry.group.test.dependencies] -pytest = "^7.1.2" +pytest = "^8.0.0" pytest-cov = "^4.0.0" pytest-xdist = "^3.1.0" coverage = { version = "^7.0.5", extras = ["toml"] } @@ -70,7 +70,7 @@ networkx = ">=3.0.0" [tool.poetry.group.dev.dependencies] pre-commit = "^3.0.0" -ruff = "==0.1.14" # keep in sync with .pre-commit-config.yaml +ruff = ">=0.2.0" [tool.poetry.group.doc.dependencies] sphinx = "^7.0.1" @@ -87,17 +87,17 @@ sphinxcontrib-bibtex = "^2.5.0" line-length = 120 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", "T10"] -unfixable = ["B"] -ignore = ["E501", "B024", "N818"] -flake8-tidy-imports.ban-relative-imports = "all" -flake8-pytest-style.parametrize-values-type = "tuple" -mccabe.max-complexity = 15 +namespace-packages = ["roseau"] +lint.select = ["E", "F", "C90", "W", "B", "UP", "I", "RUF100", "TID", "SIM", "PT", "PIE", "N", "C4", "NPY", "T10"] +lint.unfixable = ["B"] +lint.ignore = ["E501", "B024", "N818"] +lint.flake8-tidy-imports.ban-relative-imports = "all" +lint.flake8-pytest-style.parametrize-values-type = "tuple" +lint.mccabe.max-complexity = 15 -[tool.ruff.per-file-ignores] -"*.ipynb" = ["E402", "F403", "F405", "B018"] +[tool.ruff.lint.per-file-ignores] +"*.ipynb" = ["E402", "F403", "F405"] [tool.coverage.run] branch = true @@ -137,4 +137,5 @@ addopts = "--color=yes -n=0 --import-mode=importlib" testpaths = ["roseau/load_flow/"] filterwarnings = [ 'ignore:.*utcfromtimestamp:DeprecationWarning:dateutil.*', # dateutil is imported by pandas, not us + 'ignore:(?s).*Pyarrow will become a required dependency of pandas:DeprecationWarning:.*', ] diff --git a/roseau/load_flow/_solvers.py b/roseau/load_flow/_solvers.py index 838d0e73..d937fd8d 100644 --- a/roseau/load_flow/_solvers.py +++ b/roseau/load_flow/_solvers.py @@ -70,14 +70,14 @@ def solve_load_flow(self, max_iterations: int, tolerance: float) -> tuple[int, f Required tolerance value on the residuals for the convergence. max_iterations: - The maximum number of allowed iterations + The maximum number of allowed iterations. Returns: - The number of iterations and the final residual + The number of iterations and the final residual. """ lic = get_license() if lic is None: - activate_license(None) + activate_license(key=None) return self._cy_solver.solve_load_flow(max_iterations=max_iterations, tolerance=tolerance) def reset_inputs(self): diff --git a/roseau/load_flow/conftest.py b/roseau/load_flow/conftest.py index d8079b39..d0c80b2b 100644 --- a/roseau/load_flow/conftest.py +++ b/roseau/load_flow/conftest.py @@ -78,6 +78,11 @@ def dgs_network_path(request) -> Path: return request.param +@pytest.fixture() +def test_networks_path() -> Path: + return TEST_ALL_NETWORKS_DATA_FOLDER + + # # Utils # diff --git a/roseau/load_flow/io/dict.py b/roseau/load_flow/io/dict.py index 7292ea25..0475ae45 100644 --- a/roseau/load_flow/io/dict.py +++ b/roseau/load_flow/io/dict.py @@ -5,9 +5,12 @@ 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 copy import logging from typing import TYPE_CHECKING +import numpy as np + from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow.models import ( AbstractBranch, @@ -33,8 +36,8 @@ """The current version of the network JSON file format.""" -def network_from_dict( - data: JsonDict, +def network_from_dict( # noqa: C901 + data: JsonDict, *, include_results: bool = True ) -> tuple[ dict[Id, Bus], dict[Id, AbstractBranch], @@ -42,6 +45,7 @@ def network_from_dict( dict[Id, VoltageSource], dict[Id, Ground], dict[Id, PotentialRef], + bool, ]: """Create the electrical network elements from a dictionary. @@ -49,9 +53,13 @@ def network_from_dict( data: The dictionary containing the network data. + include_results: + If True (default) and the results of the load flow are included in the dictionary, + the results are also loaded into the network. + Returns: The buses, branches, loads, sources, grounds and potential refs to construct the electrical - network. + network and a boolean indicating if the network has results. """ version = data.get("version", 0) if version == 0: @@ -60,38 +68,66 @@ def network_from_dict( f"(version {NETWORK_JSON_VERSION}). Please save the network again." ) data = v0_to_v1_converter(data) + include_results = False # V0 network dictionaries didn't have results else: # If we arrive here, we dealt with all legacy versions, it must be the current one assert version == NETWORK_JSON_VERSION, f"Unsupported network file version {version}." + + data = copy.deepcopy(data) # Make a copy to avoid modifying the original + + # Track if ALL results are included in the network + has_results = include_results + # Lines and transformers parameters - lines_params = {lp["id"]: LineParameters.from_dict(lp) for lp in data["lines_params"]} - transformers_params = {tp["id"]: TransformerParameters.from_dict(tp) for tp in data["transformers_params"]} + lines_params = { + lp["id"]: LineParameters.from_dict(lp, include_results=include_results) for lp in data["lines_params"] + } + transformers_params = { + tp["id"]: TransformerParameters.from_dict(tp, include_results=include_results) + for tp in data["transformers_params"] + } # Buses, loads and 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"]} + buses: dict[Id, Bus] = {} + for bus_data in data["buses"]: + bus = Bus.from_dict(bus_data, include_results=include_results) + buses[bus.id] = bus + has_results = has_results and not bus._no_results + loads: dict[Id, AbstractLoad] = {} + for load_data in data["loads"]: + load_data["bus"] = buses[load_data["bus"]] + load = AbstractLoad.from_dict(load_data, include_results=include_results) + loads[load.id] = load + has_results = has_results and not load._no_results + sources: dict[Id, VoltageSource] = {} + for source_data in data["sources"]: + source_data["bus"] = buses[source_data["bus"]] + source = VoltageSource.from_dict(source_data, include_results=include_results) + sources[source.id] = source + has_results = has_results and not source._no_results # Grounds and potential refs grounds: dict[Id, Ground] = {} for ground_data in data["grounds"]: - 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 + for ground_bus_data in ground_data["buses"]: + ground_bus_data["bus"] = buses[ground_bus_data.pop("id")] + ground = Ground.from_dict(ground_data, include_results=include_results) + grounds[ground.id] = ground + has_results = has_results and not ground._no_results + potential_refs: dict[Id, PotentialRef] = {} for pref_data in data["potential_refs"]: if "bus" in pref_data: - bus_or_ground = buses[pref_data["bus"]] + pref_data["element"] = buses[pref_data.pop("bus")] elif "ground" in pref_data: - bus_or_ground = grounds[pref_data["ground"]] + pref_data["element"] = grounds[pref_data.pop("ground")] else: 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"]] = PotentialRef( - pref_data["id"], element=bus_or_ground, phase=pref_data.get("phases") - ) + p_ref = PotentialRef.from_dict(pref_data, include_results=include_results) + potential_refs[p_ref.id] = p_ref + has_results = has_results and not p_ref._no_results # Branches branches_dict: dict[Id, AbstractBranch] = {} @@ -102,27 +138,38 @@ def network_from_dict( bus1 = buses[branch_data["bus1"]] bus2 = buses[branch_data["bus2"]] geometry = AbstractBranch._parse_geometry(branch_data.get("geometry")) + branch: AbstractBranch if branch_data["type"] == "line": - assert phases1 == phases2 + assert phases1 == phases2, "Line phases1 and phases2 must be the same" length = branch_data["length"] 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] = Line( + branch = 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] = Transformer( - id, bus1, bus2, parameters=tp, phases1=phases1, phases2=phases2, geometry=geometry - ) + branch = Transformer(id, bus1, bus2, parameters=tp, phases1=phases1, phases2=phases2, geometry=geometry) elif branch_data["type"] == "switch": - assert phases1 == phases2 - branches_dict[id] = Switch(id, bus1, bus2, phases=phases1, geometry=geometry) + assert phases1 == phases2, "Switch phases1 and phases2 must be the same" + branch = Switch(id, bus1, bus2, phases=phases1, geometry=geometry) else: msg = f"Unknown branch type for branch {id}: {branch_data['type']}" logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_BRANCH_TYPE) + if include_results and "results" in branch_data: + currents1 = np.array( + [complex(i[0], i[1]) for i in branch_data["results"]["currents1"]], dtype=np.complex128 + ) + currents2 = np.array( + [complex(i[0], i[1]) for i in branch_data["results"]["currents2"]], dtype=np.complex128 + ) + branch._res_currents = (currents1, currents2) + branch._fetch_results = False + branch._no_results = False + branches_dict[id] = branch + has_results = has_results and not branch._no_results # Short-circuits short_circuits = data.get("short_circuits") @@ -132,25 +179,26 @@ def network_from_dict( ground = grounds[ground_id] if ground_id is not None else None buses[sc["bus_id"]].add_short_circuit(*sc["short_circuit"]["phases"], ground=ground) - return buses, branches_dict, loads, sources, grounds, potential_refs + return buses, branches_dict, loads, sources, grounds, potential_refs, has_results -def network_to_dict(en: "ElectricalNetwork", *, _lf_only: bool) -> JsonDict: +def network_to_dict(en: "ElectricalNetwork", *, include_results: bool) -> JsonDict: """Return a dictionary of the current network data. Args: en: The electrical network. - _lf_only: - Internal argument, please do not use. + include_results: + If True (default), the results of the load flow are included in the dictionary. + If no results are available, this option is ignored. Returns: The created dictionary. """ # Export the grounds and the pref - grounds = [ground.to_dict() for ground in en.grounds.values()] - potential_refs = [p_ref.to_dict() for p_ref in en.potential_refs.values()] + grounds = [ground.to_dict(include_results=include_results) for ground in en.grounds.values()] + potential_refs = [p_ref.to_dict(include_results=include_results) for p_ref in en.potential_refs.values()] # Export the buses, loads and sources buses: list[JsonDict] = [] @@ -158,14 +206,14 @@ def network_to_dict(en: "ElectricalNetwork", *, _lf_only: bool) -> JsonDict: sources: list[JsonDict] = [] short_circuits: list[JsonDict] = [] for bus in en.buses.values(): - buses.append(bus.to_dict(_lf_only=_lf_only)) + buses.append(bus.to_dict(include_results=include_results)) for element in bus._connected_elements: if isinstance(element, AbstractLoad): assert element.bus is bus - loads.append(element.to_dict()) + loads.append(element.to_dict(include_results=include_results)) elif isinstance(element, VoltageSource): assert element.bus is bus - sources.append(element.to_dict()) + sources.append(element.to_dict(include_results=include_results)) for sc in bus.short_circuits: short_circuits.append({"bus_id": bus.id, "short_circuit": sc}) @@ -174,7 +222,7 @@ def network_to_dict(en: "ElectricalNetwork", *, _lf_only: bool) -> JsonDict: lines_params_dict: dict[Id, LineParameters] = {} transformers_params_dict: dict[Id, TransformerParameters] = {} for branch in en.branches.values(): - branches.append(branch.to_dict(_lf_only=_lf_only)) + branches.append(branch.to_dict(include_results=include_results)) if isinstance(branch, Line): params_id = branch.parameters.id if params_id in lines_params_dict and branch.parameters != lines_params_dict[params_id]: @@ -195,13 +243,13 @@ def network_to_dict(en: "ElectricalNetwork", *, _lf_only: bool) -> JsonDict: # Line parameters line_params: list[JsonDict] = [] for lp in lines_params_dict.values(): - line_params.append(lp.to_dict(_lf_only=_lf_only)) + line_params.append(lp.to_dict(include_results=include_results)) line_params.sort(key=lambda x: x["id"]) # Always keep the same order # Transformer parameters transformer_params: list[JsonDict] = [] for tp in transformers_params_dict.values(): - transformer_params.append(tp.to_dict(_lf_only=_lf_only)) + transformer_params.append(tp.to_dict(include_results=include_results)) transformer_params.sort(key=lambda x: x["id"]) # Always keep the same order res = { diff --git a/roseau/load_flow/io/tests/test_dict.py b/roseau/load_flow/io/tests/test_dict.py index 91331f8b..4a6fb4d5 100644 --- a/roseau/load_flow/io/tests/test_dict.py +++ b/roseau/load_flow/io/tests/test_dict.py @@ -56,45 +56,37 @@ def test_to_dict(): # Same id, different line parameters -> fail with pytest.raises(RoseauLoadFlowException) as e: - en.to_dict() + en.to_dict(include_results=False) assert "There are multiple line parameters with id 'test'" in e.value.msg assert e.value.code == RoseauLoadFlowExceptionCode.JSON_LINE_PARAMETERS_DUPLICATES # Same id, same line parameters -> ok lp2 = LineParameters("test", z_line=np.eye(4, dtype=complex), y_shunt=np.eye(4, dtype=complex)) line2.parameters = lp2 - en.to_dict() + en.to_dict(include_results=False) # Dict content line2.parameters = lp1 lp1.max_current = 1000 - res = en.to_dict() - assert "geometry" in res["buses"][0] - assert "geometry" in res["buses"][1] - assert "geometry" in res["branches"][0] - 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) + res = en.to_dict(include_results=False) + res_bus0, res_bus1 = res["buses"] + res_line0, res_line1 = res["branches"] + assert "geometry" in res_bus0 + assert "geometry" in res_bus1 + assert "geometry" in res_line0 + assert "geometry" in res_line1 + assert np.isclose(res_bus0["min_voltage"], 0.9 * vn) + assert np.isclose(res_bus1["max_voltage"], 1.1 * vn) 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] - assert "geometry" not in res["buses"][1] - assert "geometry" not in res["branches"][0] - assert "geometry" not in res["branches"][1] - assert "min_voltage" not in res["buses"][0] - assert "max_voltage" not in res["buses"][1] - 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 + assert "results" not in res_bus0 + assert "results" not in res_bus1 + assert "results" not in res_line0 + assert "results" not in res_line1 # Same id, different transformer parameters -> fail ground = Ground("ground") @@ -128,7 +120,7 @@ def test_to_dict(): # Same id, different transformer parameters -> fail with pytest.raises(RoseauLoadFlowException) as e: - en.to_dict() + en.to_dict(include_results=False) assert "There are multiple transformer parameters with id 't'" in e.value.msg assert e.value.code == RoseauLoadFlowExceptionCode.JSON_TRANSFORMER_PARAMETERS_DUPLICATES @@ -137,25 +129,18 @@ def test_to_dict(): "t", type="Dyn11", uhv=20000, ulv=400, sn=160 * 1e3, p0=460, i0=2.3 / 100, psc=2350, vsc=4 / 100 ) transformer2.parameters = tp2 - en.to_dict() + en.to_dict(include_results=False) # Dict content transformer2.parameters = tp1 tp1.max_power = 180_000 - res = en.to_dict() + res = en.to_dict(include_results=False) assert "geometry" in res["buses"][0] assert "geometry" in res["buses"][1] assert "geometry" in res["branches"][0] assert "geometry" in res["branches"][1] assert np.isclose(res["transformers_params"][0]["max_power"], 180_000) - res = en.to_dict(_lf_only=True) - assert "geometry" not in res["buses"][0] - assert "geometry" not in res["buses"][1] - assert "geometry" not in res["branches"][0] - assert "geometry" not in res["branches"][1] - assert "max_power" not in res["transformers_params"][0] - def test_v0_to_v1_converter(monkeypatch): # Do not change `dict_v0` or the network manually, add/update the converters until the test passes @@ -962,7 +947,7 @@ def test_v0_to_v1_converter(monkeypatch): potential_refs=potential_refs, ) - net_dict = net.to_dict() + net_dict = net.to_dict(include_results=False) expected_dict = dict_v0 expected_dict = v0_to_v1_converter(expected_dict) # Uncomment the following lines as needed when new versions are added diff --git a/roseau/load_flow/license.py b/roseau/load_flow/license.py index e5f020d0..17af94e6 100644 --- a/roseau/load_flow/license.py +++ b/roseau/load_flow/license.py @@ -5,7 +5,7 @@ import certifi from platformdirs import user_cache_dir -from roseau.load_flow import RoseauLoadFlowException, RoseauLoadFlowExceptionCode +from roseau.load_flow.exceptions 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__) diff --git a/roseau/load_flow/models/branches.py b/roseau/load_flow/models/branches.py index accb2f5a..e050c777 100644 --- a/roseau/load_flow/models/branches.py +++ b/roseau/load_flow/models/branches.py @@ -166,10 +166,10 @@ def _cy_connect(self) -> None: # Json Mixin interface # @classmethod - def from_dict(cls, data: JsonDict) -> Self: + def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: return cls(**data) # not used anymore - def to_dict(self, *, _lf_only: bool = False) -> JsonDict: + def _to_dict(self, include_results: bool) -> JsonDict: res = { "id": self.id, "type": self.branch_type, @@ -178,15 +178,22 @@ def to_dict(self, *, _lf_only: bool = False) -> JsonDict: "bus1": self.bus1.id, "bus2": self.bus2.id, } - if not _lf_only and self.geometry is not None: + if self.geometry is not None: res["geometry"] = self.geometry.__geo_interface__ + if include_results: + currents1, currents2 = self._res_currents_getter(warning=True) + res["results"] = { + "currents1": [[i.real, i.imag] for i in currents1], + "currents2": [[i.real, i.imag] for i in currents2], + } return res - def results_from_dict(self, data: JsonDict) -> None: + 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 + self._no_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 bf5e6a8c..0471e5ca 100644 --- a/roseau/load_flow/models/buses.py +++ b/roseau/load_flow/models/buses.py @@ -331,12 +331,11 @@ def res_voltage_unbalance(self) -> Q_[float]: # Json Mixin interface # @classmethod - def from_dict(cls, data: JsonDict) -> Self: + def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: geometry = cls._parse_geometry(data.get("geometry")) - potentials = data.get("potentials") - if potentials is not None: + if (potentials := data.get("potentials")) is not None: potentials = [complex(v[0], v[1]) for v in potentials] - return cls( + self = cls( id=data["id"], phases=data["phases"], geometry=geometry, @@ -344,23 +343,33 @@ def from_dict(cls, data: JsonDict) -> Self: min_voltage=data.get("min_voltage"), max_voltage=data.get("max_voltage"), ) + if include_results and "results" in data: + self._res_potentials = np.array( + [complex(v[0], v[1]) for v in data["results"]["potentials"]], dtype=np.complex128 + ) + self._fetch_results = False + self._no_results = False + return self - def to_dict(self, *, _lf_only: bool = False) -> JsonDict: + def _to_dict(self, include_results: bool) -> JsonDict: res = {"id": self.id, "phases": self.phases} 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: - res["geometry"] = self.geometry.__geo_interface__ - if self.min_voltage is not None: - res["min_voltage"] = self.min_voltage.magnitude - if self.max_voltage is not None: - res["max_voltage"] = self.max_voltage.magnitude + if self.geometry is not None: + res["geometry"] = self.geometry.__geo_interface__ + if self.min_voltage is not None: + res["min_voltage"] = self.min_voltage.magnitude + if self.max_voltage is not None: + res["max_voltage"] = self.max_voltage.magnitude + if include_results: + potentials = self._res_potentials_getter(warning=True) + res["results"] = {"potentials": [[v.real, v.imag] for v in potentials]} return res - def results_from_dict(self, data: JsonDict) -> None: + 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 + self._no_results = False def _results_to_dict(self, warning: bool) -> JsonDict: return { diff --git a/roseau/load_flow/models/core.py b/roseau/load_flow/models/core.py index 66d3335c..9d3b6a7a 100644 --- a/roseau/load_flow/models/core.py +++ b/roseau/load_flow/models/core.py @@ -43,6 +43,8 @@ def __init__(self, id: Id, **kwargs: Any) -> None: self._network: ElectricalNetwork | None = None self._cy_element: CyElement | None = None self._fetch_results = False + self._no_results = True + self._results_valid = True @property def network(self) -> Optional["ElectricalNetwork"]: diff --git a/roseau/load_flow/models/grounds.py b/roseau/load_flow/models/grounds.py index 95d554e0..ad83ea2b 100644 --- a/roseau/load_flow/models/grounds.py +++ b/roseau/load_flow/models/grounds.py @@ -96,21 +96,31 @@ def connect(self, bus: "Bus", phase: str = "n") -> None: # Json Mixin interface # @classmethod - def from_dict(cls, data: JsonDict) -> Self: + def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: self = cls(data["id"]) - self._connected_buses = data["buses"] + for bus_data in data["buses"]: + self.connect(bus_data["bus"], bus_data["phase"]) + if include_results and "results" in data: + self._res_potential = complex(*data["results"]["potential"]) + self._fetch_results = False + self._no_results = False return self - def to_dict(self, *, _lf_only: bool = False) -> JsonDict: + def _to_dict(self, include_results: bool) -> JsonDict: # Shunt lines and potential references will have the ground in their dict not here. - return { + res = { "id": self.id, "buses": [{"id": bus_id, "phase": phase} for bus_id, phase in self._connected_buses.items()], } + if include_results: + v = self._res_potential_getter(warning=True) + res["results"] = {"potential": [v.real, v.imag]} + return res - def results_from_dict(self, data: JsonDict) -> None: + def _results_from_dict(self, data: JsonDict) -> None: self._res_potential = complex(*data["potential"]) self._fetch_results = False + self._no_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 a2de8314..d05bf6cf 100644 --- a/roseau/load_flow/models/lines/lines.py +++ b/roseau/load_flow/models/lines/lines.py @@ -84,17 +84,16 @@ 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._check_elements() self._check_loop() - self._n = len(self._phases) + self._n = len(phases) self._cy_element = CySwitch(self._n) self._cy_connect() @property def phases(self) -> str: - """The phases of the source.""" - return self._phases + """The phases of the switch. This is an alias for :attr:`phases1` and :attr:`phases2`.""" + return self._phases1 def _check_loop(self) -> None: """Check that there are no switch loop, raise an exception if it is the case""" @@ -213,7 +212,6 @@ def __init__( self._initialized = False super().__init__(id, bus1, bus2, phases1=phases, phases2=phases, geometry=geometry, **kwargs) - self._phases = phases self.ground = ground self.length = length self.parameters = parameters @@ -234,7 +232,7 @@ def __init__( # Connect the ground self._connect(self.ground) - self._n = len(self._phases) + self._n = len(phases) if parameters.with_shunt: self._cy_element = CyShuntLine( n=self._n, @@ -251,8 +249,8 @@ def __init__( @property def phases(self) -> str: - """The phases of the source.""" - return self._phases + """The phases of the line. This is an alias for :attr:`phases1` and :attr:`phases2`.""" + return self._phases1 @property @ureg_wraps("km", (None,)) @@ -431,12 +429,10 @@ def res_violated(self) -> bool | None: # # Json Mixin interface # - def to_dict(self, *, _lf_only: bool = False) -> JsonDict: - res = { - **super().to_dict(_lf_only=_lf_only), - "length": self._length, - "params_id": self.parameters.id, - } + def _to_dict(self, include_results: bool) -> JsonDict: + res = super()._to_dict(include_results=include_results) + res["length"] = self._length + res["params_id"] = self.parameters.id if self.ground is not None: res["ground"] = self.ground.id return res diff --git a/roseau/load_flow/models/lines/parameters.py b/roseau/load_flow/models/lines/parameters.py index f5c60061..02cbe466 100644 --- a/roseau/load_flow/models/lines/parameters.py +++ b/roseau/load_flow/models/lines/parameters.py @@ -966,7 +966,7 @@ def get_catalogue( # Json Mixin interface # @classmethod - def from_dict(cls, data: JsonDict) -> Self: + def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: """Line parameters constructor from dict. Args: @@ -992,21 +992,25 @@ def from_dict(cls, data: JsonDict) -> Self: section=data.get("section"), ) - def to_dict(self, *, _lf_only: bool = False) -> JsonDict: - """Return the line parameters information as a dictionary format.""" + def _to_dict(self, include_results: bool) -> JsonDict: res = {"id": self.id, "z_line": [self._z_line.real.tolist(), self._z_line.imag.tolist()]} if self.with_shunt: res["y_shunt"] = [self._y_shunt.real.tolist(), self._y_shunt.imag.tolist()] - if not _lf_only and self.max_current is not None: + if self.max_current is not None: res["max_current"] = self.max_current.magnitude - if not _lf_only and self._line_type is not None: + if self._line_type is not None: res["line_type"] = self._line_type.name - if not _lf_only and self._conductor_type is not None: + if self._conductor_type is not None: res["conductor_type"] = self._conductor_type.name - if not _lf_only and self._insulator_type is not None: + if self._insulator_type is not None: res["insulator_type"] = self._insulator_type.name - if not _lf_only and self._section is not None: + if self._section is not None: res["section"] = self._section + for k, v in res.items(): + if isinstance(v, np.integer): + res[k] = int(v) + elif isinstance(v, np.floating): + res[k] = float(v) return res def _results_to_dict(self, warning: bool) -> NoReturn: @@ -1014,7 +1018,7 @@ def _results_to_dict(self, warning: bool) -> NoReturn: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.JSON_NO_RESULTS) - def results_from_dict(self, data: JsonDict) -> None: + def _results_from_dict(self, data: JsonDict) -> None: msg = f"The {type(self).__name__} has no results to import." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.JSON_NO_RESULTS) diff --git a/roseau/load_flow/models/loads/flexible_parameters.py b/roseau/load_flow/models/loads/flexible_parameters.py index 76050ad0..e55be0b1 100644 --- a/roseau/load_flow/models/loads/flexible_parameters.py +++ b/roseau/load_flow/models/loads/flexible_parameters.py @@ -295,8 +295,8 @@ def q_u( # Json Mixin interface # @classmethod - def from_dict(cls, data: JsonDict) -> Self: - alpha = data["alpha"] if "alpha" in data else cls._DEFAULT_ALPHA + def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: + alpha = data.get("alpha", cls._DEFAULT_ALPHA) if data["type"] == "constant": return cls.constant() elif data["type"] == "p_max_u_production": @@ -312,7 +312,7 @@ def from_dict(cls, data: JsonDict) -> Self: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_CONTROL_TYPE) - def to_dict(self, *, _lf_only: bool = False) -> JsonDict: + def _to_dict(self, include_results: bool) -> JsonDict: if self.type == "constant": return {"type": "constant"} elif self.type == "p_max_u_production": @@ -338,7 +338,7 @@ def _results_to_dict(self, warning: bool) -> NoReturn: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.JSON_NO_RESULTS) - def results_from_dict(self, data: JsonDict) -> NoReturn: + def _results_from_dict(self, data: JsonDict) -> NoReturn: msg = f"The {type(self).__name__} has no results to import." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.JSON_NO_RESULTS) @@ -420,12 +420,12 @@ def epsilon(self) -> float: # Json Mixin interface # @classmethod - def from_dict(cls, data: JsonDict) -> Self: - alpha = data["alpha"] if "alpha" in data else cls._DEFAULT_ALPHA - epsilon = data["epsilon"] if "epsilon" in data else cls._DEFAULT_EPSILON + def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: + alpha = data.get("alpha", cls._DEFAULT_ALPHA) + epsilon = data.get("epsilon", cls._DEFAULT_EPSILON) return cls(type=data["type"], alpha=alpha, epsilon=epsilon) - def to_dict(self, *, _lf_only: bool = False) -> JsonDict: + def _to_dict(self, include_results: bool) -> JsonDict: return {"type": self.type, "alpha": self._alpha, "epsilon": self._epsilon} def _results_to_dict(self, warning: bool) -> NoReturn: @@ -433,7 +433,7 @@ def _results_to_dict(self, warning: bool) -> NoReturn: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.JSON_NO_RESULTS) - def results_from_dict(self, data: JsonDict) -> NoReturn: + def _results_from_dict(self, data: JsonDict) -> NoReturn: msg = f"The {type(self).__name__} has no results to import." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.JSON_NO_RESULTS) @@ -949,10 +949,10 @@ def pq_u_consumption( # Json Mixin interface # @classmethod - def from_dict(cls, data: JsonDict) -> Self: - control_p = Control.from_dict(data["control_p"]) - control_q = Control.from_dict(data["control_q"]) - projection = Projection.from_dict(data["projection"]) + def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: + control_p = Control.from_dict(data["control_p"], include_results=include_results) + control_q = Control.from_dict(data["control_q"], include_results=include_results) + projection = Projection.from_dict(data["projection"], include_results=include_results) q_min = data.get("q_min", None) q_max = data.get("q_max", None) return cls( @@ -964,11 +964,11 @@ def from_dict(cls, data: JsonDict) -> Self: q_max=q_max, ) - def to_dict(self, *, _lf_only: bool = False) -> JsonDict: + def _to_dict(self, include_results: bool) -> JsonDict: res = { - "control_p": self.control_p.to_dict(), - "control_q": self.control_q.to_dict(), - "projection": self.projection.to_dict(), + "control_p": self.control_p.to_dict(include_results=include_results), + "control_q": self.control_q.to_dict(include_results=include_results), + "projection": self.projection.to_dict(include_results=include_results), "s_max": self._s_max, } if self._q_min is not None: @@ -982,7 +982,7 @@ def _results_to_dict(self, warning: bool) -> NoReturn: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.JSON_NO_RESULTS) - def results_from_dict(self, data: JsonDict) -> NoReturn: + def _results_from_dict(self, data: JsonDict) -> NoReturn: msg = f"The {type(self).__name__} has no results to import." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.JSON_NO_RESULTS) diff --git a/roseau/load_flow/models/loads/loads.py b/roseau/load_flow/models/loads/loads.py index 9178347a..67d59c79 100644 --- a/roseau/load_flow/models/loads/loads.py +++ b/roseau/load_flow/models/loads/loads.py @@ -191,27 +191,50 @@ def _raise_disconnected_error(self) -> None: # Json Mixin interface # @classmethod - def from_dict(cls, data: JsonDict) -> "AbstractLoad": + def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> "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 = [FlexibleParameter.from_dict(fp_dict) for fp_dict in fp_data_list] + fp = [FlexibleParameter.from_dict(fp_dict, include_results=include_results) for fp_dict in fp_data_list] else: fp = None - return PowerLoad(data["id"], data["bus"], powers=powers, phases=data["phases"], flexible_params=fp) + self = 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 CurrentLoad(data["id"], data["bus"], currents=currents, phases=data["phases"]) + self = 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 ImpedanceLoad(data["id"], data["bus"], impedances=impedances, phases=data["phases"]) + self = ImpedanceLoad(data["id"], data["bus"], impedances=impedances, phases=data["phases"]) else: msg = f"Unknown load type for load {data['id']!r}" logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LOAD_TYPE) + if include_results and "results" in data: + self._res_currents = np.array( + [complex(i[0], i[1]) for i in data["results"]["currents"]], dtype=np.complex128 + ) + self._fetch_results = False + self._no_results = False + return self + + def _to_dict(self, include_results: bool) -> JsonDict: + self._raise_disconnected_error() + complex_array = getattr(self, f"_{self._type}s") + res = { + "id": self.id, + "bus": self.bus.id, + "phases": self.phases, + f"{self._type}s": [[value.real, value.imag] for value in complex_array], + } + if include_results: + currents = self._res_currents_getter(warning=True) + res["results"] = {"currents": [[i.real, i.imag] for i in currents]} + return res - def results_from_dict(self, data: JsonDict) -> None: + 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 + self._no_results = False def _results_to_dict(self, warning: bool) -> JsonDict: return { @@ -360,20 +383,26 @@ def res_flexible_powers(self) -> Q_[ComplexArray]: # # Json Mixin interface # - def to_dict(self, *, _lf_only: bool = False) -> JsonDict: - self._raise_disconnected_error() - res = { - "id": self.id, - "bus": self.bus.id, - "phases": self.phases, - "powers": [[s.real, s.imag] for s in self._powers], - } + @classmethod + def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> AbstractLoad: + self = super().from_dict(data, include_results=include_results) + if self.is_flexible and include_results and "results" in data: + self._res_flexible_powers = np.array( + [complex(p[0], p[1]) for p in data["results"]["powers"]], dtype=np.complex128 + ) + return self + + def _to_dict(self, include_results: bool) -> JsonDict: + res = super()._to_dict(include_results=include_results) if self.flexible_params is not None: - res["flexible_params"] = [fp.to_dict() for fp in self.flexible_params] + res["flexible_params"] = [fp.to_dict(include_results=include_results) for fp in self.flexible_params] + if self.is_flexible and include_results: + flexible_powers = self._res_flexible_powers_getter(warning=False) + res["results"]["powers"] = [[s.real, s.imag] for s in flexible_powers] return res - def results_from_dict(self, data: JsonDict) -> None: - super().results_from_dict(data=data) + def _results_from_dict(self, data: JsonDict) -> None: + super()._results_from_dict(data=data) if self.is_flexible: self._res_flexible_powers = np.array([complex(p[0], p[1]) for p in data["powers"]], dtype=np.complex128) @@ -436,15 +465,6 @@ def currents(self, value: ComplexArrayLike1D) -> None: 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() - return { - "id": self.id, - "bus": self.bus.id, - "phases": self.phases, - "currents": [[i.real, i.imag] for i in self._currents], - } - class ImpedanceLoad(AbstractLoad): """A constant impedance load.""" @@ -494,12 +514,3 @@ def impedances(self, impedances: ComplexArrayLike1D) -> None: 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() - return { - "id": self.id, - "bus": self.bus.id, - "phases": self.phases, - "impedances": [[z.real, z.imag] for z in self._impedances], - } diff --git a/roseau/load_flow/models/potential_refs.py b/roseau/load_flow/models/potential_refs.py index 3a01ee0e..07d139a0 100644 --- a/roseau/load_flow/models/potential_refs.py +++ b/roseau/load_flow/models/potential_refs.py @@ -100,10 +100,15 @@ def res_current(self) -> Q_[complex]: # Jso Mixin interface # @classmethod - def from_dict(cls, data: JsonDict) -> Self: - return cls(data["id"], data["element"], phase=data.get("phases")) - - def to_dict(self, *, _lf_only: bool = False) -> JsonDict: + def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: + self = cls(data["id"], data["element"], phase=data.get("phases")) + if include_results and "results" in data: + self._res_current = complex(*data["results"]["current"]) + self._fetch_results = False + self._no_results = False + return self + + def _to_dict(self, include_results: bool) -> JsonDict: res = {"id": self.id} e = self.element if isinstance(e, Bus): @@ -113,11 +118,15 @@ def to_dict(self, *, _lf_only: bool = False) -> JsonDict: res["ground"] = e.id else: raise AssertionError(f"Unexpected element type {type(e).__name__}") + if include_results: + i = self._res_current_getter(warning=True) + res["results"] = {"current": [i.real, i.imag]} return res - def results_from_dict(self, data: JsonDict) -> None: + def _results_from_dict(self, data: JsonDict) -> None: self._res_current = complex(*data["current"]) self._fetch_results = False + self._no_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 a7f003dd..dd0f6c8a 100644 --- a/roseau/load_flow/models/sources.py +++ b/roseau/load_flow/models/sources.py @@ -183,22 +183,34 @@ def _raise_disconnected_error(self) -> None: # Json Mixin interface # @classmethod - def from_dict(cls, data: JsonDict) -> Self: + def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: voltages = [complex(v[0], v[1]) for v in data["voltages"]] - return cls(data["id"], data["bus"], voltages=voltages, phases=data["phases"]) - - def to_dict(self, *, _lf_only: bool = False) -> JsonDict: + self = cls(data["id"], data["bus"], voltages=voltages, phases=data["phases"]) + if include_results and "results" in data: + self._res_currents = np.array( + [complex(i[0], i[1]) for i in data["results"]["currents"]], dtype=np.complex128 + ) + self._fetch_results = False + self._no_results = False + return self + + def _to_dict(self, include_results: bool) -> JsonDict: self._raise_disconnected_error() - return { + res = { "id": self.id, "bus": self.bus.id, "phases": self.phases, "voltages": [[v.real, v.imag] for v in self._voltages], } + if include_results: + currents = self._res_currents_getter(warning=True) + res["results"] = {"currents": [[i.real, i.imag] for i in currents]} + return res - def results_from_dict(self, data: JsonDict) -> None: + 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 + self._no_results = False def _results_to_dict(self, warning: bool) -> JsonDict: return { diff --git a/roseau/load_flow/models/tests/conftest.py b/roseau/load_flow/models/tests/conftest.py index 449b3eec..a5465142 100644 --- a/roseau/load_flow/models/tests/conftest.py +++ b/roseau/load_flow/models/tests/conftest.py @@ -1,4 +1,3 @@ -import json from pathlib import Path import pytest @@ -12,8 +11,4 @@ @pytest.fixture(scope="session", params=NETWORK_FILES, ids=[x.stem for x in NETWORK_FILES]) def network_with_results(request) -> ElectricalNetwork: - with open(request.param) as f: - data = json.load(f) - net = ElectricalNetwork.from_dict(data["network"]) - net.results_from_dict(data["results"]) - return net + return ElectricalNetwork.from_json(request.param, include_results=True) diff --git a/roseau/load_flow/models/tests/data/Network_1Ph.json b/roseau/load_flow/models/tests/data/Network_1Ph.json index 495bfc4f..6c9035bc 100644 --- a/roseau/load_flow/models/tests/data/Network_1Ph.json +++ b/roseau/load_flow/models/tests/data/Network_1Ph.json @@ -1,119 +1,67 @@ { - "network": { - "version": 1, - "grounds": [ - { - "id": "gnd", - "buses": [ - { - "id": "bus1", - "phase": "n" - } - ] + "version": 1, + "grounds": [ + { + "id": "gnd", + "buses": [ + { + "id": "bus1", + "phase": "n" + } + ], + "results": { + "potential": [0.0, 0.0] } - ], - "potential_refs": [ - { - "id": "pref", - "ground": "gnd" + } + ], + "potential_refs": [ + { + "id": "pref", + "ground": "gnd", + "results": { + "current": [0.0, 3.552713678800501e-15] } - ], - "buses": [ - { - "id": "bus1", - "phases": "an", + } + ], + "buses": [ + { + "id": "bus1", + "phases": "an", + "potentials": [ + [20000.0, 0.0], + [0.0, 0.0] + ], + "results": { "potentials": [ [20000.0, 0.0], [0.0, 0.0] ] - }, - { - "id": "bus2", - "phases": "an", - "potentials": [ - [20000.0, 0.0], - [0.0, 0.0] - ] - } - ], - "branches": [ - { - "id": "line", - "type": "line", - "phases1": "an", - "phases2": "an", - "bus1": "bus1", - "bus2": "bus2", - "length": 10, - "params_id": "lp" - } - ], - "loads": [ - { - "id": "load", - "bus": "bus2", - "phases": "an", - "powers": [[1500000.0, -500000.0]] - } - ], - "sources": [ - { - "id": "vs", - "bus": "bus1", - "phases": "an", - "voltages": [[20000.0, 0.0]] - } - ], - "lines_params": [ - { - "id": "lp", - "model": "z_neutral", - "z_line": [ - [ - [0.05, 0.0], - [0.0, 0.05] - ], - [ - [0.05, 0.0], - [0.0, 0.05] - ] - ] } - ], - "transformers_params": [] - }, - "results": { - "info": { - "status": "success", - "solver": "newton", - "solver_params": {}, - "iterations": 2, - "tolerance": 1e-6, - "residual": 1.9468870959826745e-12 }, - "buses": [ - { - "id": "bus1", - "phases": "an", - "potentials": [ - [20000.0, 0.0], - [0.0, 0.0] - ] - }, - { - "id": "bus2", - "phases": "an", + { + "id": "bus2", + "phases": "an", + "potentials": [ + [20000.0, 0.0], + [0.0, 0.0] + ], + "results": { "potentials": [ [19974.685919734027, -50.00000000000026], [25.31408026597291, 50.00000000000026] ] } - ], - "branches": [ - { - "id": "line", - "phases1": "an", - "phases2": "an", + } + ], + "branches": [ + { + "id": "line", + "type": "line", + "phases1": "an", + "phases2": "an", + "bus1": "bus1", + "bus2": "bus2", + "results": { "currents1": [ [75.31408026597299, 24.68591973402753], [-75.31408026597317, -24.685919734027355] @@ -122,39 +70,53 @@ [-75.31408026597299, -24.68591973402753], [75.31408026597317, 24.685919734027355] ] - } - ], - "loads": [ - { - "id": "load", - "phases": "an", + }, + "length": 10, + "params_id": "lp" + } + ], + "loads": [ + { + "id": "load", + "bus": "bus2", + "phases": "an", + "powers": [[1500000.0, -500000.0]], + "results": { "currents": [ [75.31408026597121, 24.685919734028797], [-75.31408026597121, -24.685919734028797] ] } - ], - "sources": [ - { - "id": "vs", - "phases": "an", + } + ], + "sources": [ + { + "id": "vs", + "bus": "bus1", + "phases": "an", + "voltages": [[20000.0, 0.0]], + "results": { "currents": [ [-75.31408026597317, -24.685919734027358], [75.31408026597317, 24.685919734027358] ] } - ], - "grounds": [ - { - "id": "gnd", - "potential": [0.0, 0.0] - } - ], - "potential_refs": [ - { - "id": "pref", - "current": [0.0, 3.552713678800501e-15] - } - ] - } + } + ], + "lines_params": [ + { + "id": "lp", + "z_line": [ + [ + [0.05, 0.0], + [0.0, 0.05] + ], + [ + [0.05, 0.0], + [0.0, 0.05] + ] + ] + } + ], + "transformers_params": [] } diff --git a/roseau/load_flow/models/tests/data/Network_3Ph_Balanced.json b/roseau/load_flow/models/tests/data/Network_3Ph_Balanced.json index b44b8bbe..f0e633bd 100644 --- a/roseau/load_flow/models/tests/data/Network_3Ph_Balanced.json +++ b/roseau/load_flow/models/tests/data/Network_3Ph_Balanced.json @@ -1,107 +1,45 @@ { - "network": { - "version": 1, - "grounds": [ - { - "id": "gnd", - "buses": [ - { - "id": "bus1", - "phase": "n" - } - ] - } - ], - "potential_refs": [ - { - "id": "pref", - "ground": "gnd" - } - ], - "buses": [ - { - "id": "bus1", - "phases": "abcn" - }, - { - "id": "bus2", - "phases": "abcn" - } - ], - "branches": [ - { - "id": "line", - "type": "line", - "phases1": "abcn", - "phases2": "abcn", - "bus1": "bus1", - "bus2": "bus2", - "length": 10, - "params_id": "lp" - } - ], - "loads": [ - { - "id": "load", - "bus": "bus2", - "phases": "abcn", - "powers": [ - [2000000.0, 0.0], - [2000000.0, 0.0], - [2000000.0, 0.0] - ] - } - ], - "sources": [ - { - "id": "vs", - "bus": "bus1", - "phases": "abcn", - "voltages": [ - [20000.0, 0.0], - [-9999.999999999996, -17320.508075688773], - [-9999.999999999996, 17320.508075688773] - ] + "version": 1, + "grounds": [ + { + "id": "gnd", + "buses": [ + { + "id": "bus1", + "phase": "n" + } + ], + "results": { + "potential": [0.0, 0.0] } - ], - "lines_params": [ - { - "id": "lp", - "model": "z_neutral", - "z_line": [ - [ - [0.05, 0.0, 0.0, 0.0], - [0.0, 0.05, 0.0, 0.0], - [0.0, 0.0, 0.05, 0.0], - [0.0, 0.0, 0.0, 0.05] - ], - [ - [0.05, 0.0, 0.0, 0.0], - [0.0, 0.05, 0.0, 0.0], - [0.0, 0.0, 0.05, 0.0], - [0.0, 0.0, 0.0, 0.05] - ] - ] + } + ], + "potential_refs": [ + { + "id": "pref", + "ground": "gnd", + "results": { + "current": [2.1648223024119769e-19, -5.440413584818354e-15] } - ], - "transformers_params": [] - }, - "results": { - "info": {}, - "buses": [ - { - "id": "bus1", - "phases": "abcn", + } + ], + "buses": [ + { + "id": "bus1", + "phases": "abcn", + "results": { "potentials": [ [20000.0, 0.0], [-9999.999999999996, -17320.508075688773], [-9999.999999999996, 17320.508075688773], [0.0, 0.0] ] - }, - { - "id": "bus2", - "phases": "abcn", + } + }, + { + "id": "bus2", + "phases": "abcn", + "results": { "potentials": [ [19949.748740546165, -49.99999999999989], [-10018.175640462301, -17251.98920842959], @@ -109,12 +47,17 @@ [3.544638799965941e-15, -4.655042719028526e-16] ] } - ], - "branches": [ - { - "id": "line", - "phases1": "abcn", - "phases2": "abcn", + } + ], + "branches": [ + { + "id": "line", + "type": "line", + "phases1": "abcn", + "phases2": "abcn", + "bus1": "bus1", + "bus2": "bus2", + "results": { "currents1": [ [100.25125945383468, -0.2512594538348978], [-50.3432267968783, -86.69450772148775], @@ -127,12 +70,22 @@ [49.90803265695649, -86.94576717532254], [3.0791345280630885e-15, -4.010143071868794e-15] ] - } - ], - "loads": [ - { - "id": "load", - "phases": "abcn", + }, + "length": 10, + "params_id": "lp" + } + ], + "loads": [ + { + "id": "load", + "bus": "bus2", + "phases": "abcn", + "powers": [ + [2000000.0, 0.0], + [2000000.0, 0.0], + [2000000.0, 0.0] + ], + "results": { "currents": [ [100.25125945383483, -0.25125945383483067], [-50.34322679687937, -86.69450772148842], @@ -140,11 +93,19 @@ [-2.842170943040401e-14, -1.4210854715202004e-14] ] } - ], - "sources": [ - { - "id": "vs", - "phases": "abcn", + } + ], + "sources": [ + { + "id": "vs", + "bus": "bus1", + "phases": "abcn", + "voltages": [ + [20000.0, 0.0], + [-9999.999999999996, -17320.508075688773], + [-9999.999999999996, 17320.508075688773] + ], + "results": { "currents": [ [-100.25125945383482, 0.25125945383502946], [50.34322679687954, 86.69450772148834], @@ -152,18 +113,26 @@ [3.0793510102933297e-15, -9.450556656687148e-15] ] } - ], - "grounds": [ - { - "id": "gnd", - "potential": [0.0, 0.0] - } - ], - "potential_refs": [ - { - "id": "pref", - "current": [2.1648223024119769e-19, -5.440413584818354e-15] - } - ] - } + } + ], + "lines_params": [ + { + "id": "lp", + "z_line": [ + [ + [0.05, 0.0, 0.0, 0.0], + [0.0, 0.05, 0.0, 0.0], + [0.0, 0.0, 0.05, 0.0], + [0.0, 0.0, 0.0, 0.05] + ], + [ + [0.05, 0.0, 0.0, 0.0], + [0.0, 0.05, 0.0, 0.0], + [0.0, 0.0, 0.05, 0.0], + [0.0, 0.0, 0.0, 0.05] + ] + ] + } + ], + "transformers_params": [] } diff --git a/roseau/load_flow/models/tests/data/Network_3Ph_Unbalanced.json b/roseau/load_flow/models/tests/data/Network_3Ph_Unbalanced.json index f817d13b..da843cdf 100644 --- a/roseau/load_flow/models/tests/data/Network_3Ph_Unbalanced.json +++ b/roseau/load_flow/models/tests/data/Network_3Ph_Unbalanced.json @@ -1,126 +1,57 @@ { - "network": { - "version": 1, - "grounds": [ - { - "id": "gnd", - "buses": [ - { - "id": "bus1", - "phase": "n" - } - ] + "version": 1, + "grounds": [ + { + "id": "gnd", + "buses": [ + { + "id": "bus1", + "phase": "n" + } + ], + "results": { + "potential": [0.0, 0.0] } - ], - "potential_refs": [ - { - "id": "pref", - "ground": "gnd" + } + ], + "potential_refs": [ + { + "id": "pref", + "ground": "gnd", + "results": { + "current": [0.0, 0.0] } - ], - "buses": [ - { - "id": "bus1", - "phases": "abcn", + } + ], + "buses": [ + { + "id": "bus1", + "phases": "abcn", + "potentials": [ + [20000.0, 0.0], + [-9999.999999999996, -17320.508075688773], + [-9999.999999999996, 17320.508075688773], + [0.0, 0.0] + ], + "results": { "potentials": [ [20000.0, 0.0], [-9999.999999999996, -17320.508075688773], [-9999.999999999996, 17320.508075688773], [0.0, 0.0] ] - }, - { - "id": "bus2", - "phases": "abcn", - "potentials": [ - [20000.0, 0.0], - [-9999.999999999996, -17320.508075688773], - [-9999.999999999996, 17320.508075688773], - [0.0, 0.0] - ] - } - ], - "branches": [ - { - "id": "line", - "type": "line", - "phases1": "abcn", - "phases2": "abcn", - "bus1": "bus1", - "bus2": "bus2", - "length": 10, - "params_id": "lp" - } - ], - "loads": [ - { - "id": "load", - "bus": "bus2", - "phases": "abcn", - "powers": [ - [2000000.0, 0.0], - [1000000.0, 500000.0], - [1500000.0, 0.0] - ] - } - ], - "sources": [ - { - "id": "vs", - "bus": "bus1", - "phases": "abcn", - "voltages": [ - [20000.0, 0.0], - [-9999.999999999996, -17320.508075688773], - [-9999.999999999996, 17320.508075688773] - ] } - ], - "lines_params": [ - { - "id": "lp", - "model": "z_neutral", - "z_line": [ - [ - [0.05, 0.0, 0.0, 0.0], - [0.0, 0.05, 0.0, 0.0], - [0.0, 0.0, 0.05, 0.0], - [0.0, 0.0, 0.0, 0.05] - ], - [ - [0.05, 0.0, 0.0, 0.0], - [0.0, 0.05, 0.0, 0.0], - [0.0, 0.0, 0.05, 0.0], - [0.0, 0.0, 0.0, 0.05] - ] - ] - } - ], - "transformers_params": [] - }, - "results": { - "info": { - "status": "success", - "solver": "newton", - "solver_params": {}, - "iterations": 2, - "tolerance": 1e-6, - "residual": 1.2896350654045818e-12 }, - "buses": [ - { - "id": "bus1", - "phases": "abcn", - "potentials": [ - [20000.0, 0.0], - [-9999.999999999996, -17320.508075688773], - [-9999.999999999996, 17320.508075688773], - [0.0, 0.0] - ] - }, - { - "id": "bus2", - "phases": "abcn", + { + "id": "bus2", + "phases": "abcn", + "potentials": [ + [20000.0, 0.0], + [-9999.999999999996, -17320.508075688773], + [-9999.999999999996, 17320.508075688773], + [0.0, 0.0] + ], + "results": { "potentials": [ [19949.70918868138, -49.914688038585716], [-9992.082295943741, -17281.739546644596], @@ -128,12 +59,17 @@ [-8.994030317973111, 25.000690539556487] ] } - ], - "branches": [ - { - "id": "line", - "phases1": "abcn", - "phases2": "abcn", + } + ], + "branches": [ + { + "id": "line", + "type": "line", + "phases1": "abcn", + "phases2": "abcn", + "bus1": "bus1", + "bus2": "bus2", + "results": { "currents1": [ [100.20549935720612, -0.3761232800346832], [-46.68623310043222, -30.850824987921442], @@ -146,12 +82,22 @@ [37.5126060351904, -65.2216691254871], [16.006660221583374, 33.9947208575296] ] - } - ], - "loads": [ - { - "id": "load", - "phases": "abcn", + }, + "length": 10, + "params_id": "lp" + } + ], + "loads": [ + { + "id": "load", + "bus": "bus2", + "phases": "abcn", + "powers": [ + [2000000.0, 0.0], + [1000000.0, 500000.0], + [1500000.0, 0.0] + ], + "results": { "currents": [ [100.20549935720497, -0.37612328003408035], [-46.68623310043201, -30.850824987922692], @@ -159,11 +105,19 @@ [-16.00666022158243, -33.99472085752997] ] } - ], - "sources": [ - { - "id": "vs", - "phases": "abcn", + } + ], + "sources": [ + { + "id": "vs", + "bus": "bus1", + "phases": "abcn", + "voltages": [ + [20000.0, 0.0], + [-9999.999999999996, -17320.508075688773], + [-9999.999999999996, 17320.508075688773] + ], + "results": { "currents": [ [-100.20549935720585, 0.37612328003441564], [46.68623310043194, 30.85082498792273], @@ -171,18 +125,26 @@ [16.006660221583374, 33.9947208575296] ] } - ], - "grounds": [ - { - "id": "gnd", - "potential": [0.0, 0.0] - } - ], - "potential_refs": [ - { - "id": "pref", - "current": [0.0, 0.0] - } - ] - } + } + ], + "lines_params": [ + { + "id": "lp", + "z_line": [ + [ + [0.05, 0.0, 0.0, 0.0], + [0.0, 0.05, 0.0, 0.0], + [0.0, 0.0, 0.05, 0.0], + [0.0, 0.0, 0.0, 0.05] + ], + [ + [0.05, 0.0, 0.0, 0.0], + [0.0, 0.05, 0.0, 0.0], + [0.0, 0.0, 0.05, 0.0], + [0.0, 0.0, 0.0, 0.05] + ] + ] + } + ], + "transformers_params": [] } diff --git a/roseau/load_flow/models/tests/data/Network_3Ph_Unbalanced_Shunt.json b/roseau/load_flow/models/tests/data/Network_3Ph_Unbalanced_Shunt.json index 013fe973..064b2f25 100644 --- a/roseau/load_flow/models/tests/data/Network_3Ph_Unbalanced_Shunt.json +++ b/roseau/load_flow/models/tests/data/Network_3Ph_Unbalanced_Shunt.json @@ -1,149 +1,73 @@ { - "network": { - "version": 1, - "grounds": [ - { - "id": "gnd", - "buses": [ - { - "id": "sb", - "phase": "n" - } - ] + "version": 1, + "grounds": [ + { + "id": "gnd", + "buses": [ + { + "id": "sb", + "phase": "n" + } + ], + "results": { + "potential": [0.0, 0.0] } - ], - "potential_refs": [ - { - "id": "pref", - "ground": "gnd" + } + ], + "potential_refs": [ + { + "id": "pref", + "ground": "gnd", + "results": { + "current": [2.4975160828333287e-12, 1.8161918366801921e-12] } - ], - "buses": [ - { - "id": "sb", - "phases": "abcn", + } + ], + "buses": [ + { + "id": "sb", + "phases": "abcn", + "potentials": [ + [11547.005383792517, 0.0], + [-5773.502691896257, -10000.000000000002], + [-5773.502691896257, 10000.000000000002], + [0.0, 0.0] + ], + "results": { "potentials": [ [11547.005383792517, 0.0], [-5773.502691896257, -10000.000000000002], [-5773.502691896257, 10000.000000000002], [0.0, 0.0] ] - }, - { - "id": "lb", - "phases": "abc", - "potentials": [ - [11547.005383792517, 0.0], - [-5773.502691896257, -10000.000000000002], - [-5773.502691896257, 10000.000000000002] - ] - } - ], - "branches": [ - { - "id": "line", - "type": "line", - "phases1": "abc", - "phases2": "abc", - "bus1": "sb", - "bus2": "lb", - "length": 10, - "params_id": "lp", - "ground": "gnd" - } - ], - "loads": [ - { - "id": "load", - "bus": "lb", - "phases": "abc", - "powers": [ - [800000.0, 599999.9999999999], - [500000.0, 0.0], - [400000.0, 299999.99999999994] - ] - } - ], - "sources": [ - { - "id": "vs", - "bus": "sb", - "phases": "abcn", - "voltages": [ - [11547.005383792517, 0.0], - [-5773.502691896257, -10000.000000000002], - [-5773.502691896257, 10000.000000000002] - ] - } - ], - "lines_params": [ - { - "id": "lp", - "model": "zy_neutral", - "z_line": [ - [ - [0.12918333333333334, 0.0, 0.0], - [0.0, 0.12918333333333334, 0.0], - [0.0, 0.0, 0.12918333333333334] - ], - [ - [0.10995533333333332, 0.05497783333333334, 0.05497783333333334], - [0.05497783333333334, 0.10995533333333332, 0.05497783333333334], - [0.05497783333333334, 0.05497783333333334, 0.10995533333333332] - ] - ], - "y_shunt": [ - [ - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0], - [0.0, 0.0, 0.0] - ], - [ - [4.930205666666666e-5, 6.073716666666661e-7, 6.073716666666661e-7], - [6.073716666666661e-7, 4.930205666666666e-5, 6.073716666666661e-7], - [6.073716666666661e-7, 6.073716666666661e-7, 4.930205666666666e-5] - ] - ] } - ], - "transformers_params": [] - }, - "results": { - "info": { - "solver": "newton", - "solver_params": {}, - "tolerance": 1e-6, - "max_iterations": 20, - "status": "success", - "iterations": 2, - "residual": 8.284928298962768e-12 }, - "buses": [ - { - "id": "sb", - "phases": "abcn", - "potentials": [ - [11547.005383792517, 0.0], - [-5773.502691896257, -10000.000000000002], - [-5773.502691896257, 10000.000000000002], - [0.0, 0.0] - ] - }, - { - "id": "lb", - "phases": "abc", + { + "id": "lb", + "phases": "abc", + "potentials": [ + [11547.005383792517, 0.0], + [-5773.502691896257, -10000.000000000002], + [-5773.502691896257, 10000.000000000002] + ], + "results": { "potentials": [ [11455.159113672085, 1.0958017067068864], [-5723.334202012903, -9947.26661034906], [-5731.824911659177, 9946.17080864235] ] } - ], - "branches": [ - { - "id": "line", - "phases1": "abc", - "phases2": "abc", + } + ], + "branches": [ + { + "id": "line", + "type": "line", + "phases1": "abc", + "phases2": "abc", + "bus1": "sb", + "bus2": "lb", + "results": { "currents1": [ [59.88964164257595, -23.524538885127185], [-45.15366676852227, -21.973732003466466], @@ -154,23 +78,42 @@ [50.01029608953468, 19.17455774824157], [9.879612351638343, -48.29951240597802] ] - } - ], - "loads": [ - { - "id": "load", - "phases": "abc", + }, + "length": 10, + "params_id": "lp", + "ground": "gnd" + } + ], + "loads": [ + { + "id": "load", + "bus": "lb", + "phases": "abc", + "powers": [ + [800000.0, 599999.9999999999], + [500000.0, 0.0], + [400000.0, 299999.99999999994] + ], + "results": { "currents": [ [59.88990844117971, -29.124954657740865], [-50.010296089540994, -19.174557748241565], [-9.879612351638723, 48.299512405982426] ] } - ], - "sources": [ - { - "id": "vs", - "phases": "abcn", + } + ], + "sources": [ + { + "id": "vs", + "bus": "sb", + "phases": "abcn", + "voltages": [ + [11547.005383792517, 0.0], + [-5773.502691896257, -10000.000000000002], + [-5773.502691896257, 10000.000000000002] + ], + "results": { "currents": [ [-59.88964164257699, 23.524538885127143], [45.15366676852186, 21.973732003464814], @@ -178,18 +121,36 @@ [2.498404261253029e-12, 1.8188563719392925e-12] ] } - ], - "grounds": [ - { - "id": "gnd", - "potential": [0.0, 0.0] - } - ], - "potential_refs": [ - { - "id": "pref", - "current": [2.4975160828333287e-12, 1.8161918366801921e-12] - } - ] - } + } + ], + "lines_params": [ + { + "id": "lp", + "z_line": [ + [ + [0.12918333333333334, 0.0, 0.0], + [0.0, 0.12918333333333334, 0.0], + [0.0, 0.0, 0.12918333333333334] + ], + [ + [0.10995533333333332, 0.05497783333333334, 0.05497783333333334], + [0.05497783333333334, 0.10995533333333332, 0.05497783333333334], + [0.05497783333333334, 0.05497783333333334, 0.10995533333333332] + ] + ], + "y_shunt": [ + [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0] + ], + [ + [4.930205666666666e-5, 6.073716666666661e-7, 6.073716666666661e-7], + [6.073716666666661e-7, 4.930205666666666e-5, 6.073716666666661e-7], + [6.073716666666661e-7, 6.073716666666661e-7, 4.930205666666666e-5] + ] + ] + } + ], + "transformers_params": [] } diff --git a/roseau/load_flow/models/tests/test_line_parameters.py b/roseau/load_flow/models/tests/test_line_parameters.py index fc5c77c6..0d9d7b7f 100644 --- a/roseau/load_flow/models/tests/test_line_parameters.py +++ b/roseau/load_flow/models/tests/test_line_parameters.py @@ -1,3 +1,4 @@ +import json import re import numpy as np @@ -341,7 +342,7 @@ def test_from_name_lv(): def test_from_name_mv(): - with pytest.raises(RoseauLoadFlowException) as e, pytest.warns(FutureWarning): + 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 @@ -495,3 +496,12 @@ def test_max_current(): lp.max_current = Q_(3, "kA") assert lp.max_current == Q_(3_000, "A") + + +def test_json_serialization(): + lp = LineParameters("test", z_line=np.eye(3), max_current=np.int64(100), section=np.float64(150)) + lp_dict = lp.to_dict() + assert isinstance(lp_dict["z_line"], list) + assert isinstance(lp_dict["max_current"], int) + assert isinstance(lp_dict["section"], float) + json.dumps(lp_dict) diff --git a/roseau/load_flow/models/tests/test_loads.py b/roseau/load_flow/models/tests/test_loads.py index 4682e3b7..63e2e498 100644 --- a/roseau/load_flow/models/tests/test_loads.py +++ b/roseau/load_flow/models/tests/test_loads.py @@ -268,13 +268,13 @@ def test_loads_to_dict(): values = [1 + 2j, 3 + 4j, 5 + 6j] # Power load - assert PowerLoad("load_s1", bus, phases="abcn", powers=values).to_dict() == { + assert PowerLoad("load_s1", bus, phases="abcn", powers=values).to_dict(include_results=False) == { "id": "load_s1", "bus": "bus", "phases": "abcn", "powers": [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], } - assert PowerLoad("load_s2", bus, phases="abc", powers=values).to_dict() == { + assert PowerLoad("load_s2", bus, phases="abc", powers=values).to_dict(include_results=False) == { "id": "load_s2", "bus": "bus", "phases": "abc", @@ -282,13 +282,13 @@ def test_loads_to_dict(): } # Current load - assert CurrentLoad("load_i1", bus, phases="abcn", currents=values).to_dict() == { + assert CurrentLoad("load_i1", bus, phases="abcn", currents=values).to_dict(include_results=False) == { "id": "load_i1", "bus": "bus", "phases": "abcn", "currents": [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], } - assert CurrentLoad("load_i2", bus, phases="abc", currents=values).to_dict() == { + assert CurrentLoad("load_i2", bus, phases="abc", currents=values).to_dict(include_results=False) == { "id": "load_i2", "bus": "bus", "phases": "abc", @@ -296,13 +296,13 @@ def test_loads_to_dict(): } # Impedance load - assert ImpedanceLoad("load_z1", bus, phases="abcn", impedances=values).to_dict() == { + assert ImpedanceLoad("load_z1", bus, phases="abcn", impedances=values).to_dict(include_results=False) == { "id": "load_z1", "bus": "bus", "phases": "abcn", "impedances": [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], } - assert ImpedanceLoad("load_z2", bus, phases="abc", impedances=values).to_dict() == { + assert ImpedanceLoad("load_z2", bus, phases="abc", impedances=values).to_dict(include_results=False) == { "id": "load_z2", "bus": "bus", "phases": "abc", @@ -331,14 +331,16 @@ def test_loads_to_dict(): } fp = [FlexibleParameter.constant()] * 3 flex_load = PowerLoad("load_f1", bus, phases="abcn", powers=values, flexible_params=fp) - assert flex_load.to_dict() == expected_dict + assert flex_load.to_dict(include_results=False) == expected_dict parsed_flex_load = PowerLoad.from_dict(expected_dict | {"bus": bus}) assert isinstance(parsed_flex_load, PowerLoad) assert parsed_flex_load.id == flex_load.id assert parsed_flex_load.bus.id == flex_load.bus.id assert parsed_flex_load.phases == flex_load.phases assert np.allclose(parsed_flex_load.powers, flex_load.powers) - assert [p.to_dict() for p in parsed_flex_load.flexible_params] == [p.to_dict() for p in flex_load.flexible_params] + assert [p.to_dict(include_results=False) for p in parsed_flex_load.flexible_params] == [ + p.to_dict(include_results=False) for p in flex_load.flexible_params + ] def test_loads_units(): diff --git a/roseau/load_flow/models/transformers/parameters.py b/roseau/load_flow/models/transformers/parameters.py index 9ee332e2..b8c23410 100644 --- a/roseau/load_flow/models/transformers/parameters.py +++ b/roseau/load_flow/models/transformers/parameters.py @@ -261,7 +261,7 @@ def to_zyk(self) -> tuple[Q_[complex], Q_[complex], Q_[float], float]: # Json Mixin interface # @classmethod - def from_dict(cls, data: JsonDict) -> Self: + def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: return cls( id=data["id"], type=data["type"], # Type of the transformer @@ -275,7 +275,7 @@ def from_dict(cls, data: JsonDict) -> Self: max_power=data.get("max_power"), # Maximum power loading (VA) ) - def to_dict(self, *, _lf_only: bool = False) -> JsonDict: + def _to_dict(self, include_results: bool) -> JsonDict: res = { "id": self.id, "sn": self._sn, @@ -287,8 +287,13 @@ def to_dict(self, *, _lf_only: bool = False) -> JsonDict: "vsc": self._vsc, "type": self.type, } - if not _lf_only and self.max_power is not None: + if self.max_power is not None: res["max_power"] = self.max_power.magnitude + for k, v in res.items(): + if isinstance(v, np.integer): + res[k] = int(v) + elif isinstance(v, np.floating): + res[k] = float(v) return res def _results_to_dict(self, warning: bool) -> NoReturn: @@ -296,7 +301,7 @@ def _results_to_dict(self, warning: bool) -> NoReturn: logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.JSON_NO_RESULTS) - def results_from_dict(self, data: JsonDict) -> NoReturn: + def _results_from_dict(self, data: JsonDict) -> NoReturn: msg = f"The {type(self).__name__} has no results to import." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.JSON_NO_RESULTS) diff --git a/roseau/load_flow/models/transformers/transformers.py b/roseau/load_flow/models/transformers/transformers.py index dca30bad..fc1594e5 100644 --- a/roseau/load_flow/models/transformers/transformers.py +++ b/roseau/load_flow/models/transformers/transformers.py @@ -186,8 +186,11 @@ def max_power(self) -> Q_[float] | None: # for all transformers that share the parameters. It is better to set it on the parameters. return self.parameters.max_power - def to_dict(self, *, _lf_only: bool = False) -> JsonDict: - return {**super().to_dict(_lf_only=_lf_only), "params_id": self.parameters.id, "tap": self.tap} + def _to_dict(self, include_results: bool) -> JsonDict: + res = super()._to_dict(include_results=include_results) + res["tap"] = self.tap + res["params_id"] = self.parameters.id + return res def _compute_phases_three( self, diff --git a/roseau/load_flow/network.py b/roseau/load_flow/network.py index ca53f1e5..80ccb1e0 100644 --- a/roseau/load_flow/network.py +++ b/roseau/load_flow/network.py @@ -139,7 +139,6 @@ def __init__( self._check_validity(constructed=False) self._create_network() self._valid = True - self._results_valid: bool = False self._solver = AbstractSolver.from_dict(data={"name": self._DEFAULT_SOLVER, "params": {}}, network=self) def __repr__(self) -> str: @@ -184,11 +183,15 @@ def _elements_as_dict(elements: MapOrSeq[_E], error_code: RoseauLoadFlowExceptio @classmethod def from_element(cls, initial_bus: Bus) -> Self: - """Construct the network from only one element and add the others automatically. + """Construct the network from only one element (bus) and add the others automatically. Args: initial_bus: - Any bus of the network. + Any bus of the network. The network is constructed from this bus and all the + elements connected to it. This is usually the main source bus of the network. + + Returns: + The network constructed from the given bus and all the elements connected to it. """ buses: list[Bus] = [] branches: list[AbstractBranch] = [] @@ -534,6 +537,7 @@ def solve_load_flow( ) logger.debug(f"The load flow converged after {iterations} iterations and {end - start:.3n} s.") + self._no_results = False # Lazily update the results of the elements for element in chain( @@ -545,6 +549,7 @@ def solve_load_flow( self.potential_refs.values(), ): element._fetch_results = True + element._no_results = False # The results are now valid self._results_valid = True @@ -578,35 +583,6 @@ def _handle_error(self, e: RuntimeError) -> NoReturn: 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. - - Args: - data: - The results returned by the solver. - """ - for bus_data in data["buses"]: - bus = self.buses[bus_data["id"]] - bus.results_from_dict(bus_data) - for branch_data in data["branches"]: - branch = self.branches[branch_data["id"]] - branch.results_from_dict(branch_data) - for load_data in data["loads"]: - load = self.loads[load_data["id"]] - load.results_from_dict(load_data) - for source_data in data["sources"]: - source = self.sources[source_data["id"]] - source.results_from_dict(data=source_data) - for ground_data in data["grounds"]: - ground = self.grounds[ground_data["id"]] - ground.results_from_dict(ground_data) - for p_ref_data in data["potential_refs"]: - p_ref = self.potential_refs[p_ref_data["id"]] - p_ref.results_from_dict(p_ref_data) - - # The results are now valid - self._results_valid = True - # # Properties to access the load flow results as dataframes # @@ -1300,6 +1276,9 @@ def _propagate_potentials(self) -> None: phase_displacement = element.parameters.phase_displacement if phase_displacement is None: phase_displacement = 0 + if element.parameters.type == "center" and "n" not in element.bus1.phases: + # "n" is mandatory in the bus2 but not in the bus1 + potentials = np.append(potentials, 0.0) elements.append((e, potentials * k * np.exp(phase_displacement * -1j * np.pi / 6.0))) else: elements.append((e, potentials)) @@ -1346,18 +1325,24 @@ def _check_ref(elements: Iterable[Element]) -> None: # Network saving/loading # @classmethod - def from_dict(cls, data: JsonDict) -> Self: + def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: """Construct an electrical network from a dict created with :meth:`to_dict`. Args: data: The dictionary containing the network data. + include_results: + If True (default) and the results of the load flow are included in the dictionary, + the results are also loaded into the element. + Returns: The constructed network. """ - buses, branches, loads, sources, grounds, p_refs = network_from_dict(data) - return cls( + buses, branches, loads, sources, grounds, p_refs, has_results = network_from_dict( + data, include_results=include_results + ) + network = cls( buses=buses, branches=branches, loads=loads, @@ -1365,27 +1350,24 @@ def from_dict(cls, data: JsonDict) -> Self: grounds=grounds, potential_refs=p_refs, ) + network._no_results = not has_results + network._results_valid = has_results + return network - def to_dict(self, *, _lf_only: bool = False) -> JsonDict: - """Convert the electrical network to a dictionary. - - Args: - _lf_only: - Internal argument, please do not use. - """ - return network_to_dict(self, _lf_only=_lf_only) + def _to_dict(self, include_results: bool) -> JsonDict: + return network_to_dict(self, include_results=include_results) # # Results saving/loading # - def results_from_dict(self, data: JsonDict) -> None: + def _results_from_dict(self, data: JsonDict) -> None: """Load the results of a load flow from a dict created by :meth:`results_to_dict`. The results are stored in the network elements. Args: data: - The dictionary containing the results as returned by the solver. + The dictionary containing the load flow results. """ # Checks on the provided data for key, self_elements, name in ( @@ -1413,7 +1395,28 @@ def results_from_dict(self, data: JsonDict) -> None: raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LOAD_FLOW_RESULT) # The results are assigned to all elements - self._results_from_dict(data) + for bus_data in data["buses"]: + bus = self.buses[bus_data["id"]] + bus._results_from_dict(bus_data) + for branch_data in data["branches"]: + branch = self.branches[branch_data["id"]] + branch._results_from_dict(branch_data) + for load_data in data["loads"]: + load = self.loads[load_data["id"]] + load._results_from_dict(load_data) + for source_data in data["sources"]: + source = self.sources[source_data["id"]] + source._results_from_dict(data=source_data) + for ground_data in data["grounds"]: + ground = self.grounds[ground_data["id"]] + ground._results_from_dict(ground_data) + for p_ref_data in data["potential_refs"]: + p_ref = self.potential_refs[p_ref_data["id"]] + p_ref._results_from_dict(p_ref_data) + + # The results are now valid + self._results_valid = True + self._no_results = False def _results_to_dict(self, warning: bool) -> JsonDict: """Get the voltages and currents computed by the load flow and return them as a dict.""" diff --git a/roseau/load_flow/tests/data/networks/single_phase_network.json b/roseau/load_flow/tests/data/networks/single_phase_network.json new file mode 100644 index 00000000..4a95612a --- /dev/null +++ b/roseau/load_flow/tests/data/networks/single_phase_network.json @@ -0,0 +1,129 @@ +{ + "version": 1, + "grounds": [ + { + "id": "ground", + "buses": [ + { + "id": "bus1", + "phase": "n" + } + ], + "results": { + "potential": [0.0, 0.0] + } + } + ], + "potential_refs": [ + { + "id": "pref", + "ground": "ground", + "results": { + "current": [-1.2500243895541274e-13, 0.0] + } + } + ], + "buses": [ + { + "id": "bus0", + "phases": "bn", + "geometry": { + "type": "Point", + "coordinates": [-1.318375372111463, 48.64794139348595] + }, + "results": { + "potentials": [ + [19999.94999975, 0.0], + [-0.050000250001249996, 0.0] + ] + } + }, + { + "id": "bus1", + "phases": "bn", + "geometry": { + "type": "Point", + "coordinates": [-1.320149235966572, 48.64971306653889] + }, + "results": { + "potentials": [ + [19999.899999499998, 0.0], + [0.0, 0.0] + ] + } + } + ], + "branches": [ + { + "id": "line", + "type": "line", + "phases1": "bn", + "phases2": "bn", + "bus1": "bus0", + "bus2": "bus1", + "geometry": { + "type": "LineString", + "coordinates": [ + [-1.318375372111463, 48.64794139348595], + [-1.320149235966572, 48.64971306653889] + ] + }, + "results": { + "currents1": [ + [0.005000025000117603, 0.0], + [-0.005000025000125, 0.0] + ], + "currents2": [ + [-0.005000025000117603, -0.0], + [0.005000025000125, -0.0] + ] + }, + "length": 1.0, + "params_id": "test" + } + ], + "loads": [ + { + "id": "load", + "bus": "bus1", + "phases": "bn", + "powers": [[100.0, 0.0]], + "results": { + "currents": [ + [0.005000025000250002, -0.0], + [-0.005000025000250002, 0.0] + ] + } + } + ], + "sources": [ + { + "id": "vs", + "bus": "bus0", + "phases": "bn", + "voltages": [[20000.0, 0.0]], + "results": { + "currents": [ + [-0.005000025000125, 0.0], + [0.005000025000125, 0.0] + ] + } + } + ], + "lines_params": [ + { + "id": "test", + "z_line": [ + [ + [10.0, 0.0], + [0.0, 10.0] + ], + [ + [0.0, 0.0], + [0.0, 0.0] + ] + ] + } + ], + "transformers_params": [] +} diff --git a/roseau/load_flow/tests/data/networks/small_network.json b/roseau/load_flow/tests/data/networks/small_network.json new file mode 100644 index 00000000..fb7870f3 --- /dev/null +++ b/roseau/load_flow/tests/data/networks/small_network.json @@ -0,0 +1,153 @@ +{ + "version": 1, + "grounds": [ + { + "id": "ground", + "buses": [ + { + "id": "bus1", + "phase": "n" + } + ], + "results": { + "potential": [0.0, 0.0] + } + } + ], + "potential_refs": [ + { + "id": "pref", + "ground": "ground", + "results": { + "current": [1.0842021724855044e-18, -2.891203383964549e-19] + } + } + ], + "buses": [ + { + "id": "bus0", + "phases": "abcn", + "geometry": { + "type": "Point", + "coordinates": [-1.318375372111463, 48.64794139348595] + }, + "results": { + "potentials": [ + [20000.0, 2.891203383964549e-18], + [-10000.000000000002, -17320.508076], + [-10000.000000000002, 17320.508076], + [-1.3476481215690672e-12, 2.891203383964549e-18] + ] + } + }, + { + "id": "bus1", + "phases": "abcn", + "geometry": { + "type": "Point", + "coordinates": [-1.320149235966572, 48.64971306653889] + }, + "results": { + "potentials": [ + [19999.949999875, 2.8911961559741588e-18], + [-9999.974999937502, -17320.464774621556], + [-9999.974999937502, 17320.464774621556], + [0.0, 0.0] + ] + } + } + ], + "branches": [ + { + "id": "line", + "type": "line", + "phases1": "abcn", + "phases2": "abcn", + "bus1": "bus0", + "bus2": "bus1", + "geometry": { + "type": "LineString", + "coordinates": [ + [-1.318375372111463, 48.64794139348595], + [-1.320149235966572, 48.64971306653889] + ] + }, + "results": { + "currents1": [ + [0.005000012500022422, 7.227990390093038e-25], + [-0.002500006250011211, -0.004330137844226556], + [-0.002500006250011211, 0.004330137844226556], + [-1.3476481215690672e-13, 2.891203383964549e-19] + ], + "currents2": [ + [-0.005000012500022422, -7.227990390093038e-25], + [0.002500006250011211, 0.004330137844226556], + [0.002500006250011211, -0.004330137844226556], + [1.3476481215690672e-13, -2.891203383964549e-19] + ] + }, + "length": 1.0, + "params_id": "test" + } + ], + "loads": [ + { + "id": "load", + "bus": "bus1", + "phases": "abcn", + "powers": [ + [100.0, 0.0], + [100.0, 0.0], + [100.0, 0.0] + ], + "results": { + "currents": [ + [0.0050000125000625, 7.228026530113222e-25], + [-0.002500006249963868, -0.004330137844254964], + [-0.002500006249963868, 0.004330137844254964], + [-1.3476372795473424e-13, 0.0] + ] + } + } + ], + "sources": [ + { + "id": "vs", + "bus": "bus0", + "phases": "abcn", + "voltages": [ + [20000.0, 0.0], + [-10000.0, -17320.508076], + [-10000.0, 17320.508076] + ], + "results": { + "currents": [ + [-0.00500001250003125, -8.673617379884035e-19], + [0.0025000062499482426, 0.004330137844227901], + [0.0025000062499482426, -0.0043301378442279], + [1.3476481215690672e-13, -2.891203383964549e-19] + ] + } + } + ], + "lines_params": [ + { + "id": "test", + "z_line": [ + [ + [10.0, 0.0, 0.0, 0.0], + [0.0, 10.0, 0.0, 0.0], + [0.0, 0.0, 10.0, 0.0], + [0.0, 0.0, 0.0, 10.0] + ], + [ + [0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0] + ] + ] + } + ], + "transformers_params": [] +} diff --git a/roseau/load_flow/tests/test_electrical_network.py b/roseau/load_flow/tests/test_electrical_network.py index 16a210ea..0ef0db07 100644 --- a/roseau/load_flow/tests/test_electrical_network.py +++ b/roseau/load_flow/tests/test_electrical_network.py @@ -1,3 +1,4 @@ +import contextlib import itertools as it import re import warnings @@ -6,10 +7,10 @@ import geopandas as gpd import networkx as nx import numpy as np +import numpy.testing as npt import pandas as pd import pytest from pandas.testing import assert_frame_equal -from shapely import LineString, Point from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow.models import ( @@ -29,149 +30,24 @@ from roseau.load_flow.units import Q_ from roseau.load_flow.utils import BranchTypeDtype, PhaseDtype, VoltagePhaseDtype - -@pytest.fixture() -def small_network() -> ElectricalNetwork: - # Build a small network - point1 = Point(-1.318375372111463, 48.64794139348595) - point2 = Point(-1.320149235966572, 48.64971306653889) - line_string = LineString([point1, point2]) - - ground = Ground("ground") - source_bus = Bus("bus0", phases="abcn", geometry=point1) - load_bus = Bus("bus1", phases="abcn", geometry=point2) - ground.connect(load_bus) - - voltages = [20000.0 + 0.0j, -10000.0 - 17320.508076j, -10000.0 + 17320.508076j] - vs = VoltageSource("vs", source_bus, voltages=voltages, phases="abcn") - load = PowerLoad("load", load_bus, powers=[100, 100, 100], phases="abcn") - pref = PotentialRef("pref", element=ground) - - lp = LineParameters("test", z_line=10 * np.eye(4, dtype=complex)) - line = Line("line", source_bus, load_bus, phases="abcn", parameters=lp, length=1.0, geometry=line_string) - - return ElectricalNetwork( - buses=[source_bus, load_bus], - branches=[line], - loads=[load], - sources=[vs], - grounds=[ground], - potential_refs=[pref], - ) +# The following networks are generated using the scripts/genereate_test_networks.py script @pytest.fixture() -def single_phase_network() -> ElectricalNetwork: - # Build a small single-phase network - # ---------------------------------- +def small_network(test_networks_path) -> ElectricalNetwork: + # Loadthe network from the JSON file (without results) + return ElectricalNetwork.from_json(test_networks_path / "small_network.json", include_results=False) - # Phase "b" is chosen to catch errors where the index of the first phase may be assumed to be 0 - phases = "bn" - # Network geometry - point1 = Point(-1.318375372111463, 48.64794139348595) - point2 = Point(-1.320149235966572, 48.64971306653889) - line_string = LineString([point1, point2]) - - # Network elements - bus0 = Bus("bus0", phases=phases, geometry=point1) - bus1 = Bus("bus1", phases=phases, geometry=point2) - - ground = Ground("ground") - ground.connect(bus1) - pref = PotentialRef("pref", element=ground) - - vs = VoltageSource("vs", bus0, voltages=[20000.0 + 0.0j], phases=phases) - load = PowerLoad("load", bus1, powers=[100], phases=phases) - - lp = LineParameters("test", z_line=10 * np.eye(2, dtype=complex)) - line = Line("line", bus0, bus1, phases=phases, parameters=lp, length=1.0, geometry=line_string) - - return ElectricalNetwork( - buses=[bus0, bus1], - branches=[line], - loads=[load], - sources=[vs], - grounds=[ground], - potential_refs=[pref], - ) +@pytest.fixture() +def small_network_with_results(test_networks_path) -> ElectricalNetwork: + # Load the network from the JSON file (with results, no need to invoke the solver) + return ElectricalNetwork.from_json(test_networks_path / "small_network.json", include_results=True) @pytest.fixture() -def good_json_results() -> dict: - return { - "buses": [ - { - "id": "bus0", - "phases": "abcn", - "potentials": [ - [20000.0, 2.891203383964549e-18], - [-10000.000000000002, -17320.508076], - [-10000.000000000002, 17320.508076], - [-1.3476481215690672e-12, 2.891203383964549e-18], - ], - }, - { - "id": "bus1", - "phases": "abcn", - "potentials": [ - [19999.949999875, 2.8911961559741588e-18], - [-9999.974999937502, -17320.464774621556], - [-9999.974999937502, 17320.464774621556], - [0.0, 0.0], - ], - }, - ], - "branches": [ - { - "id": "line", - "phases1": "abcn", - "phases2": "abcn", - "currents1": [ - [0.005000012500022422, 7.227990390093038e-25], - [-0.002500006250011211, -0.004330137844226556], - [-0.002500006250011211, 0.004330137844226556], - [-1.3476481215690672e-13, 2.891203383964549e-19], - ], - "currents2": [ - [-0.005000012500022422, -7.227990390093038e-25], - [0.002500006250011211, 0.004330137844226556], - [0.002500006250011211, -0.004330137844226556], - [1.3476481215690672e-13, -2.891203383964549e-19], - ], - } - ], - "loads": [ - { - "id": "load", - "phases": "abcn", - "currents": [ - [0.0050000125000625, 7.228026530113222e-25], - [-0.002500006249963868, -0.004330137844254964], - [-0.002500006249963868, 0.004330137844254964], - [-1.3476372795473424e-13, 0.0], - ], - } - ], - "sources": [ - { - "id": "vs", - "phases": "abcn", - "currents": [ - [-0.00500001250003125, -8.673617379884035e-19], - [0.0025000062499482426, 0.004330137844227901], - [0.0025000062499482426, -0.0043301378442279], - [1.3476481215690672e-13, -2.891203383964549e-19], - ], - } - ], - "grounds": [ - {"id": "ground", "potential": [0.0, 0.0]}, - ], - "potential_refs": [ - {"id": "pref", "current": [1.0842021724855044e-18, -2.891203383964549e-19]}, - ], - } +def single_phase_network(test_networks_path) -> ElectricalNetwork: + return ElectricalNetwork.from_json(test_networks_path / "single_phase_network.json", include_results=True) @contextmanager @@ -589,11 +465,11 @@ def test_empty_network(): assert exc_info.value.msg == "Cannot create a network without elements." -def test_buses_voltages(small_network: ElectricalNetwork, good_json_results): - assert isinstance(small_network, ElectricalNetwork) - small_network.results_from_dict(good_json_results) - small_network.buses["bus0"].max_voltage = 21_000 - small_network.buses["bus1"].min_voltage = 20_000 +def test_buses_voltages(small_network_with_results): + assert isinstance(small_network_with_results, ElectricalNetwork) + en = small_network_with_results + en.buses["bus0"].max_voltage = 21_000 + en.buses["bus1"].min_voltage = 20_000 voltage_records = [ { @@ -646,7 +522,7 @@ def test_buses_voltages(small_network: ElectricalNetwork, good_json_results): }, ] - buses_voltages = small_network.res_buses_voltages + buses_voltages = en.res_buses_voltages expected_buses_voltages = ( pd.DataFrame.from_records(voltage_records) .astype( @@ -698,8 +574,6 @@ def test_single_phase_network(single_phase_network: ElectricalNetwork): line = single_phase_network.branches["line"] load = single_phase_network.loads["load"] - single_phase_network.solve_load_flow() - # Test results of elements # ------------------------ assert np.allclose(source_bus.res_potentials.m_as("V"), [19999.94999975 + 0j, -0.050000250001249996 + 0j]) @@ -711,7 +585,7 @@ def test_single_phase_network(single_phase_network: ElectricalNetwork): # Test results of network # ----------------------- # Buses results - pd.testing.assert_frame_equal( + assert_frame_equal( single_phase_network.res_buses, pd.DataFrame.from_records( [ @@ -725,7 +599,7 @@ def test_single_phase_network(single_phase_network: ElectricalNetwork): .set_index(["bus_id", "phase"]), ) # Buses voltages results - pd.testing.assert_frame_equal( + assert_frame_equal( single_phase_network.res_buses_voltages, pd.DataFrame.from_records( [ @@ -759,7 +633,7 @@ def test_single_phase_network(single_phase_network: ElectricalNetwork): .set_index(["bus_id", "phase"]), ) # Branches results - pd.testing.assert_frame_equal( + assert_frame_equal( single_phase_network.res_branches, pd.DataFrame.from_records( [ @@ -803,7 +677,7 @@ def test_single_phase_network(single_phase_network: ElectricalNetwork): ) # Transformers results - pd.testing.assert_frame_equal( + assert_frame_equal( single_phase_network.res_transformers, pd.DataFrame.from_records( [], @@ -836,7 +710,7 @@ def test_single_phase_network(single_phase_network: ElectricalNetwork): .set_index(["transformer_id", "phase"]), ) # Lines results - pd.testing.assert_frame_equal( + assert_frame_equal( single_phase_network.res_lines, pd.DataFrame.from_records( [ @@ -895,7 +769,7 @@ def test_single_phase_network(single_phase_network: ElectricalNetwork): check_exact=False, ) # Switches results - pd.testing.assert_frame_equal( + assert_frame_equal( single_phase_network.res_switches, pd.DataFrame.from_records( [], @@ -924,7 +798,7 @@ def test_single_phase_network(single_phase_network: ElectricalNetwork): .set_index(["switch_id", "phase"]), ) # Loads results - pd.testing.assert_frame_equal( + assert_frame_equal( single_phase_network.res_loads, pd.DataFrame.from_records( [ @@ -999,34 +873,35 @@ def test_network_elements(small_network: ElectricalNetwork): assert element.network == small_network_2 -def test_network_results_warning(small_network: ElectricalNetwork, recwarn): # noqa: C901 +def test_network_results_warning(small_network, small_network_with_results, recwarn): # noqa: C901 + en = small_network # network well-defined using the constructor - for bus in small_network.buses.values(): - assert bus.network == small_network - for load in small_network.loads.values(): - assert load.network == small_network - for source in small_network.sources.values(): - assert source.network == small_network - for branch in small_network.branches.values(): - assert branch.network == small_network - for ground in small_network.grounds.values(): - assert ground.network == small_network - for p_ref in small_network.potential_refs.values(): - assert p_ref.network == small_network + for bus in en.buses.values(): + assert bus.network == en + for load in en.loads.values(): + assert load.network == en + for source in en.sources.values(): + assert source.network == en + for branch in en.branches.values(): + assert branch.network == en + for ground in en.grounds.values(): + assert ground.network == en + for p_ref in en.potential_refs.values(): + assert p_ref.network == en # All the results function raises an exception - for bus in small_network.buses.values(): + for bus in en.buses.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = bus.res_potentials assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN with pytest.raises(RoseauLoadFlowException) as e: _ = bus.res_voltages assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN - for branch in small_network.branches.values(): + for branch in en.branches.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = branch.res_currents assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN - for load in small_network.loads.values(): + for load in en.loads.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = load.res_currents assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN @@ -1034,70 +909,70 @@ def test_network_results_warning(small_network: ElectricalNetwork, recwarn): # with pytest.raises(RoseauLoadFlowException) as e: _ = load.res_flexible_powers assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN - for source in small_network.sources.values(): + for source in en.sources.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = source.res_currents assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN - for ground in small_network.grounds.values(): + for ground in en.grounds.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = ground.res_potential assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN - for p_ref in small_network.potential_refs.values(): + for p_ref in en.potential_refs.values(): with pytest.raises(RoseauLoadFlowException) as e: _ = p_ref.res_current assert e.value.code == RoseauLoadFlowExceptionCode.LOAD_FLOW_NOT_RUN - # Solve a load flow - small_network.solve_load_flow() + # Network with results + en = small_network_with_results # No warning when getting results (they are up-to-date) recwarn.clear() - for bus in small_network.buses.values(): + for bus in en.buses.values(): _ = bus.res_potentials _ = bus.res_voltages - for branch in small_network.branches.values(): + for branch in en.branches.values(): _ = branch.res_currents - for load in small_network.loads.values(): + for load in en.loads.values(): _ = load.res_currents if load.is_flexible and isinstance(load, PowerLoad): _ = load.res_flexible_powers - for source in small_network.sources.values(): + for source in en.sources.values(): _ = source.res_currents - for ground in small_network.grounds.values(): + for ground in en.grounds.values(): _ = ground.res_potential - for p_ref in small_network.potential_refs.values(): + for p_ref in en.potential_refs.values(): _ = p_ref.res_current assert len(recwarn) == 0 # Modify something - load = small_network.loads["load"] + load = en.loads["load"] load.powers = [200, 200, 200] # VA # Ensure that a warning is raised no matter which result is requested expected_message = ( "The results of this element may be outdated. Please re-run a load flow to ensure the validity of results." ) - for bus in small_network.buses.values(): + for bus in en.buses.values(): with check_result_warning(expected_message=expected_message): _ = bus.res_potentials with check_result_warning(expected_message=expected_message): _ = bus.res_voltages - for branch in small_network.branches.values(): + for branch in en.branches.values(): with check_result_warning(expected_message=expected_message): _ = branch.res_currents - for load in small_network.loads.values(): + for load in en.loads.values(): with check_result_warning(expected_message=expected_message): _ = load.res_currents if load.is_flexible and isinstance(load, PowerLoad): with check_result_warning(expected_message=expected_message): _ = load.res_flexible_powers - for source in small_network.sources.values(): + for source in en.sources.values(): with check_result_warning(expected_message=expected_message): _ = source.res_currents - for ground in small_network.grounds.values(): + for ground in en.grounds.values(): with check_result_warning(expected_message=expected_message): _ = ground.res_potential - for p_ref in small_network.potential_refs.values(): + for p_ref in en.potential_refs.values(): with check_result_warning(expected_message=expected_message): _ = p_ref.res_current @@ -1106,41 +981,41 @@ def test_network_results_warning(small_network: ElectricalNetwork, recwarn): # "The results of this network may be outdated. Please re-run a load flow to ensure the validity of results." ) with check_result_warning(expected_message=expected_message): - _ = small_network.res_buses + _ = en.res_buses with check_result_warning(expected_message=expected_message): - _ = small_network.res_buses_voltages + _ = en.res_buses_voltages with check_result_warning(expected_message=expected_message): - _ = small_network.res_branches + _ = en.res_branches with check_result_warning(expected_message=expected_message): - _ = small_network.res_loads + _ = en.res_loads with check_result_warning(expected_message=expected_message): - _ = small_network.res_sources + _ = en.res_sources with check_result_warning(expected_message=expected_message): - _ = small_network.res_loads_flexible_powers + _ = en.res_loads_flexible_powers -def test_load_flow_results_frames(small_network: ElectricalNetwork, good_json_results: dict): - small_network.results_from_dict(good_json_results) - small_network.buses["bus0"].min_voltage = 21_000 +def test_load_flow_results_frames(small_network_with_results): + en = small_network_with_results + en.buses["bus0"].min_voltage = 21_000 # Buses results expected_res_buses = ( pd.DataFrame.from_records( [ - {"bus_id": "bus0", "phase": "a", "potential": 20000 + 2.89120e-18j}, - {"bus_id": "bus0", "phase": "b", "potential": -10000.00000 - 17320.50807j}, - {"bus_id": "bus0", "phase": "c", "potential": -10000.00000 + 17320.50807j}, - {"bus_id": "bus0", "phase": "n", "potential": -1.34764e-12 + 2.89120e-18j}, - {"bus_id": "bus1", "phase": "a", "potential": 19999.94999 + 2.89119e-18j}, - {"bus_id": "bus1", "phase": "b", "potential": -9999.97499 - 17320.46477j}, - {"bus_id": "bus1", "phase": "c", "potential": -9999.97499 + 17320.46477j}, + {"bus_id": "bus0", "phase": "a", "potential": 20000 + 2.89120338e-18j}, + {"bus_id": "bus0", "phase": "b", "potential": -10000.000000 - 17320.508076j}, + {"bus_id": "bus0", "phase": "c", "potential": -10000.000000 + 17320.508076j}, + {"bus_id": "bus0", "phase": "n", "potential": -1.347648e-12 + 2.891203e-18j}, + {"bus_id": "bus1", "phase": "a", "potential": 19999.949999875 + 2.891196e-18j}, + {"bus_id": "bus1", "phase": "b", "potential": -9999.97499993 - 17320.4647746j}, + {"bus_id": "bus1", "phase": "c", "potential": -9999.97499993 + 17320.4647746j}, {"bus_id": "bus1", "phase": "n", "potential": 0j}, ] ) .astype({"bus_id": object, "phase": PhaseDtype, "potential": complex}) .set_index(["bus_id", "phase"]) ) - assert_frame_equal(small_network.res_buses, expected_res_buses, rtol=1e-4) + assert_frame_equal(en.res_buses, expected_res_buses, rtol=1e-5) # Buses voltages results expected_res_buses_voltages = ( @@ -1208,7 +1083,7 @@ def test_load_flow_results_frames(small_network: ElectricalNetwork, good_json_re ) .set_index(["bus_id", "phase"]) ) - assert_frame_equal(small_network.res_buses_voltages, expected_res_buses_voltages, rtol=1e-4) + assert_frame_equal(en.res_buses_voltages, expected_res_buses_voltages, rtol=1e-5) # Branches results expected_res_branches = ( @@ -1275,7 +1150,7 @@ def test_load_flow_results_frames(small_network: ElectricalNetwork, good_json_re ) .set_index(["branch_id", "phase"]) ) - assert_frame_equal(small_network.res_branches, expected_res_branches, rtol=1e-4) + assert_frame_equal(en.res_branches, expected_res_branches, rtol=1e-4) # Transformers results expected_res_transformers = ( @@ -1310,7 +1185,7 @@ def test_load_flow_results_frames(small_network: ElectricalNetwork, good_json_re ) .set_index(["transformer_id", "phase"]) ) - assert_frame_equal(small_network.res_transformers, expected_res_transformers) + assert_frame_equal(en.res_transformers, expected_res_transformers) # Lines results expected_res_lines_records = [ @@ -1402,10 +1277,10 @@ def test_load_flow_results_frames(small_network: ElectricalNetwork, good_json_re .astype(expected_res_lines_dtypes) .set_index(["line_id", "phase"]) ) - assert_frame_equal(small_network.res_lines, expected_res_lines, rtol=1e-4, atol=1e-5) + assert_frame_equal(en.res_lines, expected_res_lines, rtol=1e-4, atol=1e-5) # Lines with violated max current - small_network.branches["line"].parameters.max_current = 0.002 + en.branches["line"].parameters.max_current = 0.002 expected_res_lines_violated_records = [ d | {"max_current": 0.002, "violated": d["phase"] != "n"} for d in expected_res_lines_records ] @@ -1414,7 +1289,7 @@ def test_load_flow_results_frames(small_network: ElectricalNetwork, good_json_re .astype(expected_res_lines_dtypes) .set_index(["line_id", "phase"]) ) - assert_frame_equal(small_network.res_lines, expected_res_violated_lines, rtol=1e-4, atol=1e-5) + assert_frame_equal(en.res_lines, expected_res_violated_lines, rtol=1e-4, atol=1e-5) # Switches results expected_res_switches = ( @@ -1445,7 +1320,7 @@ def test_load_flow_results_frames(small_network: ElectricalNetwork, good_json_re ) .set_index(["switch_id", "phase"]) ) - assert_frame_equal(small_network.res_switches, expected_res_switches) + assert_frame_equal(en.res_switches, expected_res_switches) # Loads results expected_res_loads = ( @@ -1492,7 +1367,7 @@ def test_load_flow_results_frames(small_network: ElectricalNetwork, good_json_re ) .set_index(["load_id", "phase"]) ) - assert_frame_equal(small_network.res_loads, expected_res_loads, rtol=1e-4) + assert_frame_equal(en.res_loads, expected_res_loads, rtol=1e-4) # Sources results expected_res_sources = ( @@ -1539,7 +1414,7 @@ def test_load_flow_results_frames(small_network: ElectricalNetwork, good_json_re ) .set_index(["source_id", "phase"]) ) - assert_frame_equal(small_network.res_sources, expected_res_sources, rtol=1e-4) + assert_frame_equal(en.res_sources, expected_res_sources, rtol=1e-4) # Grounds results expected_res_grounds = ( @@ -1551,7 +1426,7 @@ def test_load_flow_results_frames(small_network: ElectricalNetwork, good_json_re .astype({"ground_id": object, "potential": complex}) .set_index(["ground_id"]) ) - assert_frame_equal(small_network.res_grounds, expected_res_grounds) + assert_frame_equal(en.res_grounds, expected_res_grounds) # Potential refs results expected_res_potential_refs = ( @@ -1563,23 +1438,18 @@ 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, check_exact=False) + assert_frame_equal(en.res_potential_refs, expected_res_potential_refs, check_exact=False) # No flexible loads - assert small_network.res_loads_flexible_powers.empty + assert en.res_loads_flexible_powers.empty # Let's add a flexible load fp = FlexibleParameter.p_max_u_consumption(u_min=16000, u_down=17000, s_max=1000) - load = small_network.loads["load"] + load = en.loads["load"] assert isinstance(load, PowerLoad) load._flexible_params = [fp, fp, fp] - good_json_results = good_json_results.copy() - good_json_results["loads"][0]["powers"] = [ - [99.99999999999994, 0.0], - [99.99999999999994, 0.0], - [99.99999999999994, 0.0], - ] - small_network.results_from_dict(good_json_results) + load._res_flexible_powers = 100 * np.ones(3, dtype=np.complex128) + load._fetch_results = False expected_res_flex_powers = ( pd.DataFrame.from_records( [ @@ -1603,10 +1473,10 @@ def test_load_flow_results_frames(small_network: ElectricalNetwork, good_json_re .astype({"load_id": object, "phase": VoltagePhaseDtype, "power": complex}) .set_index(["load_id", "phase"]) ) - assert_frame_equal(small_network.res_loads_flexible_powers, expected_res_flex_powers, rtol=1e-4) + assert_frame_equal(en.res_loads_flexible_powers, expected_res_flex_powers, rtol=1e-5) -def test_solver_warm_start(small_network: ElectricalNetwork, good_json_results, monkeypatch): +def test_solver_warm_start(small_network: ElectricalNetwork, monkeypatch): load: PowerLoad = small_network.loads["load"] load_bus = small_network.buses["bus1"] @@ -1625,6 +1495,7 @@ def _reset_inputs(): monkeypatch.setattr(small_network, "_propagate_potentials", _propagate_potentials) monkeypatch.setattr(small_network, "_reset_inputs", _reset_inputs) + monkeypatch.setattr(small_network._solver, "solve_load_flow", lambda *_, **__: (1, 1e-20)) # First case: network is valid, no results yet -> no warm start propagate_potentials_called = False @@ -1675,9 +1546,6 @@ def _reset_inputs(): 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" - good_json_results["loads"].append(new_load_result) assert new_load.network is small_network assert not small_network._valid assert not small_network._results_valid @@ -1841,3 +1709,111 @@ def test_to_graph(small_network: ElectricalNetwork): for branch in small_network.branches.values(): edge_data = g.edges[branch.bus1.id, branch.bus2.id] assert edge_data == {"id": branch.id, "type": branch.branch_type, "geom": branch.geometry} + + +def test_serialization(small_network, small_network_with_results): + def assert_results(en_dict: dict, included: bool): + for bus_data in en_dict["buses"]: + assert ("results" in bus_data) == included + for branch_data in en_dict["branches"]: + assert ("results" in branch_data) == included + for source_data in en_dict["sources"]: + assert ("results" in source_data) == included + for load_data in en_dict["loads"]: + assert ("results" in load_data) == included + for ground_data in en_dict["grounds"]: + assert ("results" in ground_data) == included + for p_ref_data in en_dict["potential_refs"]: + assert ("results" in p_ref_data) == included + + # No results: include_results is ignored + en = small_network + en_dict_with_results = en.to_dict(include_results=True) + en_dict_without_results = en.to_dict(include_results=False) + assert_results(en_dict_with_results, included=False) + assert_results(en_dict_without_results, included=False) + assert en_dict_with_results == en_dict_without_results + new_en = ElectricalNetwork.from_dict(en_dict_without_results) + assert new_en.to_dict() == en_dict_without_results + + # Has results: include_results is respected + en = small_network_with_results + en_dict_with_results = en.to_dict(include_results=True) + en_dict_without_results = en.to_dict(include_results=False) + assert_results(en_dict_with_results, included=True) + assert_results(en_dict_without_results, included=False) + assert en_dict_with_results != en_dict_without_results + # round triping + assert ElectricalNetwork.from_dict(en_dict_with_results).to_dict() == en_dict_with_results + assert ElectricalNetwork.from_dict(en_dict_without_results).to_dict() == en_dict_without_results + # default is to include the results + assert en.to_dict() == en_dict_with_results + + # Has invalid results: cannot include them + en.loads["load"].powers += Q_(1, "VA") # <- invalidate the results + with pytest.raises(RoseauLoadFlowException) as e: + en.to_dict(include_results=True) + assert e.value.msg == ( + "Trying to convert ElectricalNetwork with invalid results to a dict. Either call " + "`en.solve_load_flow()` before converting or pass `include_results=False`." + ) + assert e.value.code == RoseauLoadFlowExceptionCode.BAD_LOAD_FLOW_RESULT + en_dict_without_results = en.to_dict(include_results=False) + # round triping without the results should still work + assert ElectricalNetwork.from_dict(en_dict_without_results).to_dict() == en_dict_without_results + + +def test_deprecated_results_methods(small_network_with_results, tmp_path): + en = small_network_with_results + en_dict_res = en.to_dict(include_results=True) + en_dict_no_res = en.to_dict(include_results=False) + + with pytest.warns(DeprecationWarning) as record: + res_dict = en.results_to_dict() + assert len(record) == 1 + assert record[0].message.args[0] == ( + "Method `results_to_dict()` is deprecated. Method `to_dict()` now includes the results by default." + ) + + new_en = ElectricalNetwork.from_dict(en_dict_no_res) + with pytest.warns(DeprecationWarning) as record: + new_en.results_from_dict(res_dict) + assert len(record) == 1 + assert record[0].message.args[0] == ( + "Method `results_from_dict()` is deprecated. Method `from_dict()` now includes the results by default." + ) + assert new_en.to_dict() == en_dict_res + + tmp_file = tmp_path / "results.json" + with pytest.warns(DeprecationWarning) as record: + en.results_to_json(tmp_file) + assert len(record) == 1 + assert record[0].message.args[0] == ( + "Method `results_to_json()` is deprecated. Method `to_json()` now includes the results by default." + ) + + new_en = ElectricalNetwork.from_dict(en_dict_no_res) + with pytest.warns(DeprecationWarning) as record: + new_en.results_from_json(tmp_file) + assert len(record) == 1 + assert record[0].message.args[0] == ( + "Method `results_from_json()` is deprecated. Method `from_json()` now includes the results by default." + ) + assert new_en.to_dict() == en_dict_res + + +def test_propagate_potentials_center_transformers(): + # Source is located at the primary side of the transformer + bus1 = Bus(id="bus1", phases="ab") + PotentialRef(id="pref", element=bus1) + VoltageSource(id="vs", bus=bus1, voltages=[20000]) + tp = TransformerParameters( + id="test", type="center", sn=160000, uhv=20000.0, ulv=400.0, i0=0.023, p0=460.0, psc=2350.0, vsc=0.04 + ) + bus2 = Bus(id="bus2", phases="abn") + PotentialRef(id="pref2", element=bus2) + Transformer(id="transfo", bus1=bus1, bus2=bus2, parameters=tp) + en = ElectricalNetwork.from_element(bus2) + with contextlib.suppress(RoseauLoadFlowException): # No valid license + en.solve_load_flow() # propagate the potentials + npt.assert_allclose(bus2.potentials.m_as("V"), np.array([200, -200, 0], dtype=np.complex128)) diff --git a/roseau/load_flow/utils/mixins.py b/roseau/load_flow/utils/mixins.py index 99afd138..a7d7eb3c 100644 --- a/roseau/load_flow/utils/mixins.py +++ b/roseau/load_flow/utils/mixins.py @@ -8,7 +8,7 @@ from typing import Generic, NoReturn, TypeVar, overload import pandas as pd -from typing_extensions import Self +from typing_extensions import Self, deprecated from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode from roseau.load_flow.typing import Id, JsonDict, StrPath @@ -35,38 +35,84 @@ def __repr__(self) -> str: class JsonMixin(metaclass=ABCMeta): """Mixin for classes that can be serialized to and from JSON.""" + _no_results = True + _results_valid = False + @classmethod @abstractmethod - def from_dict(cls, data: JsonDict) -> Self: - """Create an element from a dictionary.""" + def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: + """Create an element from a dictionary created with :meth:`to_dict`. + + Note: + This method does not work on all classes that define it as some of them require + additional information to be constructed. It can only be safely used on the + `ElectricNetwork`, `LineParameters` and `TransformerParameters` classes. + + Args: + data: + The dictionary containing the element's data. + + include_results: + If True (default) and the results of the load flow are included in the dictionary, + the results are also loaded into the element. + + Returns: + The constructed element. + """ raise NotImplementedError @classmethod - def from_json(cls, path: StrPath) -> Self: - """Construct an electrical network from a json file created with :meth:`to_json`. + def from_json(cls, path: StrPath, *, include_results: bool = True) -> Self: + """Construct an element from a JSON file created with :meth:`to_json`. + + Note: + This method does not work on all classes that define it as some of them require + additional information to be constructed. It can only be safely used on the + `ElectricNetwork`, `LineParameters` and `TransformerParameters` classes. Args: path: The path to the network data file. + include_results: + If True (default) and the results of the load flow are included in the file, + the results are also loaded into the element. + Returns: - The constructed network. + The constructed element. """ data = json.loads(Path(path).read_text()) - return cls.from_dict(data=data) + return cls.from_dict(data=data, include_results=include_results) @abstractmethod - def to_dict(self, *, _lf_only: bool = False) -> JsonDict: - """Return the element information as a dictionary format. + def _to_dict(self, include_results: bool) -> JsonDict: + """Return the element information as a dictionary format.""" + raise NotImplementedError + + def to_dict(self, *, include_results: bool = True) -> JsonDict: + """Convert the element to a dictionary. Args: - _lf_only: - Internal argument, please do not use. + include_results: + If True (default), the results of the load flow are included in the dictionary. + If no results are available, this option is ignored. + + Returns: + A JSON serializable dictionary with the element's data. """ - raise NotImplementedError + if include_results and self._no_results: + include_results = False + if include_results and not self._results_valid: + msg = ( + f"Trying to convert {type(self).__name__} with invalid results to a dict. Either " + f"call `en.solve_load_flow()` before converting or pass `include_results=False`." + ) + logger.error(msg) + raise RoseauLoadFlowException(msg, code=RoseauLoadFlowExceptionCode.BAD_LOAD_FLOW_RESULT) + return self._to_dict(include_results=include_results) - def to_json(self, path: StrPath) -> Path: - """Save the current network to a json file. + def to_json(self, path: StrPath, *, include_results: bool = True) -> Path: + """Save this element to a JSON file. .. note:: The path is `expanded`_ then `resolved`_ before writing the file. @@ -81,27 +127,41 @@ def to_json(self, path: StrPath) -> Path: path: The path to the output file to write the network to. + include_results: + If True (default), the results of the load flow are included in the JSON file. + If no results are available, this option is ignored. + Returns: The expanded and resolved path of the written file. """ - res = self.to_dict() + res = self.to_dict(include_results=include_results) output = json.dumps(res, ensure_ascii=False, indent=2) - output = re.sub(r"\[\s+(.*),\s+(.*)\s+]", r"[\1, \2]", output) + # Collapse multi-line arrays of 2-to-4 elements into single line + # e.g complex value represented as [real, imag] or rows of the z_line matrix + output = re.sub(r"\[(?:\s+(\S+,))?(?:\s+?( \S+,))??(?:\s+?( \S+,))??\s+?( \S+)\s+\]", r"[\1\2\3\4]", output) if not output.endswith("\n"): output += "\n" path = Path(path).expanduser().resolve() path.write_text(output) return path - def results_to_dict(self) -> JsonDict: - """Return the results of the element as a dictionary format""" - return self._results_to_dict(True) - @abstractmethod def _results_to_dict(self, warning: bool) -> JsonDict: """Return the results of the element as a dictionary format""" raise NotImplementedError + @deprecated( + "Method `results_to_dict()` is deprecated. Method `to_dict()` now includes the results by default.", + category=DeprecationWarning, + ) + def results_to_dict(self) -> JsonDict: + """Return the results of the element as a dictionary format""" + return self._results_to_dict(warning=True) + + @deprecated( + "Method `results_to_json()` is deprecated. Method `to_json()` now includes the results by default.", + category=DeprecationWarning, + ) def results_to_json(self, path: StrPath) -> Path: """Write the results of the load flow to a json file. @@ -121,7 +181,7 @@ def results_to_json(self, path: StrPath) -> Path: Returns: The expanded and resolved path of the written file. """ - dict_results = self.results_to_dict() + dict_results = self._results_to_dict(warning=True) output = json.dumps(dict_results, indent=4) output = re.sub(r"\[\s+(.*),\s+(.*)\s+]", r"[\1, \2]", output) path = Path(path).expanduser().resolve() @@ -131,10 +191,23 @@ def results_to_json(self, path: StrPath) -> Path: return path @abstractmethod - def results_from_dict(self, data: JsonDict) -> None: + def _results_from_dict(self, data: JsonDict) -> None: """Fill an element with the provided results' dictionary.""" raise NotImplementedError + @deprecated( + "Method `results_from_dict()` is deprecated. Method `from_dict()` now includes the results by default.", + category=DeprecationWarning, + ) + def results_from_dict(self, data: JsonDict) -> None: + """Fill an element with the provided results' dictionary.""" + self._no_results = False + return self._results_from_dict(data) + + @deprecated( + "Method `results_from_json()` is deprecated. Method `from_json()` now includes the results by default.", + category=DeprecationWarning, + ) def results_from_json(self, path: StrPath) -> None: """Load the results of a load flow from a json file created by :meth:`results_to_json`. @@ -145,7 +218,7 @@ def results_from_json(self, path: StrPath) -> None: The path to the JSON file containing the results. """ data = json.loads(Path(path).read_text()) - self.results_from_dict(data) + self._results_from_dict(data) class CatalogueMixin(Generic[_T], metaclass=ABCMeta): diff --git a/scripts/generate_test_networks.py b/scripts/generate_test_networks.py new file mode 100644 index 00000000..dcfa67fa --- /dev/null +++ b/scripts/generate_test_networks.py @@ -0,0 +1,83 @@ +from importlib import resources +from pathlib import Path + +import numpy as np +from shapely import LineString, Point + +import roseau.load_flow as lf + +TEST_NETWORKS_PATH = Path(resources.files("roseau.load_flow")) / "tests" / "data" / "networks" + + +def generate_small_network() -> None: + # Build a small network + point1 = Point(-1.318375372111463, 48.64794139348595) + point2 = Point(-1.320149235966572, 48.64971306653889) + line_string = LineString([point1, point2]) + + ground = lf.Ground("ground") + source_bus = lf.Bus("bus0", phases="abcn", geometry=point1) + load_bus = lf.Bus("bus1", phases="abcn", geometry=point2) + ground.connect(load_bus) + + voltages = [20000.0 + 0.0j, -10000.0 - 17320.508076j, -10000.0 + 17320.508076j] + vs = lf.VoltageSource("vs", source_bus, voltages=voltages, phases="abcn") + load = lf.PowerLoad("load", load_bus, powers=[100, 100, 100], phases="abcn") + pref = lf.PotentialRef("pref", element=ground) + + lp = lf.LineParameters("test", z_line=10 * np.eye(4, dtype=complex)) + line = lf.Line("line", source_bus, load_bus, phases="abcn", parameters=lp, length=1.0, geometry=line_string) + + en = lf.ElectricalNetwork( + buses=[source_bus, load_bus], + branches=[line], + loads=[load], + sources=[vs], + grounds=[ground], + potential_refs=[pref], + ) + en.solve_load_flow() + en.to_json(TEST_NETWORKS_PATH / "small_network.json") + + +def generate_single_phase_network() -> None: + # Build a small single-phase network + # ---------------------------------- + + # Phase "b" is chosen to catch errors where the index of the first phase may be assumed to be 0 + phases = "bn" + + # Network geometry + point1 = Point(-1.318375372111463, 48.64794139348595) + point2 = Point(-1.320149235966572, 48.64971306653889) + line_string = LineString([point1, point2]) + + # Network elements + bus0 = lf.Bus("bus0", phases=phases, geometry=point1) + bus1 = lf.Bus("bus1", phases=phases, geometry=point2) + + ground = lf.Ground("ground") + ground.connect(bus1) + pref = lf.PotentialRef("pref", element=ground) + + vs = lf.VoltageSource("vs", bus0, voltages=[20000.0 + 0.0j], phases=phases) + load = lf.PowerLoad("load", bus1, powers=[100], phases=phases) + + lp = lf.LineParameters("test", z_line=10 * np.eye(2, dtype=complex)) + line = lf.Line("line", bus0, bus1, phases=phases, parameters=lp, length=1.0, geometry=line_string) + + en = lf.ElectricalNetwork( + buses=[bus0, bus1], + branches=[line], + loads=[load], + sources=[vs], + grounds=[ground], + potential_refs=[pref], + ) + en.solve_load_flow() + en.to_json(TEST_NETWORKS_PATH / "single_phase_network.json") + + +if __name__ == "__main__": + generate_small_network() + generate_single_phase_network()