diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..eac7872 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + target-branch: "develop" + schedule: + interval: "monthly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..48dbba4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI + +on: + push: + branches: [main, develop] + tags: + - "*" + paths-ignore: + - "doc/**" + - ".vscode/**" + pull_request: + branches: [main, develop] + paths-ignore: + - "doc/**" + - ".vscode/**" + +env: + CI: true + UV_SYSTEM_PYTHON: 1 + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + with: + lfs: false + + - name: Create LFS file list + run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id + + - name: Cache git LFS + uses: actions/cache@v4 + with: + path: .git/lfs + key: git-lfs-v1-${{ matrix.python-version }}-${{ hashFiles('.lfs-assets-id') }} + restore-keys: | + git-lfs-v1-${{ matrix.python-version }} + git-lfs-v1 + git-lfs + + - name: Git LFS + run: | + git lfs checkout + git lfs pull + git lfs prune --verify-remote + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + uv sync --frozen --all-extras + env: + GIT_LFS_SKIP_SMUDGE: 1 + + - name: Test with pytest + run: | + uv run pytest -vv -n=auto --durations=25 --cov-report html --cov-config pyproject.toml roseau + env: + ROSEAU_LOAD_FLOW_LICENSE_KEY: ${{ secrets.ROSEAU_LOAD_FLOW_LICENSE_KEY }} + + - name: Archive code coverage results + uses: actions/upload-artifact@v4 + if: ${{ always() }} + with: + name: code-coverage-report-${{ runner.os }}-python-${{ matrix.python-version }} + path: htmlcov/ diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..6833c79 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,27 @@ +name: pre-commit + +on: + push: + branches: [main, develop] + paths-ignore: + - ".vscode/**" + - ".idea/**" + tags: + - "*" + pull_request: + branches: [main, develop] + paths-ignore: + - ".vscode/**" + - ".idea/**" + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + lfs: false + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - uses: pre-commit/action@v3.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 41d180e..ad93317 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,11 +11,11 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.4.30 + rev: 0.5.4 hooks: - id: uv-lock - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.2 + rev: v0.8.0 hooks: - id: ruff args: [--fix] @@ -27,13 +27,8 @@ repos: files: ^doc/.*\.md$ args: [-l 90] - repo: https://github.com/rbubley/mirrors-prettier - rev: v3.3.3 + rev: v3.4.0 hooks: - id: prettier args: ["--print-width", "120"] require_serial: true - - repo: https://github.com/cmhughes/latexindent.pl - rev: V3.24.4 - hooks: - - id: latexindent - args: [-l, -m, -s, -wd] diff --git a/pyproject.toml b/pyproject.toml index 7765514..9954446 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,13 +7,12 @@ authors = [ { name = "Sébastien Vallet", email = "sebastien.vallet@roseautechnologies.com" }, { name = "Benoît Vinot", email = "benoit.vinot@roseautechnologies.com" }, { name = "Florent Cadoux", email = "florent.cadoux@roseautechnologies.com" }, - { name = "Louise Muller", email = "louise.muller@roseautechnologies.com" }, - { name = "Victor Gouin" }, ] maintainers = [ { name = "Ali Hamdan", email = "ali.hamdan@roseautechnologies.com" }, ] readme = "README.md" +license = { file = "LICENSE.md" } classifiers = [ "Development Status :: 3 - Alpha", # "License :: OSI Approved :: The 3-Clause BSD License (BSD-3-Clause)", # https://github.com/pypa/trove-classifiers/issues/70 @@ -58,12 +57,14 @@ dev = [ "pytest-cov>=5.0.0", "pytest-xdist>=3.1.0", "coverage[toml]>=7.0.5", - "coverage-conditional-plugin>=0.9.0", ] [tool.uv] managed = true +[tool.uv.sources] +roseau-load-flow = { git = "https://github.com/RoseauTechnologies/Roseau_Load_Flow.git", branch = "develop" } + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" @@ -97,11 +98,6 @@ docstring-code-format = true [tool.coverage.run] branch = true omit = ["roseau/load_flow_single/__about__.py"] -plugins = ["coverage_conditional_plugin"] - -[tool.coverage.coverage_conditional_plugin.rules] -no-cover-if-py-gte-311 = "sys_version_info >= (3, 11)" -no-cover-if-py-lt-311 = "sys_version_info < (3, 11)" [tool.coverage.paths] source = ["roseau/load_flow_single/"] diff --git a/roseau/load_flow_single/__about__.py b/roseau/load_flow_single/__about__.py new file mode 100644 index 0000000..c492dc7 --- /dev/null +++ b/roseau/load_flow_single/__about__.py @@ -0,0 +1,15 @@ +__authors__ = ", ".join( + ( + "Ali Hamdan ", + "Sébastien Vallet ", + "Benoît Vinot ", + "Florent Cadoux ", + ) +) +__copyright__ = "Roseau Technologies 2018" +__credits__ = "Roseau Technologies" +__license__ = "BSD-3-Clause" +__maintainer__ = "Ali Hamdan" +__email__ = "ali.hamdan@roseautechnologies.com" +__status__ = "In development" +__url__ = "https://github.com/RoseauTechnologies/Roseau_Load_Flow_Single/" diff --git a/roseau/load_flow_single/__init__.py b/roseau/load_flow_single/__init__.py index 41fb21d..e91ca7b 100644 --- a/roseau/load_flow_single/__init__.py +++ b/roseau/load_flow_single/__init__.py @@ -1,3 +1,20 @@ +import importlib.metadata + +from roseau.load_flow import exceptions, license, show_versions, testing, typing, units, utils +from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode +from roseau.load_flow.license import License, activate_license, deactivate_license, get_license +from roseau.load_flow.units import Q_, ureg +from roseau.load_flow.utils import Insulator, LineType, Material, constants +from roseau.load_flow_single.__about__ import ( + __authors__, + __copyright__, + __credits__, + __email__, + __license__, + __maintainer__, + __status__, + __url__, +) from roseau.load_flow_single.models.branches import AbstractBranch from roseau.load_flow_single.models.buses import Bus from roseau.load_flow_single.models.core import Element @@ -16,7 +33,19 @@ from roseau.load_flow_single.models.transformers import Transformer, TransformerParameters from roseau.load_flow_single.network import ElectricalNetwork +__version__ = importlib.metadata.version("roseau-load-flow-single") + __all__ = [ + # RLFS elements + "__authors__", + "__copyright__", + "__credits__", + "__email__", + "__license__", + "__maintainer__", + "__status__", + "__url__", + "__version__", "Element", "Line", "LineParameters", @@ -34,4 +63,29 @@ "Projection", "Control", "AbstractBranch", + # Other imports from RLF to have the same interface + # utils + "Insulator", + "LineType", + "Material", + "utils", + "constants", + # License + "License", + "activate_license", + "deactivate_license", + "get_license", + "license", + # Units + "Q_", + "units", + "ureg", + # Exceptions + "RoseauLoadFlowException", + "RoseauLoadFlowExceptionCode", + "exceptions", + # Other + "show_versions", + "testing", + "typing", ] diff --git a/roseau/load_flow_single/conftest.py b/roseau/load_flow_single/conftest.py index 795b9c6..3ea27a3 100644 --- a/roseau/load_flow_single/conftest.py +++ b/roseau/load_flow_single/conftest.py @@ -1,10 +1,12 @@ +import importlib +import inspect +import os from pathlib import Path -import platformdirs import pytest from _pytest.monkeypatch import MonkeyPatch -from roseau.load_flow import activate_license +import roseau from roseau.load_flow.utils.log import set_logging_config HERE = Path(__file__).parent.expanduser().absolute() @@ -30,27 +32,6 @@ ] -@pytest.fixture(autouse=True, scope="session") -def _log_setup(): - """A basic fixture (automatically used) to set the log level""" - set_logging_config(verbosity="debug") - - -@pytest.fixture(autouse=True, scope="session") -def _license_setup(_log_setup, tmp_path_factory): - """A basic fixture (automatically used) to activate a license for the tests""" - license_folderpath = tmp_path_factory.mktemp("roseau-test") - - def _user_cache_dir(): - return str(license_folderpath) - - mpatch = MonkeyPatch() - mpatch.setattr(target=platformdirs, name="user_cache_dir", value=_user_cache_dir) - activate_license(key=None) # Use the environment variable `ROSEAU_LOAD_FLOW_LICENSE_KEY` - yield - mpatch.undo() - - @pytest.fixture(params=["impedance", "power"], ids=["impedance", "power"]) def network_load_data_name(request) -> str: return request.param @@ -74,3 +55,66 @@ def test_networks_path() -> Path: @pytest.fixture(params=THREE_PHASES_TRANSFORMER_TYPES, ids=THREE_PHASES_TRANSFORMER_TYPES) def three_phases_transformer_type(request) -> str: return request.param + + +@pytest.fixture(autouse=True) +def patch_engine(request): + mpatch = MonkeyPatch() + + if "no_patch_engine" in request.keywords: + # A load flow must be solved in the test + # Skip if no license key in the environment + if os.getenv("ROSEAU_LOAD_FLOW_LICENSE_KEY") is None: # pragma: no-cover + pytest.skip( + reason="This test requires a license key. Please set ROSEAU_LOAD_FLOW_LICENSE_KEY in your environment." + ) + + # Activate logging + set_logging_config("debug") + else: + # Patch the engine + + class Foo: + def __init__(self, *args, **kwargs): # Accept all constructor parameters + pass + + def __getattr__(self, attr): # Accept all methods + if attr.startswith("__array"): # Let numpy interface + return object.__getattr__(self, attr) + else: + return self.foo + + def foo(self, *args, **kwargs): + pass + + def bar(*args, **kwargs): # pragma: no-cover + pass + + # Get all roseau.load_flow_single and roseau.load_flow submodules + for dp in (Path(roseau.load_flow_single.__file__).parent, Path(roseau.load_flow.__file__).parent): + relative_to = dp.parents[1] + for dirpath, _, filenames in os.walk(dp): # TODO In Python 3.12 use rlf_directory_path.walk() + dirpath = Path(dirpath) # TODO Useless in Python 3.12 + for p in dirpath.parts: + if p in {"tests", "__pycache__", "data"}: + break + else: + base_module = str(dirpath.relative_to(relative_to)).replace("/", ".") + for f in filenames: + if not f.endswith(".py"): + continue + module = importlib.import_module(f"{base_module}.{f.removesuffix('.py')}") + for _, klass in inspect.getmembers( + module, + lambda member: inspect.isclass(member) + and "load_flow_engine." in member.__module__ + and member.__name__.startswith("Cy") + and member.__name__ != "CyLicense", # Test of the static methods of this class + ): + mpatch.setattr(f"{module.__name__}.{klass.__name__}", Foo) + + # Also patch the activate license function of the _solvers module + mpatch.setattr("roseau.load_flow.license.cy_activate_license", bar) + + yield mpatch + mpatch.undo() diff --git a/roseau/load_flow_single/io/dict.py b/roseau/load_flow_single/io/dict.py index d9eba01..30fba21 100644 --- a/roseau/load_flow_single/io/dict.py +++ b/roseau/load_flow_single/io/dict.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, TypeVar from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode +from roseau.load_flow.io.dict import NETWORK_JSON_VERSION from roseau.load_flow.typing import Id, JsonDict from roseau.load_flow_single.models import ( AbstractBranch, @@ -29,8 +30,6 @@ logger = logging.getLogger(__name__) -NETWORK_JSON_VERSION = 2 -"""The current version of the network JSON file format.""" _T = TypeVar("_T", bound=AbstractBranch) @@ -87,13 +86,31 @@ def network_from_dict( """ data = copy.deepcopy(data) # Make a copy to avoid modifying the original - # version = data.get("version", 0) - # TODO version check - # Check that the network is single phase is_multiphase = data.get("is_multiphase", True) assert not is_multiphase, f"Unsupported phase selection {is_multiphase=}." + # Check on the version + # 3 was the first version to support RLFS + version = data.get("version", 3) + if version <= 2: + msg = ( + f"The version {version} of the network file can not be single-phased. This featured appeared in the " + f"version 3..." + ) + logger.error(msg) + raise AssertionError(msg) + # elif version <= NETWORK_JSON_VERSION: + # logger.warning( + # f"Got an outdated network file (version {version}), trying to update to the current format " + # f"(version {NETWORK_JSON_VERSION}). Please save the network again." + # ) + # if version == 3: + # data = v3_to_v4_converter(data) + 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}." + # Track if ALL results are included in the network has_results = include_results diff --git a/roseau/load_flow_single/models/lines/lines.py b/roseau/load_flow_single/models/lines/lines.py index 6214b01..87c0375 100644 --- a/roseau/load_flow_single/models/lines/lines.py +++ b/roseau/load_flow_single/models/lines/lines.py @@ -67,16 +67,18 @@ def __init__( if parameters.with_shunt: self._cy_element = CyShuntLine( n=1, - y_shunt=parameters._y_shunt.reshape(1) * self._length, - z_line=parameters._z_line.reshape(1) * self._length, + y_shunt=np.array([parameters._y_shunt * self._length], dtype=np.complex128), + z_line=np.array([parameters._z_line * self._length], dtype=np.complex128), ) else: - self._cy_element = CySimplifiedLine(n=1, z_line=parameters._z_line.reshape(1) * self._length) + self._cy_element = CySimplifiedLine( + n=1, z_line=np.array([parameters._z_line * self._length], dtype=np.complex128) + ) self._cy_connect() # Cache values used in results calculations - self._z_line = parameters._z_line[0][0] * self._length - self._y_shunt = parameters._y_shunt[0][0] * self._length + self._z_line = parameters._z_line * self._length + self._y_shunt = parameters._y_shunt * self._length self._z_line_inv = 1.0 / self._z_line self._yg = self._y_shunt # y_ig = Y_ia + Y_ib + Y_ic + Y_in for i in {a, b, c, n} @@ -85,8 +87,8 @@ def _update_internal_parameters(self, parameters: LineParameters, length: float) self._parameters = parameters self._length = length - self._z_line = parameters._z_line[0][0] * length - self._y_shunt = parameters._y_shunt[0][0] * length + self._z_line = parameters._z_line * length + self._y_shunt = parameters._y_shunt * length self._z_line_inv = 1.0 / self._z_line self._yg = self._y_shunt @@ -124,21 +126,11 @@ def parameters(self) -> LineParameters: @parameters.setter def parameters(self, value: LineParameters) -> None: - shape = (1, 1) - if value._z_line.shape != shape: - msg = f"Incorrect z_line dimensions for line {self.id!r}: {value._z_line.shape} instead of {shape}" - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_Z_LINE_SHAPE) - if value.with_shunt: if self._initialized and not self.with_shunt: msg = "Cannot set line parameters with a shunt to a line that does not have shunt components." logger.error(msg) raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_LINE_MODEL) - if value._y_shunt.shape != shape: - msg = f"Incorrect y_shunt dimensions for line {self.id!r}: {value._y_shunt.shape} instead of {shape}" - logger.error(msg) - raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_Y_SHUNT_SHAPE) else: if self._initialized and self.with_shunt: msg = "Cannot set line parameters without a shunt to a line that has shunt components." @@ -153,13 +145,13 @@ def parameters(self, value: LineParameters) -> None: @ureg_wraps("ohm", (None,)) def z_line(self) -> Q_[Complex]: """Impedance of the line (in Ohm).""" - return self._parameters._z_line[0][0] * self._length + return self._z_line @property @ureg_wraps("S", (None,)) def y_shunt(self) -> Q_[Complex]: """Shunt admittance of the line (in Siemens).""" - return self._parameters._y_shunt[0][0] * self._length + return self._y_shunt @property @ureg_wraps("", (None,)) @@ -181,15 +173,14 @@ def ampacity(self) -> Q_[float] | None: """The ampacity of the line (in A).""" # Do not add a setter. The user must know that if they change the ampacity, it changes # for all lines that share the parameters. It is better to set it on the parameters. - amp = self._parameters.ampacities - return amp[0] if amp is not None else None + return self._parameters.ampacity @property def max_current(self) -> Q_[float] | None: """The maximum current of the line (in A). It takes into account the `max_loading` of the line and the `ampacity` of the parameters.""" # Do not add a setter. Only `max_loading` can be altered by the user - amp = self._parameters._ampacities + amp = self._parameters.ampacity return None if amp is None else Q_(amp[0] * self._max_loading, "A") @property @@ -270,11 +261,11 @@ def res_power_losses(self) -> Q_[Complex]: @property def res_loading(self) -> Q_[float] | None: """Get the loading of the line (unitless).""" - amp = self._parameters._ampacities + amp = self._parameters._ampacity if amp is None: return None current1, current2 = self._res_currents_getter(warning=True) - i_max = amp[0] * self._max_loading + i_max = amp * self._max_loading return Q_(max(abs(current1), abs(current2)) / i_max, "") @property @@ -284,11 +275,11 @@ def res_violated(self) -> bool | None: Returns ``None`` if the ampacities or the `max_loading` is not set are not set. """ - amp = self._parameters._ampacities + amp = self._parameters._ampacity if amp is None: return None current1, current2 = self._res_currents_getter(warning=True) - i_max = amp[0] * self._max_loading + i_max = amp * self._max_loading return abs(current1) > i_max or abs(current2) > i_max # diff --git a/roseau/load_flow_single/models/lines/parameters.py b/roseau/load_flow_single/models/lines/parameters.py index 2cb5c9f..f1240b9 100644 --- a/roseau/load_flow_single/models/lines/parameters.py +++ b/roseau/load_flow_single/models/lines/parameters.py @@ -1,29 +1,39 @@ import logging +import re +from pathlib import Path +from typing import Literal, NoReturn, TypeAlias, TypeVar import numpy as np +import pandas as pd +from typing_extensions import Self -from roseau.load_flow.models.lines.parameters import LineParameters as TriLineParameters -from roseau.load_flow.typing import ComplexArrayLike2D, Id +from roseau.load_flow._compat import StrEnum +from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode +from roseau.load_flow.models import LineParameters as MultiLineParameters +from roseau.load_flow.typing import Complex, Float, Id, JsonDict from roseau.load_flow.units import Q_, ureg_wraps -from roseau.load_flow.utils import Insulator, LineType, Material +from roseau.load_flow.utils import CatalogueMixin, F, Identifiable, Insulator, JsonMixin, LineType, Material logger = logging.getLogger(__name__) -class LineParameters(TriLineParameters): +_StrEnumType: TypeAlias = TypeVar("_StrEnumType", bound=StrEnum) + + +class LineParameters(Identifiable, JsonMixin, CatalogueMixin[pd.DataFrame]): """Parameters that define electrical models of lines.""" - @ureg_wraps(None, (None, None, "ohm/km", "S/km", "A", None, None, None, "mm²")) + @ureg_wraps(None, (None, None, "ohm/km", "S/km", "A", None, None, None, "mm**2")) def __init__( self, id: Id, - z_line: complex | ComplexArrayLike2D, - y_shunt: complex | ComplexArrayLike2D | None = None, - ampacities: float | None = None, + z_line: complex, + y_shunt: complex | None = None, + ampacity: float | Q_[float] | None = None, line_type: LineType | None = None, - materials: Material | None = None, - insulators: Insulator | None = None, - sections: float | Q_[float] | None = None, + material: Material | None = None, + insulator: Insulator | None = None, + section: float | Q_[float] | None = None, ) -> None: """LineParameters constructor. @@ -32,15 +42,15 @@ def __init__( A unique ID of the line parameters, typically its canonical name. z_line: - The Z matrix of the line (Ohm/km). + The Z of the line (Ohm/km). y_shunt: - The Y matrix of the line (Siemens/km). This field is optional if the line has no shunt part. + The Y of the line (Siemens/km). This field is optional if the line has no shunt part. - ampacities: - The ampacities of the line (A). The ampacities are optional, they are + ampacity: + The ampacity of the line (A). The ampacity is optional, it is not used in the load flow but can be used to check for overloading. - See also :meth:`Line.res_violated `. + See also :meth:`Line.res_violated `. line_type: The type of the line (overhead, underground, twisted). The line type is optional, @@ -48,27 +58,868 @@ def __init__( automatically filled when the line parameters are created from a geometric model or from the catalogue. - materials: + material: The type of the conductor material (Aluminum, Copper, ...). The material is - optional, it is informative only and is not used in the load flow. This field gets + optional, it is informative only and are not used in the load flow. This field gets automatically filled when the line parameters are created from a geometric model or from the catalogue. - insulators: + insulator: The type of the cable insulator (PVC, XLPE, ...). The insulator is optional, - it is informative only and is not used in the load flow. This field gets + it is informative only and are not used in the load flow. This field gets automatically filled when the line parameters are created from a geometric model or from the catalogue. + + section: + The section of the conductor. The section is optional, it is informative only and is not used in + the load flow. This field gets automatically filled when the line parameters are created from a + geometric model or from the catalogue. + """ + super().__init__(id) + self._check_values(id=id, value=z_line, name="z_line") + self._z_line = z_line + if y_shunt is None: + self._y_shunt = 0 + self._with_shunt = False + else: + self._y_shunt = y_shunt + self._with_shunt = not np.isclose(y_shunt, 0) + self._check_values(id=id, value=y_shunt, name="y_shunt") + + # Parameters that are not used in the load flow + self._line_type = None if pd.isna(line_type) else LineType(line_type) + self._ampacity = self._check_positive_float(value=ampacity, name="ampacity", unit="A") + self._material = None if pd.isna(material) else Material(material) + self._insulator = None if pd.isna(insulator) else Insulator(insulator) + self._section = self._check_positive_float(value=section, name="section", unit="mm²") + + def __repr__(self) -> str: + s = f"<{type(self).__name__}: id={self.id!r}" + if self._line_type is not None: + s += f", line_type={str(self._line_type)!r}" + if self._insulator is not None: + s += f", insulator={self._insulator}" + if self._material is not None: + s += f", material={self._material}" + if self._section is not None: + s += f", section={self._section}" + if self._ampacity is not None: + s += f", ampacity={self._ampacity}" + s += ">" + return s + + def __eq__(self, other: object) -> bool: + if not isinstance(other, LineParameters): + return NotImplemented + return ( + self.id == other.id + and np.allclose(self._z_line, other._z_line) + and np.allclose(self._y_shunt, other._y_shunt) + and self._line_type == other._line_type + and self._material == other._material + and self._insulator == other._insulator + and ( + (self._section is None and other._section is None) + or ( + self._section is not None + and other._section is not None + and np.isclose(self._section, other._section) + ) + ) + and ( + (self._ampacity is None and other._ampacity is None) + or ( + self._ampacity is not None + and other._ampacity is not None + and np.isclose(self._ampacity, other._ampacity) + ) + ) + ) + + @property + def line_type(self) -> LineType | None: + """The type of the line. Informative only, it has no impact on the load flow.""" + return self._line_type + + @property + @ureg_wraps("ohm/km", (None,)) + def z_line(self) -> Q_[Complex]: + """Retrieve the impedance matrix of the line. + + Returns: + The impedance of the line. + """ + return self._z_line + + @property + @ureg_wraps("S/km", (None,)) + def y_shunt(self) -> Q_[Complex]: + """Retrieve the shunt admittance matrix of the line. + + Returns: + The shunt admittance of the line. + """ + return self._y_shunt + + @property + def with_shunt(self) -> bool: + """`True` if the shunt admittance matrix is not null for the given line's phases. + + Returns: + True if the line has a not-null shunt admittance matrix. + """ + return self._with_shunt + + @property + def material(self) -> Material | None: + """The material of the conductor. Informative only, it has no impact on the load flow. + + Returns: + The material of the line to model. + """ + return self._material + + @property + def section(self) -> Q_[Float] | None: + """The cross-section area of the cable (in mm²). Informative only, it has no impact on the load flow. + + Returns: + The section of the line to model. + """ + return None if self._section is None else Q_(self._section, "mm**2") + + @property + def insulator(self) -> Insulator | None: + """The insulator of the conductor. Informative only, it has no impact on the load flow. + + Returns: + The insulator of the line to model. """ - z_line_tri = [[z_line]] if np.isscalar(z_line) else z_line - y_shunt_tri = [[y_shunt]] if y_shunt is not None and np.isscalar(y_shunt) else y_shunt - super().__init__( - id, - z_line=z_line_tri, - y_shunt=y_shunt_tri, - ampacities=ampacities, + return self._insulator + + @property + def ampacity(self) -> Q_[Float] | None: + """The ampacities of the line (A) if it is set. Informative only, it has no impact on the load flow. + + Returns: + The ampacities of the line to model. + """ + return None if self._ampacity is None else Q_(self._ampacity, "A") + + @ampacity.setter + @ureg_wraps(None, (None, "A")) + def ampacity(self, value: float | Q_[float] | None) -> None: + self._ampacity = self._check_positive_float(value=value, name="ampacity", unit="A") + + @material.setter + def material(self, value: Material | None) -> None: + self._material = None if pd.isna(value) else Material(value) + + @insulator.setter + def insulator(self, value: Insulator | None) -> None: + self._insulator = None if pd.isna(value) else Insulator(value) + + @section.setter + @ureg_wraps(None, (None, "mm**2")) + def section(self, value: float | Q_[float] | None) -> None: + self._section = self._check_positive_float(value=value, name="section", unit="mm²") + + @classmethod + @ureg_wraps(None, (None, None, "ohm/km", "ohm/km", "S/km", "S/km", "A")) + def from_sym( + cls, + id: Id, + z0: complex | Q_[complex], + z1: complex | Q_[complex], + y0: complex | Q_[complex], + y1: complex | Q_[complex], + ampacity: float | Q_[float] | None = None, + ) -> Self: + """Create line parameters from a symmetric model. + + Args: + id: + A unique ID of the line parameters, typically its canonical name. + + z0: + Impedance - zero sequence - :math:`r_0+x_0\\cdot j` (ohms/km) + + z1: + Impedance - direct sequence - :math:`r_1+x_1\\cdot j` (ohms/km) + + y0: + Admittance - zero sequence - :math:`g_0+b_0\\cdot j` (Siemens/km) + + y1: + Conductance - direct sequence - :math:`g_1+b_1\\cdot j` (Siemens/km) + + ampacity + An optional ampacity for the line parameters (A). It is not used in the load flow. + + Returns: + The created line parameters. + """ + zs = (z0 + 2 * z1) / 3 + ys = (y0 + 2 * y1) / 3 + return cls(id=id, z_line=zs, y_shunt=ys, ampacity=ampacity) + + @classmethod + @ureg_wraps(None, (None, None, None, None, None, None, None, "mm**2", "mm**2", "m", "m", "A")) + def from_geometry( + cls, + id: Id, + *, + line_type: LineType, + material: Material | None = None, + material_neutral: Material | None = None, + insulator: Insulator | None = None, + insulator_neutral: Insulator | None = None, + section: float | Q_[float], + section_neutral: float | Q_[float] | None = None, + height: float | Q_[float], + external_diameter: float | Q_[float], + ampacity: float | Q_[float] | None = None, + ) -> Self: + """Create line parameters from its geometry. + + Args: + id: + The id of the line parameters type. + + line_type: + Overhead or underground. See also :class:`~roseau.load_flow.LineType`. + + material: + Material of the conductor. If ``None``, ``ACSR`` is used for overhead lines and ``AL`` + for underground or twisted lines. See also :class:`~roseau.load_flow.Material`. + + material_neutral: + Material of the conductor If ``None``, it will be the same as the insulator of the + other phases. + + insulator: + Type of insulator. If ``None``, ``XLPE`` is used for twisted lines and ``PVC`` for + underground lines. See also :class:`~roseau.load_flow.Insulator`. Please provide + :attr:`~roseau.load_flow.Insulator.NONE` for cable without insulator. + + insulator_neutral: + Type of insulator. If ``None``, it will be the same as the insulator of the other phases. See also + :class:`~roseau.load_flow.Insulator`. Please provide :attr:`~roseau.load_flow.Insulator.NONE` for + cable without insulator. + + section: + Cross-section surface area of the phases (mm²). + + section_neutral: + Cross-section surface area of the neutral (mm²). If ``None`` it will be the same as the + section of the other phases. + + height: + Height of the line (m). It must be positive for overhead lines and negative for + underground lines. + + external_diameter: + External diameter of the cable (m). + + ampacity: + An optional ampacity of the phases of the line (A). It is not used in the load flow. + + Returns: + The created line parameters. + + See Also: + :ref:`Line parameters alternative constructor documentation ` + """ + parameters = MultiLineParameters.from_geometry( + id=id, line_type=line_type, - materials=materials, - insulators=insulators, - sections=sections, + material=material, + material_neutral=material_neutral, + insulator=insulator, + insulator_neutral=insulator_neutral, + section=section, + section_neutral=section_neutral, + height=height, + external_diameter=external_diameter, + ampacity=ampacity, ) + return cls.from_roseau_load_flow(parameters=parameters) + + @classmethod + def from_coiffier_model(cls, name: str, id: Id | None = None) -> Self: + """Get the electrical parameters of a MV line using Alain Coiffier's method (France specific model). + + Args: + name: + The canonical name of the line parameters. It must be in the format + `LineType_Material_CrossSection`. E.g. "U_AL_150". + + id: + A unique ID for the created line parameters object (optional). If ``None`` + (default), the id of the created object will be the canonical name. + + Returns: + The corresponding line parameters. + """ + parameters = MultiLineParameters.from_coiffier_model(name=name, id=id) + return cls.from_roseau_load_flow(parameters=parameters) + + # + # Constructors from other software + # + @classmethod + def from_roseau_load_flow(cls, parameters: MultiLineParameters) -> Self: + """Create a *Roseau Load Flow Single* line parameters from a multiphase *Roseau Load Flow* line parameter. + + Args: + parameters: + The multiphase line parameter. + + Returns: + The single phase line parameter + """ + materials = parameters.materials + sections = parameters.sections + insulators = parameters.insulators + ampacities = parameters.ampacities + + return cls( + id=parameters.id, + z_line=parameters.z_line[0, 0], + y_shunt=parameters.y_shunt[0, 0], + line_type=parameters.line_type, + material=materials[0] if materials is not None else None, + section=sections[0] if sections is not None else None, + insulator=insulators[0] if insulators is not None else None, + ampacity=ampacities[0] if ampacities is not None else None, + ) + + @classmethod + @ureg_wraps( + None, (None, None, "ohm/km", "ohm/km", "ohm/km", "ohm/km", "µS/km", "µS/km", "kA", None, None, None, "mm**2") + ) + def from_power_factory( + cls, + id: Id, + *, + r0: float | Q_[float], + r1: float | Q_[float], + x0: float | Q_[float], + x1: float | Q_[float], + b0: float | Q_[float], + b1: float | Q_[float], + inom: float | Q_[float] | None = None, + cohl: Literal[0, "Cable", 1, "OHL"] = "Cable", + conductor: Literal["Al", "Cu", "Ad", "As", "Ds"] | None = None, + insulation: Literal[0, "PVC", 1, "XLPE", 2, "Mineral", 3, "Paper", 4, "EPR"] | None = None, + section: float | Q_[float] | None = None, + ) -> Self: + """Create a line parameters object from PowerFactory "TypLne" data. + + Args: + id: + A unique ID of the line parameters. + + r0: + PwF parameter `rline0` (AC-Resistance R0'). Zero sequence resistance in (ohms/km). + + r1: + PwF parameter `rline` (AC-Resistance R1'). Direct sequence resistance in (ohms/km). + + x0: + PwF parameter `xline0` (Reactance X0'). Zero sequence reactance in (ohms/km). + + x1: + PwF parameter `xline` (Reactance X1'). Direct sequence reactance in (ohms/km). + + b0: + PwF parameter `bline0` (Susceptance B0'). Zero sequence susceptance in (µS/km). + + b1: + PwF parameter `bline` (Susceptance B'). Direct sequence susceptance in (µS/km). + + cohl: + PwF parameter `cohl_` (Cable/OHL). The type of the line; `'Cable'` or `0` mean an + underground cable and `'OHL'` or `1` mean an overhead line. + + inom: + PwF parameter `sline` or `InomAir` (Rated Current in ground or in air). The rated + current in (kA) of the line. It is used as the ampacity for analysis of network + constraint violations. Pass the `sline` parameter if the line is an underground + cable (cohl='Cable') or the `InomAir` parameter if the line is an overhead line + (cohl='OHL'). + + conductor: + PwF parameter `mlei` (Conductor Material). The material used for the conductors. + It can be one of: `'Al'` (Aluminium), `'Cu'` (Copper), `'Ad'` (Aldrey AlMgSi), + `'As'` (Aluminium-Steel), `'Ds'` (Aldrey-Steel). + + insulation: + PwF parameter `imiso` (Insulation Material). The material used for the conductor's + insulation. It can be one of `'PVC'` (`0`), `'XLPE'` (`1`), `'Mineral'` (`2`), + `'Paper'` (`3`) or `'EPR'` (`4`). If ``None`` is provided, the insulation is not filled in the + resulting instance. + + section: + PwF parameter `qurs` (Nominal Cross-Section). The nominal cross-sectional area of + the conductors in (mm²). + + Returns: + The created line parameters. + """ + parameters = MultiLineParameters.from_power_factory( + id=id, + r0=r0, + r1=r1, + x0=x0, + x1=x1, + b0=b0, + b1=b1, + inom=inom, + cohl=cohl, + conductor=conductor, + insulation=insulation, + section=section, + nphase=1, + nneutral=0, + ) + return cls.from_roseau_load_flow(parameters=parameters) + + @classmethod + @ureg_wraps(None, (None, None, "ohm/km", "ohm/km", "ohm/km", "ohm/km", "nF/km", "nF/km", "Hz", "A", None)) + def from_open_dss( + cls, + id: Id, + *, + r1: float | Q_[float], + r0: float | Q_[float], + x1: float | Q_[float], + x0: float | Q_[float], + c1: float | Q_[float] = 3.4, # default value used in OpenDSS + c0: float | Q_[float] = 1.6, # default value used in OpenDSS + basefreq: float | Q_[float] = F, + normamps: float | Q_[float] | None = None, + linetype: str | None = None, + ) -> Self: + """Create a line parameters object from OpenDSS "LineCode" data. + + Args: + id: + The unique ID of the line parameters. + + r1: + OpenDSS parameter: `R1`. Positive-sequence resistance in (ohm/km). + + r0: + OpenDSS parameter: `R0`. Positive-sequence resistance in (ohm/km). + + x1: + OpenDSS parameter: `X1`. Positive-sequence reactance in (ohm/km). + + x0: + OpenDSS parameter: `X0`. Positive-sequence reactance in (ohm/km). + + c1: + OpenDSS parameter: `C1`. Positive-sequence capacitance in (nF/km). + + c0: + OpenDSS parameter: `C0`. Positive-sequence capacitance in (nF/km). + + basefreq: + OpenDSS parameter: `BaseFreq`. Frequency at which impedances are specified (Hz). + Defaults to 50 Hz. + + normamps: + OpenDSS parameter: `NormAmps`. Normal ampere limit on line (A). This is the so-called + Planning Limit. It may also be the value above which load will have to be dropped + in a contingency. Usually about 75% - 80% of the emergency (one-hour) rating. + This value is passed to `ampacities` and used for violation checks. + + linetype: + OpenDSS parameter: `LineType`. Code designating the type of line. Only ``"OH"`` + (overhead) and ``"UG"`` (underground) are currently supported. + + Returns: + The corresponding line parameters object. + + Example usage:: + + # DSS command: `New linecode.240sq nphases=3 R1=0.127 X1=0.072 R0=0.342 X0=0.089 units=km` + lp = LineParameters.from_open_dss( + id="linecode-240sq", + r1=Q_(0.127, "ohm/km"), + x1=Q_(0.072, "ohm/km"), + r0=Q_(0.342, "ohm/km"), + x0=Q_(0.089, "ohm/km"), + c1=Q_(3.4, "nF/km"), # default value used in OpenDSS code + c0=Q_(1.6, "nF/km"), # default value used in OpenDSS code + ) + + # DSS command: `New LineCode.16sq NPhases=1 R1=0.350, X1=0.025, R0=0.366, X0=0.025, C1=1.036, C0=0.488 Units=kft NormAmps=400` + lp = LineParameters.from_open_dss( + id="linecode-16sq", + r1=Q_(0.350, "ohm/kft"), + x1=Q_(0.025, "ohm/kft"), + r0=Q_(0.366, "ohm/kft"), + x0=Q_(0.025, "ohm/kft"), + c1=Q_(1.036, "nF/kft"), + c0=Q_(0.488, "nF/kft"), + normamps=Q_(400, "A"), + ) + """ + parameters = MultiLineParameters.from_open_dss( + id=id, + r1=r1, + r0=r0, + x1=x1, + x0=x0, + c1=c1, + c0=c0, + basefreq=basefreq, + normamps=normamps, + linetype=linetype, + nphases=1, + ) + return cls.from_roseau_load_flow(parameters=parameters) + + # + # Catalogue Mixin + # + @classmethod + def catalogue_path(cls) -> Path: + return MultiLineParameters.catalogue_path() + + @classmethod + def catalogue_data(cls) -> pd.DataFrame: + # TODO: Delete from the catalogue of RLF lines with a different neutral section to only keep one version of + # each. Currently, all the lines have the same phase section and neutral section + + return MultiLineParameters.catalogue_data().drop( + columns=[ + "resistance_neutral", + "reactance_neutral", + "susceptance_neutral", + "ampacity_neutral", + "material_neutral", + "insulator_neutral", + "section_neutral", + ] + ) + + @classmethod + def _get_catalogue( + cls, + name: str | re.Pattern[str] | None, + line_type: str | None, + material: str | None, + insulator: str | None, + section: float | None, + raise_if_not_found: bool, + ) -> tuple[pd.DataFrame, str]: + catalogue_data = cls.catalogue_data() + + # Filter on strings/regular expressions + query_msg_list = [] + for value, column_name, display_name, display_name_plural in [ + (name, "name", "name", "names"), + ]: + if pd.isna(value): + continue + + mask = cls._filter_catalogue_str(value, strings=catalogue_data[column_name]) + if raise_if_not_found and mask.sum() == 0: + cls._raise_not_found_in_catalogue( + value=repr(value), + name=display_name, + name_plural=display_name_plural, + strings=catalogue_data[column_name], + query_msg_list=query_msg_list, + ) + catalogue_data = catalogue_data.loc[mask, :] + query_msg_list.append(f"{display_name}={value!r}") + + # Filter on enumerated types + for value, column_name, display_name, enum_class in ( + (line_type, "type", "line_type", LineType), + (material, "material", "material", Material), + (insulator, "insulator", "insulator", Insulator), + ): + if pd.isna(value): + continue + + enum_series = pd.Series( + data=[ + None if isna else enum_class(x) + for isna, x in zip( + catalogue_data[column_name].isna(), catalogue_data[column_name].values, strict=True + ) + ], + index=catalogue_data.index, + ) + try: + mask = enum_series == enum_class(value) + except RoseauLoadFlowException: + mask = pd.Series(data=False, index=catalogue_data.index) + if raise_if_not_found and mask.sum() == 0: + cls._raise_not_found_in_catalogue( + value=repr(value), + name=display_name, + name_plural=display_name + "s", + strings=enum_series, + query_msg_list=query_msg_list, + ) + catalogue_data = catalogue_data.loc[mask, :] + query_msg_list.append(f"{display_name}={value!r}") + + # Filter on floats + for value, column_name, display_name, display_name_plural, unit in [ + (section, "section", "cross-section", "cross-sections", "mm²"), + ]: + if value is None: + continue + + mask = np.isclose(catalogue_data[column_name], value) + if raise_if_not_found and mask.sum() == 0: + cls._raise_not_found_in_catalogue( + value=f"{value:.1f} {unit}", + name=display_name, + name_plural=display_name_plural, + strings=catalogue_data[column_name].apply(lambda x: f"{x:.1f} {unit}"), # noqa: B023 + query_msg_list=query_msg_list, + ) + catalogue_data = catalogue_data.loc[mask, :] + query_msg_list.append(f"{display_name}={value!r} {unit}") + + return catalogue_data, ", ".join(query_msg_list) + + @classmethod + @ureg_wraps(None, (None, None, None, None, None, "mm**2", None)) + def from_catalogue( + cls, + name: str | re.Pattern[str] | None = None, + line_type: str | None = None, + material: str | None = None, + insulator: str | None = None, + section: float | Q_[float] | None = None, + id: Id | None = None, + ) -> Self: + """Create line parameters from a catalogue. + + Args: + name: + The name of the line parameters to get from the catalogue. It can be a regular + expression. + + line_type: + The type of the line parameters to get. It can be ``"overhead"``, ``"twisted"``, or + ``"underground"``. See also :class:`~roseau.load_flow.LineType`. + + material: + The type of the conductor material (Al, Cu, ...) of the phases. See also + :class:`~roseau.load_flow.Material`. + + insulator: + The insulator of the phases. See also :class:`~roseau.load_flow.Insulator`. Please provide + :attr:`~roseau.load_flow.Insulator.NONE` for cable without insulator. + + section: + The cross-section surface area of the phases (mm²). + + id: + A unique ID for the created line parameters object (optional). If ``None`` + (default), the id of the created object will be its name in the catalogue. Note + that this parameter is not used in the data filtering. + + Returns: + The created line parameters. + """ + catalogue_data, query_info = cls._get_catalogue( + name=name, + line_type=line_type, + material=material, + insulator=insulator, + section=section, + raise_if_not_found=True, + ) + + try: + cls._assert_one_found( + found_data=catalogue_data["name"].tolist(), display_name="line parameters", query_info=query_info + ) + except RoseauLoadFlowException as e: + if name is None and id is not None: + e.msg += " Did you mean to filter by name instead of id?" + raise + idx = catalogue_data.index[0] + name = str(catalogue_data.at[idx, "name"]) + r = catalogue_data.at[idx, "resistance"] + x = catalogue_data.at[idx, "reactance"] + b = catalogue_data.at[idx, "susceptance"] + line_type = LineType(catalogue_data.at[idx, "type"]) + material = Material(catalogue_data.at[idx, "material"]) + insulator = catalogue_data.at[idx, "insulator"] # Converted in the LineParameters creator + section = catalogue_data.at[idx, "section"] + ampacity = catalogue_data.at[idx, "ampacity"] + if pd.isna(ampacity): + ampacity = None + z_line = r + x * 1j + y_shunt = b * 1j + if id is None: + id = name + return cls( + id=id, + z_line=z_line, + y_shunt=y_shunt, + ampacity=ampacity, + line_type=line_type, + material=material, + insulator=insulator, + section=section, + ) + + @classmethod + @ureg_wraps(None, (None, None, None, None, None, "mm**2")) + def get_catalogue( + cls, + name: str | re.Pattern[str] | None = None, + line_type: str | None = None, + material: str | None = None, + insulator: str | None = None, + section: float | Q_[float] | None = None, + ) -> pd.DataFrame: + """Get the catalogue of available lines. + + You can use the parameters below to filter the catalogue. If you do not specify any + parameter, all the catalogue will be returned. + + Args: + name: + The name of the line parameters to get from the catalogue. It can be a regular + expression. + + line_type: + The type of the line parameters to get. It can be ``"overhead"``, ``"twisted"``, or + ``"underground"``. See also :class:`~roseau.load_flow.LineType`. + + material: + The type of the conductor material (Al, Cu, ...) of the phases. See also + :class:`~roseau.load_flow.Material`. + + insulator: + The insulator of the phases. See also :class:`~roseau.load_flow.Insulator`. Please provide + :attr:`~roseau.load_flow.Insulator.NONE` for cable without insulator. + + section: + The cross-section surface area of the phases (mm²). + + Returns: + The catalogue data as a dataframe. + """ + catalogue_data, _ = cls._get_catalogue( + name=name, + line_type=line_type, + material=material, + insulator=insulator, + section=section, + raise_if_not_found=False, + ) + return catalogue_data.rename( + columns={ + "name": "Name", + "resistance": "Phase resistance (ohm/km)", + "reactance": "Phase reactance (ohm/km)", + "susceptance": "Phase susceptance (S/km)", + "ampacity": "Phase ampacity (A)", + "type": "Line type", + "material": "Phase material", + "insulator": "Phase insulator", + "section": "Phase cross-section (mm²)", + } + ).set_index("Name") + + # + # Json Mixin interface + # + @classmethod + def from_dict(cls, data: JsonDict, *, include_results: bool = True) -> Self: + """Line parameters constructor from dict. + + Args: + data: + The dictionary data of the line parameters. + + 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. Useless here as line parameters don't contain results. + + Returns: + The created line parameters. + """ + z_line = complex(data["z_line"][0], data["z_line"][1]) + y_shunt = complex(data["y_shunt"][0], data["y_shunt"][1]) if "y_shunt" in data else None + return cls( + id=data["id"], + z_line=z_line, + y_shunt=y_shunt, + ampacity=data.get("ampacity"), + line_type=data.get("line_type"), + material=data.get("material"), + insulator=data.get("insulator"), + section=data.get("section"), + ) + + def _to_dict(self, include_results: bool) -> JsonDict: + res = {"id": self.id, "z_line": [self._z_line.real, self._z_line.imag]} + if self._with_shunt: + res["y_shunt"] = [self._y_shunt.real, self._y_shunt.imag] + if self._ampacity is not None: + res["ampacity"] = self._ampacity + if self._line_type is not None: + res["line_type"] = self._line_type.name + if self._material is not None: + res["material"] = self._material.name + if self._insulator is not None: + res["insulator"] = self._insulator.name + if self._section is not None: + res["section"] = self._section + return res + + def _results_to_dict(self, warning: bool, full: bool) -> NoReturn: + msg = f"The {type(self).__name__} has no results to export." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.JSON_NO_RESULTS) + + # + # Utilities + # + @staticmethod + def _check_values(id: Id, value: complex, name: Literal["z_line", "y_shunt"]) -> None: + """Check the z_line and y_shunt values""" + # Check that the real coefficients are non-negative + if value.real < 0.0: + msg = f"The {name} value of line type {id!r} has coefficients with negative real part." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode[f"BAD_{name.upper()}_VALUE"]) + + # Ensure that z_line is not 0 + if name == "z_line" and np.isclose(value, 0): + msg = f"The z_line value of line type {id!r} can't be zero." + logger.error(msg) + raise RoseauLoadFlowException(msg=msg, code=RoseauLoadFlowExceptionCode.BAD_Z_LINE_VALUE) + + @staticmethod + def _check_positive_float(value: Float | None, name: Literal["section", "ampacity"], unit: str) -> Float | None: + value_isna = pd.isna(value) + if value_isna: + return None + elif value <= 0: + msg = f"{name.title()} must be positive: {value} {unit} was provided." + logger.error(msg) + error_code = ( + RoseauLoadFlowExceptionCode.BAD_AMPACITIES_VALUE + if name == "ampacity" + else RoseauLoadFlowExceptionCode.BAD_SECTIONS_VALUE + ) + raise RoseauLoadFlowException(msg=msg, code=error_code) + else: + return float(value) diff --git a/roseau/load_flow_single/models/loads/flexible_parameters.py b/roseau/load_flow_single/models/loads/flexible_parameters.py index 74ec976..98128a9 100644 --- a/roseau/load_flow_single/models/loads/flexible_parameters.py +++ b/roseau/load_flow_single/models/loads/flexible_parameters.py @@ -4,8 +4,8 @@ from typing_extensions import Self from roseau.load_flow.exceptions import RoseauLoadFlowException, RoseauLoadFlowExceptionCode -from roseau.load_flow.models.loads.flexible_parameters import Control as TriControl -from roseau.load_flow.models.loads.flexible_parameters import FlexibleParameter as TriFlexibleParameter +from roseau.load_flow.models.loads.flexible_parameters import Control as MultiControl +from roseau.load_flow.models.loads.flexible_parameters import FlexibleParameter as MultiFlexibleParameter from roseau.load_flow.models.loads.flexible_parameters import Projection from roseau.load_flow.typing import ComplexArray, ControlType, FloatArrayLike1D, ProjectionType from roseau.load_flow.units import Q_, ureg_wraps @@ -14,7 +14,7 @@ logger = logging.getLogger(__name__) -class Control(TriControl): +class Control(MultiControl): """Control class for flexible loads. This class contains the information needed to formulate the control equations. This includes the control type, @@ -40,8 +40,8 @@ def __init__( u_down: float | Q_[float], u_up: float | Q_[float], u_max: float | Q_[float], - alpha: float = TriControl._DEFAULT_ALPHA, - epsilon: float = TriControl._DEFAULT_EPSILON, + alpha: float = MultiControl._DEFAULT_ALPHA, + epsilon: float = MultiControl._DEFAULT_EPSILON, ) -> None: """Control constructor. @@ -86,7 +86,7 @@ def __init__( ) -class FlexibleParameter(TriFlexibleParameter): +class FlexibleParameter(MultiFlexibleParameter): """Flexible parameters of a flexible load. This class encapsulate single-phase flexibility information of a flexible load: diff --git a/roseau/load_flow_single/models/tests/data/small_network.json b/roseau/load_flow_single/models/tests/data/small_network.json index 5eea52d..73554ae 100644 --- a/roseau/load_flow_single/models/tests/data/small_network.json +++ b/roseau/load_flow_single/models/tests/data/small_network.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "is_multiphase": false, "buses": [ { @@ -72,7 +72,7 @@ "lines_params": [ { "id": "test", - "z_line": [[[10.0]], [[0.0]]] + "z_line": [10, 0] } ], "transformers_params": [] diff --git a/roseau/load_flow_single/models/tests/data/small_shunt_network.json b/roseau/load_flow_single/models/tests/data/small_shunt_network.json index ead6a45..ac0c22f 100644 --- a/roseau/load_flow_single/models/tests/data/small_shunt_network.json +++ b/roseau/load_flow_single/models/tests/data/small_shunt_network.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "is_multiphase": false, "buses": [ { @@ -72,8 +72,8 @@ "lines_params": [ { "id": "test", - "z_line": [[[10.0]], [[0.0]]], - "y_shunt": [[[0.01]], [[0.0]]] + "z_line": [10, 0], + "y_shunt": [0.01, 0.0] } ], "transformers_params": [] diff --git a/roseau/load_flow_single/models/tests/test_buses.py b/roseau/load_flow_single/models/tests/test_buses.py index 606ea1a..c91fb72 100644 --- a/roseau/load_flow_single/models/tests/test_buses.py +++ b/roseau/load_flow_single/models/tests/test_buses.py @@ -40,9 +40,9 @@ def test_voltage_limits(): assert bus.max_voltage == Q_(434.6, "V") # Can be reset to None - bus.nominal_voltage = None bus.min_voltage_level = None bus.max_voltage_level = None + bus.nominal_voltage = None assert bus.min_voltage_level is None assert bus.max_voltage_level is None assert bus.min_voltage is None @@ -60,9 +60,9 @@ def test_voltage_limits(): # NaNs are converted to None for na in (np.nan, float("nan"), pd.NA): - bus.nominal_voltage = na bus.min_voltage_level = na bus.max_voltage_level = na + bus.nominal_voltage = na assert bus.nominal_voltage is None assert bus.min_voltage_level is None assert bus.max_voltage_level is None @@ -70,9 +70,9 @@ def test_voltage_limits(): assert bus.max_voltage is None # Min/Max voltage values defined without nominal voltage are useless - bus.nominal_voltage = None bus.min_voltage_level = None bus.max_voltage_level = None + bus.nominal_voltage = None with pytest.warns( UserWarning, match=r"The min voltage level of the bus 'bus' is useless without a nominal voltage. Please define a nominal " @@ -102,7 +102,6 @@ def test_voltage_limits(): bus.nominal_voltage = Q_(400, "V") bus.min_voltage_level = None bus.max_voltage_level = None - bus.nominal_voltage = None # Bad values bus.min_voltage_level = 0.95 diff --git a/roseau/load_flow_single/models/tests/test_line_parameters.py b/roseau/load_flow_single/models/tests/test_line_parameters.py index 2766b30..150da3c 100644 --- a/roseau/load_flow_single/models/tests/test_line_parameters.py +++ b/roseau/load_flow_single/models/tests/test_line_parameters.py @@ -22,14 +22,14 @@ def test_line_parameters(): with pytest.raises(RoseauLoadFlowException) as e: LineParameters("test", z_line=z_line, y_shunt=y_shunt) assert e.value.code == RoseauLoadFlowExceptionCode.BAD_Z_LINE_VALUE - assert e.value.msg == "The z_line matrix of line type 'test' has coefficients with negative real part." + assert e.value.msg == "The z_line value of line type 'test' has coefficients with negative real part." # Negative real values (Y) y_shunt = -3 with pytest.raises(RoseauLoadFlowException): LineParameters(id="test", z_line=z_line, y_shunt=y_shunt) assert e.value.code == RoseauLoadFlowExceptionCode.BAD_Z_LINE_VALUE - assert e.value.msg == "The z_line matrix of line type 'test' has coefficients with negative real part." + assert e.value.msg == "The z_line value of line type 'test' has coefficients with negative real part." # Adding/Removing a shunt to a line is not allowed lp1 = LineParameters(id="lp1", z_line=1.0, y_shunt=1.0) @@ -52,8 +52,8 @@ def test_from_geometry(): # line_data = {"dpp": 0, "dpn": 0, "dsh": 0.04} # Working example - z_line, y_shunt, line_type, materials, insulators, sections = LineParameters._from_geometry( - "test", + lp = LineParameters.from_geometry( + id="test", line_type=LineType.OVERHEAD, material=Material.AL, material_neutral=None, @@ -82,7 +82,7 @@ def test_from_geometry(): ] ) - npt.assert_allclose(z_line, nplin.inv(y_line_expected), rtol=0.04, atol=0.02) + assert np.isclose(lp.z_line.m, nplin.inv(y_line_expected)[0, 0], rtol=0.04, atol=0.02) y_shunt_expected = np.array( [ [ @@ -111,12 +111,12 @@ def test_from_geometry(): ], ] ) - npt.assert_allclose(y_shunt, y_shunt_expected, rtol=0.001) + npt.assert_allclose(lp.y_shunt.m, y_shunt_expected[0, 0], rtol=0.001) - assert line_type == LineType.OVERHEAD - assert np.array_equal(materials, np.array([Material.AL] * 4, dtype=np.object_)) - assert np.array_equal(insulators, np.array([Insulator.PEX] * 4, dtype=np.object_)) - npt.assert_allclose(sections, [150, 150, 150, 70]) + assert lp.line_type == LineType.OVERHEAD + assert lp.material == Material.AL + assert lp.insulator == Insulator.PEX + assert np.isclose(lp.section.m, 150) # Test None insulator lp = LineParameters.from_geometry( @@ -129,8 +129,8 @@ def test_from_geometry(): external_diameter=0.04, ) # The default insulator for overhead lines - assert np.array_equal(lp.insulators, np.array([Insulator.NONE] * 4, dtype=np.object_)) - np.testing.assert_allclose(lp.y_shunt.m.real, 0.0) + assert lp.insulator == Insulator.NONE + assert np.isclose(lp.y_shunt.m.real, 0.0) lp = LineParameters.from_geometry( id="test", line_type=LineType.OVERHEAD, @@ -141,8 +141,8 @@ def test_from_geometry(): height=10, external_diameter=0.04, ) - assert np.array_equal(lp.insulators, np.array([Insulator.NONE] * 4, dtype=np.object_)) - np.testing.assert_allclose(lp.y_shunt.m.real, 0.0) + assert lp.insulator == Insulator.NONE + assert np.isclose(lp.y_shunt.m.real, 0.0) lp = LineParameters.from_geometry( id="test", @@ -153,22 +153,22 @@ def test_from_geometry(): height=-0.5, external_diameter=0.04, ) - np.testing.assert_allclose( + assert np.isclose( lp.y_shunt.m.imag * 4, # because InsulatorType.IP has 4x epsilon_r LineParameters.from_geometry( id="test", line_type=lp.line_type, - material=lp.materials[0], + material=lp.material, insulator=Insulator.IP, # 4x epsilon_r of InsulatorType.NONE - section=lp.sections[0], + section=lp.section, height=-0.5, external_diameter=0.04, ).y_shunt.m.imag, ) # The same but precise the neutral types - z_line, y_shunt, line_type, materials, insulators, sections = LineParameters._from_geometry( - "test", + lp = LineParameters.from_geometry( + id="test", line_type=LineType.OVERHEAD, material=Material.AL, material_neutral=Material.AL, @@ -179,16 +179,16 @@ def test_from_geometry(): height=10, external_diameter=0.04, ) - npt.assert_allclose(z_line, nplin.inv(y_line_expected), rtol=0.04, atol=0.02) - npt.assert_allclose(y_shunt, y_shunt_expected, rtol=0.001) - assert line_type == LineType.OVERHEAD - assert np.array_equal(materials, np.array([Material.AL] * 4, dtype=np.object_)) - assert np.array_equal(insulators, np.array([Insulator.PEX] * 4, dtype=np.object_)) - npt.assert_allclose(sections, [150, 150, 150, 70]) + assert np.isclose(lp.z_line.m, nplin.inv(y_line_expected)[0, 0], rtol=0.04, atol=0.02) + assert np.isclose(lp.y_shunt.m, y_shunt_expected[0, 0], rtol=0.001) + assert lp.line_type == LineType.OVERHEAD + assert lp.material == Material.AL + assert lp.insulator == Insulator.PEX + assert np.isclose(lp.section.m, 150) # line_data = {"dpp": 0, "dpn": 0, "dsh": 0.04} # Working example (also with string types) - z_line, y_shunt, line_type, materials, insulators, sections = LineParameters._from_geometry( + lp = LineParameters.from_geometry( id="test", line_type="UNDERGROUND", material="AL", @@ -215,7 +215,7 @@ def test_from_geometry(): [-0.03859093131793137, 0.20837873067712717, -0.03859093131792582, -0.6182914857776997], ] ) - npt.assert_allclose(z_line, nplin.inv(y_line_expected), rtol=0.04, atol=0.02) + assert np.isclose(lp.z_line.m, nplin.inv(y_line_expected)[0, 0], rtol=0.04, atol=0.02) y_shunt_expected = np.array( [ [ @@ -244,142 +244,54 @@ def test_from_geometry(): ], ] ) - - npt.assert_allclose(y_shunt, y_shunt_expected, rtol=0.3) - - assert isinstance(line_type, LineType) - assert line_type == LineType.UNDERGROUND - assert np.array_equal(materials, np.array([Material.AL] * 4, dtype=np.object_)) - assert np.array_equal(insulators, np.array([Insulator.PVC] * 4, dtype=np.object_)) - npt.assert_allclose(sections, [150, 150, 150, 70]) - - # Mix two lines to check that the neutral is different - z_line_1, y_shunt_1, line_type, materials, insulators, sections = LineParameters._from_geometry( - "test", - line_type=LineType.OVERHEAD, - material=Material.AL, - material_neutral=Material.AL, - insulator=Insulator.PEX, - insulator_neutral=Insulator.PEX, - section=150, - section_neutral=70, - height=10, - external_diameter=0.04, - ) - z_line_2, y_shunt_2, line_type, materials, insulators, sections = LineParameters._from_geometry( - "test", - line_type=LineType.OVERHEAD, - material=Material.AM, - material_neutral=Material.AM, - insulator=Insulator.XLPE, - insulator_neutral=Insulator.XLPE, - section=150, - section_neutral=70, - height=10, - external_diameter=0.04, - ) - - z_line, y_shunt, line_type, materials, insulators, sections = LineParameters._from_geometry( - "test", - line_type=LineType.OVERHEAD, - material=Material.AL, - material_neutral=Material.AM, - insulator=Insulator.PEX, - insulator_neutral=Insulator.XLPE, - section=150, - section_neutral=70, - height=10, - external_diameter=0.04, - ) - z_line_expected = z_line_1.copy() - z_line_expected[3, 3] = z_line_2[3, 3] - y_shunt_expected = y_shunt_1.copy() - y_shunt_expected[3, 3] = y_shunt_2[3, 3] - - npt.assert_allclose(z_line, z_line_expected) - npt.assert_allclose(y_shunt, y_shunt_expected) + assert np.isclose(lp.y_shunt.m, y_shunt_expected[0, 0], rtol=0.3) + assert isinstance(lp.line_type, LineType) + assert lp.line_type == LineType.UNDERGROUND + assert lp.material == Material.AL + assert lp.insulator == Insulator.PVC + assert np.isclose(lp.section.m, 150) def test_sym(): # With the bad model of PwF # line_data = {"id": "NKBA NOR 25.00 kV", "un": 25000.0, "in": 277.0000100135803} - z_line, y_shunt = LineParameters._sym_to_zy( - id="NKBA NOR 25.00 kV", z0=0.0j, z1=1.0 + 1.0j, zn=0.0j, zpn=0.0j, y0=0.0j, y1=1e-06j, bn=0.0, bpn=0.0 - ) - z_line_expected = (1 + 1j) * np.eye(3) - npt.assert_allclose(z_line, z_line_expected) - y_shunt_expected = 1e-6j * np.eye(3) - npt.assert_allclose(y_shunt, y_shunt_expected) + z0 = 0.0j + z1 = 1.0 + 1.0j + y0 = 0.0j + y1 = 1e-06j + lp = LineParameters.from_sym(id="NKBA NOR 25.00 kV", z0=z0, z1=z1, y0=y0, y1=y1) + zs = (z0 + 2 * z1) / 3 + ys = (y0 + 2 * y1) / 3 + assert np.isclose(lp.z_line.m, zs) + assert np.isclose(lp.y_shunt.m, ys) # line_data = {"id": "NKBA 4x150 1.00 kV", "un": 1000.0, "in": 361.0000014305115} # Downgraded model because of PwF bad data - z_line, y_shunt = LineParameters._sym_to_zy( - id="NKBA 4x150 1.00 kV", - z0=0.5 + 0.3050000071525574j, - z1=0.125 + 0.0860000029206276j, - zn=0.0j, - zpn=0.0j, - y0=0.0j, - y1=0.0j, - bn=0.0, - bpn=0.0, + lp = LineParameters.from_sym( + id="NKBA 4x150 1.00 kV", z0=0.5 + 0.3050000071525574j, z1=0.125 + 0.0860000029206276j, y0=0.0j, y1=0.0j ) - z_line_expected = np.array( - [ - [0.25 + 0.159j, 0.125 + 0.073j, 0.125 + 0.073j], - [0.125 + 0.073j, 0.25 + 0.159j, 0.125 + 0.073j], - [0.125 + 0.073j, 0.125 + 0.073j, 0.25 + 0.159j], - ], - dtype=complex, - ) - npt.assert_allclose(z_line, z_line_expected) - y_shunt_expected = np.zeros(shape=(3, 3), dtype=complex) - npt.assert_allclose(y_shunt, y_shunt_expected) + assert np.isclose(lp.z_line.m, 0.25 + 0.159j) + assert np.isclose(lp.y_shunt.m, 0) # First line # line_data = {"id": "sym_neutral_underground_line_example", "un": 400.0, "in": 150} - - z_line, y_shunt = LineParameters._sym_to_zy( + lp = LineParameters.from_sym( id="sym_neutral_underground_line_example", z0=0.188 + 0.8224j, z1=0.188 + 0.0812j, - zn=0.4029 + 0.3522j, - zpn=0.2471j, y0=0.000010462 + 0.000063134j, y1=0.000010462 + 0.00022999j, - bn=0.00011407, - bpn=-0.000031502, - ) - z_line_expected = np.array( - [ - [0.188 + 0.32826667j, 0.0 + 0.24706667j, 0.0 + 0.24706667j, 0.0 + 0.2471j], - [0.0 + 0.24706667j, 0.188 + 0.32826667j, 0.0 + 0.24706667j, 0.0 + 0.2471j], - [0.0 + 0.24706667j, 0.0 + 0.24706667j, 0.188 + 0.32826667j, 0.0 + 0.2471j], - [0.0 + 0.2471j, 0.0 + 0.2471j, 0.0 + 0.2471j, 0.4029 + 0.3522j], - ] ) - npt.assert_allclose(z_line, z_line_expected) - y_shunt_expected = np.array( - [ - [1.0462e-05 + 1.74371333e-04j, 0 - 5.56186667e-05j, 0 - 5.56186667e-05j, -0 - 3.15020000e-05j], - [0 - 5.56186667e-05j, 1.0462e-05 + 1.74371333e-04j, 0 - 5.56186667e-05j, -0 - 3.15020000e-05j], - [0 - 5.56186667e-05j, 0 - 5.56186667e-05j, 1.0462e-05 + 1.74371333e-04j, -0 - 3.15020000e-05j], - [-0 - 3.15020000e-05j, -0 - 3.15020000e-05j, -0 - 3.15020000e-05j, 0 + 1.14070000e-04j], - ] - ) - npt.assert_allclose(y_shunt, y_shunt_expected) + assert np.isclose(lp.z_line.m, 0.188 + 0.32826667j) + assert np.isclose(lp.y_shunt.m, 1.0462e-05 + 1.74371333e-04j) # Second line # line_data = {"id": "sym_line_example", "un": 20000.0, "in": 309} - z_line, y_shunt = LineParameters._sym_to_zy( - id="sym_line_example", z0=0.2 + 0.1j, z1=0.2 + 0.1j, zn=0.4029, y0=0.00014106j, y1=0.00014106j - ) - z_line_expected = (0.2 + 0.1j) * np.eye(3) - npt.assert_allclose(z_line, z_line_expected) - y_shunt_expected = 0.00014106j * np.eye(3) - npt.assert_allclose(y_shunt, y_shunt_expected) + lp = LineParameters.from_sym(id="sym_line_example", z0=0.2 + 0.1j, z1=0.2 + 0.1j, y0=0.00014106j, y1=0.00014106j) + assert np.isclose(lp.z_line.m, 0.2 + 0.1j) + assert np.isclose(lp.y_shunt.m, 0.00014106j) def test_from_coiffier_model(): @@ -401,24 +313,24 @@ def test_from_coiffier_model(): # Working example with defaults lp = LineParameters.from_coiffier_model("U_AL_150") - z_line_expected = (0.1767 + 0.1j) * np.eye(3) - y_shunt_expected = 0.00014106j * np.eye(3) + z_line_expected = 0.1767 + 0.1j + y_shunt_expected = 0.00014106j assert lp.id == "U_AL_150" assert lp.line_type == LineType.UNDERGROUND - assert np.array_equal(lp.materials, np.array([Material.AL] * 3, dtype=np.object_)) - assert np.allclose(lp.sections.m, 150) - npt.assert_allclose(lp.z_line.m_as("ohm/km"), z_line_expected, rtol=0.01, atol=0.01, strict=True) - npt.assert_allclose(lp.y_shunt.m_as("S/km"), y_shunt_expected, rtol=0.01, atol=0.01, strict=True) - npt.assert_allclose(lp.ampacities.m_as("A"), [368.689292] * 3, strict=True) + assert lp.material == Material.AL + assert np.isclose(lp.section.m, 150) + assert np.isclose(lp.z_line.m_as("ohm/km"), z_line_expected, rtol=0.01, atol=0.01) + assert np.isclose(lp.y_shunt.m_as("S/km"), y_shunt_expected, rtol=0.01, atol=0.01) + assert np.isclose(lp.ampacity.m_as("A"), 368.689292) # Working example with custom arguments - lp2 = LineParameters.from_coiffier_model("O_CU_54", nb_phases=2, id="lp2") + lp2 = LineParameters.from_coiffier_model("O_CU_54", id="lp2") assert lp2.id == "lp2" assert lp2.line_type == LineType.OVERHEAD - assert np.array_equal(lp2.materials, np.array([Material.CU] * 2, dtype=np.object_)) - assert np.allclose(lp2.sections.m, 54) - assert lp2.z_line.m.shape == (2, 2) - assert lp2.y_shunt.m.shape == (2, 2) + assert lp2.material == Material.CU + assert np.isclose(lp2.section.m, 54) + assert isinstance(lp2.z_line.m, complex) + assert isinstance(lp2.y_shunt.m, complex) def test_catalogue_data(): @@ -434,20 +346,13 @@ def test_catalogue_data(): for row in catalogue_data.itertuples(): assert re.match(r"^[UOT]_[A-Z]+_\d+(?:_\w+)?$", row.name) assert isinstance(row.resistance, float) - assert isinstance(row.resistance_neutral, float) assert isinstance(row.reactance, float) - assert isinstance(row.reactance_neutral, float) assert isinstance(row.susceptance, float) - assert isinstance(row.susceptance_neutral, float) assert isinstance(row.ampacity, int | float) - assert isinstance(row.ampacity_neutral, int | float) LineType(row.type) # Check that the type is valid Material(row.material) # Check that the material is valid - Material(row.material_neutral) # Check that the material is valid pd.isna(row.insulator) or Insulator(row.insulator) # Check that the insulator is valid - pd.isna(row.insulator_neutral) or Insulator(row.insulator_neutral) # Check that the insulator is valid assert isinstance(row.section, int | float) - assert isinstance(row.section_neutral, int | float) def test_from_catalogue(): @@ -510,292 +415,184 @@ def test_from_catalogue(): # Success lp = LineParameters.from_catalogue(name="U_AL_150") assert lp.id == "U_AL_150" - assert lp.z_line.shape == (3, 3) - assert lp.y_shunt.shape == (3, 3) - assert (lp.ampacities > 0).all() + assert isinstance(lp.z_line, Q_) + assert isinstance(lp.z_line.m, complex) + assert isinstance(lp.y_shunt, Q_) + assert isinstance(lp.y_shunt.m, complex) + assert isinstance(lp.ampacity, Q_) + assert isinstance(lp.ampacity.m, float) + assert lp.ampacity.m > 0 assert lp.line_type == LineType.UNDERGROUND - assert np.array_equal(lp.materials, np.array([Material.AL] * 3)) - assert lp.insulators is None - assert np.allclose(lp.sections.m, 150) + assert lp.material == Material.AL + assert lp.insulator is None + assert isinstance(lp.section, Q_) + assert lp.section.m == 150 # Success, overridden ID lp = LineParameters.from_catalogue(name="U_AL_150", id="lp1") assert lp.id == "lp1" - # Success, single-phase line - lp = LineParameters.from_catalogue(name="U_AL_150", nb_phases=2) - assert lp.z_line.shape == (2, 2) - assert lp.y_shunt.shape == (2, 2) - def test_get_catalogue(): # Get the entire catalogue catalogue = LineParameters.get_catalogue() assert isinstance(catalogue, pd.DataFrame) - assert catalogue.shape == (355, 15) + assert catalogue.shape == (355, 8) # Filter on a single attribute for field_name, value, expected_size in ( ("name", r"U_AL_150.*", 1), ("line_type", "OvErHeAd", 122), ("material", "Cu", 121), - ("material_neutral", "Cu", 121), # ("insulator", Insulator.SE, 240), ("section", 150, 9), - ("section_neutral", 150, 9), ("section", Q_(1.5, "cm²"), 9), - ("section_neutral", Q_(1.5, "cm²"), 9), ): filtered_catalogue = LineParameters.get_catalogue(**{field_name: value}) - assert filtered_catalogue.shape == (expected_size, 15) + assert filtered_catalogue.shape == (expected_size, 8) # Filter on two attributes for field_name, value, expected_size in ( ("name", r"U_AL_150.*", 1), ("line_type", "OvErHeAd", 122), ("section", 150, 9), - ("section_neutral", 150, 9), ): filtered_catalogue = LineParameters.get_catalogue(**{field_name: value}) - assert filtered_catalogue.shape == (expected_size, 15) + assert filtered_catalogue.shape == (expected_size, 8) # No results empty_catalogue = LineParameters.get_catalogue(section=15000) - assert empty_catalogue.shape == (0, 15) + assert empty_catalogue.shape == (0, 8) -def test_insulators(): - z_line = np.eye(3, dtype=complex) - y_shunt = np.eye(3, dtype=complex) +def test_insulator(): + z_line = 1 + 2j + y_shunt = 0.5j # None-like values - lp = LineParameters("test", z_line=z_line, y_shunt=y_shunt, insulators=None) - assert lp.insulators is None - lp.insulators = np.nan - assert lp.insulators is None - lp.insulators = pd.NA - assert lp.insulators is None + lp = LineParameters("test", z_line=z_line, y_shunt=y_shunt, insulator=None) + assert lp.insulator is None + lp.insulator = np.nan + assert lp.insulator is None + lp.insulator = pd.NA + assert lp.insulator is None # Single values - lp.insulators = Insulator.EPR - assert len(lp.insulators) == 3 - assert np.array_equal(lp.insulators, np.array([Insulator.EPR] * 3)) + lp.insulator = Insulator.EPR + assert lp.insulator == Insulator.EPR - lp.insulators = Insulator.EPR.name - assert len(lp.insulators) == 3 - assert np.array_equal(lp.insulators, np.array([Insulator.EPR] * 3)) + lp.insulator = Insulator.EPR.name + assert lp.insulator == Insulator.EPR # Special case for Insulator.NONE - lp.insulators = Insulator.NONE.name - assert len(lp.insulators) == 3 - assert np.array_equal(lp.insulators, np.array([Insulator.NONE] * 3)) + lp.insulator = Insulator.NONE.name + assert lp.insulator == Insulator.NONE - # Arrays - lp.insulators = [Insulator.EPR, Insulator.XLPE, Insulator.LDPE] - assert len(lp.insulators) == 3 - assert np.array_equal(lp.insulators, np.array([Insulator.EPR, Insulator.XLPE, Insulator.LDPE])) - lp.insulators = [Insulator.NONE, Insulator.NONE, Insulator.NONE] - assert len(lp.insulators) == 3 - assert np.array_equal(lp.insulators, np.array([Insulator.NONE, Insulator.NONE, Insulator.NONE])) - - # Arrays with all none - lp.insulators = [pd.NA, np.nan, None] - assert lp.insulators is None - - # Errors - with pytest.raises(RoseauLoadFlowException) as e: - lp.insulators = [pd.NA, np.nan, Insulator.LDPE] - assert e.value.msg == "Insulators cannot contain null values: [, nan, ldpe] was provided." - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_INSULATORS_VALUE - - with pytest.raises(RoseauLoadFlowException) as e: - lp.insulators = ["invalid", Insulator.XLPE, "XLPE"] - assert e.value.msg == "'invalid' cannot be converted into a Insulator." - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_INSULATOR - - with pytest.raises(RoseauLoadFlowException) as e: - lp.insulators = [Insulator.XLPE, Insulator.HDPE] - assert e.value.msg == "Incorrect number of insulators: 2 instead of 3" - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_INSULATORS_SIZE - - -def test_materials(): - z_line = np.eye(3, dtype=complex) - y_shunt = np.eye(3, dtype=complex) +def test_material(): + z_line = 1 + 2j + y_shunt = 0.5j # None-like values - lp = LineParameters("test", z_line=z_line, y_shunt=y_shunt, materials=None) - assert lp.materials is None - lp.materials = np.nan - assert lp.materials is None - lp.materials = pd.NA - assert lp.materials is None + lp = LineParameters("test", z_line=z_line, y_shunt=y_shunt, material=None) + assert lp.material is None + lp.material = np.nan + assert lp.material is None + lp.material = pd.NA + assert lp.material is None # Single values - lp.materials = Material.AAAC - assert len(lp.materials) == 3 - assert np.array_equal(lp.materials, np.array([Material.AAAC] * 3)) + lp.material = Material.AAAC + assert lp.material == Material.AAAC - lp.materials = Material.AAAC.name - assert len(lp.materials) == 3 - assert np.array_equal(lp.materials, np.array([Material.AAAC] * 3)) - - # Arrays - lp.materials = [Material.AAAC, Material.AL, Material.CU] - assert len(lp.materials) == 3 - assert np.array_equal(lp.materials, np.array([Material.AAAC, Material.AL, Material.CU])) + lp.material = Material.AAAC.name + assert lp.material == Material.AAAC # Errors with pytest.raises(RoseauLoadFlowException) as e: - lp.materials = [np.nan, float("nan"), pd.NA] - assert e.value.msg == "Materials cannot contain null values: [nan, nan, ] was provided." - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_MATERIALS_VALUE - - with pytest.raises(RoseauLoadFlowException) as e: - lp.materials = [np.nan, float("nan"), Material.AM] - assert e.value.msg == "Materials cannot contain null values: [nan, nan, am] was provided." - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_MATERIALS_VALUE - - with pytest.raises(RoseauLoadFlowException) as e: - lp.materials = ["invalid", Material.AM, "AM"] + lp.material = "invalid" assert e.value.msg == "'invalid' cannot be converted into a Material." assert e.value.code == RoseauLoadFlowExceptionCode.BAD_MATERIAL - with pytest.raises(RoseauLoadFlowException) as e: - lp.materials = [Material.AM, Material.AL] - assert e.value.msg == "Incorrect number of materials: 2 instead of 3" - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_MATERIALS_SIZE - -def test_sections(): - z_line = np.eye(3, dtype=complex) - y_shunt = np.eye(3, dtype=complex) +def test_section(): + z_line = 1 + 2j + y_shunt = 0.5j # None-like values - lp = LineParameters("test", z_line=z_line, y_shunt=y_shunt, sections=None) - assert lp.sections is None - lp.sections = np.nan - assert lp.sections is None - lp.sections = pd.NA - assert lp.sections is None + lp = LineParameters("test", z_line=z_line, y_shunt=y_shunt, section=None) + assert lp.section is None + lp.section = np.nan + assert lp.section is None + lp.section = pd.NA + assert lp.section is None # Single values - lp.sections = 4 - assert len(lp.sections) == 3 - assert np.allclose(lp.sections.magnitude, 4) - - lp.sections = Q_(4, "cm**2") - assert len(lp.sections) == 3 - assert np.allclose(lp.sections.magnitude, 400) + lp.section = 4 + assert np.isclose(lp.section.magnitude, 4) - # Arrays - lp.sections = Q_([4, 5, 6], "mm**2") - assert len(lp.sections) == 3 - assert np.allclose(lp.sections.magnitude, [4, 5, 6]) + lp.section = Q_(4, "cm**2") + assert np.isclose(lp.section.magnitude, 400) # Errors with pytest.raises(RoseauLoadFlowException) as e: - lp.sections = [np.nan, float("nan"), pd.NA] - assert e.value.msg == "Sections cannot contain null values: [nan, nan, ] mm² was provided." - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_SECTIONS_VALUE - - with pytest.raises(RoseauLoadFlowException) as e: - lp.sections = [np.nan, float("nan"), 3] - assert e.value.msg == "Sections cannot contain null values: [nan, nan, 3] mm² was provided." - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_SECTIONS_VALUE - - with pytest.raises(RoseauLoadFlowException) as e: - lp.sections = [3, -1, 3.0] - assert e.value.msg == "Sections must be positive: [3.0, -1.0, 3.0] mm² was provided." + lp.section = -1.0 + assert e.value.msg == "Section must be positive: -1.0 mm² was provided." assert e.value.code == RoseauLoadFlowExceptionCode.BAD_SECTIONS_VALUE with pytest.raises(RoseauLoadFlowException) as e: - lp.sections = [3, 0, 3.0] - assert e.value.msg == "Sections must be positive: [3.0, 0.0, 3.0] mm² was provided." + lp.section = 0 + assert e.value.msg == "Section must be positive: 0 mm² was provided." assert e.value.code == RoseauLoadFlowExceptionCode.BAD_SECTIONS_VALUE - with pytest.raises(RoseauLoadFlowException) as e: - lp.sections = [3, 3] - assert e.value.msg == "Incorrect number of sections: 2 instead of 3" - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_SECTIONS_SIZE - -def test_ampacities(): - z_line = np.eye(3, dtype=complex) - y_shunt = np.eye(3, dtype=complex) +def test_ampacity(): + z_line = 1 + 2j + y_shunt = 0.5j # None-like values - lp = LineParameters("test", z_line=z_line, y_shunt=y_shunt, ampacities=None) - assert lp.ampacities is None - lp.ampacities = np.nan - assert lp.ampacities is None - lp.ampacities = pd.NA - assert lp.ampacities is None - lp.ampacities = [np.nan, float("nan"), pd.NA] - assert lp.ampacities is None + lp = LineParameters("test", z_line=z_line, y_shunt=y_shunt, ampacity=None) + assert lp.ampacity is None + lp.ampacity = np.nan + assert lp.ampacity is None + lp.ampacity = pd.NA + assert lp.ampacity is None # Single values - lp.ampacities = 4 - assert len(lp.ampacities) == 3 - assert np.allclose(lp.ampacities.magnitude, 4) - - lp.ampacities = Q_(4, "kA") - assert len(lp.ampacities) == 3 - assert np.allclose(lp.ampacities.magnitude, 4000) + lp.ampacity = 4 + assert np.isclose(lp.ampacity.magnitude, 4) - # Arrays - lp.ampacities = Q_([4, 5, 6], "A") - assert len(lp.ampacities) == 3 - assert np.allclose(lp.ampacities.magnitude, [4, 5, 6]) - - # Array with nan - lp.ampacities = Q_([4, np.nan, 6], "A") - assert len(lp.ampacities) == 3 - npt.assert_allclose(lp.ampacities.magnitude, [4, np.nan, 6], equal_nan=True) + lp.ampacity = Q_(4, "kA") + assert np.isclose(lp.ampacity.magnitude, 4000) # Errors with pytest.raises(RoseauLoadFlowException) as e: - lp.ampacities = [1, 2] - assert e.value.msg == "Incorrect number of ampacities: 2 instead of 3" - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_AMPACITIES_SIZE - - with pytest.raises(RoseauLoadFlowException) as e: - lp.ampacities = -2 - assert e.value.msg == "Ampacities must be positive: -2 A was provided." + lp.ampacity = -2 + assert e.value.msg == "Ampacity must be positive: -2 A was provided." assert e.value.code == RoseauLoadFlowExceptionCode.BAD_AMPACITIES_VALUE with pytest.raises(RoseauLoadFlowException) as e: - lp.ampacities = 0 - assert e.value.msg == "Ampacities must be positive: 0 A was provided." - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_AMPACITIES_VALUE - - with pytest.raises(RoseauLoadFlowException) as e: - lp.ampacities = [1, np.nan, -2] - assert e.value.msg == "Ampacities must be positive: [1.0, nan, -2.0] A was provided." - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_AMPACITIES_VALUE - - with pytest.raises(RoseauLoadFlowException) as e: - lp.ampacities = [1, np.nan, 0] - assert e.value.msg == "Ampacities must be positive: [1.0, nan, 0.0] A was provided." + lp.ampacity = 0 + assert e.value.msg == "Ampacity must be positive: 0 A was provided." assert e.value.code == RoseauLoadFlowExceptionCode.BAD_AMPACITIES_VALUE def test_json_serialization(tmp_path): - lp = LineParameters(id="test", z_line=np.eye(3), sections=np.float64(150), ampacities=[1, float("nan"), np.nan]) + lp = LineParameters(id="test", z_line=1, section=np.float64(150), ampacity=np.nan, line_type=LineType.OVERHEAD.name) + assert lp.line_type == LineType.OVERHEAD path = tmp_path / "lp.json" lp.to_json(path) lp_dict = LineParameters.from_json(path).to_dict() - assert isinstance(lp_dict["z_line"], list) - npt.assert_allclose(lp_dict["sections"], [150, 150, 150]) - npt.assert_allclose(lp_dict["ampacities"], [1, np.nan, np.nan]) + assert lp_dict["z_line"] == [1, 0] + assert lp_dict["line_type"] == LineType.OVERHEAD.name + assert np.isclose(lp_dict["section"], 150) + assert "ampacity" not in lp_dict def test_from_open_dss(): # DSS command: `New linecode.240sq nphases=3 R1=0.127 X1=0.072 R0=0.342 X0=0.089 units=km` lp240sq = LineParameters.from_open_dss( id="linecode-240sq", - nphases=3, r1=Q_(0.127, "ohm/km"), x1=Q_(0.072, "ohm/km"), r0=Q_(0.342, "ohm/km"), @@ -805,20 +602,15 @@ def test_from_open_dss(): ) assert lp240sq.id == "linecode-240sq" zs_e = 0.19866666666666669 + 0.07766666666666666j - zm_e = 0.07166666666666667 + 0.005666666666666667j - z_line_expected = [[zs_e, zm_e, zm_e], [zm_e, zs_e, zm_e], [zm_e, zm_e, zs_e]] - np.testing.assert_allclose(lp240sq.z_line.m, z_line_expected) + assert np.isclose(lp240sq.z_line.m, zs_e) ys_e = 8.796459430051418e-07j - ym_e = -1.8849555921538752e-07j - y_shunt_expected = [[ys_e, ym_e, ym_e], [ym_e, ys_e, ym_e], [ym_e, ym_e, ys_e]] - np.testing.assert_allclose(lp240sq.y_shunt.m, y_shunt_expected) + assert np.isclose(lp240sq.y_shunt.m, ys_e) assert lp240sq.line_type is None - assert lp240sq.ampacities is None + assert lp240sq.ampacity is None # DSS command: `New LineCode.16sq NPhases=1 R1=0.350, X1=0.025, R0=0.366, X0=0.025, C1=1.036, C0=0.488 Units=kft NormAmps=400 LineType=OH` lp16sq = LineParameters.from_open_dss( id="linecode-16sq", - nphases=1, r1=Q_(0.350, "ohm/kft"), x1=Q_(0.025, "ohm/kft"), r0=Q_(0.366, "ohm/kft"), @@ -830,33 +622,23 @@ def test_from_open_dss(): ) assert lp16sq.id == "linecode-16sq" zs_e = 1.1657917760279966 + 0.08202099737532809j - z_line_expected = [[zs_e]] - np.testing.assert_allclose(lp16sq.z_line.m, z_line_expected) + assert np.isclose(lp16sq.z_line.m, zs_e) ys_e = 8.795360010050165e-07j - y_shunt_expected = [[ys_e]] - np.testing.assert_allclose(lp16sq.y_shunt.m, y_shunt_expected) + assert np.isclose(lp16sq.y_shunt.m, ys_e) assert lp16sq.line_type == LineType.OVERHEAD - assert np.allclose(lp16sq.ampacities.m, 400) + assert np.isclose(lp16sq.ampacity.m, 400) def test_from_power_factory(): # Parameters from tests/data/dgs/special/MV_Load.json pwf_params = { "id": "NA2YSY 1x95rm 12/20kV it", - "nphase": 3, - "nneutral": 0, "r1": 0.3225, # Ohm/km "x1": 0.125663, # Ohm/km "b1": 72.25663, # µS/km "r0": 1.29, # Ohm/km "x0": 0.502654, # Ohm/km "b0": 75.05265, # µS/km - "rn": 0, # Ohm/km - "xn": 0, # Ohm/km - "bn": 0, # µS/km - "rpn": 0, # Ohm/km - "xpn": 0, # Ohm/km - "bpn": 0, # µS/km "inom": 0.235, # kA "cohl": 0, # Cable (underground) "conductor": "Al", # Aluminium @@ -866,57 +648,26 @@ def test_from_power_factory(): na2ysy1x95rm = LineParameters.from_power_factory(**pwf_params) assert na2ysy1x95rm.id == "NA2YSY 1x95rm 12/20kV it" - zs_e = 0.645 + 0.2513266666666667j - zm_e = 0.3225 + 0.1256636666666667j - z_line_expected = [[zs_e, zm_e, zm_e], [zm_e, zs_e, zm_e], [zm_e, zm_e, zs_e]] - np.testing.assert_allclose(na2ysy1x95rm.z_line.m, z_line_expected) + assert np.isclose(na2ysy1x95rm.z_line.m, zs_e) ys_e = 7.318863666666666e-05j - ym_e = 9.320066666666643e-07j - y_shunt_expected = [[ys_e, ym_e, ym_e], [ym_e, ys_e, ym_e], [ym_e, ym_e, ys_e]] - np.testing.assert_allclose(na2ysy1x95rm.y_shunt.m, y_shunt_expected) - - assert np.allclose(na2ysy1x95rm.ampacities.m, 235), na2ysy1x95rm.ampacities + assert np.isclose(na2ysy1x95rm.y_shunt.m, ys_e) + assert np.isclose(na2ysy1x95rm.ampacity.m, 235) assert na2ysy1x95rm.line_type == LineType.UNDERGROUND - assert np.array_equal(na2ysy1x95rm.materials, np.array([Material.AL] * 3)) - assert np.array_equal(na2ysy1x95rm.insulators, np.array([Insulator.PVC] * 3)) - assert np.allclose(na2ysy1x95rm.sections.m, 95) - - # Bad phases - new_pwf_params = pwf_params | {"nphase": 4} - with pytest.raises(RoseauLoadFlowException) as e: - LineParameters.from_power_factory(**new_pwf_params) - assert e.value.msg == "Expected nphase=1, 2 or 3, got 4 for line parameters 'NA2YSY 1x95rm 12/20kV it'." - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE - new_pwf_params = pwf_params | {"nneutral": 2} - with pytest.raises(RoseauLoadFlowException) as e: - LineParameters.from_power_factory(**new_pwf_params) - assert e.value.msg == "Expected nneutral=0 or 1, got 2 for line parameters 'NA2YSY 1x95rm 12/20kV it'." - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_PHASE - - # Line has no neutral, OK to not pass neutral impedances - new_pwf_params = {k: v for k, v in pwf_params.items() if k not in {"rn", "xn", "bn", "rpn", "xpn", "bpn"}} - LineParameters.from_power_factory(**new_pwf_params) - - # This time with neutral, should raise an error - new_pwf_params["nneutral"] = 1 - with pytest.raises(RoseauLoadFlowException) as e: - LineParameters.from_power_factory(**new_pwf_params) - assert e.value.msg == ( - "Missing rn, xn, bn, rpn, xpn or bpn required with nneutral=1 for line parameters 'NA2YSY 1x95rm 12/20kV it'." - ) - assert e.value.code == RoseauLoadFlowExceptionCode.BAD_LINE_MODEL + assert na2ysy1x95rm.material == Material.AL + assert na2ysy1x95rm.insulator == Insulator.PVC + assert np.isclose(na2ysy1x95rm.section.m, 95) # String versions of line/insulator type are also accepted new_pwf_params = pwf_params | {"cohl": "OHL", "insulation": "XLPE"} lp = LineParameters.from_power_factory(**new_pwf_params) assert lp.line_type == LineType.OVERHEAD - assert np.array_equal(lp.insulators, np.array([Insulator.XLPE] * 3, dtype=np.object_)) + assert lp.insulator == Insulator.XLPE def test_results_to_dict(): # No results to export - lp = LineParameters.from_catalogue(name="U_AL_150", nb_phases=3) + lp = LineParameters.from_catalogue(name="U_AL_150") with pytest.raises(RoseauLoadFlowException) as e: lp.results_to_dict() assert e.value.msg == "The LineParameters has no results to export." @@ -924,16 +675,16 @@ def test_results_to_dict(): def test_equality(): - lp = LineParameters.from_catalogue(name="U_AL_150", nb_phases=3) + lp = LineParameters.from_catalogue(name="U_AL_150") data = { "id": lp.id, "z_line": lp.z_line, "y_shunt": lp.y_shunt, - "ampacities": lp.ampacities, + "ampacity": lp.ampacity, "line_type": lp.line_type, - "materials": lp.materials, - "insulators": lp.insulators, - "sections": lp.sections, + "material": lp.material, + "insulator": lp.insulator, + "section": lp.section, } lp2 = LineParameters(**data) assert lp2 == lp @@ -942,11 +693,11 @@ def test_equality(): "id": lp.id + " other", "z_line": lp.z_line.m + 1, "y_shunt": lp.y_shunt.m + 1, - "ampacities": lp.ampacities.m + 1, + "ampacity": lp.ampacity.m + 1, "line_type": LineType.OVERHEAD, - "materials": Material.CU, - "insulators": Insulator.XLPE, - "sections": lp.sections.m + 1, + "material": Material.CU, + "insulator": Insulator.XLPE, + "section": lp.section.m + 1, } for k, v in other_data.items(): other_lp = LineParameters(**(data | {k: v})) diff --git a/roseau/load_flow_single/models/tests/test_lines.py b/roseau/load_flow_single/models/tests/test_lines.py index 7b95eaa..65bdc8a 100644 --- a/roseau/load_flow_single/models/tests/test_lines.py +++ b/roseau/load_flow_single/models/tests/test_lines.py @@ -129,7 +129,8 @@ def test_res_violated(): assert line.res_violated is None # No constraint violated - lp.ampacities = 11 + lp = LineParameters(id="lp", z_line=1.0, ampacity=11) + line.parameters = lp assert not line.res_violated assert np.allclose(line.res_loading, 10 / 11) @@ -140,19 +141,22 @@ def test_res_violated(): assert np.allclose(line.res_loading, 10 / (11 * 0.5)) # Two violations - lp.ampacities = 9 + lp = LineParameters(id="lp", z_line=1.0, ampacity=9) + line.parameters = lp line.max_loading = 1 assert line.res_violated assert np.allclose(line.res_loading, 10 / 9) # Side 1 violation - lp.ampacities = 11 + lp = LineParameters(id="lp", z_line=1.0, ampacity=11) + line.parameters = lp line._res_currents = 12, -10 assert line.res_violated assert np.allclose(line.res_loading, 12 / 11) # Side 2 violation - lp.ampacities = 11 + lp = LineParameters(id="lp", z_line=1.0, ampacity=11) + line.parameters = lp line._res_currents = 10, -12 assert line.res_violated assert np.allclose(line.res_loading, 12 / 11) @@ -163,19 +167,22 @@ def test_res_violated(): line._res_currents = 10, -10 # No constraint violated - lp.ampacities = 11 + lp = LineParameters(id="lp", z_line=1.0, ampacity=11) + line.parameters = lp line.max_loading = 1 assert not line.res_violated assert np.allclose(line.res_loading, 10 / 11) # Side 1 violation - lp.ampacities = 11 + lp = LineParameters(id="lp", z_line=1.0, ampacity=11) + line.parameters = lp line._res_currents = 12, -10 assert line.res_violated assert np.allclose(line.res_loading, 12 / 11) # Side 2 violation - lp.ampacities = 11 + lp = LineParameters(id="lp", z_line=1.0, ampacity=11) + line.parameters = lp line._res_currents = 10, -12 assert line.res_violated assert np.allclose(line.res_loading, 12 / 11) diff --git a/roseau/load_flow_single/network.py b/roseau/load_flow_single/network.py index 0cc3b99..529ddc9 100644 --- a/roseau/load_flow_single/network.py +++ b/roseau/load_flow_single/network.py @@ -677,13 +677,11 @@ def res_lines(self) -> pd.DataFrame: power2 = potential2 * current2.conjugate() * 3.0 series_loss = du_line * series_current.conjugate() * 3.0 max_loading = line._max_loading - ampacities = line.parameters._ampacities - if ampacities is None: - ampacity = None + ampacity = line.parameters._ampacity + if ampacity is None: loading = None violated = None else: - ampacity = ampacities[0] i_max = ampacity * max_loading loading = max(abs(current1), abs(current2)) / i_max violated = loading > max_loading diff --git a/roseau/load_flow_single/tests/data/networks/all_element_network.json b/roseau/load_flow_single/tests/data/networks/all_element_network.json index a356b15..c1f2828 100644 --- a/roseau/load_flow_single/tests/data/networks/all_element_network.json +++ b/roseau/load_flow_single/tests/data/networks/all_element_network.json @@ -1,29 +1,29 @@ { - "version": 2, + "version": 3, "is_multiphase": false, "buses": [ { "id": "bus0", "results": { - "potential": [11547.005383792515, -6.323403605422406e-19] + "potential": [11547.005383792515, -6.32340360542244e-19] } }, { "id": "bus1", "results": { - "potential": [11545.451633763245, -0.7262442088080509] + "potential": [11545.451633763245, -0.726244208808051] } }, { "id": "bus2", "results": { - "potential": [226.54356547433653, 0.017201038977268608] + "potential": [226.54356547433653, 0.017201038977268594] } }, { "id": "bus3", "results": { - "potential": [226.54356547433653, 0.01720103897726845] + "potential": [226.54356547433653, 0.017201038977268577] } }, { @@ -65,8 +65,8 @@ "bus1": "bus1", "bus2": "bus2", "results": { - "current1": [4.366621746091755, -0.031725995777850506], - "current2": [-218.27336004641893, 1.5862961576714814] + "current1": [4.366621746091755, -0.03172599577785049], + "current2": [-218.27336004641893, 1.5862961576714807] }, "tap": 1.0, "params_id": "630kVA", @@ -306,27 +306,27 @@ "voltage": [20000.0, 0.0], "results": { "current": [-4.3666402893395935, -0.5578954411870708], - "potential": [11547.005383792515, -6.323403605422406e-19] + "potential": [11547.005383792515, -6.32340360542244e-19] } } ], "lines_params": [ { "id": "lp0", - "z_line": [[[0.2430129333]], [[0.0962375209]]], - "y_shunt": [[[0.0]], [[3.40441e-5]]], - "ampacities": [323.0], + "z_line": [0.2430129333, 0.0962375209], + "y_shunt": [0.0, 3.40441e-5], + "ampacity": 323.0, "line_type": "UNDERGROUND", - "materials": ["AM"], - "sections": [148.0] + "material": "AM", + "section": 148.0 }, { "id": "lp1", - "z_line": [[[0.4184]], [[0.1066400577]]], - "ampacities": [195.0], + "z_line": [0.4184, 0.1066400577], + "ampacity": 195.0, "line_type": "TWISTED", - "materials": ["AL"], - "sections": [75.0] + "material": "AL", + "section": 75.0 } ], "transformers_params": [ diff --git a/roseau/load_flow_single/tests/data/networks/small_network.json b/roseau/load_flow_single/tests/data/networks/small_network.json index 5eea52d..73554ae 100644 --- a/roseau/load_flow_single/tests/data/networks/small_network.json +++ b/roseau/load_flow_single/tests/data/networks/small_network.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "is_multiphase": false, "buses": [ { @@ -72,7 +72,7 @@ "lines_params": [ { "id": "test", - "z_line": [[[10.0]], [[0.0]]] + "z_line": [10, 0] } ], "transformers_params": [] diff --git a/roseau/load_flow_single/tests/test_electrical_network.py b/roseau/load_flow_single/tests/test_electrical_network.py index a1003b3..e5e903d 100644 --- a/roseau/load_flow_single/tests/test_electrical_network.py +++ b/roseau/load_flow_single/tests/test_electrical_network.py @@ -282,7 +282,7 @@ def test_poorly_connected_elements(): bus2 = Bus(id="b2") bus3 = Bus(id="b3") bus4 = Bus(id="b4") - lp = LineParameters.from_catalogue(name="U_AL_150", nb_phases=1) + lp = LineParameters.from_catalogue(name="U_AL_150") line1 = Line(id="l1", bus1=bus1, bus2=bus2, parameters=lp, length=1) line2 = Line(id="l2", bus1=bus3, bus2=bus4, parameters=lp, length=1) vs = VoltageSource(id="vs1", bus=bus1, voltage=20e3) @@ -832,7 +832,7 @@ def test_load_flow_results_frames(small_network_with_results): assert_frame_equal(en.res_lines, expected_res_lines, rtol=1e-4, atol=1e-5) # Lines with violated max current - en.lines["line"].parameters.ampacities = 0.002 + en.lines["line"].parameters.ampacity = 0.002 expected_res_lines_violated_records = [ d | {"ampacity": 0.002, "violated": True, "loading": 4.3301594951117295} for d in expected_res_lines_records ] diff --git a/roseau/load_flow_single/tests/test_import.py b/roseau/load_flow_single/tests/test_import.py new file mode 100644 index 0000000..0be1b55 --- /dev/null +++ b/roseau/load_flow_single/tests/test_import.py @@ -0,0 +1,32 @@ +import roseau.load_flow as rlf +import roseau.load_flow_single as rlfs + + +def test_import(): + # Ensure that RLF and RLFS have nearly the same interface + rlf_dir = set(dir(rlf)) + rlfs_dir = set(dir(rlfs)) + + assert rlf_dir - rlfs_dir == { + # Multi-phase elements + "Ground", + "PotentialRef", + # Sequences + "NegativeSequence", + "PositiveSequence", + "ZeroSequence", + "ALPHA", + "ALPHA2", + "converters", + # Plotting + "plotting", + # Underscore things + "__getattr__", + "_compat", + "_solvers", + "_wrapper", + # Typing used for a deprecation + "Any", + } + + assert rlfs_dir - rlf_dir == set() diff --git a/roseau/load_flow_single/scripts/generate_test_networks.py b/scripts/generate_test_networks.py similarity index 96% rename from roseau/load_flow_single/scripts/generate_test_networks.py rename to scripts/generate_test_networks.py index 0f4ff84..27c70fe 100644 --- a/roseau/load_flow_single/scripts/generate_test_networks.py +++ b/scripts/generate_test_networks.py @@ -79,7 +79,7 @@ def generate_all_element_network(apl=None) -> None: voltage_source0 = rlfs.VoltageSource(id="voltage_source0", bus=bus0, voltage=20e3) # Line between bus0 and bus1 (with shunt) - lp0 = rlfs.LineParameters.from_catalogue(name="U_AM_148", id="lp0", nb_phases=1) + lp0 = rlfs.LineParameters.from_catalogue(name="U_AM_148", id="lp0") line0 = rlfs.Line(id="line0", bus1=bus0, bus2=bus1, parameters=lp0, length=rlf.Q_(1.5, "km")) # Transformer between bus1 and bus2 @@ -90,16 +90,16 @@ def generate_all_element_network(apl=None) -> None: switch0 = rlfs.Switch(id="switch0", bus1=bus2, bus2=bus3) # Line between bus3 and bus4 (without shunt) - lp1_tmp = rlfs.LineParameters.from_catalogue(name="T_AL_75", id="lp1", nb_phases=1) + lp1_tmp = rlfs.LineParameters.from_catalogue(name="T_AL_75", id="lp1") lp1 = rlfs.LineParameters( id=lp1_tmp.id, z_line=lp1_tmp.z_line, y_shunt=None, # <---- No shunt - ampacities=lp1_tmp.ampacities, + ampacity=lp1_tmp.ampacity, line_type=lp1_tmp.line_type, - materials=lp1_tmp.materials, - insulators=lp1_tmp.insulators, - sections=lp1_tmp.sections, + material=lp1_tmp.material, + insulator=lp1_tmp.insulator, + section=lp1_tmp.section, ) line1 = rlfs.Line(id="line1", bus1=bus3, bus2=bus4, parameters=lp1, length=rlf.Q_(100, "m")) diff --git a/uv.lock b/uv.lock index 979e743..e0ee0ea 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -requires-python = ">=3.10, <4.0.0" +requires-python = ">=3.10" resolution-markers = [ "python_full_version < '3.11'", "python_full_version == '3.11.*'", @@ -90,61 +90,61 @@ wheels = [ [[package]] name = "coverage" -version = "7.6.7" +version = "7.6.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/68/26895f8b068e384b1ec9ab122565b913b735e6b4c618b3d265a280607edc/coverage-7.6.7.tar.gz", hash = "sha256:d79d4826e41441c9a118ff045e4bccb9fdbdcb1d02413e7ea6eb5c87b5439d24", size = 799938 } +sdist = { url = "https://files.pythonhosted.org/packages/ab/75/aecfd0a3adbec6e45753976bc2a9fed62b42cea9a206d10fd29244a77953/coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc", size = 801425 } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/c9/84898713e61208ddbe71b991d8f311d9ca175629ce5f1a46018acc643572/coverage-7.6.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:108bb458827765d538abcbf8288599fee07d2743357bdd9b9dad456c287e121e", size = 206875 }, - { url = "https://files.pythonhosted.org/packages/f0/69/7dfd65f0e284617f72d974f6dfedc7bc16f86172e5bc6ebc8b63430263f3/coverage-7.6.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c973b2fe4dc445cb865ab369df7521df9c27bf40715c837a113edaa2aa9faf45", size = 207307 }, - { url = "https://files.pythonhosted.org/packages/d1/ce/6e356b2bc751bdaadd77c714336b98ec45ccaf0cfe085b6b25d34f7cceb8/coverage-7.6.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c6b24007c4bcd0b19fac25763a7cac5035c735ae017e9a349b927cfc88f31c1", size = 235744 }, - { url = "https://files.pythonhosted.org/packages/35/49/a7ab3d5a507d32344994cab856784e8d603c0b698070f7667c3ae41e8e50/coverage-7.6.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acbb8af78f8f91b3b51f58f288c0994ba63c646bc1a8a22ad072e4e7e0a49f1c", size = 233645 }, - { url = "https://files.pythonhosted.org/packages/bd/41/de07328d2e79916fcc6cd53a5a1d18d163483519ab95f7f60fe15276811c/coverage-7.6.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad32a981bcdedb8d2ace03b05e4fd8dace8901eec64a532b00b15217d3677dd2", size = 234807 }, - { url = "https://files.pythonhosted.org/packages/e4/cc/2a669319b1295e0c52e8cfbbb163b32188b62f3b0bbe7014ef402b24b7cf/coverage-7.6.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:34d23e28ccb26236718a3a78ba72744212aa383141961dd6825f6595005c8b06", size = 233902 }, - { url = "https://files.pythonhosted.org/packages/68/71/a1bb90cb177358a2d364b3968a2069225f614d6824c3d959dee688ca0902/coverage-7.6.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e25bacb53a8c7325e34d45dddd2f2fbae0dbc230d0e2642e264a64e17322a777", size = 232363 }, - { url = "https://files.pythonhosted.org/packages/eb/dc/87551219d3437214523d1c7de0a717bead7a3369ed9bae05a7fd2854476f/coverage-7.6.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af05bbba896c4472a29408455fe31b3797b4d8648ed0a2ccac03e074a77e2314", size = 233493 }, - { url = "https://files.pythonhosted.org/packages/ca/a4/d74ae3a3fb9e55fe5d9b811ce68a6bd8df3ae0a92c336acbc00075bc24fa/coverage-7.6.7-cp310-cp310-win32.whl", hash = "sha256:796c9b107d11d2d69e1849b2dfe41730134b526a49d3acb98ca02f4985eeff7a", size = 209593 }, - { url = "https://files.pythonhosted.org/packages/77/cb/7984c4d0404e8fcc4ada226b240965ef056e7a20e61a18c9038bf88e7624/coverage-7.6.7-cp310-cp310-win_amd64.whl", hash = "sha256:987a8e3da7da4eed10a20491cf790589a8e5e07656b6dc22d3814c4d88faf163", size = 210398 }, - { url = "https://files.pythonhosted.org/packages/c6/d7/1bf7bb0943237149ad01977190ac5c2e17add1f4fe7cabc06401682137f6/coverage-7.6.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7e61b0e77ff4dddebb35a0e8bb5a68bf0f8b872407d8d9f0c726b65dfabe2469", size = 206979 }, - { url = "https://files.pythonhosted.org/packages/83/eb/863b2cd654353b94c6ad834008df813424bf3e8f110e5f655fe5dc4c423b/coverage-7.6.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a5407a75ca4abc20d6252efeb238377a71ce7bda849c26c7a9bece8680a5d99", size = 207431 }, - { url = "https://files.pythonhosted.org/packages/35/c9/d7a02a9654c41174fb99402c0fbd9583d0d2cb8714e7f948117fa7f919c4/coverage-7.6.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df002e59f2d29e889c37abd0b9ee0d0e6e38c24f5f55d71ff0e09e3412a340ec", size = 239368 }, - { url = "https://files.pythonhosted.org/packages/11/64/6c43a0ec43e5ddc5e09b0b589e3fd31def05fc463920d084e5af35fe527d/coverage-7.6.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:673184b3156cba06154825f25af33baa2671ddae6343f23175764e65a8c4c30b", size = 236769 }, - { url = "https://files.pythonhosted.org/packages/1c/dc/e77d98ae433c556c29328712a07fed0e6d159a63b2ec81039ce0a13a24a3/coverage-7.6.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e69ad502f1a2243f739f5bd60565d14a278be58be4c137d90799f2c263e7049a", size = 238634 }, - { url = "https://files.pythonhosted.org/packages/cc/84/50df3a8426d686057496171b4ccdb64528dacc4f42e94dceb7de3c598a69/coverage-7.6.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60dcf7605c50ea72a14490d0756daffef77a5be15ed1b9fea468b1c7bda1bc3b", size = 237562 }, - { url = "https://files.pythonhosted.org/packages/2e/0f/9560196247574c1ccdab64cb923d69119fd5abd5b3db28d601ab2b452861/coverage-7.6.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9c2eb378bebb2c8f65befcb5147877fc1c9fbc640fc0aad3add759b5df79d55d", size = 236197 }, - { url = "https://files.pythonhosted.org/packages/df/14/38b7c081e86e845df1867143ddb6e05bf8395f60ab3923c023a56d97cca1/coverage-7.6.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c0317288f032221d35fa4cbc35d9f4923ff0dfd176c79c9b356e8ef8ef2dff4", size = 236970 }, - { url = "https://files.pythonhosted.org/packages/8b/f3/af34f814ca3814f798878ae368b638edb91298595470614f5265f3f416fa/coverage-7.6.7-cp311-cp311-win32.whl", hash = "sha256:951aade8297358f3618a6e0660dc74f6b52233c42089d28525749fc8267dccd2", size = 209557 }, - { url = "https://files.pythonhosted.org/packages/5a/9e/5d1080d83d752873bd9dedea5524c0f5fe68a3d5e1e58c590865bd724591/coverage-7.6.7-cp311-cp311-win_amd64.whl", hash = "sha256:5e444b8e88339a2a67ce07d41faabb1d60d1004820cee5a2c2b54e2d8e429a0f", size = 210402 }, - { url = "https://files.pythonhosted.org/packages/84/30/30e9df650b9038962c62d900b093a17414d5b43b4d07d47b8698d9e7ce26/coverage-7.6.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f07ff574986bc3edb80e2c36391678a271d555f91fd1d332a1e0f4b5ea4b6ea9", size = 207172 }, - { url = "https://files.pythonhosted.org/packages/88/8b/e28f86412317b9514692fd6f9d8ac6faa12494c3f470c3c63f202e10c756/coverage-7.6.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:49ed5ee4109258973630c1f9d099c7e72c5c36605029f3a91fe9982c6076c82b", size = 207406 }, - { url = "https://files.pythonhosted.org/packages/ac/46/da1bd9a3a893f74f5ce85f35e2755fcb00a80ed21e18d300c54f64938b1c/coverage-7.6.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3e8796434a8106b3ac025fd15417315d7a58ee3e600ad4dbcfddc3f4b14342c", size = 240424 }, - { url = "https://files.pythonhosted.org/packages/f6/12/af8e932496de1997bf4a36785d025ddac6427cbaf6954f26c2edaf21a58a/coverage-7.6.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3b925300484a3294d1c70f6b2b810d6526f2929de954e5b6be2bf8caa1f12c1", size = 237456 }, - { url = "https://files.pythonhosted.org/packages/60/a2/23eb11eb60f825a84397cb94701d6f41d2e8e88ad7d0ba2b4339f38435fb/coverage-7.6.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c42ec2c522e3ddd683dec5cdce8e62817afb648caedad9da725001fa530d354", size = 239527 }, - { url = "https://files.pythonhosted.org/packages/47/9e/63b318bc469308a32b0fbd6c80e2ea05dd7a2b7e840a46b3974843083a8c/coverage-7.6.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0266b62cbea568bd5e93a4da364d05de422110cbed5056d69339bd5af5685433", size = 239011 }, - { url = "https://files.pythonhosted.org/packages/99/47/1e84b067df3f021dfbc9cba09ec9acd4cb64938648a234e5bdf3006fd08b/coverage-7.6.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e5f2a0f161d126ccc7038f1f3029184dbdf8f018230af17ef6fd6a707a5b881f", size = 237316 }, - { url = "https://files.pythonhosted.org/packages/12/9d/96baaafc948d4a0ef2248a611d41051eea0917ef881d744879dd20be7c4a/coverage-7.6.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c132b5a22821f9b143f87446805e13580b67c670a548b96da945a8f6b4f2efbb", size = 238980 }, - { url = "https://files.pythonhosted.org/packages/87/d9/97af1886ca3f612d0cea2999d33e90d2f5b8fdf9bedc2d3bc75883efec4c/coverage-7.6.7-cp312-cp312-win32.whl", hash = "sha256:7c07de0d2a110f02af30883cd7dddbe704887617d5c27cf373362667445a4c76", size = 209801 }, - { url = "https://files.pythonhosted.org/packages/f8/4d/1e31c2018b1b3738154639f94188b1f54098fbf0f80c7ec104928576d0bb/coverage-7.6.7-cp312-cp312-win_amd64.whl", hash = "sha256:fd49c01e5057a451c30c9b892948976f5d38f2cbd04dc556a82743ba8e27ed8c", size = 210587 }, - { url = "https://files.pythonhosted.org/packages/21/87/c590d0c7eeb884995d9d06b429c5e88e9fcd65d3a6a686d9476cb50b72a9/coverage-7.6.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46f21663e358beae6b368429ffadf14ed0a329996248a847a4322fb2e35d64d3", size = 207199 }, - { url = "https://files.pythonhosted.org/packages/40/ee/c88473c4f69c952f4425fabe045cb78d2027634ce50c9d7f7987d389b604/coverage-7.6.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40cca284c7c310d622a1677f105e8507441d1bb7c226f41978ba7c86979609ab", size = 207454 }, - { url = "https://files.pythonhosted.org/packages/b8/07/afda6e10c50e3a8c21020c5c1d1b4f3d7eff1c190305cef2962adf8de018/coverage-7.6.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77256ad2345c29fe59ae861aa11cfc74579c88d4e8dbf121cbe46b8e32aec808", size = 239971 }, - { url = "https://files.pythonhosted.org/packages/85/43/bd1934b75e31f2a49665be6a6b7f8bfaff7266ba19721bdb90239f5e9ed7/coverage-7.6.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87ea64b9fa52bf395272e54020537990a28078478167ade6c61da7ac04dc14bc", size = 237119 }, - { url = "https://files.pythonhosted.org/packages/2b/19/7a70458c1624724086195b40628e91bc5b9ca180cdfefcc778285c49c7b2/coverage-7.6.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d608a7808793e3615e54e9267519351c3ae204a6d85764d8337bd95993581a8", size = 239109 }, - { url = "https://files.pythonhosted.org/packages/f3/2c/3dee671415ff13c05ca68243b2264fc95a5eea57697cffa7986b68b8f608/coverage-7.6.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdd94501d65adc5c24f8a1a0eda110452ba62b3f4aeaba01e021c1ed9cb8f34a", size = 238769 }, - { url = "https://files.pythonhosted.org/packages/37/ad/e0d1228638711aeacacc98d1197af6226b6d062d12c81a6bcc17d3234533/coverage-7.6.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82c809a62e953867cf57e0548c2b8464207f5f3a6ff0e1e961683e79b89f2c55", size = 236854 }, - { url = "https://files.pythonhosted.org/packages/90/95/6467e9d9765a63c7f142703a7f212f6af114bd73a6c1cffeb7ad7f003a86/coverage-7.6.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb684694e99d0b791a43e9fc0fa58efc15ec357ac48d25b619f207c41f2fd384", size = 238701 }, - { url = "https://files.pythonhosted.org/packages/b2/7a/fc11a163f0fd6ce8539d0f1b565873fe6903b900214ff71b5d80d16154c3/coverage-7.6.7-cp313-cp313-win32.whl", hash = "sha256:963e4a08cbb0af6623e61492c0ec4c0ec5c5cf74db5f6564f98248d27ee57d30", size = 209865 }, - { url = "https://files.pythonhosted.org/packages/f2/91/58be3a56efff0c3481e48e2caa56d5d6f3c5c8d385bf4adbecdfd85484b0/coverage-7.6.7-cp313-cp313-win_amd64.whl", hash = "sha256:14045b8bfd5909196a90da145a37f9d335a5d988a83db34e80f41e965fb7cb42", size = 210597 }, - { url = "https://files.pythonhosted.org/packages/34/7e/fed983809c2eccb09c5ddccfdb08efb7f2dd1ae3454dabf1c92c5a2e9946/coverage-7.6.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f2c7a045eef561e9544359a0bf5784b44e55cefc7261a20e730baa9220c83413", size = 207944 }, - { url = "https://files.pythonhosted.org/packages/c7/e0/2c1a157986a3927c3920e8e3938a3fdf33ea22b6f371dc3b679f13f619e2/coverage-7.6.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dd4e4a49d9c72a38d18d641135d2fb0bdf7b726ca60a103836b3d00a1182acd", size = 208215 }, - { url = "https://files.pythonhosted.org/packages/35/2f/77b086b228f6443ae5499467d1629c7428925b390cd171350c403bc00f14/coverage-7.6.7-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c95e0fa3d1547cb6f021ab72f5c23402da2358beec0a8e6d19a368bd7b0fb37", size = 250930 }, - { url = "https://files.pythonhosted.org/packages/60/d8/2ffea937d89ee328fc6e47c2515b890735bdf3f195d507d1c78b5fa96939/coverage-7.6.7-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f63e21ed474edd23f7501f89b53280014436e383a14b9bd77a648366c81dce7b", size = 246647 }, - { url = "https://files.pythonhosted.org/packages/b2/81/efbb3b00a7f7eb5f54a3b3b9f19b26d770a0b7d3870d651f07d2451c5504/coverage-7.6.7-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead9b9605c54d15be228687552916c89c9683c215370c4a44f1f217d2adcc34d", size = 249006 }, - { url = "https://files.pythonhosted.org/packages/eb/91/ce36990cbefaf7909e96c888ed4d83f3471fc1be3273a5beda10896cde0f/coverage-7.6.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0573f5cbf39114270842d01872952d301027d2d6e2d84013f30966313cadb529", size = 248500 }, - { url = "https://files.pythonhosted.org/packages/75/3f/b8c87dfdd96276870fb4abc7e2957cba7d20d8a435fcd816d807869ec833/coverage-7.6.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e2c8e3384c12dfa19fa9a52f23eb091a8fad93b5b81a41b14c17c78e23dd1d8b", size = 246388 }, - { url = "https://files.pythonhosted.org/packages/a0/51/62273e1d5c25bb8fbef5fbbadc75b4a3e08c11b80516d0a97c25e5cced5b/coverage-7.6.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:70a56a2ec1869e6e9fa69ef6b76b1a8a7ef709972b9cc473f9ce9d26b5997ce3", size = 247669 }, - { url = "https://files.pythonhosted.org/packages/75/e5/d7772e56a7eace80e98ac39f2756d4b690fc0ce2384418174e02519a26a8/coverage-7.6.7-cp313-cp313t-win32.whl", hash = "sha256:dbba8210f5067398b2c4d96b4e64d8fb943644d5eb70be0d989067c8ca40c0f8", size = 210510 }, - { url = "https://files.pythonhosted.org/packages/2d/12/f2666e4e36b43221391ffcd971ab0c50e19439c521c2c87cd7e0b49ddba2/coverage-7.6.7-cp313-cp313t-win_amd64.whl", hash = "sha256:dfd14bcae0c94004baba5184d1c935ae0d1231b8409eb6c103a5fd75e8ecdc56", size = 211660 }, - { url = "https://files.pythonhosted.org/packages/e1/ec/dc663f7d34651aca74a531d10800595d9ec28a78b8306705721900b17a23/coverage-7.6.7-pp39.pp310-none-any.whl", hash = "sha256:0ddcb70b3a3a57581b450571b31cb774f23eb9519c2aaa6176d3a84c9fc57671", size = 199113 }, + { url = "https://files.pythonhosted.org/packages/31/86/6ed22e101badc8eedf181f0c2f65500df5929c44c79991cf45b9bf741424/coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50", size = 206988 }, + { url = "https://files.pythonhosted.org/packages/3b/04/16853c58bacc02b3ff5405193dfc6c66632442d931b23dd7b9452dc55cf3/coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf", size = 207418 }, + { url = "https://files.pythonhosted.org/packages/f8/eb/8a91520d04215eb549d6a7d7d3a79cbb1d78b5dd0814f4b23bf97521d580/coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee", size = 235860 }, + { url = "https://files.pythonhosted.org/packages/00/10/bf1ede5b54ae1bbf39921a5dd4cc84aee79041ed301ec8955064785ddb90/coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6", size = 233766 }, + { url = "https://files.pythonhosted.org/packages/5c/ea/741d9233eb502906e0d18ccf4c15c4fb74ff0e85fd8ee967590194b889a1/coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d", size = 234924 }, + { url = "https://files.pythonhosted.org/packages/18/43/b2cfd4413a5b64ab27c289228b0c45b4527d1b99381cc9d6a00bfd515da4/coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331", size = 234019 }, + { url = "https://files.pythonhosted.org/packages/8e/95/8b2fbb9d1a79277963b6095cd51a90fb7088cd3618faf75550038331f78b/coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638", size = 232481 }, + { url = "https://files.pythonhosted.org/packages/4d/d7/9e939508a39ef67605b715ca89c6522214aceb27c2db9152ae3ae1cf8626/coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed", size = 233609 }, + { url = "https://files.pythonhosted.org/packages/ba/e2/1c5fb52eafcffeebaa9db084bff47e7c3cf4f97db752226c232cee4d530b/coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e", size = 209669 }, + { url = "https://files.pythonhosted.org/packages/31/31/6a56469609a252549dd4b090815428d5521edd4642440d987573a450c069/coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a", size = 210509 }, + { url = "https://files.pythonhosted.org/packages/ab/9f/e98211980f6e2f439e251737482aa77906c9b9c507824c71a2ce7eea0402/coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4", size = 207093 }, + { url = "https://files.pythonhosted.org/packages/fd/c7/8bab83fb9c20f7f8163c5a20dcb62d591b906a214a6dc6b07413074afc80/coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94", size = 207536 }, + { url = "https://files.pythonhosted.org/packages/1e/d6/00243df625f1b282bb25c83ce153ae2c06f8e7a796a8d833e7235337b4d9/coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4", size = 239482 }, + { url = "https://files.pythonhosted.org/packages/1e/07/faf04b3eeb55ffc2a6f24b65dffe6e0359ec3b283e6efb5050ea0707446f/coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1", size = 236886 }, + { url = "https://files.pythonhosted.org/packages/43/23/c79e497bf4d8fcacd316bebe1d559c765485b8ec23ac4e23025be6bfce09/coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb", size = 238749 }, + { url = "https://files.pythonhosted.org/packages/b5/e5/791bae13be3c6451e32ef7af1192e711c6a319f3c597e9b218d148fd0633/coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8", size = 237679 }, + { url = "https://files.pythonhosted.org/packages/05/c6/bbfdfb03aada601fb8993ced17468c8c8e0b4aafb3097026e680fabb7ce1/coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a", size = 236317 }, + { url = "https://files.pythonhosted.org/packages/67/f9/f8e5a4b2ce96d1b0e83ae6246369eb8437001dc80ec03bb51c87ff557cd8/coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0", size = 237084 }, + { url = "https://files.pythonhosted.org/packages/f0/70/b05328901e4debe76e033717e1452d00246c458c44e9dbd893e7619c2967/coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801", size = 209638 }, + { url = "https://files.pythonhosted.org/packages/70/55/1efa24f960a2fa9fbc44a9523d3f3c50ceb94dd1e8cd732168ab2dc41b07/coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9", size = 210506 }, + { url = "https://files.pythonhosted.org/packages/76/ce/3edf581c8fe429ed8ced6e6d9ac693c25975ef9093413276dab6ed68a80a/coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee", size = 207285 }, + { url = "https://files.pythonhosted.org/packages/09/9c/cf102ab046c9cf8895c3f7aadcde6f489a4b2ec326757e8c6e6581829b5e/coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a", size = 207522 }, + { url = "https://files.pythonhosted.org/packages/39/06/42aa6dd13dbfca72e1fd8ffccadbc921b6e75db34545ebab4d955d1e7ad3/coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d", size = 240543 }, + { url = "https://files.pythonhosted.org/packages/a0/20/2932971dc215adeca8eeff446266a7fef17a0c238e881ffedebe7bfa0669/coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb", size = 237577 }, + { url = "https://files.pythonhosted.org/packages/ac/85/4323ece0cd5452c9522f4b6e5cc461e6c7149a4b1887c9e7a8b1f4e51146/coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649", size = 239646 }, + { url = "https://files.pythonhosted.org/packages/77/52/b2537487d8f36241e518e84db6f79e26bc3343b14844366e35b090fae0d4/coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787", size = 239128 }, + { url = "https://files.pythonhosted.org/packages/7c/99/7f007762012186547d0ecc3d328da6b6f31a8c99f05dc1e13dcd929918cd/coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c", size = 237434 }, + { url = "https://files.pythonhosted.org/packages/97/53/e9b5cf0682a1cab9352adfac73caae0d77ae1d65abc88975d510f7816389/coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443", size = 239095 }, + { url = "https://files.pythonhosted.org/packages/0c/50/054f0b464fbae0483217186478eefa2e7df3a79917ed7f1d430b6da2cf0d/coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad", size = 209895 }, + { url = "https://files.pythonhosted.org/packages/df/d0/09ba870360a27ecf09e177ca2ff59d4337fc7197b456f22ceff85cffcfa5/coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4", size = 210684 }, + { url = "https://files.pythonhosted.org/packages/9a/84/6f0ccf94a098ac3d6d6f236bd3905eeac049a9e0efcd9a63d4feca37ac4b/coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb", size = 207313 }, + { url = "https://files.pythonhosted.org/packages/db/2b/e3b3a3a12ebec738c545897ac9f314620470fcbc368cdac88cf14974ba20/coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63", size = 207574 }, + { url = "https://files.pythonhosted.org/packages/db/c0/5bf95d42b6a8d21dfce5025ce187f15db57d6460a59b67a95fe8728162f1/coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365", size = 240090 }, + { url = "https://files.pythonhosted.org/packages/57/b8/d6fd17d1a8e2b0e1a4e8b9cb1f0f261afd422570735899759c0584236916/coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002", size = 237237 }, + { url = "https://files.pythonhosted.org/packages/d4/e4/a91e9bb46809c8b63e68fc5db5c4d567d3423b6691d049a4f950e38fbe9d/coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3", size = 239225 }, + { url = "https://files.pythonhosted.org/packages/31/9c/9b99b0591ec4555b7292d271e005f27b465388ce166056c435b288db6a69/coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022", size = 238888 }, + { url = "https://files.pythonhosted.org/packages/a6/85/285c2df9a04bc7c31f21fd9d4a24d19e040ec5e2ff06e572af1f6514c9e7/coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e", size = 236974 }, + { url = "https://files.pythonhosted.org/packages/cb/a1/95ec8522206f76cdca033bf8bb61fff56429fb414835fc4d34651dfd29fc/coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b", size = 238815 }, + { url = "https://files.pythonhosted.org/packages/8d/ac/687e9ba5e6d0979e9dab5c02e01c4f24ac58260ef82d88d3b433b3f84f1e/coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146", size = 209957 }, + { url = "https://files.pythonhosted.org/packages/2f/a3/b61cc8e3fcf075293fb0f3dee405748453c5ba28ac02ceb4a87f52bdb105/coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28", size = 210711 }, + { url = "https://files.pythonhosted.org/packages/ee/4b/891c8b9acf1b62c85e4a71dac142ab9284e8347409b7355de02e3f38306f/coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d", size = 208053 }, + { url = "https://files.pythonhosted.org/packages/18/a9/9e330409b291cc002723d339346452800e78df1ce50774ca439ade1d374f/coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451", size = 208329 }, + { url = "https://files.pythonhosted.org/packages/9c/0d/33635fd429f6589c6e1cdfc7bf581aefe4c1792fbff06383f9d37f59db60/coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764", size = 251052 }, + { url = "https://files.pythonhosted.org/packages/23/32/8a08da0e46f3830bbb9a5b40614241b2e700f27a9c2889f53122486443ed/coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf", size = 246765 }, + { url = "https://files.pythonhosted.org/packages/56/3f/3b86303d2c14350fdb1c6c4dbf9bc76000af2382f42ca1d4d99c6317666e/coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5", size = 249125 }, + { url = "https://files.pythonhosted.org/packages/36/cb/c4f081b9023f9fd8646dbc4ef77be0df090263e8f66f4ea47681e0dc2cff/coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4", size = 248615 }, + { url = "https://files.pythonhosted.org/packages/32/ee/53bdbf67760928c44b57b2c28a8c0a4bf544f85a9ee129a63ba5c78fdee4/coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83", size = 246507 }, + { url = "https://files.pythonhosted.org/packages/57/49/5a57910bd0af6d8e802b4ca65292576d19b54b49f81577fd898505dee075/coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b", size = 247785 }, + { url = "https://files.pythonhosted.org/packages/bd/37/e450c9f6b297c79bb9858407396ed3e084dcc22990dd110ab01d5ceb9770/coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71", size = 210605 }, + { url = "https://files.pythonhosted.org/packages/44/79/7d0c7dd237c6905018e2936cd1055fe1d42e7eba2ebab3c00f4aad2a27d7/coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc", size = 211777 }, + { url = "https://files.pythonhosted.org/packages/32/df/0d2476121cd0bfb9ca2413efe02289c474b82c4b134863bef4b89ec7bcfa/coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce", size = 199230 }, ] [package.optional-dependencies] @@ -152,19 +152,6 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] -[[package]] -name = "coverage-conditional-plugin" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage" }, - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2e/6e/82f411d325a38cc24289060ca5f80d990ee8d026f4de9782006acf061f9b/coverage_conditional_plugin-0.9.0.tar.gz", hash = "sha256:6893dab0542695dbd5ea714281dae0dfec8d0e36480ba32d839e9fa7344f8215", size = 10579 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/83/df10dd1911cb1695274da836e786ade7eaace9ed625b14056eb0bd6117d8/coverage_conditional_plugin-0.9.0-py3-none-any.whl", hash = "sha256:1b37bc469019d2ab5b01f5eee453abe1846b3431e64e209720c2a9ec4afb8130", size = 7317 }, -] - [[package]] name = "cycler" version = "0.12.1" @@ -854,7 +841,7 @@ wheels = [ [[package]] name = "roseau-load-flow" version = "0.10.0" -source = { git = "ssh://git@github.com/RoseauTechnologies/Roseau_Load_Flow.git?branch=develop#ed1c3dbf0c85ae1e1d1f5a2409cf58281b47d541" } +source = { git = "https://github.com/RoseauTechnologies/Roseau_Load_Flow.git?branch=develop#2c1c2f67ece66c40713ae0fe5c9b5689cc524900" } dependencies = [ { name = "certifi" }, { name = "geopandas" }, @@ -921,7 +908,6 @@ plot = [ [package.dev-dependencies] dev = [ { name = "coverage", extra = ["toml"] }, - { name = "coverage-conditional-plugin" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, @@ -939,7 +925,7 @@ requires-dist = [ { name = "platformdirs", specifier = ">=4.0.0" }, { name = "pyproj", specifier = ">=3.3.0" }, { name = "regex", specifier = ">=2022.1.18" }, - { name = "roseau-load-flow", git = "ssh://git@github.com/RoseauTechnologies/Roseau_Load_Flow.git?branch=develop" }, + { name = "roseau-load-flow", git = "https://github.com/RoseauTechnologies/Roseau_Load_Flow.git?branch=develop" }, { name = "roseau-load-flow-engine", specifier = ">=0.16.0a0" }, { name = "shapely", specifier = ">=2.0.0" }, { name = "typing-extensions", specifier = ">=4.6.2" }, @@ -948,7 +934,6 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "coverage", extras = ["toml"], specifier = ">=7.0.5" }, - { name = "coverage-conditional-plugin", specifier = ">=0.9.0" }, { name = "pytest", specifier = ">=8.0.0" }, { name = "pytest-cov", specifier = ">=5.0.0" }, { name = "pytest-xdist", specifier = ">=3.1.0" }, @@ -1000,11 +985,41 @@ wheels = [ [[package]] name = "tomli" -version = "2.1.0" +version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/e4/1b6cbcc82d8832dd0ce34767d5c560df8a3547ad8cbc427f34601415930a/tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8", size = 16622 } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/f7/4da0ffe1892122c9ea096c57f64c2753ae5dd3ce85488802d11b0992cc6d/tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391", size = 13750 }, + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] [[package]]