diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b5cce10..adae829 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -67,7 +67,7 @@ jobs: path: . - name: SonarQube Scan - uses: sonarsource/sonarqube-scan-action@59db25f34e16620e48ab4bb9e4a5dce155cb5432 + uses: sonarsource/sonarqube-scan-action@7006c4492b2e0ee0f816d36501671557c97f5995 env: SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} @@ -116,7 +116,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: License Header Check - uses: B1NARY-GR0UP/nwa@de997f652a66feac9b4dc819bdf80671985b50d0 + uses: B1NARY-GR0UP/nwa@3e1823c61ba2818a156c77b4eedc7905d6a8557c with: version: latest # (optional) version of nwa to use; default: latest cmd: check # (optional) command to execute; options: `check`, `add`, `update`, `remove`; default: check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 94395c0..a7a6ee3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -85,7 +85,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.14 + rev: v0.15.17 hooks: # Run the linter. - id: ruff diff --git a/.vscode/settings.json b/.vscode/settings.json index a3a1838..655bee7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "python.testing.pytestArgs": [ - "tests" + "tests", "-n", "auto" ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true diff --git a/Dockerfile b/Dockerfile index 46e132e..aee64c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/astral-sh/uv:0.11.16@sha256:440fd6477af86a2f1b38080c539f1672cd22acb1b1a47e321dba5158ab08864d AS uv +FROM ghcr.io/astral-sh/uv:0.11.21@sha256:ff07b86af50d4d9391d9daf4ff89ce427bc544f9aae87057e69a1cc0aa369946 AS uv FROM python:3.13.6-bullseye@sha256:f58f33e0563f2ba81c7afe6259cd912f0c33413da93c75cc3a70a941c17afa8c diff --git a/docs/dc_plus/injection_outage.md b/docs/dc_plus/injection_outage.md new file mode 100644 index 0000000..344ebbb --- /dev/null +++ b/docs/dc_plus/injection_outage.md @@ -0,0 +1,32 @@ +# Injection Outage + +`non_voltage_regulating_injection_outage_dx` computes one-step AC state updates for a batch of injection outages around the N-0 operating point. + +The Jacobian is kept fixed. Each outage only contributes an active and reactive power mismatch at the affected bus, and the batched update is evaluated with the base inverse Jacobian. + +$$ +\Delta X = - M J^{-T} +$$ + +where `J` is the base-case Jacobian and `M` is the batch of outage mismatch vectors in Jacobian ordering. + +## Batch API + +The function expects a 2D outage array with shape `(n_contingencies, n_outages)`. + +- Each row is one contingency. +- A row may contain multiple outage indices. +- Negative entries are ignored and can be used as padding. +- The return value has shape `(n_contingencies, n_eq)`. + +## Scope + +This update is intended for non-voltage-regulating injections such as loads and other fixed-power injections. + +## Generator Behavior + +The function can also be applied to voltage-regulating generator outages, but this is only an approximation. A voltage-regulating generator outage should normally trigger a PV to PQ bus-type change. The current implementation does not model that switch, so it does not capture the resulting voltage drop at the regulated bus. + +## HVDC + +- HVDC converter outages should be outaged in pairs. DC+ models the HVDC line as a pair of generator + load injections, one at each terminal. diff --git a/pyproject.toml b/pyproject.toml index efe6e29..cc5345c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,14 +32,15 @@ requires-python = ">=3.11,<3.12" dependencies = [ - "pydantic>=2.13.4,<3.0.0", + "pydantic>=2.8.2,<3.0.0", "pandapower>=3.0.0,<4.0.0", "pypowsybl>=1.14.0", "numpydantic>=1.6.4,<2.0.0", "pypowsybl-jupyter>=1.3.0", "matplotlib>=3.10.8", - "jax>=0.10.0", - "jax-dataclasses (>=1.6.2,<2.0.0)" + "jax (>=0.4.30)", + "jax-dataclasses (>=1.6.2,<2.0.0)", + "equinox>=0.13.8", ] license = "MPL-2.0" @@ -83,9 +84,14 @@ doc = [ "mkdocs-material>=9.6.8", "mkdocs-autorefs>=1.4.2", "mkdocs-material-extensions>=1.3.1", - "mkdocs-include-markdown-plugin>=7.1.6", + "mkdocs-include-markdown-plugin>=7.3.0", "mkdocs-techdocs-core >=1.5.4", - "mkdocstrings[python]>=1.0.4", + "mkdocstrings[python]>=0.30.0", +] +devgpu =[ + "jax[cuda12]==0.6.2", + "tensorflow>=2.20.0,<3.0.0", + "tensorboard>=2.20.0,<3.0.0" ] # TOOLS diff --git a/src/dc_plus/example_grids/pypowsbl/example_grids.py b/src/dc_plus/example_grids/pypowsbl/example_grids.py index 2aae34b..c9a7972 100644 --- a/src/dc_plus/example_grids/pypowsbl/example_grids.py +++ b/src/dc_plus/example_grids/pypowsbl/example_grids.py @@ -983,6 +983,25 @@ def basic_node_breaker_network_powsybl() -> pypowsybl.network.Network: pypowsybl.network.create_metrix_tutorial_six_buses_network, # HVDC ] +POWSYBL_NETWORKS_SHORT_LIST = [ + pypowsybl.network.create_ieee9, + pypowsybl.network.create_ieee14, + pypowsybl.network.create_ieee30, + pypowsybl.network.create_ieee57, + pypowsybl.network.create_micro_grid_nl_network, + basic_node_breaker_network_powsybl, + create_complex_grid_battery_hvdc_svc_3w_trafo, +] + +POWSYBL_NETWORKS_IEEE = [ + pypowsybl.network.create_ieee9, + pypowsybl.network.create_ieee14, + pypowsybl.network.create_ieee30, + pypowsybl.network.create_ieee57, + pypowsybl.network.create_ieee118, + pypowsybl.network.create_ieee300, +] + # TODO: check this network and add to POWSYBL_NETWORKS if possible (currently fails) POWSYBL_NETWORKS_NOT_IMPLEMENTED = [ pypowsybl.network.create_micro_grid_be_network, diff --git a/src/dc_plus/importing/import_helpers.py b/src/dc_plus/importing/import_helpers.py index 102c86f..3da897d 100644 --- a/src/dc_plus/importing/import_helpers.py +++ b/src/dc_plus/importing/import_helpers.py @@ -10,15 +10,49 @@ Helpers independent of the specific source (e.g., Powsybl, Pandapower). """ +from typing import TypeVar + import numpy as np +import pandas as pd +import pandera.pandas as pa +import pandera.typing as pat +from jaxtyping import Complex128 + +from dc_plus.importing.import_schema import ( + BranchParamSchema, + BusParamSchema, + InjectionParamSchema, + LimitParamSchema, + ShuntParamSchema, + TapChangerParamSchema, + TapPositionParamSchema, +) +from dc_plus.interfaces.network_information import ShuntSectionInformation + +DataFrameModelT = TypeVar("DataFrameModelT", bound=pa.DataFrameModel) -from dc_plus.importing.import_schema import BranchParamSchema, BusParamSchema, InjectionParamSchema, ShuntParamSchema + +def _empty_schema_dataframe(schema_model: type[DataFrameModelT]) -> pat.DataFrame[DataFrameModelT]: + """Create an empty validated dataframe for a Pandera schema model. + + Parameters + ---------- + schema_model : type[pa.DataFrameModel] + The Pandera dataframe model that defines the expected columns and dtypes. + + Returns + ------- + pd.DataFrame + An empty dataframe validated against the provided schema model. + """ + return schema_model.validate(pd.DataFrame().reindex(columns=list(schema_model.to_schema().columns.keys()))) def _remove_isolated_buses_injections( - buses: BusParamSchema, injections: InjectionParamSchema -) -> tuple[BusParamSchema, InjectionParamSchema]: - """Remove isolated buses and corresponding injections. + buses: BusParamSchema, + injections: InjectionParamSchema | ShuntParamSchema, +) -> InjectionParamSchema | ShuntParamSchema: + """Remove isolated buses and corresponding bus-connected elements. Keeps only main grid buses. @@ -26,20 +60,165 @@ def _remove_isolated_buses_injections( ---------- buses : BusParamSchema The bus parameters of the network. - injections : InjectionParamSchema - The injection parameters of the network. + injections : InjectionParamSchema | ShuntParamSchema + The bus-connected elements of the network. Returns ------- - tuple[BusParamSchema, InjectionParamSchema] - The bus and injection parameters of the network without isolated buses. + InjectionParamSchema | ShuntParamSchema + The filtered bus-connected elements without isolated buses. """ main_grid = buses[buses["grid_island_id"] == 0] - injections = injections[injections["bus_index"].isin(main_grid["id_int"])] + injections = injections[(injections["bus_index"] >= 0) & injections["bus_index"].isin(main_grid["id_int"])] return injections -def _remove_isolated_branches(buses: BusParamSchema, branches: BranchParamSchema) -> BranchParamSchema: +def _filter_main_grid_network_data( + buses: BusParamSchema, + branches: BranchParamSchema, + injections: InjectionParamSchema, + shunts: ShuntParamSchema, +) -> tuple[ + BusParamSchema, + BranchParamSchema, + InjectionParamSchema, + ShuntParamSchema, +]: + """Keep only the main grid across all imported parameter tables.""" + buses = _remove_isolated_buses(buses) + injections = _remove_isolated_buses_injections(buses, injections) + shunts = _remove_isolated_buses_injections(buses, shunts) + branches = _remove_isolated_branches(buses, branches) + buses, branches, injections, shunts = _reindex_main_grid_network_data(buses, branches, injections, shunts) + return buses, branches, injections, shunts + + +def _remap_bus_reference_column( + dataframe: pd.DataFrame, + column: str, + bus_index_by_old_id: dict[int, int], +) -> pd.DataFrame: + """Remap a bus reference column to compact main-grid-local indices.""" + if column not in dataframe.columns: + return dataframe + + dataframe[column] = dataframe[column].map(bus_index_by_old_id).fillna(-1).astype(int) + return dataframe + + +def _reindex_main_grid_network_data( + buses: BusParamSchema, + branches: BranchParamSchema, + injections: InjectionParamSchema, + shunts: ShuntParamSchema, +) -> tuple[ + BusParamSchema, + BranchParamSchema, + InjectionParamSchema, + ShuntParamSchema, +]: + """Compact main-grid bus numbering and remap all surviving bus references.""" + buses = buses.sort_values("id_int").reset_index(drop=True).copy() + bus_index_by_old_id = {int(old_id): new_idx for new_idx, old_id in enumerate(buses["id_int"].to_numpy(dtype=int))} + buses["id_int"] = np.arange(len(buses), dtype=int) + + branches = branches.copy() + branches = _remap_bus_reference_column(branches, "from_bus_index", bus_index_by_old_id) + branches = _remap_bus_reference_column(branches, "to_bus_index", bus_index_by_old_id) + + injections = injections.copy() + injections = _remap_bus_reference_column(injections, "bus_index", bus_index_by_old_id) + injections = _remap_bus_reference_column(injections, "regulated_bus_id_int", bus_index_by_old_id) + + shunts = shunts.copy() + shunts = _remap_bus_reference_column(shunts, "bus_index", bus_index_by_old_id) + shunts = _remap_bus_reference_column(shunts, "regulated_bus_id_int", bus_index_by_old_id) + + return buses, branches, injections, shunts + + +def _filter_branch_tap_data( + branches: BranchParamSchema, + tap_changers: TapChangerParamSchema, + tap_positions: TapPositionParamSchema, +) -> tuple[TapChangerParamSchema, TapPositionParamSchema]: + """Keep only transformer tap data belonging to the provided branch table.""" + branch_ids = branches["id_str"].astype(str) + tap_changers = tap_changers[tap_changers["id_str"].astype(str).isin(branch_ids)] + tap_positions = tap_positions[tap_positions["id_str"].astype(str).isin(branch_ids)] + return tap_changers, tap_positions + + +def _get_unique_limit_names(limits: LimitParamSchema) -> np.ndarray: + """Get unique limit names while preserving importer order.""" + if limits.empty: + return np.array([], dtype=str) + return limits["name"].astype(str).drop_duplicates().to_numpy(dtype=str) + + +def _get_branch_current_limits( + branches: BranchParamSchema, + limits: LimitParamSchema, +) -> tuple[np.ndarray, np.ndarray]: + """Map imported branch limits to a branch-aligned 2D array.""" + limit_names = _get_unique_limit_names(limits) + branch_current_limits = np.full((len(branches), len(limit_names)), np.nan, dtype=float) + if limits.empty or branches.empty: + return limit_names, branch_current_limits + + branch_index_by_id = {str(branch_id): idx for idx, branch_id in enumerate(branches["id_str"].astype(str).values)} + limit_index_by_name = {limit_name: idx for idx, limit_name in enumerate(limit_names)} + + for limit in limits.to_dict("records"): + branch_idx = branch_index_by_id.get(str(limit["element_id_str"])) + limit_idx = limit_index_by_name.get(str(limit["name"])) + if branch_idx is None or limit_idx is None: + continue + branch_current_limits[branch_idx, limit_idx] = float(limit["value"]) + + return limit_names, branch_current_limits + + +def _get_shunt_section_information(shunts: ShuntParamSchema) -> ShuntSectionInformation: + """Build branch-independent shunt section metadata from imported shunt tables.""" + n_shunts = len(shunts) + if n_shunts == 0: + return ShuntSectionInformation.empty(n_shunts=0) + + max_section_count = shunts["max_section_count"].fillna(0).astype(int).to_numpy() + section_count = shunts["section_count"].fillna(0).astype(int).to_numpy() + n_max_shunt_sections = int(max_section_count.max()) if max_section_count.size else 0 + shunt_conductance_at_section = np.zeros((n_shunts, n_max_shunt_sections), dtype=float) + shunt_susceptance_at_section = np.zeros((n_shunts, n_max_shunt_sections), dtype=float) + + for shunt_idx in range(n_shunts): + n_sections = int(max_section_count[shunt_idx]) + if n_sections <= 0: + continue + + scaling_sections = int(section_count[shunt_idx]) if int(section_count[shunt_idx]) > 0 else n_sections + if scaling_sections <= 0: + continue + + conductance_step = float(shunts.iloc[shunt_idx]["g"]) / scaling_sections + susceptance_step = float(shunts.iloc[shunt_idx]["b"]) / scaling_sections + active_sections = np.arange(1, n_sections + 1, dtype=float) + shunt_conductance_at_section[shunt_idx, :n_sections] = conductance_step * active_sections + shunt_susceptance_at_section[shunt_idx, :n_sections] = susceptance_step * active_sections + + return ShuntSectionInformation( + n_max_shunt_sections=n_max_shunt_sections, + min_shunt_section=np.zeros(n_shunts, dtype=int), + max_shunt_section=max_section_count, + shunt_conductance_at_section=shunt_conductance_at_section, + shunt_susceptance_at_section=shunt_susceptance_at_section, + ) + + +def _remove_isolated_branches( + buses: BusParamSchema, + branches: BranchParamSchema, +) -> BranchParamSchema: """Remove isolated branches. Keeps only branches that are connected to main grid buses. @@ -58,7 +237,10 @@ def _remove_isolated_branches(buses: BusParamSchema, branches: BranchParamSchema """ main_grid = buses[buses["grid_island_id"] == 0] branches = branches[ - (branches["from_bus_index"].isin(main_grid["id_int"])) & (branches["to_bus_index"].isin(main_grid["id_int"])) + (branches["from_bus_index"] >= 0) + & (branches["to_bus_index"] >= 0) + & (branches["from_bus_index"].isin(main_grid["id_int"])) + & (branches["to_bus_index"].isin(main_grid["id_int"])) ] return branches @@ -82,9 +264,42 @@ def _remove_isolated_buses(buses: BusParamSchema) -> BusParamSchema: return main_grid +def _get_branch_admittance_terms( + r: np.ndarray, + x: np.ndarray, + g1: np.ndarray, + b1: np.ndarray, + g2: np.ndarray, + b2: np.ndarray, + rho: np.ndarray, + alpha: np.ndarray, +) -> tuple[ + Complex128[np.ndarray, " n_branches"], + Complex128[np.ndarray, " n_branches"], + Complex128[np.ndarray, " n_branches"], + Complex128[np.ndarray, " n_branches"], +]: + """Build reusable branch admittance primitives from electrical parameters.""" + if np.any(np.isclose(r + x, 0.0, rtol=1e-8)): + y_series = np.zeros_like(r, dtype=complex) + else: + y_series = 1 / (r + 1j * x) + y_charging_from = g1 + 1j * b1 + y_charging_to = g2 + 1j * b2 + rho_alpha = rho * np.exp(1j * alpha) + return y_series, y_charging_from, y_charging_to, rho_alpha + + def _get_admittance_branches( branches: BranchParamSchema, -) -> tuple[np.complex128, np.complex128, np.complex128, np.complex128, np.complex128]: +) -> tuple[ + Complex128[np.ndarray, " n_branches"], + Complex128[np.ndarray, " n_branches"], + Complex128[np.ndarray, " n_branches"], + Complex128[np.ndarray, " n_branches"], + Complex128[np.ndarray, " n_branches"], + Complex128[np.ndarray, " n_branches"], +]: """Get the admittance matrix of the branches. Returns @@ -94,10 +309,19 @@ def _get_admittance_branches( [branch_effective_admittance_from_to, branch_effective_admittance_from_from, branch_effective_admittance_to_to, branch_effective_admittance_to_from, branch_effective_admittance_series] """ - y_series = 1 / (branches["r"] + 1j * branches["x"]) - y_charging_from = branches["g1"] + 1j * branches["b1"] - y_charging_to = branches["g2"] + 1j * branches["b2"] - rho_alpha = branches["rho"] * np.exp(1j * branches["alpha"]) + y_series, y_charging_from, y_charging_to, rho_alpha = _get_branch_admittance_terms( + r=branches["r"].to_numpy(dtype=float), + x=branches["x"].to_numpy(dtype=float), + g1=branches["g1"].to_numpy(dtype=float), + b1=branches["b1"].to_numpy(dtype=float), + g2=branches["g2"].to_numpy(dtype=float), + b2=branches["b2"].to_numpy(dtype=float), + rho=branches["rho"].to_numpy(dtype=float), + alpha=branches["alpha"].to_numpy(dtype=float), + ) + # check that y_series is not 0 for any branch + if np.any(np.isclose(y_series, 0.0)): + raise ValueError("Zero impedance branch detected. Check network Data!") y_charging_symmetric = (y_charging_from + y_charging_to) / 2 y_ff = (y_series + y_charging_from) / (rho_alpha * np.conj(rho_alpha)) @@ -123,7 +347,7 @@ def _get_bus_admittance_shunts( Float[np.ndarray, "n_buses"] The node admittance of the shunts. """ - y_shunt = shunts["g"] + 1j * shunts["b"] + y_shunt = shunts["g"].to_numpy(dtype=float) + 1j * shunts["b"].to_numpy(dtype=float) return y_shunt @@ -146,7 +370,11 @@ def _get_bus_active_power_injections( The nodal active power injections. """ p_injections = np.zeros(n_buses) - np.add.at(p_injections, injections.bus_index.values, injections.p.values) + np.add.at( + p_injections, + injections["bus_index"].to_numpy(dtype=int), + injections["p"].to_numpy(dtype=float), + ) return p_injections @@ -169,5 +397,9 @@ def _get_bus_reactive_power_injections( The nodal reactive power injections. """ q_injections = np.zeros(n_buses) - np.add.at(q_injections, injections.bus_index.values, injections.q.values) + np.add.at( + q_injections, + injections["bus_index"].to_numpy(dtype=int), + injections["q"].to_numpy(dtype=float), + ) return q_injections diff --git a/src/dc_plus/importing/import_schema.py b/src/dc_plus/importing/import_schema.py index 5a4db84..33e249f 100644 --- a/src/dc_plus/importing/import_schema.py +++ b/src/dc_plus/importing/import_schema.py @@ -5,7 +5,7 @@ # you can obtain one at https://mozilla.org/MPL/2.0/. # Mozilla Public License, version 2.0 -"""Import data from powsybl network.""" +"""Schemas for imported network data before preprocessing into solver structures.""" import logging @@ -64,6 +64,7 @@ class InjectionParamSchema(pa.DataFrameModel): i: pat.Series[float] = pa.Field(coerce=True, nullable=True) setpoint_p: pat.Series[float] = pa.Field(coerce=True, nullable=True) setpoint_q: pat.Series[float] = pa.Field(coerce=True, nullable=True) + voltage_setpoint: pat.Series[float] = pa.Field(coerce=True, nullable=True) min_q: pat.Series[float] = pa.Field(coerce=True, nullable=True) max_q: pat.Series[float] = pa.Field(coerce=True, nullable=True) min_p: pat.Series[float] = pa.Field(coerce=True, nullable=True) @@ -112,8 +113,13 @@ class BusParamSchema(pa.DataFrameModel): id_str: pat.Series[str] = pa.Field(coerce=True, description="String ID of the bus, e.g. the UCTE or CGMES id.") name: pat.Series[str] = pa.Field(coerce=True) voltage_magnitude: pat.Series[float] = pa.Field(coerce=True, nullable=True) + regulating_generator_reached_limit: pat.Series[bool] = pa.Field(coerce=True) voltage_angle: pat.Series[float] = pa.Field(coerce=True, nullable=True) bus_type: pat.Series[int] = pa.Field(coerce=True, description="0:Slack, 1:PV, 2:PQ") + is_angle_reference: pat.Series[bool] = pa.Field( + coerce=True, + description="True for the bus used as the angle reference, independent of its voltage-control mode.", + ) grid_island_id: pat.Series[int] = pa.Field( coerce=True, description="ID of the grid island the bus belongs to. 0 indicates the main grid." ) @@ -140,3 +146,73 @@ class Config: """Define Pandera class config.""" strict = True + + +class TapChangerParamSchema(pa.DataFrameModel): + """Tap changer parameter needed for the DC+ network model.""" + + id_str: pat.Series[str] = pa.Field(coerce=True, description="Corresponding element ID, e.g. a branch_id_str.") + min_tap_pos: pat.Series[int] = pa.Field( + coerce=True, description="The minimum tap position of the tap changer, must be found in to TapPositionParamSchema." + ) + max_tap_pos: pat.Series[int] = pa.Field( + coerce=True, description="The maximum tap position of the tap changer, must be found in to TapPositionParamSchema." + ) + current_tap_pos: pat.Series[int] = pa.Field( + coerce=True, description="The current tap position of the tap changer, must be found in to TapPositionParamSchema." + ) + step_count: pat.Series[int] = pa.Field(coerce=True, description="The number of steps of the tap changer.") + side: pat.Series[str] = pa.Field( + coerce=True, description="The side of the tap changer, must be same as in TapPositionParamSchema." + ) + neutral_r: pat.Series[float] = pa.Field(coerce=True, description="The resistance at the neutral tap position.") + neutral_x: pat.Series[float] = pa.Field(coerce=True, description="The reactance at the neutral tap position.") + neutral_g1: pat.Series[float] = pa.Field( + coerce=True, description="The conductance at the from side of the neutral tap position." + ) + neutral_b1: pat.Series[float] = pa.Field( + coerce=True, description="The susceptance at the from side of the neutral tap position." + ) + neutral_g2: pat.Series[float] = pa.Field( + coerce=True, description="The conductance at the to side of the neutral tap position." + ) + neutral_b2: pat.Series[float] = pa.Field( + coerce=True, description="The susceptance at the to side of the neutral tap position." + ) + + class Config: + """Define Pandera class config.""" + + strict = True + + +class TapPositionParamSchema(pa.DataFrameModel): + """Generic Tap position parameter needed for transformer ratio and phase tap-step imports. + + Note: The tap position parameter are the offset values at the tap position, based on the neutral position. + There are different ways to model tap positions e.g. relative to the neutral position. + """ + + id_str: pat.Series[str] = pa.Field(coerce=True, description="Corresponding element ID, e.g. a branch_id_str.") + position: pat.Series[int] = pa.Field(coerce=True, description="The tap position of the tap changer.") + offset_r: pat.Series[float] = pa.Field(coerce=True, description="The offset resistance at the tap position.") + offset_x: pat.Series[float] = pa.Field(coerce=True, description="The offset reactance at the tap position.") + offset_g1: pat.Series[float] = pa.Field( + coerce=True, description="The offset conductance at the from side of the tap position." + ) + offset_g2: pat.Series[float] = pa.Field( + coerce=True, description="The offset conductance at the to side of the tap position." + ) + offset_b1: pat.Series[float] = pa.Field( + coerce=True, description="The offset susceptance at the from side of the tap position." + ) + offset_b2: pat.Series[float] = pa.Field( + coerce=True, description="The offset susceptance at the to side of the tap position." + ) + offset_alpha: pat.Series[float] = pa.Field(coerce=True, description="The offset branch phase angle at the tap position.") + offset_rho: pat.Series[float] = pa.Field(coerce=True, description="The offset branch tap ratio at the tap position.") + + class Config: + """Define Pandera class config.""" + + strict = True diff --git a/src/dc_plus/importing/pandapower/pandapower_import.py b/src/dc_plus/importing/pandapower/pandapower_import.py index 3337a70..919658b 100644 --- a/src/dc_plus/importing/pandapower/pandapower_import.py +++ b/src/dc_plus/importing/pandapower/pandapower_import.py @@ -218,7 +218,10 @@ def _get_transformer_parameters_pandapower(net: pandapowerNet, split_trafo_charg return BranchParamSchema.validate(df) -def _get_branches_parameter_pandapower(net: pandapowerNet, split_trafo_charging: bool = True) -> pd.DataFrame: +def _get_branches_parameter_pandapower( + net: pandapowerNet, + split_trafo_charging: bool = True, +) -> BranchParamSchema: """Extract branch parameters from pandapower network. Parameters @@ -246,7 +249,7 @@ def _get_branches_parameter_pandapower(net: pandapowerNet, split_trafo_charging: return BranchParamSchema.validate(branches) -def _get_buses_pandapower(net: pandapowerNet, slack_id: NotImplementedError) -> pd.DataFrame: +def _get_buses_pandapower(net: pandapowerNet, slack_id: int) -> BusParamSchema: """Extract bus parameters from pandapower network. Parameters @@ -272,11 +275,10 @@ def _get_buses_pandapower(net: pandapowerNet, slack_id: NotImplementedError) -> continue res_bus = net.res_bus.loc[idx] + is_angle_reference = idx == slack_id # Determine bus type - if idx == slack_id: - bus_type = BusType.SLACK - elif idx in net.gen["bus"].values or idx in net.sgen["bus"].values: + if idx in net.gen["bus"].values or idx in net.sgen["bus"].values: # Check if voltage control is enabled is_voltage_controlled = False if idx in net.gen["bus"].values: @@ -285,8 +287,13 @@ def _get_buses_pandapower(net: pandapowerNet, slack_id: NotImplementedError) -> if not is_voltage_controlled and idx in net.sgen["bus"].values: sgen_at_bus = net.sgen[net.sgen["bus"] == idx] is_voltage_controlled = sgen_at_bus.get("controllable", pd.Series([False])).any() + else: + is_voltage_controlled = is_angle_reference - bus_type = BusType.PV if is_voltage_controlled else BusType.PQ + if is_angle_reference and is_voltage_controlled: + bus_type = BusType.SLACK + elif is_voltage_controlled: + bus_type = BusType.PV else: bus_type = BusType.PQ @@ -302,11 +309,13 @@ def _get_buses_pandapower(net: pandapowerNet, slack_id: NotImplementedError) -> "voltage_magnitude": res_bus["vm_pu"] if pd.notna(res_bus["vm_pu"]) else np.nan, "voltage_angle": (np.deg2rad(res_bus["va_degree"]) if pd.notna(res_bus["va_degree"]) else np.nan), "bus_type": int(bus_type), + "is_angle_reference": is_angle_reference, "grid_island_id": grid_island_id, } ) df = pd.DataFrame(buses) + df["regulating_generator_reached_limit"] = False return BusParamSchema.validate(df) @@ -342,7 +351,8 @@ def _process_generators_pandapower(net: pandapowerNet) -> list[dict]: if pd.notna(res_gen.get("va_degree")) else np.nan, # Current not directly available "setpoint_p": gen["p_mw"] / net.sn_mva if pd.notna(gen["p_mw"]) else np.nan, - "setpoint_q": gen.get("vm_pu") if pd.notna(gen.get("vm_pu")) else np.nan, + "setpoint_q": gen.get("q_mvar") / net.sn_mva if pd.notna(gen.get("q_mvar")) else np.nan, + "voltage_setpoint": gen.get("vm_pu") if pd.notna(gen.get("vm_pu")) else np.nan, "min_q": gen["min_q_mvar"] / net.sn_mva if pd.notna(gen.get("min_q_mvar")) else np.nan, "max_q": gen["max_q_mvar"] / net.sn_mva if pd.notna(gen.get("max_q_mvar")) else np.nan, "min_p": gen["min_p_mw"] / net.sn_mva if pd.notna(gen.get("min_p_mw")) else np.nan, @@ -391,6 +401,7 @@ def _process_static_generators_pandapower(net: pandapowerNet) -> list[dict]: "i": np.nan, "setpoint_p": sgen["p_mw"] / net.sn_mva if pd.notna(sgen["p_mw"]) else np.nan, "setpoint_q": sgen["q_mvar"] / net.sn_mva if pd.notna(sgen.get("q_mvar")) else np.nan, + "voltage_setpoint": np.nan, "min_q": sgen.get("min_q_mvar", np.nan) / net.sn_mva if pd.notna(sgen.get("min_q_mvar")) else np.nan, "max_q": sgen.get("max_q_mvar", np.nan) / net.sn_mva if pd.notna(sgen.get("max_q_mvar")) else np.nan, "min_p": sgen.get("min_p_mw", np.nan) / net.sn_mva if pd.notna(sgen.get("min_p_mw")) else np.nan, @@ -438,7 +449,8 @@ def _process_external_grids_pandapower(net: pandapowerNet) -> list[dict]: "q": res_ext_grid["q_mvar"] / net.sn_mva if pd.notna(res_ext_grid["q_mvar"]) else np.nan, "i": np.nan, "setpoint_p": np.nan, # External grid adjusts P to balance system - "setpoint_q": ext_grid.get("vm_pu") if pd.notna(ext_grid.get("vm_pu")) else np.nan, + "setpoint_q": np.nan, + "voltage_setpoint": ext_grid.get("vm_pu") if pd.notna(ext_grid.get("vm_pu")) else np.nan, "min_q": ext_grid.get("min_q_mvar", np.nan) / net.sn_mva if pd.notna(ext_grid.get("min_q_mvar")) else np.nan, "max_q": ext_grid.get("max_q_mvar", np.nan) / net.sn_mva if pd.notna(ext_grid.get("max_q_mvar")) else np.nan, "min_p": ext_grid.get("min_p_mw", np.nan) / net.sn_mva if pd.notna(ext_grid.get("min_p_mw")) else np.nan, @@ -487,6 +499,7 @@ def _process_loads_pandapower(net: pandapowerNet) -> list[dict]: "i": np.nan, "setpoint_p": -load["p_mw"] / net.sn_mva if pd.notna(load["p_mw"]) else np.nan, "setpoint_q": -load["q_mvar"] / net.sn_mva if pd.notna(load["q_mvar"]) else np.nan, + "voltage_setpoint": np.nan, "min_q": np.nan, "max_q": np.nan, "min_p": np.nan, @@ -534,6 +547,7 @@ def _process_storage_pandapower(net: pandapowerNet) -> list[dict]: "i": np.nan, "setpoint_p": storage["p_mw"] / net.sn_mva if pd.notna(storage["p_mw"]) else np.nan, "setpoint_q": np.nan, + "voltage_setpoint": np.nan, "min_q": np.nan, "max_q": np.nan, "min_p": storage.get("min_p_mw", np.nan) / net.sn_mva if pd.notna(storage.get("min_p_mw")) else np.nan, @@ -549,7 +563,7 @@ def _process_storage_pandapower(net: pandapowerNet) -> list[dict]: return storage_injections -def _get_injections_pandapower(net: pandapowerNet) -> pd.DataFrame: +def _get_injections_pandapower(net: pandapowerNet) -> InjectionParamSchema: """Extract injection parameters from pandapower network. Parameters @@ -582,7 +596,7 @@ def _get_injections_pandapower(net: pandapowerNet) -> pd.DataFrame: return InjectionParamSchema.validate(df) -def _get_shunts_pandapower(net: pandapowerNet) -> pd.DataFrame: +def _get_shunts_pandapower(net: pandapowerNet) -> ShuntParamSchema: """Extract shunt parameters from pandapower network. Parameters @@ -648,7 +662,7 @@ def _get_shunts_pandapower(net: pandapowerNet) -> pd.DataFrame: return ShuntParamSchema.validate(df) -def _get_limits_parameter_pandapower(net: pandapowerNet) -> pd.DataFrame: +def _get_limits_parameter_pandapower(net: pandapowerNet) -> LimitParamSchema: """Extract limit parameters from pandapower network. Parameters diff --git a/src/dc_plus/importing/powsybl/powsybl_import.py b/src/dc_plus/importing/powsybl/powsybl_import.py index 1d0b45e..15dd4bf 100644 --- a/src/dc_plus/importing/powsybl/powsybl_import.py +++ b/src/dc_plus/importing/powsybl/powsybl_import.py @@ -8,24 +8,80 @@ """Import data from powsybl network.""" import logging +from enum import StrEnum import numpy as np import pandas as pd from pypowsybl.network import Network +from dc_plus.importing.import_helpers import _empty_schema_dataframe from dc_plus.importing.import_schema import ( BranchParamSchema, BusParamSchema, InjectionParamSchema, LimitParamSchema, ShuntParamSchema, + TapChangerParamSchema, + TapPositionParamSchema, ) +from dc_plus.interfaces.network_information import BusType logger = logging.getLogger(__name__) + +class PowsyblSide(StrEnum): + """Enum for "side" in Powsybl.""" + + NONE = "" + ONE = "ONE" + TWO = "TWO" + THREE = "THREE" + + DANGLING_BUS_STRING_SUFFIX = "_dangling_bus" +def _get_effective_q_limited_bus_ids( + injections: InjectionParamSchema, + q_limit_tolerance: float = 1e-10, +) -> np.ndarray: + """Return locally regulated buses whose imported reactive power is clamped to a Q limit. + + Powsybl can keep ``voltage_regulation`` enabled on an injection even when the + solved reactive power is already pinned to its active limit. In that case the + imported bus should behave like PQ for downstream solver semantics. + """ + injection_bus = injections["bus_index"].to_numpy(dtype=int) + regulated_bus = injections["regulated_bus_id_int"].to_numpy(dtype=int) + connected = injections["connected"].to_numpy(dtype=bool) + voltage_regulation = injections["voltage_regulation"].to_numpy(dtype=bool) + + local_regulation_mask = connected & voltage_regulation & (injection_bus == regulated_bus) & (regulated_bus >= 0) + if not np.any(local_regulation_mask): + return np.array([], dtype=int) + + n_buses = int(max(np.max(injection_bus, initial=-1), np.max(regulated_bus, initial=-1)) + 1) + regulating_bus_q = np.zeros(n_buses, dtype=float) + regulating_q_min = np.zeros(n_buses, dtype=float) + regulating_q_max = np.zeros(n_buses, dtype=float) + regulating_bus_count = np.zeros(n_buses, dtype=np.int32) + + local_bus_indices = injection_bus[local_regulation_mask] + local_reactive_power = injections["q"].to_numpy(dtype=float)[local_regulation_mask] + local_q_min = np.nan_to_num(injections["min_q"].to_numpy(dtype=float)[local_regulation_mask], nan=-np.inf) + local_q_max = np.nan_to_num(injections["max_q"].to_numpy(dtype=float)[local_regulation_mask], nan=np.inf) + + np.add.at(regulating_bus_count, local_bus_indices, 1) + np.add.at(regulating_bus_q, local_bus_indices, local_reactive_power) + np.add.at(regulating_q_min, local_bus_indices, local_q_min) + np.add.at(regulating_q_max, local_bus_indices, local_q_max) + + has_local_regulation = regulating_bus_count > 0 + effective_lower_limit_mask = has_local_regulation & (regulating_bus_q <= (regulating_q_min + q_limit_tolerance)) + effective_upper_limit_mask = has_local_regulation & (regulating_bus_q >= (regulating_q_max - q_limit_tolerance)) + return np.flatnonzero(effective_lower_limit_mask | effective_upper_limit_mask).astype(int) + + def _get_unpaired_dangling_lines(net: Network) -> pd.DataFrame: """Get the unpaired dangling lines of the network. @@ -49,7 +105,7 @@ def _get_unpaired_dangling_lines(net: Network) -> pd.DataFrame: return dangling_lines -def _get_branches_id_int(net: Network) -> pd.Series: +def _get_branches_id_int(net: Network) -> pd.DataFrame: """Get the branches id_int of the network. Gets the branches id_int from the network. @@ -62,7 +118,7 @@ def _get_branches_id_int(net: Network) -> pd.Series: Returns ------- - pd.Series + pd.DataFrame The branches id_int of the network. """ branches = net.get_branches(attributes=[]) @@ -95,7 +151,7 @@ def _get_dangling_bus_ids(net: Network) -> pd.DataFrame: return dangling_lines -def _get_bus_ids_with_dangling_buses(net: Network) -> pd.Series: +def _get_bus_ids_with_dangling_buses(net: Network) -> pd.DataFrame: """Get the bus ids including dangling buses of the network. Gets the bus ids including dangling buses from the network. @@ -108,7 +164,7 @@ def _get_bus_ids_with_dangling_buses(net: Network) -> pd.Series: Returns ------- - pd.Series + pd.DataFrame The bus ids including dangling buses of the network. """ bus = net.get_buses(attributes=[]) @@ -146,8 +202,9 @@ def _get_branches_with_bus_index(net: Network, branches: pd.DataFrame) -> pd.Dat The branches with bus index of the network. """ bus = _get_bus_ids_with_dangling_buses(net) - branches["from_bus_index"] = branches["bus1_id"].map(dict(zip(bus["id_str"], bus.index, strict=False))) - branches["to_bus_index"] = branches["bus2_id"].map(dict(zip(bus["id_str"], bus.index, strict=False))) + bus_index_by_id = dict(zip(bus["id_str"], bus.index, strict=False)) + branches["from_bus_index"] = branches["bus1_id"].map(bus_index_by_id).fillna(-1).astype(int) + branches["to_bus_index"] = branches["bus2_id"].map(bus_index_by_id).fillna(-1).astype(int) return branches @@ -178,7 +235,8 @@ def _get_injection_with_bus_index( The injections with bus index of the network. """ bus = _get_bus_ids_with_dangling_buses(net) - injections[target_column] = injections[source_column].map(dict(zip(bus["id_str"], bus.index, strict=False))) + bus_index_by_id = dict(zip(bus["id_str"], bus.index, strict=False)) + injections[target_column] = injections[source_column].map(bus_index_by_id).fillna(-1).astype(int) return injections @@ -199,10 +257,8 @@ def _get_tie_line_parameter(net: Network) -> BranchParamSchema: """ tie_lines = net.get_tie_lines() if tie_lines.empty: - tie_lines = pd.DataFrame(columns=list(BranchParamSchema.__annotations__.keys())) - tie_lines = BranchParamSchema.validate(tie_lines) - return tie_lines - available_attributes = ["name", "connected", "r", "x", "g", "b", "p", "q", "i", "bus_id"] + return _empty_schema_dataframe(BranchParamSchema) + available_attributes = ["name", "r", "x", "g", "b", "p", "q", "i", "bus_id"] dangling_lines = net.get_dangling_lines(attributes=available_attributes) tie_lines = tie_lines.merge(dangling_lines, how="left", left_on=["dangling_line1_id"], right_index=True) tie_lines = tie_lines.merge( @@ -283,9 +339,7 @@ def _get_dangling_line_branch_parameter(net: Network) -> BranchParamSchema: dangling_lines = dangling_lines[~dangling_lines["paired"]] dangling_lines.drop(columns=["paired"], inplace=True) if dangling_lines.empty: - dangling_lines = pd.DataFrame(columns=list(BranchParamSchema.__annotations__.keys())) - dangling_lines = BranchParamSchema.validate(dangling_lines) - return dangling_lines + return _empty_schema_dataframe(BranchParamSchema) dangling_lines.rename(columns={"bus_id": "bus1_id"}, inplace=True) dangling_lines["bus2_id"] = _get_dangling_bus_ids(net)["dangling_bus_id"].values dangling_lines = _get_branches_with_bus_index(net, dangling_lines) @@ -355,9 +409,7 @@ def _get_line_parameter(net: Network) -> BranchParamSchema: ] lines = net.get_lines(attributes=available_attributes) if lines.empty: - lines = pd.DataFrame(columns=list(BranchParamSchema.__annotations__.keys())) - lines = BranchParamSchema.validate(lines) - return lines + return _empty_schema_dataframe(BranchParamSchema) lines["connected"] = lines["connected1"] & lines["connected2"] lines.drop(columns=["connected1", "connected2"], inplace=True) @@ -377,7 +429,10 @@ def _get_line_parameter(net: Network) -> BranchParamSchema: return lines -def _get_trafo_parameter(net: Network, split_trafo_charging: bool = True) -> BranchParamSchema: +def _get_trafo_parameter( + net: Network, + split_trafo_charging: bool = True, +) -> BranchParamSchema: """Get the transformer parameters of the network. Gets the transformer parameters from the network. @@ -416,9 +471,7 @@ def _get_trafo_parameter(net: Network, split_trafo_charging: bool = True) -> Bra ] transformers = net.get_2_windings_transformers(attributes=available_attributes) if transformers.empty: - transformers = pd.DataFrame(columns=list(BranchParamSchema.__annotations__.keys())) - transformers = BranchParamSchema.validate(transformers) - return transformers + return _empty_schema_dataframe(BranchParamSchema) transformers.rename( columns={ @@ -465,7 +518,10 @@ def _get_trafo_parameter(net: Network, split_trafo_charging: bool = True) -> Bra return transformers -def _get_branches_parameter_powsybl(net: Network, split_trafo_charging: bool = True) -> BranchParamSchema: +def _get_branches_parameter_powsybl( + net: Network, + split_trafo_charging: bool = True, +) -> BranchParamSchema: """Get the branches parameters of the network. Gets the branches parameters from the network. @@ -526,10 +582,117 @@ def _get_limits_parameter_powsybl(net: Network) -> LimitParamSchema: return operational_limits +def _get_tap_steps_parameter_powsybl(tap_steps: pd.DataFrame) -> TapPositionParamSchema: + """Get the tap steps parameters of the network. + + Parameters + ---------- + tap_steps : pd.DataFrame + The tap steps of the network. + expected columns: ["side", "rho", "r", "x", "g", "b"] + expected multi index: "id", "position" + either: network.get_ratio_tap_changer_steps() or network.get_phase_tap_changer_steps() + + Returns + ------- + TapPositionParamSchema + The tap steps parameters of the network. + """ + if tap_steps.empty: + return _empty_schema_dataframe(TapPositionParamSchema) + tap_steps.reset_index(drop=False, inplace=True) + tap_steps.rename(columns={"id": "id_str", "rho": "offset_rho", "r": "offset_r", "x": "offset_x"}, inplace=True) + # set none to powsybl default -> side two + tap_steps.loc[tap_steps["side"] == PowsyblSide.NONE, "side"] = PowsyblSide.TWO.value + assert set(tap_steps["side"].unique()) <= {PowsyblSide.ONE.value, PowsyblSide.TWO.value} + + # init g1, b1, g2, b2 to 0.0 + tap_steps["offset_g1"] = 0.0 + tap_steps["offset_b1"] = 0.0 + tap_steps["offset_g2"] = 0.0 + tap_steps["offset_b2"] = 0.0 + + # init alpha if missing + if "alpha" not in tap_steps.columns: + tap_steps["offset_alpha"] = 0.0 + else: + tap_steps.rename(columns={"alpha": "offset_alpha"}, inplace=True) + + # set g1, b1, g2, b2 to according side + cond_side_one = tap_steps["side"] == PowsyblSide.ONE.value + tap_steps.loc[cond_side_one, "offset_g1"] = tap_steps.loc[cond_side_one, "g"] + tap_steps.loc[cond_side_one, "offset_b1"] = tap_steps.loc[cond_side_one, "b"] + tap_steps.loc[~cond_side_one, "offset_g2"] = tap_steps.loc[~cond_side_one, "g"] + tap_steps.loc[~cond_side_one, "offset_b2"] = tap_steps.loc[~cond_side_one, "b"] + + # drop g an b columns + tap_steps = tap_steps.drop(columns=["g", "b", "side"]) + tap_steps = TapPositionParamSchema.validate(tap_steps) + return tap_steps + + +def _get_tap_changer_parameter_powsybl( + tap_changers: pd.DataFrame, + transformers: pd.DataFrame, + split_trafo_charging: bool = True, +) -> TapChangerParamSchema: + """Get the tap changer parameters of the network. + + Parameters + ---------- + tap_changers : pd.DataFrame + The tap changers of the network. + expected columns: ["id", "tap", "low_tap", "high_tap", "step_count", "side"] + expected index: "id" (transformer id) + transformers : pd.DataFrame + The transformers of the network. + expected columns: ["r", "x", "g", "b"] + expected index: "id" (transformer id) + split_trafo_charging : bool + Whether to split the transformer charging admittance into the series and shunt admittance. + Powsybl default is False, DCplus uses True. + + Returns + ------- + TapChangerParamSchema + The tap changer parameters of the network. + """ + if tap_changers.empty: + return _empty_schema_dataframe(TapChangerParamSchema) + transformers = transformers[["r", "x", "g", "b"]] + transformers.rename(columns={"r": "neutral_r", "x": "neutral_x", "g": "neutral_g2", "b": "neutral_b2"}, inplace=True) + if split_trafo_charging: + # split the transformer charging admittance into the series and shunt admittance + # DCplus uses the split Pi model + # g2, b2 -> shunt admittance on the to side + # g1, b1 -> shunt admittance on the from side + # series admittance is 0 + transformers["neutral_g1"] = transformers["neutral_g2"] / 2 + transformers["neutral_b1"] = transformers["neutral_b2"] / 2 + transformers["neutral_g2"] = transformers["neutral_g2"] / 2 + transformers["neutral_b2"] = transformers["neutral_b2"] / 2 + else: + # vanilla powsybl implementation + # set the from side shunt admittance to 0 + transformers["neutral_g1"] = 0.0 + transformers["neutral_b1"] = 0.0 + + tap_changers = tap_changers[["tap", "low_tap", "high_tap", "step_count", "side"]] + tap_changers.rename( + columns={"tap": "current_tap_pos", "low_tap": "min_tap_pos", "high_tap": "max_tap_pos"}, inplace=True + ) + tap_changers = tap_changers.merge(transformers, how="left", left_on=["id"], right_index=True) + + tap_changers["id_str"] = tap_changers.index + + tap_changers = TapChangerParamSchema.validate(tap_changers) + return tap_changers + + # ################ Injections ######################## -def _get_injection_id_int(net: Network) -> pd.Series: +def _get_injection_id_int(net: Network) -> pd.DataFrame: """Get the injection id_int of the network. Gets the injection id_int from the network. @@ -541,7 +704,7 @@ def _get_injection_id_int(net: Network) -> pd.Series: Returns ------- - pd.Series + pd.DataFrame The injection id_int of the network. """ injections = net.get_injections(attributes=[]) @@ -553,6 +716,18 @@ def _get_injection_id_int(net: Network) -> pd.Series: def _get_dangling_line_generators(net: Network) -> pd.DataFrame: + """Convert unpaired dangling lines into equivalent injection rows. + + Parameters + ---------- + net : Network + The powsybl network. + + Returns + ------- + pd.DataFrame + Injection-style rows representing dangling line boundary injections. + """ available_attributes = ["p0", "q0", "i", "connected", "paired"] dangling_lines = net.get_dangling_lines(attributes=available_attributes) # get all non-paired dangling lines -> model as injections @@ -570,6 +745,7 @@ def _get_dangling_line_generators(net: Network) -> pd.DataFrame: dangling_gen["p"] = dangling_gen["setpoint_p"] dangling_gen["q"] = dangling_gen["setpoint_q"] + dangling_gen["voltage_setpoint"] = np.nan dangling_gen["injection_type"] = "GENERATOR" dangling_gen.loc[dangling_gen["p"] > 0, "injection_type"] = "LOAD" dangling_gen["min_p"] = np.nan @@ -584,15 +760,27 @@ def _get_dangling_line_generators(net: Network) -> pd.DataFrame: def _get_generators(net: Network) -> pd.DataFrame: - """Get all generators that are connected to a node in _get_nodes()""" + """Extract generator injections from a powsybl network. + + Parameters + ---------- + net : Network + The powsybl network. + + Returns + ------- + pd.DataFrame + Generator injections in the standardized import schema. + """ available_attributes = [ "target_p", "target_q", + "target_v", "p", "q", "i", - "max_q", - "min_q", + "max_q_at_p", + "min_q_at_p", "connected", "max_p", "min_p", @@ -601,11 +789,13 @@ def _get_generators(net: Network) -> pd.DataFrame: "regulated_bus_id", ] gens = net.get_generators(attributes=available_attributes) + gens.rename(columns={"max_q_at_p": "max_q", "min_q_at_p": "min_q"}, inplace=True) gens.reset_index(drop=False, inplace=True) gens.rename(columns={"id": "id_str"}, inplace=True) injection_id_int = _get_injection_id_int(net) gens = gens.merge(injection_id_int[["id_int"]], how="left", left_on=["id_str"], right_index=True) gens.rename(columns={"target_p": "setpoint_p", "target_q": "setpoint_q"}, inplace=True) + gens.rename(columns={"target_v": "voltage_setpoint"}, inplace=True) gens = _get_injection_with_bus_index(net, gens) gens.drop(columns=["bus_id"], inplace=True) gens.rename( @@ -615,6 +805,14 @@ def _get_generators(net: Network) -> pd.DataFrame: net, gens, source_column="regulated_bus_id_str", target_column="regulated_bus_id_int" ) + # Powsybl exposes generator reactive-power limits in generator convention, + # while the solved q values imported above are in the load convention used by + # the solver. Convert the limits so they can be compared to imported q values. + generator_min_q = gens["min_q"].to_numpy(dtype=float, copy=True) + generator_max_q = gens["max_q"].to_numpy(dtype=float, copy=True) + gens["min_q"] = -generator_max_q + gens["max_q"] = -generator_min_q + gens["injection_type"] = "GENERATOR" gens = InjectionParamSchema.validate(gens) @@ -622,7 +820,18 @@ def _get_generators(net: Network) -> pd.DataFrame: def _get_battery(net: Network) -> pd.DataFrame: - """Get all batteries that are connected to a node in _get_nodes()""" + """Extract battery injections from a powsybl network. + + Parameters + ---------- + net : Network + The powsybl network. + + Returns + ------- + pd.DataFrame + Battery injections in the standardized import schema. + """ available_attributes = [ "target_p", "target_q", @@ -644,6 +853,7 @@ def _get_battery(net: Network) -> pd.DataFrame: batteries.rename(columns={"target_p": "setpoint_p", "target_q": "setpoint_q"}, inplace=True) batteries = _get_injection_with_bus_index(net, batteries) batteries.drop(columns=["bus_id"], inplace=True) + batteries["voltage_setpoint"] = np.nan batteries["injection_type"] = "GENERATOR" batteries.loc[batteries["p"] > 0, "injection_type"] = "LOAD" @@ -656,7 +866,18 @@ def _get_battery(net: Network) -> pd.DataFrame: def _get_hvdc_lcc(net: Network) -> pd.DataFrame: - """Get all lcc converter stations that are connected to a node in _get_nodes()""" + """Extract LCC HVDC converter station injections. + + Parameters + ---------- + net : Network + The powsybl network. + + Returns + ------- + pd.DataFrame + LCC converter stations represented as standardized injections. + """ available_attributes = [ "p", "q", @@ -676,6 +897,7 @@ def _get_hvdc_lcc(net: Network) -> pd.DataFrame: lcc.loc[lcc["p"] > 0, "injection_type"] = "LOAD" lcc["setpoint_p"] = np.nan lcc["setpoint_q"] = np.nan + lcc["voltage_setpoint"] = np.nan lcc["min_q"] = np.nan lcc["max_q"] = np.nan lcc["min_p"] = np.nan @@ -689,7 +911,18 @@ def _get_hvdc_lcc(net: Network) -> pd.DataFrame: def _get_hvdc_vsc(net: Network) -> pd.DataFrame: - """Get all vsc converter stations that are connected to a node in _get_nodes()""" + """Extract VSC HVDC converter station injections. + + Parameters + ---------- + net : Network + The powsybl network. + + Returns + ------- + pd.DataFrame + VSC converter stations represented as standardized injections. + """ # TODO: not sure if correct implemented available_attributes = [ "target_q", @@ -709,6 +942,7 @@ def _get_hvdc_vsc(net: Network) -> pd.DataFrame: injection_id_int = _get_injection_id_int(net) vsc = vsc.merge(injection_id_int[["id_int"]], how="left", left_on=["id_str"], right_index=True) vsc.rename(columns={"target_q": "setpoint_q"}, inplace=True) + vsc["voltage_setpoint"] = np.nan vsc = _get_injection_with_bus_index(net, vsc) # if regulating element points to the vsc -> choose bus id of the vsc vsc.loc[vsc["regulated_element_id"] == vsc["id_str"], "regulated_element_id"] = vsc.loc[ @@ -731,7 +965,18 @@ def _get_hvdc_vsc(net: Network) -> pd.DataFrame: def _get_loads(net: Network) -> pd.DataFrame: - """Get all loads that are connected to a node in _get_nodes()""" + """Extract load injections from a powsybl network. + + Parameters + ---------- + net : Network + The powsybl network. + + Returns + ------- + pd.DataFrame + Load injections in the standardized import schema. + """ available_attributes = [ "p0", "q0", @@ -749,6 +994,7 @@ def _get_loads(net: Network) -> pd.DataFrame: loads.rename(columns={"p0": "setpoint_p", "q0": "setpoint_q"}, inplace=True) loads = _get_injection_with_bus_index(net, loads) loads.drop(columns=["bus_id"], inplace=True) + loads["voltage_setpoint"] = np.nan loads["injection_type"] = "LOAD" loads["min_q"] = np.nan @@ -764,7 +1010,18 @@ def _get_loads(net: Network) -> pd.DataFrame: def _get_static_var_compensators(net: Network) -> pd.DataFrame: - """Get all static var compensators that are connected to a node in _get_nodes()""" + """Extract static var compensators as standardized injections. + + Parameters + ---------- + net : Network + The powsybl network. + + Returns + ------- + pd.DataFrame + Static var compensators represented as standardized injections. + """ available_attributes = [ "target_q", "p", @@ -782,6 +1039,7 @@ def _get_static_var_compensators(net: Network) -> pd.DataFrame: injection_id_int = _get_injection_id_int(net) svc = svc.merge(injection_id_int[["id_int"]], how="left", left_on=["id_str"], right_index=True) svc.rename(columns={"target_q": "setpoint_q"}, inplace=True) + svc["voltage_setpoint"] = np.nan svc = _get_injection_with_bus_index(net, svc) # if regulating element points to the svc -> choose bus id of the svc svc.loc[svc["regulated_element_id"] == svc["id_str"], "regulated_element_id"] = svc.loc[ @@ -798,15 +1056,27 @@ def _get_static_var_compensators(net: Network) -> pd.DataFrame: svc["setpoint_p"] = np.nan svc["min_p"] = np.nan svc["max_p"] = np.nan - svc["min_q"] = np.nan - svc["max_q"] = np.nan + # TODO: where do we get the limits from? + svc["min_q"] = -9999.0 # set a default value for q limit logic + svc["max_q"] = 9999.0 # set a default value for q limit logic svc = InjectionParamSchema.validate(svc) return svc -def _get_injections_powsybl(net: Network) -> pd.DataFrame: - """Merge information from generators, loads and dangling lines into the injections dataframe.""" +def _get_injections_powsybl(net: Network) -> InjectionParamSchema: + """Merge all supported powsybl injections into one standardized table. + + Parameters + ---------- + net : Network + The powsybl network. + + Returns + ------- + pd.DataFrame + Combined injection table covering generators, loads, converters and compensators. + """ injections = pd.concat( [ _get_generators(net), @@ -857,9 +1127,7 @@ def _get_shunts_powsybl(net: Network) -> ShuntParamSchema: ] shunts = net.get_shunt_compensators(attributes=available_attributes) if shunts.empty: - shunts = pd.DataFrame(columns=list(ShuntParamSchema.__annotations__.keys())) - shunts = ShuntParamSchema.validate(shunts) - return shunts + return _empty_schema_dataframe(ShuntParamSchema) shunts.reset_index(drop=False, inplace=True) shunts.rename(columns={"id": "id_str"}, inplace=True) injection_id_int = _get_injection_id_int(net) @@ -896,28 +1164,45 @@ def _get_dangling_buses(net: Network) -> BusParamSchema: The dangling buses of the network. """ dangling_lines = _get_unpaired_dangling_lines(net) - available_attributes = ["boundary_v_mag", "boundary_v_angle"] + available_attributes = ["boundary_v_mag", "boundary_v_angle", "bus_id"] dangling_lines = net.get_dangling_lines(attributes=available_attributes).loc[dangling_lines.index] dangling_lines.rename(columns={"boundary_v_mag": "voltage_magnitude", "boundary_v_angle": "voltage_angle"}, inplace=True) + buses = net.get_buses(all_attributes=True).sort_index().reset_index(drop=False) + bus_id_column = "id" if "id" in buses.columns else "index" + buses.rename(columns={bus_id_column: "id_str"}, inplace=True) + buses["grid_island_id"] = _get_grid_island_ids(buses) + grid_island_by_bus_id = buses.set_index("id_str")["grid_island_id"].to_dict() + dangling_buses = _get_dangling_bus_ids(net) dangling_buses.rename(columns={"dangling_bus_id": "id_str"}, inplace=True) dangling_buses["name"] = dangling_buses["id_str"] dangling_buses = dangling_buses.merge(dangling_lines, how="left", left_index=True, right_index=True) - # TODO: get values depending on the connected state and the bus_id of the dangling line - dangling_buses["grid_island_id"] = 0 + dangling_buses["grid_island_id"] = dangling_buses["bus_id"].map(grid_island_by_bus_id).fillna(-1).astype(int) dangling_buses["bus_type"] = 2 # PQ bus + dangling_buses["is_angle_reference"] = False bus_order_int_ids = _get_bus_ids_with_dangling_buses(net) dangling_buses = dangling_buses.merge(bus_order_int_ids, how="left", on=["id_str"]) + dangling_buses["regulating_generator_reached_limit"] = False dangling_buses = dangling_buses[ - ["id_str", "id_int", "name", "voltage_magnitude", "voltage_angle", "bus_type", "grid_island_id"] + [ + "id_str", + "id_int", + "name", + "voltage_magnitude", + "voltage_angle", + "bus_type", + "is_angle_reference", + "grid_island_id", + "regulating_generator_reached_limit", + ] ] dangling_buses = BusParamSchema.validate(dangling_buses) return dangling_buses -def _get_buses_powsybl(net: Network, slack_id: str, injections: InjectionParamSchema) -> BusParamSchema: +def _get_buses_powsybl(net: Network, slack_id: str, injections: InjectionParamSchema, reference_id: str) -> BusParamSchema: """Get the bus parameters of the network. Gets the bus parameters from the network. @@ -930,6 +1215,8 @@ def _get_buses_powsybl(net: Network, slack_id: str, injections: InjectionParamSc The id of the slack bus. injections : InjectionParamSchema The injections of the network. + reference_id : str + The id of the reference bus for angle reference. Returns ------- @@ -942,6 +1229,8 @@ def _get_buses_powsybl(net: Network, slack_id: str, injections: InjectionParamSc buses.reset_index(drop=False, inplace=True) buses.rename(columns={"id": "id_str"}, inplace=True) buses["bus_type"] = 2 # PQ bus is default + effective_reference_id = reference_id if buses["id_str"].eq(reference_id).any() else slack_id + buses["is_angle_reference"] = buses["id_str"].eq(effective_reference_id) buses.rename(columns={"v_mag": "voltage_magnitude", "v_angle": "voltage_angle"}, inplace=True) @@ -966,15 +1255,32 @@ def _get_buses_powsybl(net: Network, slack_id: str, injections: InjectionParamSc buses = buses.merge(bus_order_int_ids, how="left", on=["id_str"]) # now the main grid "0" has only connected_component == 0 and synchronous_component == 0 - buses = buses[["id_str", "id_int", "name", "voltage_magnitude", "voltage_angle", "bus_type", "grid_island_id"]] + buses = buses[ + [ + "id_str", + "id_int", + "name", + "voltage_magnitude", + "voltage_angle", + "bus_type", + "is_angle_reference", + "grid_island_id", + ] + ] dangling_buses = _get_dangling_buses(net) buses = pd.concat([buses, dangling_buses], ignore_index=True) buses.sort_values(by=["id_int"], inplace=True) - # set bus types - pv_buses = injections[(injections["voltage_regulation"])]["regulated_bus_id_int"].unique() - buses.loc[buses["id_int"].isin(pv_buses), "bus_type"] = 1 # PV bus - buses.loc[buses["id_str"] == slack_id, "bus_type"] = 0 # slack bus + # Bus type tracks voltage control; the reference-angle role is stored separately. + voltage_control_buses = injections[(injections["connected"]) & (injections["voltage_regulation"])][ + "regulated_bus_id_int" + ].unique() + buses.loc[buses["id_int"].isin(voltage_control_buses), "bus_type"] = BusType.PV + effective_q_limited_buses = _get_effective_q_limited_bus_ids(injections) + buses["regulating_generator_reached_limit"] = False + buses.loc[buses["id_int"].isin(effective_q_limited_buses), "regulating_generator_reached_limit"] = True + slack_mask = buses["id_str"].eq(slack_id) & buses["id_int"].isin(voltage_control_buses) + buses.loc[slack_mask, "bus_type"] = BusType.SLACK buses = BusParamSchema.validate(buses) return buses @@ -994,7 +1300,7 @@ def _get_grid_island_ids(buses: pd.DataFrame) -> pd.Series: pd.Series The grid island ids of the buses. """ - grid_island_id = buses["connected_component"] + grid_island_id = buses["connected_component"].copy() # change grid_island_id where synchronous_component != 0 to the node index of that bus max_grid_island_id = grid_island_id.max() + 1 for island_id in range(grid_island_id.max() + 1): diff --git a/src/dc_plus/importing/powsybl/powsybl_import_helpers.py b/src/dc_plus/importing/powsybl/powsybl_import_helpers.py index 8e166bf..afd4100 100644 --- a/src/dc_plus/importing/powsybl/powsybl_import_helpers.py +++ b/src/dc_plus/importing/powsybl/powsybl_import_helpers.py @@ -9,7 +9,7 @@ import pypowsybl import pypowsybl.loadflow -from pypowsybl.loadflow import ConnectedComponentMode, VoltageInitMode +from pypowsybl.loadflow import ComponentMode, VoltageInitMode from pypowsybl.loadflow import Parameters as LoadFlowParameters from pypowsybl.network import Network @@ -56,7 +56,7 @@ def select_a_generator_as_slack_and_run_loadflow(network: Network) -> None: read_slack_bus=True, distributed_slack=True, use_reactive_limits=True, - connected_component_mode=ConnectedComponentMode.MAIN, # ConnectedComponentMode + component_mode=ComponentMode.MAIN_SYNCHRONOUS, ) loadflow_res = pypowsybl.loadflow.run_ac(network, parameters=powsybl_loadflow_parameter)[0] diff --git a/src/dc_plus/importing/powsybl/powsybl_loadflow_parameter.py b/src/dc_plus/importing/powsybl/powsybl_loadflow_parameter.py index f447bff..67238dd 100644 --- a/src/dc_plus/importing/powsybl/powsybl_loadflow_parameter.py +++ b/src/dc_plus/importing/powsybl/powsybl_loadflow_parameter.py @@ -10,7 +10,7 @@ import logging from typing import Literal -from pypowsybl.loadflow import ConnectedComponentMode, VoltageInitMode +from pypowsybl.loadflow import ComponentMode, VoltageInitMode from pypowsybl.loadflow import Parameters as LoadFlowParameters logger = logging.getLogger(__name__) @@ -59,7 +59,7 @@ def get_powsybl_loadflow_parameter( # balance_type=BalanceType.PROPORTIONAL_TO_GENERATION_REMAINING_MARGIN, # BalanceType dc_use_transformer_ratio=True, countries_to_balance=None, # Sequence[str] - connected_component_mode=ConnectedComponentMode.MAIN, # ConnectedComponentMode + component_mode=ComponentMode.MAIN_SYNCHRONOUS, dc_power_factor=None, provider_parameters=provider_param, # Dict[str, str] ) diff --git a/src/dc_plus/importing/powsybl/powsybl_network_helpers.py b/src/dc_plus/importing/powsybl/powsybl_network_helpers.py index e0dc148..ff8cd77 100644 --- a/src/dc_plus/importing/powsybl/powsybl_network_helpers.py +++ b/src/dc_plus/importing/powsybl/powsybl_network_helpers.py @@ -26,13 +26,9 @@ from dc_plus.importing.powsybl.powsybl_import_helpers import select_a_generator_as_slack_and_run_loadflow from dc_plus.importing.powsybl.powsybl_loadflow_parameter import get_powsybl_loadflow_parameter from dc_plus.interfaces.jacobian_interface import JacobianInterface -from dc_plus.interfaces.jacobian_network_data import _get_jacobian_data_from_network_data -from dc_plus.interfaces.network_information import ( - DynamicNetworkInformation, - StaticNetworkInformation, - StringNetworkInformation, -) -from dc_plus.preprocess.create_network_data import create_network_data_pypowsbl +from dc_plus.interfaces.jacobian_network_data import get_jacobian_data_from_network_data +from dc_plus.interfaces.network_information import NetworkInformation +from dc_plus.preprocess.create_network_data import create_network_data_pypowsybl logger = logging.getLogger(__name__) @@ -137,7 +133,9 @@ def get_bus_branch_ids_for_n1_results(net: Network, security_analysis_result: Se def _load_test_grid( get_net: callable, -) -> tuple[Network, StaticNetworkInformation, DynamicNetworkInformation, StringNetworkInformation, JacobianInterface]: + use_reactive_limits: bool = False, + set_limited_generators_to_not_voltage_regulator: bool = False, +) -> tuple[Network, NetworkInformation, JacobianInterface]: """Load a test pandapower or powsybl grid and prepare the network data and Jacobian.""" is_powsybl_net = isinstance(get_net(), pypowsybl.network.Network) if is_powsybl_net: @@ -157,6 +155,7 @@ def _load_test_grid( net.per_unit = True loadflow_parameter = get_powsybl_loadflow_parameter("hotstart_test") loadflow_parameter.provider_parameters["newtonRaphsonConvEpsPerEq"] = "1e-12" + loadflow_parameter.use_reactive_limits = use_reactive_limits loadflow_res_n0 = pypowsybl.loadflow.run_ac(net, parameters=loadflow_parameter)[0] if loadflow_res_n0.status != pypowsybl._pypowsybl.LoadFlowComponentStatus.CONVERGED: raise ValueError( @@ -164,10 +163,32 @@ def _load_test_grid( f"Status text: {loadflow_res_n0.status_text}, " f"Reference bus ID: {loadflow_res_n0.reference_bus_id}" ) - static_info, dynamic_info, string_info = create_network_data_pypowsbl(net) - jacobian_data = _get_jacobian_data_from_network_data(dynamic_info) - return net, static_info, dynamic_info, string_info, jacobian_data + # fix the voltage regulator to False for generators that are at their reactive limits + # ensures same behavior between pypowsybl load flow and our solver regarding reactive limits + if set_limited_generators_to_not_voltage_regulator and use_reactive_limits: + g = net.get_generators(attributes=["max_q_at_p", "min_q_at_p", "q", "voltage_regulator_on"]) + threshold = 1e-9 + powsybl_g_switch = g[(g["q"] > -g["min_q_at_p"] - threshold) | (g["q"] < -g["max_q_at_p"] + threshold)] + powsybl_g_switch = powsybl_g_switch[["voltage_regulator_on", "q"]] + powsybl_g_switch.rename(columns={"q": "target_q"}, inplace=True) + powsybl_g_switch.loc[:, "voltage_regulator_on"] = False + powsybl_g_switch.loc[:, "target_q"] = -powsybl_g_switch.loc[:, "target_q"] + net.update_generators(powsybl_g_switch) + loadflow_res_n0 = pypowsybl.loadflow.run_ac(net, parameters=loadflow_parameter)[0] + if loadflow_res_n0.status != pypowsybl._pypowsybl.LoadFlowComponentStatus.CONVERGED: + raise ValueError( + f"Load flow did not converge. Status: {loadflow_res_n0.status}, " + f"Status text: {loadflow_res_n0.status_text}, " + f"Reference bus ID: {loadflow_res_n0.reference_bus_id}" + ) + + network_info = create_network_data_pypowsybl(net) + jacobian_data = get_jacobian_data_from_network_data( + network_info.dynamic_network_data, + ) + + return net, network_info, jacobian_data def load_pandapower_net_for_powsybl(net: pandapower.pandapowerNet) -> pypowsybl.network.Network: @@ -229,6 +250,15 @@ def load_pandapower_net_for_powsybl_with_convert_from_pandapower(net: pandapower pypowsybl.network.Network The converted pypowsybl network. """ + if "sgen" in net and not net.sgen.empty: + if "max_p_mw" in net.sgen.columns: + net.sgen.loc[net.sgen["max_p_mw"].isna(), "max_p_mw"] = 500.0 + if "min_p_mw" in net.sgen.columns: + net.sgen.loc[net.sgen["min_p_mw"].isna(), "min_p_mw"] = -500.0 + if "max_q_mvar" in net.sgen.columns: + net.sgen.loc[net.sgen["max_q_mvar"].isna(), "max_q_mvar"] = 500.0 + if "min_q_mvar" in net.sgen.columns: + net.sgen.loc[net.sgen["min_q_mvar"].isna(), "min_q_mvar"] = -500.0 pypowsybl_network = pypowsybl.network.convert_from_pandapower(net) return pypowsybl_network diff --git a/src/dc_plus/interfaces/jacobian_interface.py b/src/dc_plus/interfaces/jacobian_interface.py index c8668a0..f1388fb 100644 --- a/src/dc_plus/interfaces/jacobian_interface.py +++ b/src/dc_plus/interfaces/jacobian_interface.py @@ -68,8 +68,8 @@ def __init__( inverse_jacobian: Float[np.ndarray, " n_eq_jacobian n_eq_jacobian "], is_angle_component: Bool[np.ndarray, " n_eq_jacobian"], is_magnitude_component: Bool[np.ndarray, " n_eq_jacobian"], - pvpq_indices: Int[np.ndarray, " n_pvpq_bus "], - pq_indices: Int[np.ndarray, " n_pq_bus "], + angle_bus_indices: Int[np.ndarray, " n_angle_bus "], + magnitude_bus_indices: Int[np.ndarray, " n_magnitude_bus "], n_buses: int, ) -> "JacobianInterface": """Initialize the JacobianInterface. @@ -90,10 +90,10 @@ def __init__( Boolean vector indicating which components of the jacobian correspond to angle variables. is_magnitude_component: Bool[np.ndarray, " n_eq_jacobian"] Boolean vector indicating which components of the jacobian correspond to magnitude variables. - pvpq_indices: Int[np.ndarray, " n_pvpq_bus "] - Integer vector indicating the indices of PV and PQ buses. - pq_indices: Int[np.ndarray, " n_pq_bus "] - Integer vector indicating the indices of PQ buses. + angle_bus_indices: Int[np.ndarray, " n_angle_bus "] + Integer vector indicating the indices of buses with angle variables. + magnitude_bus_indices: Int[np.ndarray, " n_magnitude_bus "] + Integer vector indicating the indices of buses with magnitude variables. n_buses: int Total number of buses in the system. @@ -110,15 +110,51 @@ def __init__( self.inverse_jacobian = inverse_jacobian self.is_angle_component = is_angle_component self.is_magnitude_component = is_magnitude_component - self.bus_angle_indices = pvpq_indices - self.bus_magnitude_indices = pq_indices + self.bus_angle_indices = angle_bus_indices + self.bus_magnitude_indices = magnitude_bus_indices angle_component_indices = np.full(n_buses, -1, dtype=np.int32) magnitude_component_indices = np.full(n_buses, -1, dtype=np.int32) - angle_component_indices[pvpq_indices] = np.flatnonzero(is_angle_component) - magnitude_component_indices[pq_indices] = np.flatnonzero(is_magnitude_component) + angle_component_indices[angle_bus_indices] = np.flatnonzero(is_angle_component) + magnitude_component_indices[magnitude_bus_indices] = np.flatnonzero(is_magnitude_component) self.angle_component_indices = angle_component_indices self.magnitude_component_indices = magnitude_component_indices + @property + def n_angle_components(self) -> int: + """Return the number of angle variables in the Jacobian layout.""" + return self.bus_angle_indices.size + + @property + def n_magnitude_components(self) -> int: + """Return the number of voltage-magnitude variables in the Jacobian layout.""" + return self.bus_magnitude_indices.size + + @property + def uses_voltage_setpoint_rows(self) -> bool: + """Return whether the lower Jacobian block includes PV voltage-control rows.""" + return self.n_angle_components == self.n_magnitude_components and np.array_equal( + self.bus_angle_indices, + self.bus_magnitude_indices, + ) + + def copy_with_inverse_jacobian( + self, + inverse_jacobian: Float[np.ndarray, " n_eq_jacobian n_eq_jacobian"], + ) -> "JacobianInterface": + """Return a copy with an updated inverse Jacobian approximation.""" + return JacobianInterface( + bus_is_used=self.bus_is_used.copy(), + jacobian_index_in_use=self.jacobian_index_in_use.copy(), + pointer_to_original_bus=self.pointer_to_original_bus.copy(), + jacobian=self.jacobian.copy(), + inverse_jacobian=inverse_jacobian, + is_angle_component=self.is_angle_component.copy(), + is_magnitude_component=self.is_magnitude_component.copy(), + angle_bus_indices=self.bus_angle_indices.copy(), + magnitude_bus_indices=self.bus_magnitude_indices.copy(), + n_buses=self.bus_is_used.size, + ) + # ruff: noqa: PLR0911, C901 def __eq__(self, value: "JacobianInterface") -> bool: """Equal definition of JacobianInterface. @@ -148,10 +184,10 @@ def __eq__(self, value: "JacobianInterface") -> bool: if not np.array_equal(self.is_magnitude_component, value.is_magnitude_component): return False - if not np.array_equal(np.sort(self.bus_angle_indices), np.sort(value.bus_angle_indices)): + if not np.array_equal(self.bus_angle_indices, value.bus_angle_indices): return False - if not np.array_equal(np.sort(self.bus_magnitude_indices), np.sort(value.bus_magnitude_indices)): + if not np.array_equal(self.bus_magnitude_indices, value.bus_magnitude_indices): return False if not np.allclose(self.inverse_jacobian, value.inverse_jacobian): diff --git a/src/dc_plus/interfaces/jacobian_network_data.py b/src/dc_plus/interfaces/jacobian_network_data.py index db6faf5..1036596 100644 --- a/src/dc_plus/interfaces/jacobian_network_data.py +++ b/src/dc_plus/interfaces/jacobian_network_data.py @@ -13,10 +13,157 @@ from scipy.sparse.linalg import inv as sparse_inv from dc_plus.interfaces.jacobian_interface import JacobianInterface -from dc_plus.interfaces.network_information import DynamicNetworkInformation +from dc_plus.interfaces.network_information import ( + BusType, + DynamicNetworkInformation, + StaticNetworkInformation, + replace_network_data, +) -def _get_jacobian_data_from_network_data(dynamic_network_data: DynamicNetworkInformation) -> JacobianInterface: +def _get_local_voltage_regulation_data( + dynamic_network_data: DynamicNetworkInformation, + static_network_data: StaticNetworkInformation, +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + """Aggregate local voltage-regulating reactive limits and fixed Q injections by bus.""" + n_buses = dynamic_network_data.n_buses + injection_to_bus = dynamic_network_data.injection_to_bus + injection_regulated_bus = dynamic_network_data.injection_regulated_bus + injection_connected = dynamic_network_data.injection_connected + injection_voltage_regulation = dynamic_network_data.injection_voltage_regulation + injection_reactive_power = dynamic_network_data.injection_reactive_power + + local_regulation_mask = ( + injection_connected + & injection_voltage_regulation + & (injection_to_bus == injection_regulated_bus) + & (injection_regulated_bus >= 0) + ) + + regulating_bus_count = np.zeros(n_buses, dtype=np.int32) + regulating_q_min = np.zeros(n_buses, dtype=float) + regulating_q_max = np.zeros(n_buses, dtype=float) + fixed_bus_reactive_power = np.zeros(n_buses, dtype=float) + + if np.any(local_regulation_mask): + regulated_buses = injection_to_bus[local_regulation_mask] + q_min = np.nan_to_num( + static_network_data.injection_limit_reactive_power_min[local_regulation_mask], + nan=-np.inf, + ) + q_max = np.nan_to_num( + static_network_data.injection_limit_reactive_power_max[local_regulation_mask], + nan=np.inf, + ) + np.add.at(regulating_bus_count, regulated_buses, 1) + np.add.at(regulating_q_min, regulated_buses, q_min) + np.add.at(regulating_q_max, regulated_buses, q_max) + + fixed_injection_mask = injection_connected & ~local_regulation_mask + if np.any(fixed_injection_mask): + np.add.at( + fixed_bus_reactive_power, + injection_to_bus[fixed_injection_mask], + injection_reactive_power[fixed_injection_mask], + ) + + has_local_regulation = regulating_bus_count > 0 + return has_local_regulation, regulating_q_min, regulating_q_max, fixed_bus_reactive_power + + +def _get_imported_local_regulating_reactive_power_by_bus( + dynamic_network_data: DynamicNetworkInformation, +) -> np.ndarray: + """Aggregate imported local regulating reactive power by bus in load convention.""" + n_buses = dynamic_network_data.n_buses + injection_to_bus = dynamic_network_data.injection_to_bus + injection_regulated_bus = dynamic_network_data.injection_regulated_bus + injection_connected = dynamic_network_data.injection_connected + injection_voltage_regulation = dynamic_network_data.injection_voltage_regulation + injection_reactive_power = dynamic_network_data.injection_reactive_power + + local_regulation_mask = ( + injection_connected + & injection_voltage_regulation + & (injection_to_bus == injection_regulated_bus) + & (injection_regulated_bus >= 0) + ) + imported_regulating_reactive_power = np.zeros(n_buses, dtype=float) + if np.any(local_regulation_mask): + np.add.at( + imported_regulating_reactive_power, + injection_to_bus[local_regulation_mask], + injection_reactive_power[local_regulation_mask], + ) + return imported_regulating_reactive_power + + +def _calculate_lower_residual( + dynamic_network_data: DynamicNetworkInformation, + mismatch: Float[np.ndarray, " n_buses"], + magnitude_bus_indices: np.ndarray, + static_network_data: StaticNetworkInformation | None, + q_limit_tolerance: float, +) -> Float[np.ndarray, " n_bus_eq"]: + """Assemble the mode-dependent lower residual block for the active Jacobian layout.""" + bus_type = dynamic_network_data.bus_type + lower_residual = mismatch[magnitude_bus_indices].imag.copy() + pv_mask = bus_type[magnitude_bus_indices] == BusType.PV + if not np.any(pv_mask): + return lower_residual + + lower_residual[pv_mask] = ( + dynamic_network_data.bus_voltage_magnitudes[magnitude_bus_indices][pv_mask] + - dynamic_network_data.bus_voltage_magnitude_setpoint[magnitude_bus_indices][pv_mask] + ) + + if static_network_data is None: + return lower_residual + + has_local_regulation, regulating_q_min, regulating_q_max, fixed_bus_reactive_power = _get_local_voltage_regulation_data( + dynamic_network_data=dynamic_network_data, + static_network_data=static_network_data, + ) + imported_regulating_reactive_power = _get_imported_local_regulating_reactive_power_by_bus(dynamic_network_data) + + local_regulated_pv_mask = pv_mask & has_local_regulation[magnitude_bus_indices] + if not np.any(local_regulated_pv_mask): + return lower_residual + + # ``mismatch`` is assembled in injection convention while the exported bus and + # injection reactive powers use the load convention. Reconstruct the actual + # local regulating-generator Q in load convention before checking limits. + net_bus_reactive_power = dynamic_network_data.bus_reactive_power - mismatch.imag + regulating_reactive_power = net_bus_reactive_power - fixed_bus_reactive_power + regulating_reactive_power_pv = regulating_reactive_power[magnitude_bus_indices] + imported_regulating_reactive_power_pv = imported_regulating_reactive_power[magnitude_bus_indices] + lower_limit_mask = local_regulated_pv_mask & ( + regulating_reactive_power_pv < (regulating_q_min[magnitude_bus_indices] - q_limit_tolerance) + ) + upper_limit_mask = local_regulated_pv_mask & ( + regulating_reactive_power_pv > (regulating_q_max[magnitude_bus_indices] + q_limit_tolerance) + ) + imported_lower_limit_mask = local_regulated_pv_mask & ( + imported_regulating_reactive_power_pv <= (regulating_q_min[magnitude_bus_indices] + q_limit_tolerance) + ) + imported_upper_limit_mask = local_regulated_pv_mask & ( + imported_regulating_reactive_power_pv >= (regulating_q_max[magnitude_bus_indices] - q_limit_tolerance) + ) + lower_limit_mask |= imported_lower_limit_mask + upper_limit_mask = (upper_limit_mask | imported_upper_limit_mask) & ~lower_limit_mask + + lower_residual[lower_limit_mask] = ( + regulating_q_min[magnitude_bus_indices][lower_limit_mask] - regulating_reactive_power_pv[lower_limit_mask] + ) + lower_residual[upper_limit_mask] = ( + regulating_q_max[magnitude_bus_indices][upper_limit_mask] - regulating_reactive_power_pv[upper_limit_mask] + ) + return lower_residual + + +def get_jacobian_data_from_network_data( + dynamic_network_data: DynamicNetworkInformation, +) -> JacobianInterface: """Get the Jacobian data from the dynamic network data. Parameters @@ -29,38 +176,84 @@ def _get_jacobian_data_from_network_data(dynamic_network_data: DynamicNetworkInf JacobianInterface The Jacobian data interface. """ - jacobian = get_jacobian_from_network_data( + jacobian = _get_jacobian_from_network_data( dynamic_network_data=dynamic_network_data, ) - n_pq = dynamic_network_data.n_pq_buses - n_pv = dynamic_network_data.n_pv_buses + angle_bus_indices = dynamic_network_data.pvpq_buses_indices_pvpq_order + magnitude_bus_indices = dynamic_network_data.pq_buses_indices + n_angle_eq = angle_bus_indices.size + n_magnitude_eq = magnitude_bus_indices.size is_angle_component = np.zeros(jacobian.shape[0], dtype=bool) - is_angle_component[: (n_pq + n_pv)] = True + is_angle_component[:n_angle_eq] = True is_magnitude_component = np.zeros(jacobian.shape[0], dtype=bool) - is_magnitude_component[(n_pq + n_pv) :] = True + is_magnitude_component[n_angle_eq : n_angle_eq + n_magnitude_eq] = True jacobian_index_in_use = np.ones(jacobian.shape[0], dtype=bool) bus_is_used = np.ones(dynamic_network_data.n_buses, dtype=bool) - pq_indices = dynamic_network_data.pq_buses_indices - pvpq_indices = dynamic_network_data.pvpq_buses_indices_pvpq_order - return JacobianInterface( bus_is_used=bus_is_used, jacobian_index_in_use=jacobian_index_in_use, pointer_to_original_bus=np.arange(dynamic_network_data.n_buses, dtype=np.int32), jacobian=jacobian, - inverse_jacobian=sparse_inv(jacobian).toarray(), + inverse_jacobian=sparse_inv(jacobian.tocsc()).toarray(), is_angle_component=is_angle_component, is_magnitude_component=is_magnitude_component, - pvpq_indices=pvpq_indices, - pq_indices=pq_indices, + angle_bus_indices=angle_bus_indices, + magnitude_bus_indices=magnitude_bus_indices, n_buses=dynamic_network_data.n_buses, ) -def get_jacobian_from_network_data( +def _get_sparse_power_jacobian_blocks( + y_bus: sparse.sparray, + voltage_magnitudes: np.ndarray, + voltage_angles: np.ndarray, + angle_bus_indices: np.ndarray, + magnitude_bus_indices: np.ndarray, +) -> tuple[sparse.csr_array, sparse.csr_array, sparse.csr_array, sparse.csr_array]: + """Build sliced AC power-Jacobian blocks from the sparse network admittance.""" + voltage = voltage_magnitudes * np.exp(1.0j * voltage_angles) + if np.any(np.isclose(voltage, 0.0, atol=1e-10)): + raise ValueError("Voltage magnitudes must be strictly positive to construct the Jacobian.") + + voltage_norm = voltage / np.abs(voltage) + current = y_bus @ voltage + + diag_voltage = sparse.diags(voltage) + diag_current = sparse.diags(current) + diag_voltage_norm = sparse.diags(voltage_norm) + + # dS/dV = diag(V) * (Ybus * diag(V/|V|))^* + diag(I)^* * diag(V/|V|) + power_jacobian_voltage_mag = ( + diag_voltage @ (y_bus @ diag_voltage_norm).conjugate() + diag_current.conjugate() @ diag_voltage_norm + ) + # dS/dtheta = 1j * diag(V) * (diag(I) - Ybus * diag(V))^* + power_jacobian_voltage_angle = 1j * diag_voltage @ (diag_current - y_bus @ diag_voltage).conjugate() + + jacobian_active_power_angle = sparse.csr_array( + power_jacobian_voltage_angle[np.ix_(angle_bus_indices, angle_bus_indices)].real + ) + jacobian_active_power_voltage = sparse.csr_array( + power_jacobian_voltage_mag[np.ix_(angle_bus_indices, magnitude_bus_indices)].real + ) + jacobian_reactive_power_angle = sparse.csr_array( + power_jacobian_voltage_angle[np.ix_(magnitude_bus_indices, angle_bus_indices)].imag + ) + jacobian_reactive_power_voltage = sparse.csr_array( + power_jacobian_voltage_mag[np.ix_(magnitude_bus_indices, magnitude_bus_indices)].imag + ) + + return ( + jacobian_active_power_angle, + jacobian_active_power_voltage, + jacobian_reactive_power_angle, + jacobian_reactive_power_voltage, + ) + + +def _get_jacobian_from_network_data( dynamic_network_data: DynamicNetworkInformation, ) -> sparse.sparray: """Calculate Jacobian. @@ -82,36 +275,44 @@ def get_jacobian_from_network_data( voltage_magnitudes = dynamic_network_data.bus_voltage_magnitudes voltage_angles = dynamic_network_data.bus_voltage_angles_rad - pq_indices = dynamic_network_data.pq_buses_indices - pqpv_indices = dynamic_network_data.pvpq_buses_indices_pvpq_order - - voltage = voltage_magnitudes * np.exp(1.0j * voltage_angles) - if np.any(voltage == 0.0): - raise ValueError("Voltage magnitudes must be strictly positive to construct the Jacobian.") - - voltage_norm = voltage / abs(voltage) - current = y_bus @ voltage - - diag_voltage = sparse.diags(voltage) - diag_current = sparse.diags(current) - diag_voltage_norm = sparse.diags(voltage_norm) - - # dS/dV = diag(V) * (Ybus * diag(V/|V|))^* + diag(I)^* * diag(V/|V|) - power_jacobian_voltage_mag = ( - diag_voltage @ (y_bus @ diag_voltage_norm).conjugate() + diag_current.conjugate() @ diag_voltage_norm + angle_bus_indices = dynamic_network_data.pvpq_buses_indices_pvpq_order + magnitude_bus_indices = dynamic_network_data.pq_buses_indices + + ( + jacobian_active_power_angle, + jacobian_active_power_voltage, + jacobian_reactive_power_angle, + jacobian_reactive_power_voltage, + ) = _get_sparse_power_jacobian_blocks( + y_bus=y_bus, + voltage_magnitudes=voltage_magnitudes, + voltage_angles=voltage_angles, + angle_bus_indices=angle_bus_indices, + magnitude_bus_indices=magnitude_bus_indices, ) - # dS/dtheta = 1j * diag(V) * (diag(I) - Ybus * diag(V))^* - power_jacobian_voltage_angle = 1j * diag_voltage @ (diag_current - y_bus @ diag_voltage).conjugate() - jacobian_active_power_angle = sparse.csr_array(power_jacobian_voltage_angle[np.ix_(pqpv_indices, pqpv_indices)].real) - jacobian_active_power_voltage = sparse.csr_array(power_jacobian_voltage_mag[np.ix_(pqpv_indices, pq_indices)].real) - jacobian_reactive_power_angle = sparse.csr_array(power_jacobian_voltage_angle[np.ix_(pq_indices, pqpv_indices)].imag) - jacobian_reactive_power_voltage = sparse.csr_array(power_jacobian_voltage_mag[np.ix_(pq_indices, pq_indices)].imag) + full_reactive_power_angle = jacobian_reactive_power_angle.toarray() + full_reactive_power_voltage = jacobian_reactive_power_voltage.toarray() + + bus_type = dynamic_network_data.bus_type[magnitude_bus_indices] + pv_mask = bus_type == BusType.PV + jacobian_reactive_power_angle = full_reactive_power_angle.copy() + jacobian_reactive_power_voltage = full_reactive_power_voltage.copy() + if np.any(pv_mask): + jacobian_reactive_power_angle[pv_mask, :] = 0.0 + jacobian_reactive_power_voltage[pv_mask, :] = 0.0 + jacobian_reactive_power_voltage[pv_mask, np.flatnonzero(pv_mask)] = 1.0 jacobian = sparse.vstack( [ sparse.hstack([jacobian_active_power_angle, jacobian_active_power_voltage], format="csr"), - sparse.hstack([jacobian_reactive_power_angle, jacobian_reactive_power_voltage], format="csr"), + sparse.hstack( + [ + sparse.csr_array(jacobian_reactive_power_angle), + sparse.csr_array(jacobian_reactive_power_voltage), + ], + format="csr", + ), ], format="csr", ) @@ -190,6 +391,9 @@ def _get_admittance_matrix_from_network_data( def calculate_nodal_mismatch_network_data( dynamic_network_data: DynamicNetworkInformation, y_matrix: sparse.sparray, + jacobian_data: JacobianInterface, + static_network_data: StaticNetworkInformation | None = None, + q_limit_tolerance: float = 1e-10, ) -> Float[np.ndarray, " n_eq_jacobian"]: """Calculate the nodal mismatches nodal mismatches. @@ -199,6 +403,14 @@ def calculate_nodal_mismatch_network_data( The dynamic network data. y_matrix : sparse.sparray The admittance matrix. + jacobian_data : JacobianInterface + Jacobian layout defining the active angle and magnitude equation order. + static_network_data : StaticNetworkInformation | None, optional + Static network information used to apply reactive-power limit residuals + for locally regulated buses when available. + q_limit_tolerance : float, optional + Numerical tolerance used when deciding whether reactive-power limits are + active. Returns ------- @@ -213,10 +425,57 @@ def calculate_nodal_mismatch_network_data( mismatch = v_pu * np.conj(y_matrix @ v_pu) - s_pu - # Jacobian ordering uses PV buses first then PQ buses for the angle (P) equations - # and PQ buses for the magnitude (Q) equations. Assemble the mismatch vector - # in the same order: [P@PV,P@PQ, Q@PQ] -> equivalently [P@(PV+PQ), Q@PQ]. - pvpq_indices = dynamic_network_data.pvpq_buses_indices_pvpq_order - pq_indices = dynamic_network_data.pq_buses_indices + angle_bus_indices = jacobian_data.bus_angle_indices + magnitude_bus_indices = jacobian_data.bus_magnitude_indices + lower_residual = _calculate_lower_residual( + dynamic_network_data=dynamic_network_data, + mismatch=mismatch, + magnitude_bus_indices=magnitude_bus_indices, + static_network_data=static_network_data, + q_limit_tolerance=q_limit_tolerance, + ) + + return np.r_[mismatch[angle_bus_indices].real, lower_residual] - return np.r_[mismatch[pvpq_indices].real, mismatch[pq_indices].imag] + +def _apply_jacobian_dx_to_network_data( + dynamic_network_data: DynamicNetworkInformation, + dx: Float[np.ndarray, " n_eq_jacobian"], + jacobian_data: JacobianInterface, +) -> DynamicNetworkInformation: + """Apply one Jacobian-ordered state increment to the network voltages. + + Parameters + ---------- + dynamic_network_data : DynamicNetworkInformation + Dynamic network data whose voltage state is updated. + dx : Float[np.ndarray, " n_eq_jacobian"] + State increment in the active Jacobian ordering. + jacobian_data : JacobianInterface + Jacobian layout defining which buses receive angle and magnitude + updates from ``dx``. + + Returns + ------- + DynamicNetworkInformation + Copy of ``dynamic_network_data`` with updated voltage angles and magnitudes. + """ + angle_bus_indices = jacobian_data.bus_angle_indices + magnitude_bus_indices = jacobian_data.bus_magnitude_indices + n_angle = angle_bus_indices.size + n_magnitude = magnitude_bus_indices.size + expected_size = n_angle + n_magnitude + + if dx.shape[0] != expected_size: + raise ValueError(f"Jacobian increment has size {dx.shape[0]}, expected {expected_size} for the given network data.") + + updated_angles = dynamic_network_data.bus_voltage_angles_rad.copy() + updated_magnitudes = dynamic_network_data.bus_voltage_magnitudes.copy() + updated_angles[angle_bus_indices] += dx[:n_angle] + updated_magnitudes[magnitude_bus_indices] += dx[n_angle : n_angle + n_magnitude] + + return replace_network_data( + dynamic_network_data, + bus_voltage_angles_rad=updated_angles, + bus_voltage_magnitudes=updated_magnitudes, + ) diff --git a/src/dc_plus/interfaces/network_information.py b/src/dc_plus/interfaces/network_information.py index 0301d4b..2b5fead 100644 --- a/src/dc_plus/interfaces/network_information.py +++ b/src/dc_plus/interfaces/network_information.py @@ -26,12 +26,12 @@ """ -from dataclasses import dataclass from enum import IntEnum -from typing import Literal, TypeAlias +from typing import Dict, Literal, TypeAlias, TypeVar import numpy as np -from jaxtyping import Bool, Float, Int +from numpydantic import NDArray, Shape +from pydantic import BaseModel, ConfigDict, field_validator, model_validator class BusType(IntEnum): @@ -91,61 +91,101 @@ class InjectionTypeBusBranch(IntEnum): GENERATOR = 1 -StringArray: TypeAlias = np.ndarray[np.str_, ...] +class _NetworkBaseModel(BaseModel): + """Shared Pydantic configuration for network information containers.""" + model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid") -@dataclass -class TransformerTapInformation: - """Contains transformer tap information for a single transformer. + +T = TypeVar("T", bound=_NetworkBaseModel) + + +def _arrays_equal(left: np.ndarray, right: np.ndarray) -> bool: + return np.array_equal(np.asarray(left), np.asarray(right), equal_nan=True) + + +def replace_network_data(obj: T, **kwargs: dict) -> T: + """Replace properties of pydantic model instances.""" + return type(obj).model_validate(obj.model_dump() | kwargs) + + +class TransformerTapInformation(_NetworkBaseModel): + """Contains branch-aligned transformer tap information. Either for ratio-changing or phase-shifting transformers. + Branches without the corresponding tap changer type are represented with zeros. A implementation of the cim:PhaseTapChangerTablePoint module. Or the pypowsybl.network.Network.get_phase_tap_changer_steps method. """ n_max_tap_positions: int - """Number of maximum tap positions. + """Number of maximum tap positions.""" - If a transformer has fewer positions, the remaining positions are padded as zeros. - """ + neutral_conductance_series: float + """Conductance of series at the neutral tap position.""" + + neutral_susceptance_series: float + """Susceptance of series at the neutral tap position.""" + + neutral_conductance_charging_from: float + """Conductance of charging from at the neutral tap position.""" + + neutral_susceptance_charging_from: float + """Susceptance of charging from at the neutral tap position.""" - min_tap_pos: Int[np.ndarray, " n_branch"] - """Minimum tap position of the tap-changing transformers.""" + neutral_conductance_charging_to: float + """Conductance of charging to at the neutral tap position.""" - max_tap_pos: Int[np.ndarray, " n_branch"] - """Maximum tap position of the tap-changing transformers.""" + neutral_susceptance_charging_to: float + """Susceptance of charging to at the neutral tap position.""" - neutral_tap_pos: Int[np.ndarray, " n_branch"] - """Neutral tap position of the tap-changing transformers.""" + neutral_shift_angle: float + """Shift angle at the neutral tap position.""" - tap_conductance_series: Float[np.ndarray, " n_branch n_max_tap_positions"] - """Conductance of series for different tap positions.""" + neutral_shift_ratio_rho: float + """Tap shift ratio at the neutral tap position.""" - tap_susceptance_series: Float[np.ndarray, " n_branch n_max_tap_positions"] - """Susceptance of series for different tap positions.""" + tap_offset_conductance_series: NDArray[Shape[" * n_max_tap_positions"], float] + """Conductance offset of series for different tap positions.""" - tap_conductance_charging_from: Float[np.ndarray, " n_branch n_max_tap_positions"] - """Conductance of charging for different tap positions.""" + tap_offset_susceptance_series: NDArray[Shape[" * n_max_tap_positions"], float] + """Susceptance offset of series for different tap positions.""" - tap_susceptance_charging_from: Float[np.ndarray, " n_branch n_max_tap_positions"] - """Susceptance of charging for different tap positions.""" + tap_offset_conductance_charging_from: NDArray[Shape[" * n_max_tap_positions"], float] + """Conductance offset of charging for different tap positions.""" - tap_conductance_charging_to: Float[np.ndarray, " n_branch n_max_tap_positions"] - """Conductance of charging for different tap positions.""" + tap_offset_susceptance_charging_from: NDArray[Shape[" * n_max_tap_positions"], float] + """Susceptance offset of charging for different tap positions.""" - tap_susceptance_charging_to: Float[np.ndarray, " n_branch n_max_tap_positions"] - """Susceptance of charging for different tap positions.""" + tap_offset_conductance_charging_to: NDArray[Shape[" * n_max_tap_positions"], float] + """Conductance offset of charging for different tap positions.""" - tap_shift_angle: Float[np.ndarray, " n_branch n_max_tap_positions"] - """Shift angle for different tap positions.""" + tap_offset_susceptance_charging_to: NDArray[Shape[" * n_max_tap_positions"], float] + """Susceptance offset of charging for different tap positions.""" - tap_shift_ratio_rho: Float[np.ndarray, " n_branch n_max_tap_positions"] - """Tap shift ratio for different tap positions.""" + tap_offset_shift_angle: NDArray[Shape[" * n_max_tap_positions"], float] + """Tap angle offset for different tap positions.""" + tap_offset_shift_ratio_rho: NDArray[Shape[" * n_max_tap_positions"], float] + """Tap ratio offset for different tap positions.""" -@dataclass -class ShuntSectionInformation: + @model_validator(mode="after") + @classmethod + def validate_array_lengths(cls, values: "TransformerTapInformation") -> dict: + """Validate that all tap offset arrays have the same length as n_max_tap_positions.""" + n_max_tap_positions = values.n_max_tap_positions + for key, value in values.model_dump().items(): + if key.startswith("tap_offset_"): + if len(value) != n_max_tap_positions: + raise ValueError( + f"Length of {key} must be equal to n_max_tap_positions ({n_max_tap_positions}), " + f"but got {len(value)}." + ) + return values + + +class ShuntSectionInformation(_NetworkBaseModel): """Contains shunt section information for shunt elements.""" n_max_shunt_sections: int @@ -154,28 +194,72 @@ class ShuntSectionInformation: If a shunt has fewer sections, the remaining sections are padded as zeros. """ - min_shunt_section: Int[np.ndarray, " n_shunts"] + min_shunt_section: NDArray[Shape[" * n_shunts"], int] """Minimum number of shunt sections.""" - max_shunt_section: Int[np.ndarray, " n_shunts"] + max_shunt_section: NDArray[Shape[" * n_shunts"], int] """Maximum number of shunt sections.""" - shunt_conductance_at_section: Float[np.ndarray, " n_shunts n_max_shunt_sections"] + shunt_conductance_at_section: NDArray[Shape[" * n_shunts, * n_max_shunt_sections"], float] """Conductance for different shunt sections. Note: this is the absolute conductance, don't add the base conductance. Note: this is in the ideal case lossless and therefore 0.0 """ - shunt_susceptance_at_section: Float[np.ndarray, " n_shunts n_max_shunt_sections"] + shunt_susceptance_at_section: NDArray[Shape[" * n_shunts, * n_max_shunt_sections"], float] """Susceptance for different shunt sections. Note: this is the absolute susceptance, don't add the base susceptance. """ - -@dataclass -class StaticNetworkInformation: + @model_validator(mode="after") + @classmethod + def validate_array_lengths(cls, values: "ShuntSectionInformation") -> dict: + """Validate that all shunt section arrays have the same length as n_max_shunt_sections.""" + n_max_shunt_sections = values.n_max_shunt_sections + n_shunts = values.min_shunt_section.shape[0] + for key, value in values.model_dump().items(): + # arrays with shape (n_shunts, n_max_shunt_sections) + if key.endswith("_at_section"): + if value.shape[1] != n_max_shunt_sections: + raise ValueError( + f"Length of {key} must be equal to n_max_shunt_sections ({n_max_shunt_sections}), " + f"but got {value.shape[1]}." + ) + # arrays with shape (n_shunts,) + if key != "n_max_shunt_sections": + if value.shape[0] != n_shunts: + raise ValueError( + f"Length of {key} must be equal to number of shunts ({n_shunts}), but got {value.shape[0]}." + ) + return values + + @classmethod + def empty(cls, n_shunts: int) -> "ShuntSectionInformation": + """Create an empty shunt section container.""" + return cls( + n_max_shunt_sections=0, + min_shunt_section=np.zeros((n_shunts), dtype=int), + max_shunt_section=np.zeros((n_shunts), dtype=int), + shunt_conductance_at_section=np.zeros((n_shunts, 0), dtype=float), + shunt_susceptance_at_section=np.zeros((n_shunts, 0), dtype=float), + ) + + def __eq__(self, other: object) -> bool: + """Check the equality of two ShuntSectionInformation objects.""" + if not isinstance(other, ShuntSectionInformation): + return NotImplemented + return ( + self.n_max_shunt_sections == other.n_max_shunt_sections + and _arrays_equal(self.min_shunt_section, other.min_shunt_section) + and _arrays_equal(self.max_shunt_section, other.max_shunt_section) + and _arrays_equal(self.shunt_conductance_at_section, other.shunt_conductance_at_section) + and _arrays_equal(self.shunt_susceptance_at_section, other.shunt_susceptance_at_section) + ) + + +class StaticNetworkInformation(_NetworkBaseModel): """Contains all static network information required for the DC+ solver. This class contains all network information and is paired with the JacobianData class. @@ -191,12 +275,17 @@ class StaticNetworkInformation: # Injection parameters - injection_limits: Float[np.ndarray, " n_injections"] - """The limits of the injections in the network. - Note: - - This is currently not used in the DC+ solver. - - Future versions may use a fixed jacobian approach to iterate and respect injection limits. - """ + injection_limit_reactive_power_min: NDArray[Shape[" * n_injections"], float] + """The minimum reactive power limits of the injections in the network.""" + + injection_limit_reactive_power_max: NDArray[Shape[" * n_injections"], float] + """The maximum reactive power limits of the injections in the network.""" + + injection_limit_active_power_min: NDArray[Shape[" * n_injections"], float] + """The minimum active power limits of the injections in the network.""" + + injection_limit_active_power_max: NDArray[Shape[" * n_injections"], float] + """The maximum active power limits of the injections in the network.""" # injection_to_bus can change due to BDSF @@ -207,38 +296,108 @@ class StaticNetworkInformation: """ # Branch parameters - n_limits: Int + n_limits: int """The number of branch limits Branches may have multiple limits (e.g., permanent, short-term-15-min). """ - branch_current_limits: Float[np.ndarray, " n_limits n_branch"] + branch_current_limits: NDArray[Shape[" * n_branches, * n_limits"], float] """The thermal limits of the branches""" # Tap Information - has_ratio_changing_transformer: Bool[np.ndarray, " n_branch"] + has_ratio_changing_transformer: NDArray[Shape[" * n_branches"], bool] """Indicates whether a branch has a ratio-changing transformer.""" - has_phase_shifting_transformer: Bool[np.ndarray, " n_branch"] + has_phase_shifting_transformer: NDArray[Shape[" * n_branches"], bool] """Indicates whether a branch has a phase-shifting transformer.""" - phase_shift_info: TransformerTapInformation - """Contains transformer tap information for the branches. + phase_shift_info: Dict[int, TransformerTapInformation] + """Contains branch-aligned phase-shifting transformer tap information. Note: this is not optional, even if no transformers are present. + key: branch index + value: TransformerTapInformation for the phase-shifting transformer of the branch. """ - ratio_shift_info: TransformerTapInformation - """Contains transformer tap information for the branches. + ratio_shift_info: Dict[int, TransformerTapInformation] + """Contains branch-aligned ratio-changing transformer tap information. Note: this is not optional, even if no transformers are present. + key: branch index + value: TransformerTapInformation for the ratio-changing transformer of the branch. """ - -@dataclass -class DynamicNetworkInformation: + @model_validator(mode="after") + @classmethod + def validate_array_shapes(cls, values: "StaticNetworkInformation") -> dict: + """Validate that all branch-aligned arrays have the same length.""" + n_branches = values.has_ratio_changing_transformer.shape[0] + n_injections = values.injection_limit_active_power_min.shape[0] + n_limits = values.n_limits + for key, value in values.model_dump().items(): + if key in ["has_ratio_changing_transformer", "has_phase_shifting_transformer"]: + if value.shape[0] != n_branches: + raise ValueError( + f"Length of {key} must be equal to number of branches ({n_branches}), but got {value.shape[0]}." + ) + if key in [ + "injection_limit_active_power_min", + "injection_limit_active_power_max", + "injection_limit_reactive_power_min", + "injection_limit_reactive_power_max", + ]: + if value.shape[0] != n_injections: + raise ValueError( + f"Length of {key} must be equal to number of injections ({n_injections}), but got {value.shape[0]}." + ) + if key == "branch_current_limits": + if value.shape != (n_branches, n_limits): + raise ValueError( + f"Shape of {key} must be equal to (number of branches, number of limits) " + f"({n_branches}, {n_limits}), but got {value.shape}." + ) + return values + + @model_validator(mode="after") + @classmethod + def validate_phase_ratio_len(cls, values: "StaticNetworkInformation") -> dict: + """Validate that all branch-aligned arrays have the same length.""" + n_ratio_changing_transformers = np.sum(values.has_ratio_changing_transformer) + n_phase_shifting_transformers = np.sum(values.has_phase_shifting_transformer) + if len(values.phase_shift_info) != n_phase_shifting_transformers: + raise ValueError( + f"Length of phase_shift_info must be equal to number of phase-shifting transformers " + f"({n_phase_shifting_transformers}), but got {len(values.phase_shift_info)}." + ) + if len(values.ratio_shift_info) != n_ratio_changing_transformers: + raise ValueError( + f"Length of ratio_shift_info must be equal to number of ratio-changing transformers " + f"({n_ratio_changing_transformers}), but got {len(values.ratio_shift_info)}." + ) + return values + + def __eq__(self, other: object) -> bool: + """Check the equality of two StaticNetworkInformation objects.""" + if not isinstance(other, StaticNetworkInformation): + return NotImplemented + return ( + _arrays_equal(self.injection_limit_reactive_power_min, other.injection_limit_reactive_power_min) + and _arrays_equal(self.injection_limit_reactive_power_max, other.injection_limit_reactive_power_max) + and _arrays_equal(self.injection_limit_active_power_min, other.injection_limit_active_power_min) + and _arrays_equal(self.injection_limit_active_power_max, other.injection_limit_active_power_max) + and self.shunt_section_info == other.shunt_section_info + and self.n_limits == other.n_limits + and _arrays_equal(self.branch_current_limits, other.branch_current_limits) + and _arrays_equal(self.has_ratio_changing_transformer, other.has_ratio_changing_transformer) + and _arrays_equal(self.has_phase_shifting_transformer, other.has_phase_shifting_transformer) + and self.phase_shift_info == other.phase_shift_info + and self.ratio_shift_info == other.ratio_shift_info + ) + + +class DynamicNetworkInformation(_NetworkBaseModel): """Contains all dynamic network information required for the DC+ solver. To initialize, the values are expected to be in per unit. @@ -252,136 +411,181 @@ class DynamicNetworkInformation: # branch data - branch_from_bus: Int[np.ndarray, " n_branch n_timestep"] + branch_from_bus: NDArray[Shape[" * n_branches"], int] """The from bus of the branches""" - branch_to_bus: Int[np.ndarray, " n_branch n_timestep"] + branch_to_bus: NDArray[Shape[" * n_branches"], int] """The to bus of the branches""" - branch_active_power_from: Float[np.ndarray, " n_branch n_timestep"] - """Active power flows from side for all branches and timesteps.""" + branch_active_power_from: NDArray[Shape[" * n_branches"], float] + """Active power flows from side for all branches.""" - branch_reactive_power_from: Float[np.ndarray, " n_branch n_timestep"] - """Reactive power flows from side for all branches and timesteps.""" + branch_reactive_power_from: NDArray[Shape[" * n_branches"], float] + """Reactive power flows from side for all branches.""" - branch_active_power_to: Float[np.ndarray, " n_branch n_timestep"] - """Active power flows to side for all branches and timesteps.""" + branch_active_power_to: NDArray[Shape[" * n_branches"], float] + """Active power flows to side for all branches.""" - branch_reactive_power_to: Float[np.ndarray, " n_branch n_timestep"] - """Reactive power flows to side for all branches and timesteps.""" + branch_reactive_power_to: NDArray[Shape[" * n_branches"], float] + """Reactive power flows to side for all branches.""" - branch_current_magnitude_from: Float[np.ndarray, " n_branch n_timestep"] - """Current magnitudes from side for all branches and timesteps.""" + branch_current_magnitude_from: NDArray[Shape[" * n_branches"], float] + """Current magnitudes from side for all branches.""" - branch_current_magnitude_to: Float[np.ndarray, " n_branch n_timestep"] - """Current magnitudes to side for all branches and timesteps.""" + branch_current_magnitude_to: NDArray[Shape[" * n_branches"], float] + """Current magnitudes to side for all branches.""" - branch_ratio_tap_positions: Int[np.ndarray, " n_branch n_timestep"] - """Ratio tap positions for all branches and timesteps.""" + branch_ratio_tap_positions: NDArray[Shape[" * n_branches"], int] + """Ratio tap positions for all branches.""" - branch_phase_tap_positions: Int[np.ndarray, " n_branch n_timestep"] - """Phase tap positions for all branches and timesteps.""" + branch_phase_tap_positions: NDArray[Shape[" * n_branches"], int] + """Phase tap positions for all branches.""" - branch_effective_admittance_from_to: Float[np.complex128, " n_branch n_timestep"] + branch_effective_admittance_from_to: NDArray[Shape[" * n_branches"], complex] """Admittance from-to for the branches. Gets updated when tap positions change. """ - branch_effective_admittance_from_from: Float[np.complex128, " n_branch n_timestep"] + branch_effective_admittance_from_from: NDArray[Shape[" * n_branches"], complex] """Admittance from-from for the branches. Gets updated when tap positions change. """ - branch_effective_admittance_to_to: Float[np.complex128, " n_branch n_timestep"] + branch_effective_admittance_to_to: NDArray[Shape[" * n_branches"], complex] """Admittance to-to for the branches. Gets updated when tap positions change. """ - branch_effective_admittance_to_from: Float[np.complex128, " n_branch n_timestep"] + branch_effective_admittance_to_from: NDArray[Shape[" * n_branches"], complex] """Admittance to-from for the branches. Gets updated when tap positions change. """ - branch_effective_admittance_series: Float[np.complex128, " n_branch n_timestep"] + branch_effective_admittance_series: NDArray[Shape[" * n_branches"], complex] """Series admittance for the branches. Gets updated when tap positions change. """ - branch_effective_admittance_charging_symmetric: Float[np.complex128, " n_branch n_timestep"] + branch_r: NDArray[Shape[" * n_branches"], float] + """Resistance for the branches.""" + + branch_x: NDArray[Shape[" * n_branches"], float] + """Reactance for the branches.""" + + branch_g_from: NDArray[Shape[" * n_branches"], float] + """Conductance from side for the branches.""" + + branch_b_from: NDArray[Shape[" * n_branches"], float] + """Susceptance from side for the branches.""" + + branch_g_to: NDArray[Shape[" * n_branches"], float] + """Conductance to side for the branches.""" + + branch_b_to: NDArray[Shape[" * n_branches"], float] + """Susceptance to side for the branches.""" + + branch_rho: NDArray[Shape[" * n_branches"], float] + """Tap ratio for the branches.""" + + branch_shift_angle_rad: NDArray[Shape[" * n_branches"], float] + """Shift angle in radians for the branches.""" + + branch_effective_admittance_charging_symmetric: NDArray[Shape[" * n_branches"], complex] """Symmetric charging admittance for the branches. Gets updated when tap positions change. """ - branch_connected: Bool[np.ndarray, " n_branch n_timestep"] - """Indicates whether a branch is connected for all branches and timesteps. + branch_connected: NDArray[Shape[" * n_branches"], bool] + """Indicates whether a branch is connected for all branches. Becomes imporant for reconnecting branches. E.g. Grid planning or canceling outage plans. """ - is_branch_symmetric: Bool[np.ndarray, " n_branch n_timestep"] + branch_is_symmetric: NDArray[Shape[" * n_branches"], bool] """Indicates whether a branch is symmetric in admittance representation. Note: this might change do to tap changes. Make sure this is updated when tap positions change. """ - is_connected_to_slack: Bool[np.ndarray, " n_branch n_timestep"] - """Indicates whether a branch is connected to the slack bus for all branches and timesteps. + branch_connected_to_slack: NDArray[Shape[" * n_branches"], bool] + """Indicates whether a branch is connected to the slack bus for all branches. is a different lodf calculation is used for those branches. Note: needs update when topology changes occur. """ # bus data - bus_voltage_magnitudes: Float[np.ndarray, " n_buses n_timestep"] - """Voltage magnitudes in per unit for all buses and timesteps.""" + bus_voltage_magnitudes: NDArray[Shape[" * n_buses"], float] + """Voltage magnitudes in per unit for all buses.""" - bus_voltage_angles_rad: Float[np.ndarray, " n_buses n_timestep"] - """Voltage angles in radians for all buses and timesteps.""" + bus_voltage_angles_rad: NDArray[Shape[" * n_buses"], float] + """Voltage angles in radians for all buses.""" - bus_active_power: Float[np.ndarray, " n_buses n_timestep"] - """Active power injections for all buses and timesteps.""" + bus_active_power: NDArray[Shape[" * n_buses"], float] + """Active power injections for all buses.""" - bus_reactive_power: Float[np.ndarray, " n_buses n_timestep"] - """Reactive power injections for all buses and timesteps.""" + bus_reactive_power: NDArray[Shape[" * n_buses"], float] + """Reactive power injections for all buses.""" - bus_type: Int[np.ndarray, " n_buses"] + bus_type: NDArray[Shape[" * n_buses"], int] """The type of each bus in the network. 0: slack 1: pv 2: pq """ + bus_is_angle_reference: NDArray[Shape[" * n_buses"], bool] + """Indicates which bus is used as the angle reference. + + This is distinct from the bus voltage-control mode. A reference bus can lose + voltage control after an outage and become a PQ bus while still fixing the + global angle reference. + """ + + bus_voltage_magnitude_setpoint: NDArray[Shape[" * n_buses"], float] + """Voltage magnitude target for each bus. + + For PV buses this is the controlled voltage setpoint used by the superset + residual. For PQ buses the value is carried only for shape consistency. + """ + # injection data - injection_to_bus: Int[np.ndarray, " n_injections n_timestep"] + injection_to_bus: NDArray[Shape[" * n_injections"], int] """The bus index for each injection.""" - injection_active_power: Float[np.ndarray, " n_injections n_timestep"] - """Active power injections for all injections and timesteps.""" + injection_active_power: NDArray[Shape[" * n_injections"], float] + """Active power injections for all injections.""" - injection_reactive_power: Float[np.ndarray, " n_injections n_timestep"] - """Reactive power injections for all injections and timesteps.""" + injection_reactive_power: NDArray[Shape[" * n_injections"], float] + """Reactive power injections for all injections.""" - injection_connected: Bool[np.ndarray, " n_injections n_timestep"] - """Indicates whether an injection is connected for all injections and timesteps.""" + injection_connected: NDArray[Shape[" * n_injections"], bool] + """Indicates whether an injection is connected for all injections.""" + + injection_voltage_regulation: NDArray[Shape[" * n_injections"], bool] + """Indicates whether an injection participates in voltage regulation.""" + + injection_regulated_bus: NDArray[Shape[" * n_injections"], int] + """The regulated bus index for each injection, or -1 when not applicable.""" # shunt data - shunt_bus_indices: Int[np.ndarray, " n_shunts n_timestep"] + shunt_bus_indices: NDArray[Shape[" * n_shunts"], int] """The bus index for each shunt.""" - shunt_active_power: Float[np.ndarray, " n_shunts n_timestep"] - """Active power injections for all shunts and timesteps.""" + shunt_active_power: NDArray[Shape[" * n_shunts"], float] + """Active power injections for all shunts.""" - shunt_reactive_power: Float[np.ndarray, " n_shunts n_timestep"] - """Reactive power injections for all shunts and timesteps.""" + shunt_reactive_power: NDArray[Shape[" * n_shunts"], float] + """Reactive power injections for all shunts.""" - shunt_section_count: Int[np.ndarray, " n_shunts n_timestep"] - """Number of active shunt sections for all shunts and timesteps.""" + shunt_section_count: NDArray[Shape[" * n_shunts"], int] + """Number of active shunt sections for all shunts.""" - shunt_effective_bus_admittance: Float[np.ndarray, " n_shunts"] + shunt_effective_bus_admittance: NDArray[Shape[" * n_shunts"], complex] """Conductance for different shunt sections.""" - shunt_connected: Bool[np.ndarray, " n_shunts n_timestep"] - """Indicates whether a shunt is connected for all shunts and timesteps. + shunt_connected: NDArray[Shape[" * n_shunts"], bool] + """Indicates whether a shunt is connected for all shunts. Becomes imporant for reconnecting shunts. E.g. Grid planning or canceling outage plans.""" # properties for easy access @@ -402,14 +606,14 @@ def n_branches(self) -> int: @property def slack_indices(self) -> np.ndarray: - """Return the indices of the slack bus. + """Return the indices of the angle-reference bus. Returns ------- slack_index : np.ndarray - The indices of the slack bus. + The indices of the angle-reference bus. """ - slack_index = np.flatnonzero(self.bus_type == BusType.SLACK) + slack_index = np.flatnonzero(self.bus_is_angle_reference) return slack_index def is_pv_bus(self, bus_index: int) -> bool: @@ -488,7 +692,7 @@ def pv_buses_mask(self) -> np.ndarray: pv_buses_mask : np.ndarray A boolean mask indicating which buses are PV buses. """ - pv_buses_mask = self.bus_type == BusType.PV + pv_buses_mask = (self.bus_type == BusType.PV) & ~self.bus_is_angle_reference return pv_buses_mask @property @@ -505,14 +709,14 @@ def pq_buses_mask(self) -> np.ndarray: @property def pvpq_buses_mask(self) -> np.ndarray: - """Return a boolean mask indicating which buses are PV or PQ buses. + """Return a boolean mask indicating which buses participate in angle equations. Returns ------- pvpq_buses_mask : np.ndarray A boolean mask indicating which buses are PV or PQ buses. """ - pvpq_buses_mask = (self.bus_type == BusType.PV) | (self.bus_type == BusType.PQ) + pvpq_buses_mask = ~self.bus_is_angle_reference return pvpq_buses_mask @property @@ -524,7 +728,7 @@ def pv_buses_indices(self) -> np.ndarray: pv_buses_indices : np.ndarray The indices of the PV buses. """ - pv_buses_indices = np.flatnonzero(self.bus_type == BusType.PV) + pv_buses_indices = np.flatnonzero(self.pv_buses_mask) return pv_buses_indices @property @@ -536,31 +740,32 @@ def pq_buses_indices(self) -> np.ndarray: pq_buses_indices : np.ndarray The indices of the PQ buses. """ - pq_buses_indices = np.flatnonzero(self.bus_type == BusType.PQ) + pq_buses_indices = np.flatnonzero(self.pq_buses_mask) return pq_buses_indices @property def pvpq_buses_indices(self) -> np.ndarray: - """Return the indices of the PV and PQ buses. + """Return the indices of buses participating in angle equations. Returns ------- pvpq_buses_indices : np.ndarray The indices of the PV and PQ buses. """ - pvpq_buses_indices = np.flatnonzero((self.bus_type == BusType.PV) | (self.bus_type == BusType.PQ)) + pvpq_buses_indices = np.flatnonzero(self.pvpq_buses_mask) return pvpq_buses_indices @property def pvpq_buses_indices_pvpq_order(self) -> np.ndarray: - """Return the indices of the PV and PQ buses. + """Return the indices of buses participating in angle equations. Returns ------- pvpq_buses_indices : np.ndarray The indices of the PV and PQ buses. """ - pvpq_buses_indices = np.concatenate((self.pv_buses_indices, self.pq_buses_indices)) + pq_angle_indices = self.pq_buses_indices[~self.bus_is_angle_reference[self.pq_buses_indices]] + pvpq_buses_indices = np.concatenate((self.pv_buses_indices, pq_angle_indices)) return pvpq_buses_indices # injections @@ -587,37 +792,118 @@ def n_shunts(self) -> int: """ return self.shunt_bus_indices.shape[0] - -@dataclass -class StringNetworkInformation: + @model_validator(mode="after") + @classmethod + def validate_array_shapes(cls, values: "DynamicNetworkInformation") -> dict: + """Validate that all branch-aligned arrays have the same length.""" + n_branches = values.branch_from_bus.shape[0] + n_buses = values.bus_voltage_magnitudes.shape[0] + n_injections = values.injection_to_bus.shape[0] + n_shunts = values.shunt_bus_indices.shape[0] + for key, value in values.model_dump().items(): + if key.startswith("branch_"): + if value.shape[0] != n_branches: + raise ValueError( + f"Length of {key} must be equal to number of branches ({n_branches}), but got {value.shape[0]}." + ) + if key.startswith("bus_"): + if value.shape[0] != n_buses: + raise ValueError( + f"Length of {key} must be equal to number of buses ({n_buses}), but got {value.shape[0]}." + ) + if key.startswith("injection_"): + if value.shape[0] != n_injections: + raise ValueError( + f"Length of {key} must be equal to number of injections ({n_injections}), but got {value.shape[0]}." + ) + if key.startswith("shunt_"): + if value.shape[0] != n_shunts: + raise ValueError( + f"Length of {key} must be equal to number of shunts ({n_shunts}), but got {value.shape[0]}." + ) + return values + + +class StringNetworkInformation(_NetworkBaseModel): """Contains all human-friendly network information required for the DC+ solver. This data will not be transferred to the GPU and for this reason seperated from the Static and Dynamic Network Information. """ - bus_ids: StringArray + bus_ids: NDArray[Shape[" * n_buses"], str] """ids of the buses, shape (n_buses,)""" - shunt_ids: StringArray + shunt_ids: NDArray[Shape[" * n_shunts"], str] """ids of the shunts, shape (n_shunts,)""" - branch_types: BranchType + branch_types: NDArray[Shape[" * n_branches"], str] """Types of the branches, shape (n_branches,). E.g., line, transformer, etc. """ - branch_ids: StringArray + branch_ids: NDArray[Shape[" * n_branches"], str] """ids of the branches, shape (n_branches,)""" - limit_names: StringArray + limit_names: NDArray[Shape[" * n_limits"], str] """Names of the branch limits, shape (n_limits,)""" - injection_types: InjectionType + injection_types: NDArray[Shape[" * n_injections"], str] """Types of the injections, shape (n_injections,). E.g., load, generator, etc. """ + injection_ids: NDArray[Shape[" * n_injections"], str] + """ids of the injections, shape (n_injections,)""" + + @field_validator("branch_types", mode="after") + @classmethod + def validate_type(cls, value: NDArray[Shape[" * n_branches"], str]) -> NDArray[Shape[" * n_branches"], str]: + """Validate that the branch and injection types are valid.""" + valid_branch_types = AssetType.__args__ # type: list + for branch_type in value: + if branch_type not in valid_branch_types: + raise ValueError(f"Invalid branch type: {branch_type}. Valid types are: {valid_branch_types}.") + return value + + @field_validator("injection_types", mode="after") + @classmethod + def validate_injection_type( + cls, value: NDArray[Shape[" * n_injections"], str] + ) -> NDArray[Shape[" * n_injections"], str]: + """Validate that the injection types are valid.""" + valid_injection_types = AssetType.__args__ # type: list + for injection_type in value: + if injection_type not in valid_injection_types: + raise ValueError(f"Invalid injection type: {injection_type}. Valid types are: {valid_injection_types}.") + return value + + +class NetworkInformation(_NetworkBaseModel): + """Contains all network information required for the DC+ solver. + + This class contains both static and dynamic network information and is paired with the JacobianData class. + + This data will be transfered to the GPU for the solving process. + Do not add gpu unfriendly data here (e.g. strings). + """ + + static_network_data: StaticNetworkInformation + """Contains all static network information required for the DC+ solver.""" + + dynamic_network_data: DynamicNetworkInformation + """Contains all dynamic network information required for the DC+ solver.""" + + string_network_data: StringNetworkInformation + """Contains all human-friendly network information required for the DC+ solver.""" + + @model_validator(mode="after") + @classmethod + def validate_consistency(cls, values: "NetworkInformation") -> dict: + """Validate the consistency of the network data.""" + _check_network_data_consistency(values.dynamic_network_data, values.string_network_data) + return values + def _check_network_data_consistency( dynamic_network_data: DynamicNetworkInformation, @@ -674,6 +960,24 @@ def _check_network_data_consistency( assert ( dynamic_network_data.branch_from_bus.shape[0] == dynamic_network_data.branch_effective_admittance_to_from.shape[0] ), "Inconsistent number of branches between branch_from_nodes and branch_effective_admittance_to_from." + assert dynamic_network_data.branch_from_bus.shape[0] == dynamic_network_data.branch_g_from.shape[0], ( + "Inconsistent number of branches between branch_from_nodes and branch_g_from." + ) + assert dynamic_network_data.branch_from_bus.shape[0] == dynamic_network_data.branch_b_from.shape[0], ( + "Inconsistent number of branches between branch_from_nodes and branch_b_from." + ) + assert dynamic_network_data.branch_from_bus.shape[0] == dynamic_network_data.branch_g_to.shape[0], ( + "Inconsistent number of branches between branch_from_nodes and branch_g_to." + ) + assert dynamic_network_data.branch_from_bus.shape[0] == dynamic_network_data.branch_b_to.shape[0], ( + "Inconsistent number of branches between branch_from_nodes and branch_b_to." + ) + assert dynamic_network_data.branch_from_bus.shape[0] == dynamic_network_data.branch_rho.shape[0], ( + "Inconsistent number of branches between branch_from_nodes and branch_rho." + ) + assert dynamic_network_data.branch_from_bus.shape[0] == dynamic_network_data.branch_shift_angle_rad.shape[0], ( + "Inconsistent number of branches between branch_from_nodes and branch_shift_angle_rad." + ) assert dynamic_network_data.branch_from_bus.shape[0] == dynamic_network_data.branch_connected.shape[0], ( "Inconsistent number of branches between branch_from_nodes and branch_connected." ) @@ -691,6 +995,9 @@ def _check_network_data_consistency( assert dynamic_network_data.bus_voltage_magnitudes.shape[0] == dynamic_network_data.bus_type.shape[0], ( "Inconsistent number of buses between bus_voltage_magnitudes and bus_type." ) + assert dynamic_network_data.bus_voltage_magnitudes.shape[0] == dynamic_network_data.bus_is_angle_reference.shape[0], ( + "Inconsistent number of buses between bus_voltage_magnitudes and bus_is_angle_reference." + ) # check injection data assert dynamic_network_data.injection_to_bus.shape[0] == dynamic_network_data.injection_active_power.shape[0], ( @@ -736,3 +1043,6 @@ def _check_network_data_consistency( assert string_network_data.injection_types.shape[0] == dynamic_network_data.injection_to_bus.shape[0], ( "Inconsistent number of injections between injection_types and injection_to_bus." ) + assert string_network_data.injection_ids.shape[0] == dynamic_network_data.injection_to_bus.shape[0], ( + "Inconsistent number of injections between injection_ids and injection_to_bus." + ) diff --git a/src/dc_plus/interfaces/network_inputs.py b/src/dc_plus/interfaces/network_inputs.py new file mode 100644 index 0000000..f5ec2fd --- /dev/null +++ b/src/dc_plus/interfaces/network_inputs.py @@ -0,0 +1,62 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""Shared JAX pytree inputs for solver topology, indices, and state data.""" + +import jax.numpy as jnp +from jax_dataclasses import pytree_dataclass +from jaxtyping import Array, Bool, Complex128, Float, Int + + +@pytree_dataclass +class JacobianComponentInputs: + """Bus-to-Jacobian component mappings shared across JAX contingency kernels.""" + + angle_component_indices: Int[Array, " n_buses"] + magnitude_component_indices: Int[Array, " n_buses"] + + +@pytree_dataclass +class VoltageStateInputs: + """Voltage-only bus state inputs shared across JAX contingency kernels.""" + + bus_voltage_magnitudes: Float[Array, "*batch n_buses"] + bus_voltage_angles_rad: Float[Array, "*batch n_buses"] + + +@pytree_dataclass +class BusStateInputs: + """Bus-level power, type, and voltage state inputs shared across JAX solvers.""" + + bus_active_power: Float[Array, "*batch n_buses"] + bus_reactive_power: Float[Array, "*batch n_buses"] + bus_type: Int[Array, "*batch n_buses"] + bus_voltage_magnitude_setpoint: Float[Array, "*batch n_buses"] + bus_voltage_magnitudes: Float[Array, "*batch n_buses"] + bus_voltage_angles_rad: Float[Array, "*batch n_buses"] + + +@pytree_dataclass +class NetworkTopologyInputs: + """Branch and shunt connectivity inputs shared across JAX solvers.""" + + branch_from: Int[jnp.ndarray, " n_branches"] + branch_to: Int[jnp.ndarray, " n_branches"] + branch_connected: Bool[jnp.ndarray, " *batch n_branches"] + shunt_to_bus: Int[jnp.ndarray, " n_shunts"] + shunt_connected: Bool[jnp.ndarray, " *batch n_shunts"] + + +@pytree_dataclass +class NetworkAdmittanceInputs: + """Branch and shunt admittance inputs shared across JAX solvers.""" + + y_ff: Complex128[jnp.ndarray, " n_branches"] + y_ft: Complex128[jnp.ndarray, " n_branches"] + y_tf: Complex128[jnp.ndarray, " n_branches"] + y_tt: Complex128[jnp.ndarray, " n_branches"] + y_shunt: Complex128[jnp.ndarray, " n_shunts"] diff --git a/src/dc_plus/jax/bsdf.py b/src/dc_plus/jax/bsdf.py index a84f597..7eae5e4 100644 --- a/src/dc_plus/jax/bsdf.py +++ b/src/dc_plus/jax/bsdf.py @@ -33,9 +33,15 @@ import jax.numpy as jnp from jaxtyping import Complex128, Float, Int +from ..interfaces.network_inputs import ( + JacobianComponentInputs, + NetworkAdmittanceInputs, + NetworkTopologyInputs, + VoltageStateInputs, +) from .low_rank_helper import _compute_branch_delta_submatrix_from_admittance -# ruff: noqa: ARG001, PLR0913, PLR0915 +# ruff: noqa: ARG001, PLR0915 def _full_rank_lodf( @@ -63,32 +69,49 @@ def _full_rank_lodf( return lodf_transposed +def _compute_shunt_delta_submatrix_from_admittance( + v_mag: Float[jnp.ndarray, ""], + y_shunt: Complex128[jnp.ndarray, ""], +) -> Float[jnp.ndarray, "2 2"]: + """Return the 2x2 Jacobian contribution for a shunt admittance at one bus.""" + conductance = jnp.real(y_shunt) + susceptance = jnp.imag(y_shunt) + dtype = jnp.result_type(v_mag, y_shunt) + + delta = jnp.array( + [ + [0.0, 2.0 * v_mag * conductance], + [0.0, -2.0 * v_mag * susceptance], + ], + dtype=dtype, + ) + return delta * -1 + + @jax.jit def _compute_bsdf_update_impl( jacobian_inv_transposed: Float[jnp.ndarray, " n_eq n_eq"], bus_to_split: int, new_bus_b_index: int, branches_connected_to_bus_b: Int[jnp.ndarray, " n_branches_B"], - branch_from: Int[jnp.ndarray, " n_branches"], - branch_to: Int[jnp.ndarray, " n_branches"], - v_mag_hat: Float[jnp.ndarray, " n_buses"], - theta_hat: Float[jnp.ndarray, " n_buses"], - y_ff: Complex128[jnp.ndarray, " n_branches"], - y_ft: Complex128[jnp.ndarray, " n_branches"], - y_tf: Complex128[jnp.ndarray, " n_branches"], - y_tt: Complex128[jnp.ndarray, " n_branches"], - angle_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], - magnitude_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], + shunt_connected_to_bus_b: Int[jnp.ndarray, " n_shunts_B"], + network_topology: NetworkTopologyInputs, + voltage_state: VoltageStateInputs, + network_admittance: NetworkAdmittanceInputs, + jacobian_components: JacobianComponentInputs, ) -> Float[jnp.ndarray, " n_eq n_eq"]: - """Legacy implementation: materialize updated inverse transpose.""" - dtype = jacobian_inv_transposed.dtype + """Materialize updated inverse transpose.""" + # get dtype -> either float32 or float64 depending on input + dtype_input = voltage_state.bus_voltage_magnitudes.dtype n_eq = jacobian_inv_transposed.shape[0] - branch_from_old = jnp.take(branch_from, branches_connected_to_bus_b, axis=0) - branch_to_old = jnp.take(branch_to, branches_connected_to_bus_b, axis=0) + branch_from_old = jnp.take(network_topology.branch_from, branches_connected_to_bus_b, axis=0) + branch_to_old = jnp.take(network_topology.branch_to, branches_connected_to_bus_b, axis=0) + shunt_bus_old = jnp.take(network_topology.shunt_to_bus, shunt_connected_to_bus_b, axis=0) branch_from_new = jnp.where(branch_from_old == bus_to_split, new_bus_b_index, branch_from_old) branch_to_new = jnp.where(branch_to_old == bus_to_split, new_bus_b_index, branch_to_old) + shunt_bus_new = jnp.where(shunt_bus_old == bus_to_split, new_bus_b_index, shunt_bus_old) base_bus_indices = jnp.array([bus_to_split, new_bus_b_index], dtype=jnp.int32) bus_candidates = jnp.concatenate( @@ -98,12 +121,14 @@ def _compute_bsdf_update_impl( branch_to_old, branch_from_new, branch_to_new, + shunt_bus_old, + shunt_bus_new, ], axis=0, ) - theta_candidates = jnp.take(angle_component_indices, bus_candidates, axis=0) - mag_candidates = jnp.take(magnitude_component_indices, bus_candidates, axis=0) + theta_candidates = jnp.take(jacobian_components.angle_component_indices, bus_candidates, axis=0) + mag_candidates = jnp.take(jacobian_components.magnitude_component_indices, bus_candidates, axis=0) comp_candidates = jnp.concatenate([theta_candidates, mag_candidates], axis=0) k_max = min(int(comp_candidates.shape[0]), n_eq) @@ -131,15 +156,20 @@ def _gather_positions(indices: jnp.ndarray) -> Tuple[jnp.ndarray, jnp.ndarray]: pos = jnp.where(valid, pos, -1) return pos, valid - theta_from_old_idx = jnp.take(angle_component_indices, branch_from_old, axis=0) - theta_to_old_idx = jnp.take(angle_component_indices, branch_to_old, axis=0) - mag_from_old_idx = jnp.take(magnitude_component_indices, branch_from_old, axis=0) - mag_to_old_idx = jnp.take(magnitude_component_indices, branch_to_old, axis=0) + theta_from_old_idx = jnp.take(jacobian_components.angle_component_indices, branch_from_old, axis=0) + theta_to_old_idx = jnp.take(jacobian_components.angle_component_indices, branch_to_old, axis=0) + mag_from_old_idx = jnp.take(jacobian_components.magnitude_component_indices, branch_from_old, axis=0) + mag_to_old_idx = jnp.take(jacobian_components.magnitude_component_indices, branch_to_old, axis=0) + + theta_from_new_idx = jnp.take(jacobian_components.angle_component_indices, branch_from_new, axis=0) + theta_to_new_idx = jnp.take(jacobian_components.angle_component_indices, branch_to_new, axis=0) + mag_from_new_idx = jnp.take(jacobian_components.magnitude_component_indices, branch_from_new, axis=0) + mag_to_new_idx = jnp.take(jacobian_components.magnitude_component_indices, branch_to_new, axis=0) - theta_from_new_idx = jnp.take(angle_component_indices, branch_from_new, axis=0) - theta_to_new_idx = jnp.take(angle_component_indices, branch_to_new, axis=0) - mag_from_new_idx = jnp.take(magnitude_component_indices, branch_from_new, axis=0) - mag_to_new_idx = jnp.take(magnitude_component_indices, branch_to_new, axis=0) + theta_shunt_old_idx = jnp.take(jacobian_components.angle_component_indices, shunt_bus_old, axis=0) + mag_shunt_old_idx = jnp.take(jacobian_components.magnitude_component_indices, shunt_bus_old, axis=0) + theta_shunt_new_idx = jnp.take(jacobian_components.angle_component_indices, shunt_bus_new, axis=0) + mag_shunt_new_idx = jnp.take(jacobian_components.magnitude_component_indices, shunt_bus_new, axis=0) old_indices = jnp.stack( [ @@ -161,23 +191,42 @@ def _gather_positions(indices: jnp.ndarray) -> Tuple[jnp.ndarray, jnp.ndarray]: axis=1, ) + shunt_old_indices = jnp.stack( + [ + theta_shunt_old_idx, + mag_shunt_old_idx, + ], + axis=1, + ) + + shunt_new_indices = jnp.stack( + [ + theta_shunt_new_idx, + mag_shunt_new_idx, + ], + axis=1, + ) + old_pos, old_valid = _gather_positions(old_indices) new_pos, new_valid = _gather_positions(new_indices) + shunt_old_pos, shunt_old_valid = _gather_positions(shunt_old_indices) + shunt_new_pos, shunt_new_valid = _gather_positions(shunt_new_indices) - vm_from_old = jnp.take(v_mag_hat, branch_from_old, axis=0) - vm_to_old = jnp.take(v_mag_hat, branch_to_old, axis=0) - th_from_old = jnp.take(theta_hat, branch_from_old, axis=0) - th_to_old = jnp.take(theta_hat, branch_to_old, axis=0) + vm_from_old = jnp.take(voltage_state.bus_voltage_magnitudes, branch_from_old, axis=0) + vm_to_old = jnp.take(voltage_state.bus_voltage_magnitudes, branch_to_old, axis=0) + th_from_old = jnp.take(voltage_state.bus_voltage_angles_rad, branch_from_old, axis=0) + th_to_old = jnp.take(voltage_state.bus_voltage_angles_rad, branch_to_old, axis=0) - vm_from_new = jnp.take(v_mag_hat, branch_from_new, axis=0) - vm_to_new = jnp.take(v_mag_hat, branch_to_new, axis=0) - th_from_new = jnp.take(theta_hat, branch_from_new, axis=0) - th_to_new = jnp.take(theta_hat, branch_to_new, axis=0) + vm_from_new = jnp.take(voltage_state.bus_voltage_magnitudes, branch_from_new, axis=0) + vm_to_new = jnp.take(voltage_state.bus_voltage_magnitudes, branch_to_new, axis=0) + th_from_new = jnp.take(voltage_state.bus_voltage_angles_rad, branch_from_new, axis=0) + th_to_new = jnp.take(voltage_state.bus_voltage_angles_rad, branch_to_new, axis=0) - y_ff_sel = jnp.take(y_ff, branches_connected_to_bus_b, axis=0) - y_ft_sel = jnp.take(y_ft, branches_connected_to_bus_b, axis=0) - y_tf_sel = jnp.take(y_tf, branches_connected_to_bus_b, axis=0) - y_tt_sel = jnp.take(y_tt, branches_connected_to_bus_b, axis=0) + y_ff_sel = jnp.take(network_admittance.y_ff, branches_connected_to_bus_b, axis=0) + y_ft_sel = jnp.take(network_admittance.y_ft, branches_connected_to_bus_b, axis=0) + y_tf_sel = jnp.take(network_admittance.y_tf, branches_connected_to_bus_b, axis=0) + y_tt_sel = jnp.take(network_admittance.y_tt, branches_connected_to_bus_b, axis=0) + y_shunt_sel = jnp.take(network_admittance.y_shunt, shunt_connected_to_bus_b, axis=0) compute_delta = jax.vmap(_compute_branch_delta_submatrix_from_admittance) delta_old = compute_delta( @@ -201,8 +250,18 @@ def _gather_positions(indices: jnp.ndarray) -> Tuple[jnp.ndarray, jnp.ndarray]: y_tt_sel, ) + compute_shunt_delta = jax.vmap(_compute_shunt_delta_submatrix_from_admittance) + delta_shunt_old = compute_shunt_delta( + jnp.take(voltage_state.bus_voltage_magnitudes, shunt_bus_old, axis=0), + y_shunt_sel, + ) + delta_shunt_new = compute_shunt_delta( + jnp.take(voltage_state.bus_voltage_magnitudes, shunt_bus_new, axis=0), + y_shunt_sel, + ) + k_shape = (k_max, k_max) - delta_block = jnp.zeros(k_shape, dtype=dtype) + delta_block = jnp.zeros(k_shape, dtype=dtype_input) def _accumulate( delta: jnp.ndarray, pos: jnp.ndarray, valid: jnp.ndarray, weight: float, target: jnp.ndarray @@ -212,10 +271,10 @@ def _accumulate( updates = delta * pair_mask.astype(delta.dtype) * weight row_idx = jnp.broadcast_to(safe_pos[:, :, None], pair_mask.shape) col_idx = jnp.broadcast_to(safe_pos[:, None, :], pair_mask.shape) - flat_rows = row_idx.reshape(-1) - flat_cols = col_idx.reshape(-1) - flat_updates = updates.reshape(-1) - flat_mask = pair_mask.reshape(-1) + flat_rows = row_idx + flat_cols = col_idx + flat_updates = updates + flat_mask = pair_mask safe_rows = jnp.where(flat_mask, flat_rows, 0) safe_cols = jnp.where(flat_mask, flat_cols, 0) safe_vals = jnp.where(flat_mask, flat_updates, 0.0) @@ -223,9 +282,11 @@ def _accumulate( delta_block = _accumulate(delta_old, old_pos, old_valid, 1.0, delta_block) delta_block = _accumulate(delta_new, new_pos, new_valid, -1.0, delta_block) + delta_block = _accumulate(delta_shunt_old, shunt_old_pos, shunt_old_valid, 1.0, delta_block) + delta_block = _accumulate(delta_shunt_new, shunt_new_pos, shunt_new_valid, -1.0, delta_block) - theta_new_idx = angle_component_indices[new_bus_b_index] - mag_new_idx = magnitude_component_indices[new_bus_b_index] + theta_new_idx = jacobian_components.angle_component_indices[new_bus_b_index] + mag_new_idx = jacobian_components.magnitude_component_indices[new_bus_b_index] theta_new_pos = jnp.take(positions, jnp.where(theta_new_idx >= 0, theta_new_idx, 0), axis=0) mag_new_pos = jnp.take(positions, jnp.where(mag_new_idx >= 0, mag_new_idx, 0), axis=0) @@ -236,8 +297,8 @@ def _accumulate( theta_row = jnp.where(theta_mask, theta_new_pos, 0) mag_row = jnp.where(mag_mask, mag_new_pos, 0) - minus_one = jnp.asarray(-1.0, dtype=dtype) - zero_val = jnp.asarray(0.0, dtype=dtype) + minus_one = jnp.asarray(-1.0, dtype=dtype_input) + zero_val = jnp.asarray(0.0, dtype=dtype_input) theta_update = jnp.where(theta_mask, minus_one, zero_val) mag_update = jnp.where(mag_mask, minus_one, zero_val) delta_block = delta_block.at[theta_row, theta_row].add(theta_update) @@ -261,31 +322,22 @@ def compute_bsdf_update( new_bus_type: int, branches_connected_to_bus_b: Int[jnp.ndarray, " n_branches_B"], shunt_connected_to_bus_b: Int[jnp.ndarray, " n_shunts_B"], - branch_from: Int[jnp.ndarray, " n_branches"], - branch_to: Int[jnp.ndarray, " n_branches"], - shunt_to_bus: Int[jnp.ndarray, " n_shunts"], - v_mag_hat: Float[jnp.ndarray, " n_buses"], - theta_hat: Float[jnp.ndarray, " n_buses"], - y_ff: Complex128[jnp.ndarray, " n_branches"], - y_ft: Complex128[jnp.ndarray, " n_branches"], - y_tf: Complex128[jnp.ndarray, " n_branches"], - y_tt: Complex128[jnp.ndarray, " n_branches"], - y_shunt: Complex128[jnp.ndarray, " n_buses"], - angle_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], - magnitude_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], + network_topology: NetworkTopologyInputs, + voltage_state: VoltageStateInputs, + network_admittance: NetworkAdmittanceInputs, + jacobian_components: JacobianComponentInputs, ) -> Float[jnp.ndarray, " n_eq n_eq"]: - """Legacy dense inverse transpose update for BSDF. + """Inverse transpose update for BSDF. This function computes the updated Jacobian inverse transpose after a bus split with branch re-attachments, using a full-rank update approach. It is intended for reference and testing purposes, and is not optimized for performance. Note: currently not supported: - - Shunt reassignments - Changes in branch parameters (e.g., series admittance, taps, phase shifts) (This would involve computing the different delta_blocks) - Changes in the type of the new bus (e.g., PQ, PV, slack) - - injection reassignments + - Injection reassignments of regulating elements, resulting in a new PV bus Parameters ---------- @@ -301,52 +353,32 @@ def compute_bsdf_update( Indices of branches connected to the bus being split. shunt_connected_to_bus_b : Int[jnp.ndarray, " n_shunts_B"] Indices of shunts connected to the bus being split. - branch_from : Int[jnp.ndarray, " n_branches"] - "From" bus indices for all branches. - branch_to : Int[jnp.ndarray, " n_branches"] - "To" bus indices for all branches. - shunt_to_bus : Int[jnp.ndarray, " n_shunts"] - Bus indices for all shunts. - v_mag_hat : Float[jnp.ndarray, " n_buses"] - Voltage magnitude estimates for all buses. - theta_hat : Float[jnp.ndarray, " n_buses"] - Voltage angle estimates for all buses. - y_ff : Complex128[jnp.ndarray, " n_branches"] - "From-From" admittance for all branches. - y_ft : Complex128[jnp.ndarray, " n_branches"] - "From-To" admittance for all branches. - y_tf : Complex128[jnp.ndarray, " n_branches"] - "To-From" admittance for all branches. - y_tt : Complex128[jnp.ndarray, " n_branches"] - "To-To" admittance for all branches. - y_shunt : Complex128[jnp.ndarray, " n_buses"] - Shunt admittance for all buses. - angle_component_indices : Int[jnp.ndarray, " n_eq_jacobian"] - Mapping from bus indices to angle component indices in the Jacobian. - magnitude_component_indices : Int[jnp.ndarray, " n_eq_jacobian"] - Mapping from bus indices to magnitude component indices in the Jacobian. + network_topology : NetworkTopologyInputs + Network topology inputs providing `branch_from`, `branch_to`, `branch_connected`, + `shunt_to_bus`, and `shunt_connected` arrays used to select affected branches and shunts. + voltage_state : VoltageStateInputs + Voltage state inputs providing `bus_voltage_magnitudes` and `bus_voltage_angles_rad` + used to compute delta blocks for old and new attachments. + network_admittance : NetworkAdmittanceInputs + Network admittance inputs (`y_ff`, `y_ft`, `y_tf`, `y_tt`, `y_shunt`) used to + compute branch and shunt Jacobian contributions. + jacobian_components : JacobianComponentInputs + Component index mappings (`angle_component_indices`, `magnitude_component_indices`) + that map bus indices to Jacobian equation row indices. Returns ------- Float[jnp.ndarray, " n_eq n_eq"] Updated Jacobian inverse transpose after the bus split and branch re-attachments. """ - jacobian_arr_transposed = jnp.asarray(jacobian_inv_transposed) - real_dtype = jacobian_arr_transposed.dtype - return _compute_bsdf_update_impl( - jacobian_inv_transposed=jacobian_arr_transposed, + jacobian_inv_transposed=jacobian_inv_transposed, bus_to_split=int(bus_to_split), new_bus_b_index=int(new_bus_b_index), - branches_connected_to_bus_b=jnp.asarray(branches_connected_to_bus_b, dtype=jnp.int32), - branch_from=jnp.asarray(branch_from, dtype=jnp.int32), - branch_to=jnp.asarray(branch_to, dtype=jnp.int32), - v_mag_hat=jnp.asarray(v_mag_hat, dtype=real_dtype), - theta_hat=jnp.asarray(theta_hat, dtype=real_dtype), - y_ff=jnp.asarray(y_ff, dtype=jnp.complex128), - y_ft=jnp.asarray(y_ft, dtype=jnp.complex128), - y_tf=jnp.asarray(y_tf, dtype=jnp.complex128), - y_tt=jnp.asarray(y_tt, dtype=jnp.complex128), - angle_component_indices=jnp.asarray(angle_component_indices, dtype=jnp.int32), - magnitude_component_indices=jnp.asarray(magnitude_component_indices, dtype=jnp.int32), + branches_connected_to_bus_b=branches_connected_to_bus_b, + shunt_connected_to_bus_b=shunt_connected_to_bus_b, + network_topology=network_topology, + voltage_state=voltage_state, + network_admittance=network_admittance, + jacobian_components=jacobian_components, ) diff --git a/src/dc_plus/jax/injection_outage.py b/src/dc_plus/jax/injection_outage.py new file mode 100644 index 0000000..d25593a --- /dev/null +++ b/src/dc_plus/jax/injection_outage.py @@ -0,0 +1,553 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""One-step fixed-Jacobian state updates for injection outages and injection changes using JAX.""" + +import jax +import jax.numpy as jnp +from jax_dataclasses import pytree_dataclass +from jaxtyping import Array, Complex128, Float, Int + +from ..interfaces.network_inputs import ( + JacobianComponentInputs, + NetworkAdmittanceInputs, + NetworkTopologyInputs, + VoltageStateInputs, +) +from .lodf_branches import _compute_monitored_branch_currents, _prepare_monitored_branch_pack +from .lodf_voltages import build_monitor_rows +from .network_state_helper import _calculate_branch_complex_power + +# ruff: noqa: PLR0913 + + +@pytree_dataclass +class InjectionOutageMonitoredResults: + """One-step post-contingency results restricted to monitored elements.""" + + n_1_theta: Float[Array, "... n_contingencies n_buses_monitored"] + n_1_voltage: Float[Array, "... n_contingencies n_buses_monitored"] + + n_1_p_from: Float[Array, "... n_contingencies n_branches_monitored"] + n_1_p_to: Float[Array, "... n_contingencies n_branches_monitored"] + n_1_q_from: Float[Array, "... n_contingencies n_branches_monitored"] + n_1_q_to: Float[Array, "... n_contingencies n_branches_monitored"] + n_1_i_from: Complex128[Array, "... n_contingencies n_branches_monitored"] + n_1_i_to: Complex128[Array, "... n_contingencies n_branches_monitored"] + + +def _collect_single_injection_outage_mismatch( + outage_injection_indices: Int[jnp.ndarray, " n_outages"], + jacobian_size: int, + injection_to_bus: Int[jnp.ndarray, " n_injections"], + injection_active_power: Float[jnp.ndarray, " n_injections"], + injection_reactive_power: Float[jnp.ndarray, " n_injections"], + jacobian_components: JacobianComponentInputs, + dtype: jnp.dtype, +) -> Float[jnp.ndarray, " n_eq"]: + """Assemble the outage-induced mismatch vector for one contingency. + + Parameters + ---------- + outage_injection_indices : Int[jnp.ndarray, " n_outages"] + Indices of injections to disconnect for one contingency. Negative entries + are ignored and can be used as padding. + jacobian_size : int + Number of equations in the Jacobian system. + injection_to_bus : Int[jnp.ndarray, " n_injections"] + Map from injection index to hosting bus index. + injection_active_power : Float[jnp.ndarray, " n_injections"] + Active power associated with each injection. + injection_reactive_power : Float[jnp.ndarray, " n_injections"] + Reactive power associated with each injection. + jacobian_components : JacobianComponentInputs + Bus-to-Jacobian component mappings for the base operating point. + dtype : jnp.dtype + Target dtype of the assembled mismatch vector. + + Returns + ------- + Float[jnp.ndarray, " n_eq"] + Mismatch vector in Jacobian component ordering. + """ + valid_outage_mask = outage_injection_indices >= 0 + safe_outages = jnp.where(valid_outage_mask, outage_injection_indices, 0) + buses = injection_to_bus[safe_outages] + + mismatch = jnp.zeros((jacobian_size,), dtype=dtype) + + p_idx = jacobian_components.angle_component_indices[buses] + valid_p = valid_outage_mask & (p_idx >= 0) + safe_p_idx = jnp.where(valid_p, p_idx, 0) + p_values = jnp.where(valid_p, -injection_active_power[safe_outages], 0.0) + mismatch = mismatch.at[safe_p_idx].add(p_values) + + q_idx = jacobian_components.magnitude_component_indices[buses] + valid_q = valid_outage_mask & (q_idx >= 0) + safe_q_idx = jnp.where(valid_q, q_idx, 0) + q_values = jnp.where(valid_q, -injection_reactive_power[safe_outages], 0.0) + mismatch = mismatch.at[safe_q_idx].add(q_values) + + return mismatch + + +def _collect_injection_outage_mismatch( + jacobian_size: int, + outage_injection_indices: Int[jnp.ndarray, " n_contingencies n_outages"], + injection_to_bus: Int[jnp.ndarray, " n_injections"], + injection_active_power: Float[jnp.ndarray, " n_injections"], + injection_reactive_power: Float[jnp.ndarray, " n_injections"], + jacobian_components: JacobianComponentInputs, + dtype: jnp.dtype, +) -> Float[jnp.ndarray, " n_contingencies n_eq"]: + """Assemble outage-induced mismatch vectors in Jacobian ordering. + + Parameters + ---------- + jacobian_size : int + Number of equations in the Jacobian system. + outage_injection_indices : Int[jnp.ndarray, " n_contingencies n_outages"] + Batched injection outage indices. Each row represents one contingency and + may include negative padding entries. + injection_to_bus : Int[jnp.ndarray, " n_injections"] + Map from injection index to hosting bus index. + injection_active_power : Float[jnp.ndarray, " n_injections"] + Active power associated with each injection. + injection_reactive_power : Float[jnp.ndarray, " n_injections"] + Reactive power associated with each injection. + jacobian_components : JacobianComponentInputs + Bus-to-Jacobian component mappings for the base operating point. + dtype : jnp.dtype + Target dtype of the assembled mismatch matrix. + + Returns + ------- + Float[jnp.ndarray, " n_contingencies n_eq"] + Batched mismatch vectors in Jacobian component ordering. + """ + + def collect_single(outage_row: Int[jnp.ndarray, " n_outages"]) -> Float[jnp.ndarray, " n_eq"]: + """Assemble the mismatch vector for a single contingency.""" + return _collect_single_injection_outage_mismatch( + outage_injection_indices=outage_row, + jacobian_size=jacobian_size, + injection_to_bus=injection_to_bus, + injection_active_power=injection_active_power, + injection_reactive_power=injection_reactive_power, + jacobian_components=jacobian_components, + dtype=dtype, + ) + + return jax.vmap(collect_single)(outage_injection_indices) + + +@jax.jit +def _calculate_fixed_jacobian_dx( + jacobian_inv_transposed: Float[jnp.ndarray, " n_eq n_eq"], + mismatch: Float[jnp.ndarray, " ... n_eq"], +) -> Float[jnp.ndarray, " ... n_eq"]: + """Map one or more mismatch vectors to fixed-Jacobian state increments. + + Parameters + ---------- + jacobian_inv_transposed : Float[jnp.ndarray, " n_eq n_eq"] + Transposed inverse Jacobian at the base operating point. + mismatch : Float[jnp.ndarray, " ... n_eq"] + One or more mismatch vectors in Jacobian ordering. + + Returns + ------- + Float[jnp.ndarray, " ... n_eq"] + Fixed-Jacobian state increments with the same leading batch shape as ``mismatch``. + """ + return -(mismatch @ jacobian_inv_transposed) + + +def _collect_single_injection_change_mismatch( + injection_active_power_change: Float[jnp.ndarray, " n_injections"], + injection_reactive_power_change: Float[jnp.ndarray, " n_injections"], + jacobian_size: int, + injection_to_bus: Int[jnp.ndarray, " n_injections"], + jacobian_components: JacobianComponentInputs, + dtype: jnp.dtype, +) -> Float[jnp.ndarray, " n_eq"]: + """Assemble one arbitrary injection-change mismatch vector.""" + buses = injection_to_bus + mismatch = jnp.zeros((jacobian_size,), dtype=dtype) + + p_idx = jacobian_components.angle_component_indices[buses] + valid_p = p_idx >= 0 + safe_p_idx = jnp.where(valid_p, p_idx, 0) + p_values = jnp.where(valid_p, injection_active_power_change, 0.0) + mismatch = mismatch.at[safe_p_idx].add(p_values) + + q_idx = jacobian_components.magnitude_component_indices[buses] + valid_q = q_idx >= 0 + safe_q_idx = jnp.where(valid_q, q_idx, 0) + q_values = jnp.where(valid_q, injection_reactive_power_change, 0.0) + mismatch = mismatch.at[safe_q_idx].add(q_values) + + return mismatch + + +def _collect_injection_change_mismatch( + jacobian_size: int, + injection_active_power_changes: Float[jnp.ndarray, " n_timesteps n_injections"], + injection_reactive_power_changes: Float[jnp.ndarray, " n_timesteps n_injections"], + injection_to_bus: Int[jnp.ndarray, " n_injections"], + jacobian_components: JacobianComponentInputs, + dtype: jnp.dtype, +) -> Float[jnp.ndarray, " n_timesteps n_eq"]: + """Assemble arbitrary injection-change mismatch vectors for a full time series.""" + + def collect_single( + injection_active_power_change: Float[jnp.ndarray, " n_injections"], + injection_reactive_power_change: Float[jnp.ndarray, " n_injections"], + ) -> Float[jnp.ndarray, " n_eq"]: + """Assemble the mismatch vector for a single time step.""" + return _collect_single_injection_change_mismatch( + injection_active_power_change=injection_active_power_change, + injection_reactive_power_change=injection_reactive_power_change, + jacobian_size=jacobian_size, + injection_to_bus=injection_to_bus, + jacobian_components=jacobian_components, + dtype=dtype, + ) + + return jax.vmap(collect_single)(injection_active_power_changes, injection_reactive_power_changes) + + +@jax.jit +def _calculate_monitor_bus_state_updates( + jacobian_inv_transposed: Float[jnp.ndarray, " n_eq n_eq"], + mismatch: Float[jnp.ndarray, " n_contingencies n_eq"], + theta_rows: Int[jnp.ndarray, " n_mon_bus"], + vm_rows: Int[jnp.ndarray, " n_mon_bus"], + theta_mask: Float[jnp.ndarray, " n_mon_bus"], + vm_mask: Float[jnp.ndarray, " n_mon_bus"], +) -> tuple[ + Float[jnp.ndarray, " n_contingencies n_mon_bus"], + Float[jnp.ndarray, " n_contingencies n_mon_bus"], +]: + """Map outage mismatch vectors to monitored bus state increments only. + + Parameters + ---------- + jacobian_inv_transposed : Float[jnp.ndarray, " n_eq n_eq"] + Transposed inverse Jacobian at the base operating point. + mismatch : Float[jnp.ndarray, " n_contingencies n_eq"] + Batched outage-induced mismatch vectors. + theta_rows : Int[jnp.ndarray, " n_mon_bus"] + Safe Jacobian row indices for monitored bus angles. + vm_rows : Int[jnp.ndarray, " n_mon_bus"] + Safe Jacobian row indices for monitored bus magnitudes. + theta_mask : Float[jnp.ndarray, " n_mon_bus"] + Mask for monitored buses with angle states in the Jacobian. + vm_mask : Float[jnp.ndarray, " n_mon_bus"] + Mask for monitored buses with magnitude states in the Jacobian. + + Returns + ------- + tuple[Float[jnp.ndarray, " n_contingencies n_mon_bus"], Float[jnp.ndarray, " n_contingencies n_mon_bus"]] + Monitored angle and magnitude increments for each contingency. + """ + dtype = jacobian_inv_transposed.dtype + theta_mask_d = theta_mask.astype(dtype) + vm_mask_d = vm_mask.astype(dtype) + theta_dx = ( + _calculate_fixed_jacobian_dx( + jacobian_inv_transposed=jacobian_inv_transposed[:, theta_rows], + mismatch=mismatch, + ) + * theta_mask_d[None, :] + ) + vm_dx = ( + _calculate_fixed_jacobian_dx( + jacobian_inv_transposed=jacobian_inv_transposed[:, vm_rows], + mismatch=mismatch, + ) + * vm_mask_d[None, :] + ) + return theta_dx, vm_dx + + +def non_voltage_regulating_injection_outage_monitor_buses( + jacobian_inv_transposed: Float[jnp.ndarray, " n_eq n_eq"], + outage_injection_indices: Int[jnp.ndarray, " n_contingencies n_outages"], + injection_to_bus: Int[jnp.ndarray, " n_injections"], + injection_active_power: Float[jnp.ndarray, " n_injections"], + injection_reactive_power: Float[jnp.ndarray, " n_injections"], + jacobian_components: JacobianComponentInputs, + monitor_bus_indices: Int[jnp.ndarray, " n_mon_bus"], + voltage_state: VoltageStateInputs, +) -> tuple[ + Float[jnp.ndarray, " n_contingencies n_mon_bus"], + Float[jnp.ndarray, " n_contingencies n_mon_bus"], +]: + """Compute monitored post-contingency bus voltages for injection outages only. + + Parameters + ---------- + jacobian_inv_transposed : Float[jnp.ndarray, " n_eq n_eq"] + Base transposed inverse Jacobian at the hot-start operating point. + outage_injection_indices : Int[jnp.ndarray, " n_contingencies n_outages"] + Batched indices of injections to disconnect. Each row is one contingency. + injection_to_bus : Int[jnp.ndarray, " n_injections"] + Map from injection index to hosting bus index. + injection_active_power : Float[jnp.ndarray, " n_injections"] + Active power of each injection at the hot-start operating point. + injection_reactive_power : Float[jnp.ndarray, " n_injections"] + Reactive power of each injection at the hot-start operating point. + jacobian_components : JacobianComponentInputs + Bus-to-Jacobian component mappings for the base operating point. + monitor_bus_indices : Int[jnp.ndarray, " n_mon_bus"] + Indices of buses whose post-contingency voltages should be returned. + voltage_state : VoltageStateInputs + Base-case voltage magnitudes and angles for all buses. + + Returns + ------- + tuple[Float[jnp.ndarray, " n_contingencies n_mon_bus"], Float[jnp.ndarray, " n_contingencies n_mon_bus"]] + Post-contingency monitored voltage angles and magnitudes. + """ + dtype = jacobian_inv_transposed.dtype + + mismatch = _collect_injection_outage_mismatch( + jacobian_size=jacobian_inv_transposed.shape[0], + outage_injection_indices=outage_injection_indices, + injection_to_bus=injection_to_bus, + injection_active_power=injection_active_power, + injection_reactive_power=injection_reactive_power, + jacobian_components=jacobian_components, + dtype=dtype, + ) + theta_rows, vm_rows, theta_mask, vm_mask = build_monitor_rows( + jacobian_components=jacobian_components, + monitor_bus_indices=monitor_bus_indices, + ) + theta_dx, vm_dx = _calculate_monitor_bus_state_updates( + jacobian_inv_transposed=jacobian_inv_transposed, + mismatch=mismatch, + theta_rows=theta_rows, + vm_rows=vm_rows, + theta_mask=theta_mask, + vm_mask=vm_mask, + ) + base_theta0 = jnp.take(voltage_state.bus_voltage_angles_rad, monitor_bus_indices, axis=0).astype(dtype) + base_vm0 = jnp.take(voltage_state.bus_voltage_magnitudes, monitor_bus_indices, axis=0).astype(dtype) + return base_theta0[None, :] + theta_dx, base_vm0[None, :] + vm_dx + + +def non_voltage_regulating_injection_outage_monitored( + jacobian_inv_transposed: Float[jnp.ndarray, " n_eq n_eq"], + outage_injection_indices: Int[jnp.ndarray, " n_contingencies n_outages"], + injection_to_bus: Int[jnp.ndarray, " n_injections"], + injection_active_power: Float[jnp.ndarray, " n_injections"], + injection_reactive_power: Float[jnp.ndarray, " n_injections"], + jacobian_components: JacobianComponentInputs, + monitor_bus_indices: Int[jnp.ndarray, " n_mon_bus"], + voltage_state: VoltageStateInputs, + network_topology: NetworkTopologyInputs, + network_admittance: NetworkAdmittanceInputs, + monitor_branch_indices: Int[jnp.ndarray, " n_mon_br"], + bus_to_mon_index: Int[jnp.ndarray, " n_buses"], +) -> InjectionOutageMonitoredResults: + """Compute monitored bus states and branch flows for injection outages. + + Parameters + ---------- + jacobian_inv_transposed : Float[jnp.ndarray, " n_eq n_eq"] + Base transposed inverse Jacobian at the hot-start operating point. + outage_injection_indices : Int[jnp.ndarray, " n_contingencies n_outages"] + Batched indices of injections to disconnect. Each row is one contingency. + injection_to_bus : Int[jnp.ndarray, " n_injections"] + Map from injection index to hosting bus index. + injection_active_power : Float[jnp.ndarray, " n_injections"] + Active power of each injection at the hot-start operating point. + injection_reactive_power : Float[jnp.ndarray, " n_injections"] + Reactive power of each injection at the hot-start operating point. + jacobian_components : JacobianComponentInputs + Bus-to-Jacobian component mappings for the base operating point. + monitor_bus_indices : Int[jnp.ndarray, " n_mon_bus"] + Indices of buses whose post-contingency voltages should be returned. + voltage_state : VoltageStateInputs + Base-case voltage magnitudes and angles for all buses. + network_topology : NetworkTopologyInputs + Network topology inputs for the base operating point. + network_admittance : NetworkAdmittanceInputs + Network admittance inputs for the base operating point. + monitor_branch_indices : Int[jnp.ndarray, " n_mon_br"] + Indices of branches whose post-contingency flows should be returned. + bus_to_mon_index : Int[jnp.ndarray, " n_buses"] + Map from bus index to monitored bus index. -1 if the bus is not monitored. + + Returns + ------- + InjectionOutageMonitoredResults + Post-contingency monitored bus states and monitored branch flows. + """ + dtype = jacobian_inv_transposed.dtype + + theta_all, vm_all = non_voltage_regulating_injection_outage_monitor_buses( + jacobian_inv_transposed=jacobian_inv_transposed, + outage_injection_indices=outage_injection_indices, + injection_to_bus=injection_to_bus, + injection_active_power=injection_active_power, + injection_reactive_power=injection_reactive_power, + jacobian_components=jacobian_components, + monitor_bus_indices=monitor_bus_indices, + voltage_state=voltage_state, + ) + ( + _, + y_ff_mon, + y_ft_mon, + y_tf_mon, + y_tt_mon, + f_pos_safe, + t_pos_safe, + end_mask, + ) = _prepare_monitored_branch_pack( + network_topology=network_topology, + network_admittance=network_admittance, + monitor_branch_indices=monitor_branch_indices, + bus_to_mon_index=bus_to_mon_index, + dtype=dtype, + ) + v_from_all, v_to_all, i_from_all, i_to_all = _compute_monitored_branch_currents( + theta_all=theta_all, + vm_all=vm_all, + f_pos_safe=f_pos_safe, + t_pos_safe=t_pos_safe, + y_ff_mon=y_ff_mon, + y_ft_mon=y_ft_mon, + y_tf_mon=y_tf_mon, + y_tt_mon=y_tt_mon, + end_mask=end_mask, + ) + complex_dtype = y_ff_mon.dtype + end_mask_complex = end_mask.astype(complex_dtype)[None, :] + end_mask_real = end_mask.astype(dtype)[None, :] + s_from_all, s_to_all = _calculate_branch_complex_power( + v_from=v_from_all, + v_to=v_to_all, + current_from=i_from_all, + current_to=i_to_all, + branch_mask=end_mask, + ) + return InjectionOutageMonitoredResults( + n_1_theta=theta_all, + n_1_voltage=vm_all, + n_1_p_from=s_from_all.real.astype(dtype) * end_mask_real, + n_1_p_to=s_to_all.real.astype(dtype) * end_mask_real, + n_1_q_from=s_from_all.imag.astype(dtype) * end_mask_real, + n_1_q_to=s_to_all.imag.astype(dtype) * end_mask_real, + n_1_i_from=i_from_all * end_mask_complex, + n_1_i_to=i_to_all * end_mask_complex, + ) + + +def non_voltage_regulating_injection_outage_dx( + jacobian_inv_transposed: Float[jnp.ndarray, " n_eq n_eq"], + outage_injection_indices: Int[jnp.ndarray, " n_contingencies n_outages"], + injection_to_bus: Int[jnp.ndarray, " n_injections"], + injection_active_power: Float[jnp.ndarray, " n_injections"], + injection_reactive_power: Float[jnp.ndarray, " n_injections"], + jacobian_components: JacobianComponentInputs, +) -> Float[jnp.ndarray, " n_contingencies n_eq"]: + """One-step state increment caused by disconnecting non voltage regulating injections. + + The Jacobian is unchanged for a pure injection outage because network + topology and admittances remain fixed. The outage only changes the nodal + power mismatch at the buses hosting the disconnected injections. + + This helper assumes the bus type pattern used by the Jacobian remains + unchanged across the outage. That is appropriate for load outages and other + contingencies that do not alter voltage-control structure. + + Note: this function still works for voltage regulating injection outages, + but the PV->PQ bus type change is not accounted for. + + Parameters + ---------- + jacobian_inv_transposed : Float[jnp.ndarray, " n_eq n_eq"] + Base transposed inverse Jacobian at the hot-start operating point. + outage_injection_indices : Int[jnp.ndarray, " n_contingencies n_outages"] + Batched indices of injections to disconnect. Each row is one contingency. + injection_to_bus : Int[jnp.ndarray, " n_injections"] + Map from injection index to hosting bus index. + injection_active_power : Float[jnp.ndarray, " n_injections"] + Active power of each injection at the hot-start operating point. + injection_reactive_power : Float[jnp.ndarray, " n_injections"] + Reactive power of each injection at the hot-start operating point. + jacobian_components : JacobianComponentInputs + Bus-to-Jacobian component mappings for the base operating point. + + Returns + ------- + Float[jnp.ndarray, " n_contingencies n_eq"] + One-step state increment for each outage in Jacobian ordering. + """ + dtype = jacobian_inv_transposed.dtype + mismatch = _collect_injection_outage_mismatch( + jacobian_size=jacobian_inv_transposed.shape[0], + outage_injection_indices=outage_injection_indices, + injection_to_bus=injection_to_bus, + injection_active_power=injection_active_power, + injection_reactive_power=injection_reactive_power, + jacobian_components=jacobian_components, + dtype=dtype, + ) + return _calculate_fixed_jacobian_dx(jacobian_inv_transposed=jacobian_inv_transposed, mismatch=mismatch) + + +def non_voltage_regulating_injection_changes_dx( + jacobian_inv_transposed: Float[jnp.ndarray, " n_eq n_eq"], + injection_active_power_changes: Float[jnp.ndarray, " n_timesteps n_injections"], + injection_reactive_power_changes: Float[jnp.ndarray, " n_timesteps n_injections"], + injection_to_bus: Int[jnp.ndarray, " n_injections"], + jacobian_components: JacobianComponentInputs, +) -> Float[jnp.ndarray, " n_timesteps n_eq"]: + """One-step fixed-Jacobian state increments for batched injection changes. + + Each row in ``injection_active_power_changes`` and ``injection_reactive_power_changes`` + represents one time step or scenario. All rows are assembled into mismatch vectors and + solved in parallel through the same fixed inverse Jacobian: + + ``dx = -(mismatch @ jacobian_inv_transposed)`` + + The injection deltas must use the same sign convention as the imported + dynamic network data: loads are positive and generators are negative. + + Parameters + ---------- + jacobian_inv_transposed : Float[jnp.ndarray, " n_eq n_eq"] + Base transposed inverse Jacobian at the hot-start operating point. + injection_active_power_changes : Float[jnp.ndarray, " n_timesteps n_injections"] + Batched active-power deltas for each injection. + injection_reactive_power_changes : Float[jnp.ndarray, " n_timesteps n_injections"] + Batched reactive-power deltas for each injection. + injection_to_bus : Int[jnp.ndarray, " n_injections"] + Map from injection index to hosting bus index. + jacobian_components : JacobianComponentInputs + Bus-to-Jacobian component mappings for the base operating point. + + Returns + ------- + Float[jnp.ndarray, " n_timesteps n_eq"] + Batched one-step state increments in Jacobian ordering. + """ + dtype = jacobian_inv_transposed.dtype + mismatch = _collect_injection_change_mismatch( + jacobian_size=jacobian_inv_transposed.shape[0], + injection_active_power_changes=injection_active_power_changes, + injection_reactive_power_changes=injection_reactive_power_changes, + injection_to_bus=injection_to_bus, + jacobian_components=jacobian_components, + dtype=dtype, + ) + return _calculate_fixed_jacobian_dx(jacobian_inv_transposed=jacobian_inv_transposed, mismatch=mismatch) diff --git a/src/dc_plus/jax/lodf_branches.py b/src/dc_plus/jax/lodf_branches.py index 02d4ed2..a1b2053 100644 --- a/src/dc_plus/jax/lodf_branches.py +++ b/src/dc_plus/jax/lodf_branches.py @@ -9,12 +9,18 @@ from typing import Tuple -import jax import jax.numpy as jnp from jax_dataclasses import pytree_dataclass -from jaxtyping import Array, Complex128, Float, Int - +from jaxtyping import Array, Bool, Complex128, Float, Int + +from ..interfaces.network_inputs import ( + JacobianComponentInputs, + NetworkAdmittanceInputs, + NetworkTopologyInputs, + VoltageStateInputs, +) from .lodf_voltages import line_outage_post_contingency_voltages +from .network_state_helper import _calculate_branch_complex_power # ruff: noqa: PLR0913 @@ -34,117 +40,9 @@ class SolverLoadflowResults: n_1_i_to: Float[Array, "... n_outages n_branches_monitored"] -def _prepare_voltages_for_currents_not_linearized( - theta_post: Float[jnp.ndarray, " n_mon_bus"], - vm_post: Float[jnp.ndarray, " n_mon_bus"], - dtype: jnp.dtype, - complex_dtype: jnp.dtype, -) -> Complex128[jnp.ndarray, " n_mon_bus"]: - """Build complex monitored-bus voltages from NR state updates. - - Parameters ---------- - theta_post: Float[jnp.ndarray, " n_mon_bus"] - Post-contingency voltage angle updates for monitored buses. - vm_post: Float[jnp.ndarray, " n_mon_bus"] - Post-contingency voltage magnitude updates for monitored buses. - dtype: jnp.dtype - Data type for intermediate computations. - complex_dtype: jnp.dtype - Complex data type for the output voltages. - - Returns - ------- - Complex128[jnp.ndarray, " n_mon_bus"] - Complex voltage values for monitored buses, ready for current calculations. - """ - one_j = jnp.asarray(1j, dtype=complex_dtype) - theta_post_real = theta_post.astype(dtype) - vm_post_real = vm_post.astype(dtype) - return vm_post_real * (jnp.cos(theta_post_real) + one_j * jnp.sin(theta_post_real)) - - -def _calculate_branch_currents( - v_post: Complex128[jnp.ndarray, " n_mon_bus"], - f_pos_safe: Int[jnp.ndarray, " n_mon_br"], - t_pos_safe: Int[jnp.ndarray, " n_mon_br"], - y_ff_mon: Complex128[jnp.ndarray, " n_mon_br"], - y_ft_mon: Complex128[jnp.ndarray, " n_mon_br"], - y_tf_mon: Complex128[jnp.ndarray, " n_mon_br"], - y_tt_mon: Complex128[jnp.ndarray, " n_mon_br"], -) -> Tuple[ - Complex128[jnp.ndarray, " n_mon_br"], - Complex128[jnp.ndarray, " n_mon_br"], - Complex128[jnp.ndarray, " n_mon_br"], - Complex128[jnp.ndarray, " n_mon_br"], -]: - """Gather branch endpoint voltages and compute monitored currents. - - Parameters - ---------- - v_post: Complex128[jnp.ndarray, " n_mon_bus"] - Complex voltage values for monitored buses. - f_pos_safe: Int[jnp.ndarray, " n_mon_br"] - Safe from bus position indices for monitored branches. - t_pos_safe: Int[jnp.ndarray, " n_mon_br"] - Safe to bus position indices for monitored branches. - y_ff_mon: Complex128[jnp.ndarray, " n_mon_br"] - Monitored branch admittance from-from components. - y_ft_mon: Complex128[jnp.ndarray, " n_mon_br"] - Monitored branch admittance from-to components. - y_tf_mon: Complex128[jnp.ndarray, " n_mon_br"] - Monitored branch admittance to-from components. - y_tt_mon: Complex128[jnp.ndarray, " n_mon_br"] - Monitored branch admittance to-to components. - - Returns - ------- - Tuple containing: - - - """ - v_from = v_post[f_pos_safe] - v_to = v_post[t_pos_safe] - - current_from = y_ff_mon * v_from + y_ft_mon * v_to - current_to = y_tf_mon * v_from + y_tt_mon * v_to - - return v_from, v_to, current_from, current_to - - -def _compute_complex_branch_powers( - v_from: Complex128[jnp.ndarray, " n_mon_br"], - v_to: Complex128[jnp.ndarray, " n_mon_br"], - current_from: Complex128[jnp.ndarray, " n_mon_br"], - current_to: Complex128[jnp.ndarray, " n_mon_br"], - end_mask: Float[jnp.ndarray, " n_mon_br"], - mon_br: Int[jnp.ndarray, " n_mon_br"], - outage_idx: Int[jnp.ndarray, ""], -) -> Tuple[ - Complex128[jnp.ndarray, " n_mon_br"], - Complex128[jnp.ndarray, " n_mon_br"], -]: - """Compute complex power flow endpoints for monitored branches.""" - end_mask_complex = end_mask.astype(v_from.dtype) - - s_from = v_from * jnp.conj(current_from) * end_mask_complex - s_to = v_to * jnp.conj(current_to) * end_mask_complex - - is_outaged = mon_br == outage_idx - zeros = jnp.zeros_like(s_from) - - s_from = jnp.where(is_outaged, zeros, s_from) - s_to = jnp.where(is_outaged, zeros, s_to) - - return s_from, s_to - - def _prepare_monitored_branch_pack( - branch_from: Int[jnp.ndarray, " n_branches"], - branch_to: Int[jnp.ndarray, " n_branches"], - y_ff: Complex128[jnp.ndarray, " n_branches"], - y_ft: Complex128[jnp.ndarray, " n_branches"], - y_tf: Complex128[jnp.ndarray, " n_branches"], - y_tt: Complex128[jnp.ndarray, " n_branches"], + network_topology: NetworkTopologyInputs, + network_admittance: NetworkAdmittanceInputs, monitor_branch_indices: Int[jnp.ndarray, " n_mon_br"], bus_to_mon_index: Int[jnp.ndarray, " n_buses"], dtype: jnp.dtype, @@ -162,12 +60,10 @@ def _prepare_monitored_branch_pack( Parameters ---------- - branch_from: Int[jnp.ndarray, " n_branches"] - From bus indices for all branches. - branch_to: Int[jnp.ndarray, " n_branches"] - To bus indices for all branches. - y_ff, y_ft, y_tf, y_tt: Complex128[jnp.ndarray, " n_branches"] - Pi-model admittance components for all branches. + network_topology : NetworkTopologyInputs + Network topology inputs for the base operating point. + network_admittance : NetworkAdmittanceInputs + Network admittance inputs for the base operating point. monitor_branch_indices: Int[jnp.ndarray, " n_mon_br"] Indices of monitored branches. bus_to_mon_index: Int[jnp.ndarray, " n_buses"] @@ -187,14 +83,14 @@ def _prepare_monitored_branch_pack( - end_mask: Float[jnp.ndarray, " n_mon_br"] Mask indicating which monitored branches have both endpoints monitored. """ - mon_br = jnp.asarray(monitor_branch_indices, dtype=jnp.int32) - y_ff_mon = jnp.take(y_ff, mon_br, axis=0) - y_ft_mon = jnp.take(y_ft, mon_br, axis=0) - y_tf_mon = jnp.take(y_tf, mon_br, axis=0) - y_tt_mon = jnp.take(y_tt, mon_br, axis=0) - - f_bus = jnp.take(branch_from, mon_br, axis=0) - t_bus = jnp.take(branch_to, mon_br, axis=0) + mon_br = monitor_branch_indices + y_ff_mon = jnp.take(network_admittance.y_ff, mon_br, axis=0) + y_ft_mon = jnp.take(network_admittance.y_ft, mon_br, axis=0) + y_tf_mon = jnp.take(network_admittance.y_tf, mon_br, axis=0) + y_tt_mon = jnp.take(network_admittance.y_tt, mon_br, axis=0) + + f_bus = jnp.take(network_topology.branch_from, mon_br, axis=0) + t_bus = jnp.take(network_topology.branch_to, mon_br, axis=0) f_pos = jnp.take(bus_to_mon_index, f_bus, axis=0) t_pos = jnp.take(bus_to_mon_index, t_bus, axis=0) @@ -225,14 +121,14 @@ def _compute_monitored_branch_currents( y_ft_mon: Complex128[jnp.ndarray, " n_mon_br"], y_tf_mon: Complex128[jnp.ndarray, " n_mon_br"], y_tt_mon: Complex128[jnp.ndarray, " n_mon_br"], - dtype: jnp.dtype, + end_mask: Float[jnp.ndarray, " n_mon_br"], ) -> Tuple[ Complex128[jnp.ndarray, " n_outages n_mon_br"], Complex128[jnp.ndarray, " n_outages n_mon_br"], Complex128[jnp.ndarray, " n_outages n_mon_br"], Complex128[jnp.ndarray, " n_outages n_mon_br"], ]: - """Vectorized monitored branch current computation for all outages. + """Compute monitored branch endpoint voltages and currents for all outages. Parameters ---------- @@ -246,8 +142,8 @@ def _compute_monitored_branch_currents( Safe to bus position indices for monitored branches. y_ff_mon, y_ft_mon, y_tf_mon, y_tt_mon: Complex128[jnp.ndarray, " n_mon_br"] Admittance components for monitored branches. - dtype: jnp.dtype - Data type for intermediate computations. + end_mask: Float[jnp.ndarray, " n_mon_br"] + Mask for monitored branches whose endpoints are available in ``theta_all``/``vm_all``. Returns ------- @@ -261,50 +157,29 @@ def _compute_monitored_branch_currents( - i_to_all: Complex128[jnp.ndarray, " n_outages n_mon_br"] Currents at the to buses of monitored branches for all outages. """ - complex_dtype = y_ff_mon.dtype - - def _per_outage( - theta_post: jnp.ndarray, vm_post: jnp.ndarray - ) -> Tuple[ - Complex128[jnp.ndarray, " n_mon_br"], - Complex128[jnp.ndarray, " n_mon_br"], - Complex128[jnp.ndarray, " n_mon_br"], - Complex128[jnp.ndarray, " n_mon_br"], - ]: - v_post = _prepare_voltages_for_currents_not_linearized( - theta_post=theta_post, - vm_post=vm_post, - dtype=dtype, - complex_dtype=complex_dtype, - ) - return _calculate_branch_currents( - v_post=v_post, - f_pos_safe=f_pos_safe, - t_pos_safe=t_pos_safe, - y_ff_mon=y_ff_mon, - y_ft_mon=y_ft_mon, - y_tf_mon=y_tf_mon, - y_tt_mon=y_tt_mon, - ) - - return jax.vmap(_per_outage)(theta_all, vm_all) + complex_dtype = jnp.result_type(theta_all, vm_all, y_ff_mon, y_ft_mon, y_tf_mon, y_tt_mon, 1j) + one_j = jnp.asarray(1j, dtype=complex_dtype) + voltage_all = vm_all.astype(complex_dtype) * (jnp.cos(theta_all) + one_j * jnp.sin(theta_all)) + + v_from_all = jnp.take(voltage_all, f_pos_safe, axis=1) + v_to_all = jnp.take(voltage_all, t_pos_safe, axis=1) + + mask_complex = end_mask.astype(complex_dtype)[None, :] + i_from_all = (v_from_all * y_ff_mon[None, :] + v_to_all * y_ft_mon[None, :]) * mask_complex + i_to_all = (v_from_all * y_tf_mon[None, :] + v_to_all * y_tt_mon[None, :]) * mask_complex + return v_from_all, v_to_all, i_from_all, i_to_all def line_outage_post_contingency_voltages_current( jacobian_inv_transposed: Float[jnp.ndarray, " n_eq n_eq"], outage_branch_idx: Int[jnp.ndarray, " n_outages"], - branch_from: Int[jnp.ndarray, " n_branches"], - branch_to: Int[jnp.ndarray, " n_branches"], - v_mag_hat: Float[jnp.ndarray, " n_buses"], - theta_hat: Float[jnp.ndarray, " n_buses"], - angle_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], - magnitude_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], - y_ff: Complex128[jnp.ndarray, " n_branches"], - y_ft: Complex128[jnp.ndarray, " n_branches"], - y_tf: Complex128[jnp.ndarray, " n_branches"], - y_tt: Complex128[jnp.ndarray, " n_branches"], + jacobian_components: JacobianComponentInputs, + network_topology: NetworkTopologyInputs, + voltage_state: VoltageStateInputs, + network_admittance: NetworkAdmittanceInputs, monitor_bus_indices: Int[jnp.ndarray, " n_mon_bus"], branch_pq_base: Float[jnp.ndarray, " n_branches 4"], + lower_residual_uses_reactive_mismatch: Bool[jnp.ndarray, " n_buses"], monitor_branch_indices: Int[jnp.ndarray, " n_mon_br"], bus_to_mon_index: Int[jnp.ndarray, " n_buses"], ) -> Tuple[ @@ -314,50 +189,23 @@ def line_outage_post_contingency_voltages_current( Complex128[jnp.ndarray, " n_outages n_mon_br"], ]: """Compute post-contingency monitored bus voltages and branch currents.""" - jacobian_inv_transposed = jnp.asarray(jacobian_inv_transposed) dtype = jacobian_inv_transposed.dtype - outage_branch_idx = jnp.asarray(outage_branch_idx, dtype=jnp.int32) - monitor_bus_indices = jnp.asarray(monitor_bus_indices, dtype=jnp.int32) - - branch_from = jnp.asarray(branch_from, dtype=jnp.int32) - branch_to = jnp.asarray(branch_to, dtype=jnp.int32) - v_mag_hat = jnp.asarray(v_mag_hat, dtype=dtype) - theta_hat = jnp.asarray(theta_hat, dtype=dtype) - angle_component_indices = jnp.asarray(angle_component_indices, dtype=jnp.int32) - magnitude_component_indices = jnp.asarray(magnitude_component_indices, dtype=jnp.int32) - y_ff = jnp.asarray(y_ff) - y_ft = jnp.asarray(y_ft) - y_tf = jnp.asarray(y_tf) - y_tt = jnp.asarray(y_tt) - branch_pq_base = jnp.asarray(branch_pq_base, dtype=dtype) - monitor_branch_indices = jnp.asarray(monitor_branch_indices, dtype=jnp.int32) - bus_to_mon_index = jnp.asarray(bus_to_mon_index, dtype=jnp.int32) - theta_all, vm_all = line_outage_post_contingency_voltages( jacobian_inv_transposed=jacobian_inv_transposed, outage_branch_idx=outage_branch_idx, - branch_from=branch_from, - branch_to=branch_to, - v_mag_hat=v_mag_hat, - theta_hat=theta_hat, - angle_component_indices=angle_component_indices, - magnitude_component_indices=magnitude_component_indices, - y_ff=y_ff, - y_ft=y_ft, - y_tf=y_tf, - y_tt=y_tt, + jacobian_components=jacobian_components, + network_topology=network_topology, + voltage_state=voltage_state, + network_admittance=network_admittance, monitor_bus_indices=monitor_bus_indices, branch_pq_base=branch_pq_base, + lower_residual_uses_reactive_mismatch=lower_residual_uses_reactive_mismatch, ) - _, y_ff_mon, y_ft_mon, y_tf_mon, y_tt_mon, f_pos_safe, t_pos_safe, _ = _prepare_monitored_branch_pack( - branch_from=branch_from, - branch_to=branch_to, - y_ff=y_ff, - y_ft=y_ft, - y_tf=y_tf, - y_tt=y_tt, + _, y_ff_mon, y_ft_mon, y_tf_mon, y_tt_mon, f_pos_safe, t_pos_safe, end_mask = _prepare_monitored_branch_pack( + network_topology=network_topology, + network_admittance=network_admittance, monitor_branch_indices=monitor_branch_indices, bus_to_mon_index=bus_to_mon_index, dtype=dtype, @@ -372,7 +220,7 @@ def line_outage_post_contingency_voltages_current( y_ft_mon=y_ft_mon, y_tf_mon=y_tf_mon, y_tt_mon=y_tt_mon, - dtype=dtype, + end_mask=end_mask, ) return theta_all, vm_all, i_from_all, i_to_all @@ -381,57 +229,29 @@ def line_outage_post_contingency_voltages_current( def line_outage_post_contingency_monitored( jacobian_inv_transposed: Float[jnp.ndarray, " n_eq n_eq"], outage_branch_idx: Int[jnp.ndarray, " n_outages"], - branch_from: Int[jnp.ndarray, " n_branches"], - branch_to: Int[jnp.ndarray, " n_branches"], - v_mag_hat: Float[jnp.ndarray, " n_buses"], - theta_hat: Float[jnp.ndarray, " n_buses"], - angle_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], - magnitude_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], - y_ff: Complex128[jnp.ndarray, " n_branches"], - y_ft: Complex128[jnp.ndarray, " n_branches"], - y_tf: Complex128[jnp.ndarray, " n_branches"], - y_tt: Complex128[jnp.ndarray, " n_branches"], + jacobian_components: JacobianComponentInputs, + network_topology: NetworkTopologyInputs, + voltage_state: VoltageStateInputs, + network_admittance: NetworkAdmittanceInputs, monitor_bus_indices: Int[jnp.ndarray, " n_mon_bus"], branch_pq_base: Float[jnp.ndarray, " n_branches 4"], + lower_residual_uses_reactive_mismatch: Bool[jnp.ndarray, " n_buses"], monitor_branch_indices: Int[jnp.ndarray, " n_mon_br"], bus_to_mon_index: Int[jnp.ndarray, " n_buses"], ) -> SolverLoadflowResults: """Compute post-contingency bus states and monitored branch powers.""" - jacobian_inv_transposed = jnp.asarray(jacobian_inv_transposed) dtype = jacobian_inv_transposed.dtype - outage_branch_idx = jnp.asarray(outage_branch_idx, dtype=jnp.int32) - monitor_bus_indices = jnp.asarray(monitor_bus_indices, dtype=jnp.int32) - - branch_from = jnp.asarray(branch_from, dtype=jnp.int32) - branch_to = jnp.asarray(branch_to, dtype=jnp.int32) - v_mag_hat = jnp.asarray(v_mag_hat, dtype=dtype) - theta_hat = jnp.asarray(theta_hat, dtype=dtype) - angle_component_indices = jnp.asarray(angle_component_indices, dtype=jnp.int32) - magnitude_component_indices = jnp.asarray(magnitude_component_indices, dtype=jnp.int32) - y_ff = jnp.asarray(y_ff) - y_ft = jnp.asarray(y_ft) - y_tf = jnp.asarray(y_tf) - y_tt = jnp.asarray(y_tt) - branch_pq_base = jnp.asarray(branch_pq_base, dtype=dtype) - monitor_branch_indices = jnp.asarray(monitor_branch_indices, dtype=jnp.int32) - bus_to_mon_index = jnp.asarray(bus_to_mon_index, dtype=jnp.int32) - theta_all, vm_all = line_outage_post_contingency_voltages( jacobian_inv_transposed=jacobian_inv_transposed, outage_branch_idx=outage_branch_idx, - branch_from=branch_from, - branch_to=branch_to, - v_mag_hat=v_mag_hat, - theta_hat=theta_hat, - angle_component_indices=angle_component_indices, - magnitude_component_indices=magnitude_component_indices, - y_ff=y_ff, - y_ft=y_ft, - y_tf=y_tf, - y_tt=y_tt, + jacobian_components=jacobian_components, + network_topology=network_topology, + voltage_state=voltage_state, + network_admittance=network_admittance, monitor_bus_indices=monitor_bus_indices, branch_pq_base=branch_pq_base, + lower_residual_uses_reactive_mismatch=lower_residual_uses_reactive_mismatch, ) ( @@ -444,12 +264,8 @@ def line_outage_post_contingency_monitored( t_pos_safe, end_mask, ) = _prepare_monitored_branch_pack( - branch_from=branch_from, - branch_to=branch_to, - y_ff=y_ff, - y_ft=y_ft, - y_tf=y_tf, - y_tt=y_tt, + network_topology=network_topology, + network_admittance=network_admittance, monitor_branch_indices=monitor_branch_indices, bus_to_mon_index=bus_to_mon_index, dtype=dtype, @@ -464,15 +280,18 @@ def line_outage_post_contingency_monitored( y_ft_mon=y_ft_mon, y_tf_mon=y_tf_mon, y_tt_mon=y_tt_mon, - dtype=dtype, + end_mask=end_mask, ) - complex_dtype = y_ff_mon.dtype - end_mask_complex = end_mask.astype(complex_dtype)[None, :] end_mask_real = end_mask.astype(dtype)[None, :] - s_from_all = v_from_all * jnp.conj(i_from_all) * end_mask_complex - s_to_all = v_to_all * jnp.conj(i_to_all) * end_mask_complex + s_from_all, s_to_all = _calculate_branch_complex_power( + v_from=v_from_all, + v_to=v_to_all, + current_from=i_from_all, + current_to=i_to_all, + branch_mask=end_mask, + ) is_outaged = mon_br[None, :] == outage_branch_idx[:, None] zeros_complex = jnp.zeros_like(s_from_all) diff --git a/src/dc_plus/jax/lodf_voltages.py b/src/dc_plus/jax/lodf_voltages.py index 2b3dee6..b91fe49 100644 --- a/src/dc_plus/jax/lodf_voltages.py +++ b/src/dc_plus/jax/lodf_voltages.py @@ -11,8 +11,14 @@ import jax import jax.numpy as jnp -from jaxtyping import Complex128, Float, Int - +from jaxtyping import Bool, Float, Int + +from ..interfaces.network_inputs import ( + JacobianComponentInputs, + NetworkAdmittanceInputs, + NetworkTopologyInputs, + VoltageStateInputs, +) from .low_rank_helper import _prepare_low_rank_factors_from_admittance # ruff: noqa: PLR0913 @@ -34,8 +40,7 @@ def _dot4_unrolled( @jax.jit def build_monitor_rows( - angle_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], - magnitude_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], + jacobian_components: JacobianComponentInputs, monitor_bus_indices: Int[jnp.ndarray, " n_bus_mon"], ) -> Tuple[ Int[jnp.ndarray, " n_bus_mon"], @@ -44,8 +49,8 @@ def build_monitor_rows( Float[jnp.ndarray, " n_bus_mon"], ]: """Precompute safe Jacobian indices and masks for monitored buses.""" - theta_idx = angle_component_indices[monitor_bus_indices] - vm_idx = magnitude_component_indices[monitor_bus_indices] + theta_idx = jacobian_components.angle_component_indices[monitor_bus_indices] + vm_idx = jacobian_components.magnitude_component_indices[monitor_bus_indices] theta_ok = theta_idx >= 0 vm_ok = vm_idx >= 0 @@ -64,16 +69,10 @@ def _compute_post_contingency_states( jacobian_inv_transposed: Float[jnp.ndarray, " n_eq n_eq"], outage_idx: Int[jnp.ndarray, ""], mismatch_vec: Float[jnp.ndarray, "4"], - branch_from: Int[jnp.ndarray, " n_branches"], - branch_to: Int[jnp.ndarray, " n_branches"], - v_mag_hat: Float[jnp.ndarray, " n_buses"], - theta_hat: Float[jnp.ndarray, " n_buses"], - angle_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], - magnitude_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], - y_ff: Complex128[jnp.ndarray, " n_branches"], - y_ft: Complex128[jnp.ndarray, " n_branches"], - y_tf: Complex128[jnp.ndarray, " n_branches"], - y_tt: Complex128[jnp.ndarray, " n_branches"], + network_topology: NetworkTopologyInputs, + voltage_state: VoltageStateInputs, + jacobian_components: JacobianComponentInputs, + network_admittance: NetworkAdmittanceInputs, base_theta0: Float[jnp.ndarray, " n_mon_bus"], base_vm0: Float[jnp.ndarray, " n_mon_bus"], theta_rows: Int[jnp.ndarray, " n_mon_bus"], @@ -92,16 +91,10 @@ def _compute_post_contingency_states( d_mat, branch_indices, branch_valid_mask = _prepare_low_rank_factors_from_admittance( branch_idx=outage_idx, - branch_from=branch_from, - branch_to=branch_to, - v_mag_hat=v_mag_hat, - theta_hat=theta_hat, - y_ff=y_ff, - y_ft=y_ft, - y_tf=y_tf, - y_tt=y_tt, - angle_component_indices=angle_component_indices, - magnitude_component_indices=magnitude_component_indices, + network_topology=network_topology, + voltage_state=voltage_state, + network_admittance=network_admittance, + jacobian_components=jacobian_components, ) d_mat = d_mat.astype(dtype) branch_indices = branch_indices.astype(jnp.int32) @@ -180,17 +173,12 @@ def _compute_post_contingency_states( def _solve_outage_voltages( jacobian_inv_transposed: Float[jnp.ndarray, " n_eq n_eq"], outage_branch_idx: Int[jnp.ndarray, " n_outages"], - branch_from: Int[jnp.ndarray, " n_branches"], - branch_to: Int[jnp.ndarray, " n_branches"], - v_mag_hat: Float[jnp.ndarray, " n_buses"], - theta_hat: Float[jnp.ndarray, " n_buses"], - angle_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], - magnitude_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], - y_ff: Complex128[jnp.ndarray, " n_branches"], - y_ft: Complex128[jnp.ndarray, " n_branches"], - y_tf: Complex128[jnp.ndarray, " n_branches"], - y_tt: Complex128[jnp.ndarray, " n_branches"], + jacobian_components: JacobianComponentInputs, + network_topology: NetworkTopologyInputs, + voltage_state: VoltageStateInputs, + network_admittance: NetworkAdmittanceInputs, branch_pq_base: Float[jnp.ndarray, "n_branches 4"], + lower_residual_uses_reactive_mismatch: Bool[jnp.ndarray, " n_buses"], base_theta0: Float[jnp.ndarray, " n_mon_bus"], base_vm0: Float[jnp.ndarray, " n_mon_bus"], theta_rows: Int[jnp.ndarray, " n_mon_bus"], @@ -206,20 +194,22 @@ def _solve_outage_voltages( def _solve_single(out_idx: jnp.ndarray) -> Tuple[Float[jnp.ndarray, " n_mon_bus"], Float[jnp.ndarray, " n_mon_bus"]]: mismatch_vec = -jnp.take(branch_pq_base, out_idx, axis=0).astype(dtype) + from_bus = jnp.take(network_topology.branch_from, out_idx, axis=0) + to_bus = jnp.take(network_topology.branch_to, out_idx, axis=0) + mismatch_vec = mismatch_vec.at[2].set( + jnp.where(jnp.take(lower_residual_uses_reactive_mismatch, from_bus, axis=0), mismatch_vec[2], 0.0) + ) + mismatch_vec = mismatch_vec.at[3].set( + jnp.where(jnp.take(lower_residual_uses_reactive_mismatch, to_bus, axis=0), mismatch_vec[3], 0.0) + ) return _compute_post_contingency_states( jacobian_inv_transposed=jacobian_inv_transposed, outage_idx=out_idx, mismatch_vec=mismatch_vec, - branch_from=branch_from, - branch_to=branch_to, - v_mag_hat=v_mag_hat, - theta_hat=theta_hat, - angle_component_indices=angle_component_indices, - magnitude_component_indices=magnitude_component_indices, - y_ff=y_ff, - y_ft=y_ft, - y_tf=y_tf, - y_tt=y_tt, + network_topology=network_topology, + voltage_state=voltage_state, + jacobian_components=jacobian_components, + network_admittance=network_admittance, base_theta0=base_theta0, base_vm0=base_vm0, theta_rows=theta_rows, @@ -234,63 +224,35 @@ def _solve_single(out_idx: jnp.ndarray) -> Tuple[Float[jnp.ndarray, " n_mon_bus" def line_outage_post_contingency_voltages( jacobian_inv_transposed: Float[jnp.ndarray, " n_eq n_eq"], outage_branch_idx: Int[jnp.ndarray, " n_outages"], - branch_from: Int[jnp.ndarray, " n_branches"], - branch_to: Int[jnp.ndarray, " n_branches"], - v_mag_hat: Float[jnp.ndarray, " n_buses"], - theta_hat: Float[jnp.ndarray, " n_buses"], - angle_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], - magnitude_component_indices: Int[jnp.ndarray, " n_eq_jacobian"], - y_ff: Complex128[jnp.ndarray, " n_branches"], - y_ft: Complex128[jnp.ndarray, " n_branches"], - y_tf: Complex128[jnp.ndarray, " n_branches"], - y_tt: Complex128[jnp.ndarray, " n_branches"], + jacobian_components: JacobianComponentInputs, + network_topology: NetworkTopologyInputs, + voltage_state: VoltageStateInputs, + network_admittance: NetworkAdmittanceInputs, monitor_bus_indices: Int[jnp.ndarray, " n_mon_bus"], branch_pq_base: Float[jnp.ndarray, "n_branches 4"], + lower_residual_uses_reactive_mismatch: Bool[jnp.ndarray, " n_buses"], ) -> Tuple[ Float[jnp.ndarray, "n_outages n_mon_bus"], Float[jnp.ndarray, "n_outages n_mon_bus"], ]: """Compute post-contingency monitored bus voltages (θ, Vm) only.""" - jacobian_inv_transposed = jnp.asarray(jacobian_inv_transposed) - dtype = jacobian_inv_transposed.dtype - - outage_branch_idx = jnp.asarray(outage_branch_idx, dtype=jnp.int32) - branch_from = jnp.asarray(branch_from, dtype=jnp.int32) - branch_to = jnp.asarray(branch_to, dtype=jnp.int32) - v_mag_hat = jnp.asarray(v_mag_hat, dtype=dtype) - theta_hat = jnp.asarray(theta_hat, dtype=dtype) - angle_component_indices = jnp.asarray(angle_component_indices, dtype=jnp.int32) - magnitude_component_indices = jnp.asarray(magnitude_component_indices, dtype=jnp.int32) - y_ff = jnp.asarray(y_ff) - y_ft = jnp.asarray(y_ft) - y_tf = jnp.asarray(y_tf) - y_tt = jnp.asarray(y_tt) - monitor_bus_indices = jnp.asarray(monitor_bus_indices, dtype=jnp.int32) - branch_pq_base = jnp.asarray(branch_pq_base, dtype=dtype) - theta_rows, vm_rows, theta_mask, vm_mask = build_monitor_rows( - angle_component_indices=angle_component_indices, - magnitude_component_indices=magnitude_component_indices, + jacobian_components=jacobian_components, monitor_bus_indices=monitor_bus_indices, ) - base_theta0 = jnp.take(theta_hat, monitor_bus_indices, axis=0) - base_vm0 = jnp.take(v_mag_hat, monitor_bus_indices, axis=0) + base_theta0 = jnp.take(voltage_state.bus_voltage_angles_rad, monitor_bus_indices, axis=0) + base_vm0 = jnp.take(voltage_state.bus_voltage_magnitudes, monitor_bus_indices, axis=0) return _solve_outage_voltages( jacobian_inv_transposed=jacobian_inv_transposed, outage_branch_idx=outage_branch_idx, - branch_from=branch_from, - branch_to=branch_to, - v_mag_hat=v_mag_hat, - theta_hat=theta_hat, - angle_component_indices=angle_component_indices, - magnitude_component_indices=magnitude_component_indices, - y_ff=y_ff, - y_ft=y_ft, - y_tf=y_tf, - y_tt=y_tt, + jacobian_components=jacobian_components, + network_topology=network_topology, + voltage_state=voltage_state, + network_admittance=network_admittance, branch_pq_base=branch_pq_base, + lower_residual_uses_reactive_mismatch=lower_residual_uses_reactive_mismatch, base_theta0=base_theta0, base_vm0=base_vm0, theta_rows=theta_rows, diff --git a/src/dc_plus/jax/low_rank_helper.py b/src/dc_plus/jax/low_rank_helper.py index 7089ffe..20fec40 100644 --- a/src/dc_plus/jax/low_rank_helper.py +++ b/src/dc_plus/jax/low_rank_helper.py @@ -18,15 +18,18 @@ import jax.numpy as jnp from jaxtyping import Complex128, Float, Int -# ruff: noqa: PLR0913 +from ..interfaces.network_inputs import ( + JacobianComponentInputs, + NetworkAdmittanceInputs, + NetworkTopologyInputs, + VoltageStateInputs, +) def _branch_state_indices( branch_idx: Int[jnp.ndarray, ""], - branch_from: Int[jnp.ndarray, " n_branches"], - branch_to: Int[jnp.ndarray, " n_branches"], - angle_component_indices: Int[jnp.ndarray, " n_nodes"], - magnitude_component_indices: Int[jnp.ndarray, " n_nodes"], + network_topology: NetworkTopologyInputs, + jacobian_components: JacobianComponentInputs, ) -> tuple[Int[jnp.ndarray, "4"], jnp.ndarray]: """Return mapping from branch end states to Jacobian indices. @@ -34,10 +37,10 @@ def _branch_state_indices( u_from, u_to) and a boolean mask indicating which of these states map into the system of equations. Invalid entries have their mask bit cleared. """ - idx_theta_f = angle_component_indices[branch_from[branch_idx]] - idx_theta_t = angle_component_indices[branch_to[branch_idx]] - idx_u_f = magnitude_component_indices[branch_from[branch_idx]] - idx_u_t = magnitude_component_indices[branch_to[branch_idx]] + idx_theta_f = jacobian_components.angle_component_indices[network_topology.branch_from[branch_idx]] + idx_theta_t = jacobian_components.angle_component_indices[network_topology.branch_to[branch_idx]] + idx_u_f = jacobian_components.magnitude_component_indices[network_topology.branch_from[branch_idx]] + idx_u_t = jacobian_components.magnitude_component_indices[network_topology.branch_to[branch_idx]] idx_arr = jnp.stack([idx_theta_f, idx_theta_t, idx_u_f, idx_u_t], dtype=jnp.int32) valid_mask = idx_arr >= 0 @@ -128,7 +131,7 @@ def _compute_branch_delta_submatrix_from_admittance( dqt_dvt = -2.0 * v_t * b_tt + v_f * (g_tf * sin_tf - b_tf * cos_tf) dqt_dvf = v_t * (g_tf * sin_tf - b_tf * cos_tf) - dtype = jnp.result_type(v_mag_from, v_mag_to, theta_from, theta_to, y_ff) + dtype_input = jnp.result_type(v_mag_from, v_mag_to, theta_from, theta_to, g_ff) delta = jnp.array( [ @@ -137,49 +140,41 @@ def _compute_branch_delta_submatrix_from_admittance( [-dqf_dthf, -dqf_dtht, -dqf_dvf, -dqf_dvt], [-dqt_dthf, -dqt_dtht, -dqt_dvf, -dqt_dvt], ], - dtype=dtype, + dtype=dtype_input, ) return delta def _prepare_low_rank_factors_from_admittance( branch_idx: Int[jnp.ndarray, ""], - branch_from: Int[jnp.ndarray, " n_branches"], - branch_to: Int[jnp.ndarray, " n_branches"], - v_mag_hat: Float[jnp.ndarray, " n_buses"], - theta_hat: Float[jnp.ndarray, " n_buses"], - y_ff: Complex128[jnp.ndarray, " n_branches"], - y_ft: Complex128[jnp.ndarray, " n_branches"], - y_tf: Complex128[jnp.ndarray, " n_branches"], - y_tt: Complex128[jnp.ndarray, " n_branches"], - angle_component_indices: Int[jnp.ndarray, " n_buses"], - magnitude_component_indices: Int[jnp.ndarray, " n_buses"], + network_topology: NetworkTopologyInputs, + voltage_state: VoltageStateInputs, + network_admittance: NetworkAdmittanceInputs, + jacobian_components: JacobianComponentInputs, ) -> Tuple[Float[jnp.ndarray, "4 4"], Int[jnp.ndarray, "4"], jnp.ndarray]: """Build low-rank factors for a line outage from pi-model admittances.""" safe_idx, valid_mask = _branch_state_indices( branch_idx=branch_idx, - branch_from=branch_from, - branch_to=branch_to, - angle_component_indices=angle_component_indices, - magnitude_component_indices=magnitude_component_indices, + network_topology=network_topology, + jacobian_components=jacobian_components, ) - f = branch_from[branch_idx] - t = branch_to[branch_idx] - v_from = v_mag_hat[f] - v_to = v_mag_hat[t] - theta_from = theta_hat[f] - theta_to = theta_hat[t] + f = network_topology.branch_from[branch_idx] + t = network_topology.branch_to[branch_idx] + v_from = voltage_state.bus_voltage_magnitudes[f] + v_to = voltage_state.bus_voltage_magnitudes[t] + theta_from = voltage_state.bus_voltage_angles_rad[f] + theta_to = voltage_state.bus_voltage_angles_rad[t] delta_full = _compute_branch_delta_submatrix_from_admittance( v_mag_from=v_from, v_mag_to=v_to, theta_from=theta_from, theta_to=theta_to, - y_ff=y_ff[branch_idx], - y_ft=y_ft[branch_idx], - y_tf=y_tf[branch_idx], - y_tt=y_tt[branch_idx], + y_ff=network_admittance.y_ff[branch_idx], + y_ft=network_admittance.y_ft[branch_idx], + y_tf=network_admittance.y_tf[branch_idx], + y_tt=network_admittance.y_tt[branch_idx], ) mask = valid_mask.astype(delta_full.dtype) diff --git a/src/dc_plus/jax/network_state_helper.py b/src/dc_plus/jax/network_state_helper.py new file mode 100644 index 0000000..f5ec5ff --- /dev/null +++ b/src/dc_plus/jax/network_state_helper.py @@ -0,0 +1,32 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""Helpers for sparse network-state calculations without constructing dense Y matrices. + +Note: The branch admittances need to be updated when a topology change occurs. +""" + +from typing import Tuple + +import jax.numpy as jnp +from jaxtyping import Complex128 + + +def _calculate_branch_complex_power( + v_from: Complex128[jnp.ndarray, " ... n_branches"], + v_to: Complex128[jnp.ndarray, " ... n_branches"], + current_from: Complex128[jnp.ndarray, " ... n_branches"], + current_to: Complex128[jnp.ndarray, " ... n_branches"], + branch_mask: Complex128[jnp.ndarray, " n_branches"], +) -> Tuple[ + Complex128[jnp.ndarray, " ... n_branches"], + Complex128[jnp.ndarray, " ... n_branches"], +]: + """Compute masked complex branch powers from endpoint voltages and currents.""" + s_from = v_from * jnp.conj(current_from) * branch_mask + s_to = v_to * jnp.conj(current_to) * branch_mask + return s_from, s_to diff --git a/src/dc_plus/numpy/__init__.py b/src/dc_plus/numpy/__init__.py index 9c761f9..a9c4411 100644 --- a/src/dc_plus/numpy/__init__.py +++ b/src/dc_plus/numpy/__init__.py @@ -10,3 +10,8 @@ This implementation is intended for reference and testing purposes only. It is currently not optimized for performance, but for readability. """ + +from .injection_changes import with_updated_injection_powers +from .injection_outage import non_voltage_regulating_injection_outage_dx + +__all__ = ["non_voltage_regulating_injection_outage_dx", "with_updated_injection_powers"] diff --git a/src/dc_plus/numpy/bsdf_full_rank.py b/src/dc_plus/numpy/bsdf_full_rank.py index 2adf2df..fc0c69f 100644 --- a/src/dc_plus/numpy/bsdf_full_rank.py +++ b/src/dc_plus/numpy/bsdf_full_rank.py @@ -17,7 +17,7 @@ _compute_branch_delta_submatrix_from_admittance, ) -# ruff: noqa: ARG001, C901, PLR0913, PLR0915 +# ruff: noqa: PLR0913 def _apply_full_rank_update( @@ -53,7 +53,693 @@ def _apply_full_rank_update( return jacobian_inv - delta_inv_jacobian -# ruff: noqa: C901, PLR0915 +def _compute_shunt_delta_submatrix_from_admittance( + v_mag: Float[np.ndarray, ""], + y_shunt: Complex128[np.ndarray, ""], +) -> Float[np.ndarray, "2 2"]: + """Return the 2x2 Jacobian contribution for a shunt admittance at one bus.""" + conductance = np.real(y_shunt) + susceptance = np.imag(y_shunt) + dtype = np.result_type(v_mag, y_shunt) + + delta = np.array( + [ + [0.0, 2.0 * v_mag * conductance], + [0.0, -2.0 * v_mag * susceptance], + ], + dtype=dtype, + ) + return delta * -1 + + +def _add_bus_component_indices( + container: set[int], + bus_idx: int, + angle_idx_map: Int[np.ndarray, " n_eq_jacobian"], + magnitude_idx_map: Int[np.ndarray, " n_eq_jacobian"], +) -> None: + """Add valid Jacobian component indices for one bus into a set. + + Parameters + ---------- + container : set[int] + Mutable set collecting Jacobian indices affected by the split update. + bus_idx : int + Bus whose angle and magnitude component indices should be added. + angle_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to angle-equation indices. + magnitude_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to magnitude-equation indices. + """ + if 0 <= bus_idx < angle_idx_map.size: + theta_idx = int(angle_idx_map[bus_idx]) + if theta_idx >= 0: + container.add(theta_idx) + if 0 <= bus_idx < magnitude_idx_map.size: + mag_idx = int(magnitude_idx_map[bus_idx]) + if mag_idx >= 0: + container.add(mag_idx) + + +def _collect_targeted_indices( + bus_to_split: int, + new_bus_b_index: int, + branch_indices_with_delta: Int[np.ndarray, " n_branch_delta"], + branches_connected_to_bus_b: Int[np.ndarray, " n_branches_B"], + shunt_connected_to_bus_b: Int[np.ndarray, " n_shunts_B"], + branch_from: Int[np.ndarray, " n_branches"], + branch_to: Int[np.ndarray, " n_branches"], + shunt_to_bus: Int[np.ndarray, " n_shunts"], + angle_idx_map: Int[np.ndarray, " n_eq_jacobian"], + magnitude_idx_map: Int[np.ndarray, " n_eq_jacobian"], + include_new_bus: bool, +) -> Int[np.ndarray, " k"]: + """Collect all Jacobian indices affected by branch and shunt reassignment. + + Parameters + ---------- + bus_to_split : int + Index of the original bus being split. + new_bus_b_index : int + Index of the new bus created by the split. + branch_indices_with_delta : Int[np.ndarray, " n_branch_delta"] + Branch indices whose Jacobian contribution changes due to reassignment and/or + transformer tap updates. + branches_connected_to_bus_b : Int[np.ndarray, " n_branches_B"] + Indices of branches reassigned to the new bus. + shunt_connected_to_bus_b : Int[np.ndarray, " n_shunts_B"] + Indices of shunts reassigned to the new bus. + branch_from : Int[np.ndarray, " n_branches"] + Branch ``from`` bus indices. + branch_to : Int[np.ndarray, " n_branches"] + Branch ``to`` bus indices. + shunt_to_bus : Int[np.ndarray, " n_shunts"] + Shunt bus indices. + angle_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to Jacobian angle-equation indices. + magnitude_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to Jacobian magnitude-equation indices. + include_new_bus : bool + Whether the newly created bus should contribute its angle and magnitude + component indices to the targeted update block. + + Returns + ------- + Int[np.ndarray, " k"] + Sorted Jacobian indices that appear in the local BSDF update block. + + Raises + ------ + IndexError + Raised when a reassigned branch or shunt index is outside the available arrays. + """ + targeted_indices: set[int] = set() + reassigned_branch_indices = {int(branch_idx) for branch_idx in branches_connected_to_bus_b} + _add_bus_component_indices(targeted_indices, bus_to_split, angle_idx_map, magnitude_idx_map) + if include_new_bus: + _add_bus_component_indices( + targeted_indices, + new_bus_b_index, + angle_idx_map, + magnitude_idx_map, + ) + + for branch_idx in branch_indices_with_delta: + if branch_idx < 0 or branch_idx >= branch_from.size: + raise IndexError("Branch index assigned to bus B is out of bounds") + + from_bus_old = int(branch_from[branch_idx]) + to_bus_old = int(branch_to[branch_idx]) + is_reassigned = branch_idx in reassigned_branch_indices + from_bus_new = new_bus_b_index if is_reassigned and from_bus_old == bus_to_split else from_bus_old + to_bus_new = new_bus_b_index if is_reassigned and to_bus_old == bus_to_split else to_bus_old + + _add_bus_component_indices(targeted_indices, from_bus_old, angle_idx_map, magnitude_idx_map) + _add_bus_component_indices(targeted_indices, to_bus_old, angle_idx_map, magnitude_idx_map) + _add_bus_component_indices(targeted_indices, from_bus_new, angle_idx_map, magnitude_idx_map) + _add_bus_component_indices(targeted_indices, to_bus_new, angle_idx_map, magnitude_idx_map) + + for shunt_idx in shunt_connected_to_bus_b: + if shunt_idx < 0 or shunt_idx >= shunt_to_bus.size: + raise IndexError("Shunt index assigned to bus B is out of bounds") + + shunt_bus_old = int(shunt_to_bus[shunt_idx]) + shunt_bus_new = new_bus_b_index if shunt_bus_old == bus_to_split else shunt_bus_old + + _add_bus_component_indices(targeted_indices, shunt_bus_old, angle_idx_map, magnitude_idx_map) + _add_bus_component_indices(targeted_indices, shunt_bus_new, angle_idx_map, magnitude_idx_map) + + return np.array(sorted(targeted_indices), dtype=int) + + +def _get_branch_row_component_indices( + bus_from: int, + bus_to: int, + angle_idx_map: Int[np.ndarray, " n_eq_jacobian"], + magnitude_idx_map: Int[np.ndarray, " n_eq_jacobian"], + reactive_power_bus_mask: np.ndarray | None = None, +) -> Int[np.ndarray, "4"]: + """Return the Jacobian component indices touched by one branch contribution. + + Parameters + ---------- + bus_from : int + ``From`` bus of the branch contribution. + bus_to : int + ``To`` bus of the branch contribution. + angle_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to angle-equation indices. + magnitude_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to magnitude-equation indices. + reactive_power_bus_mask : np.ndarray | None, optional + Optional mask indicating which buses retain reactive-power equation rows + for the branch contribution. + + Returns + ------- + Int[np.ndarray, "4"] + Component indices ordered as angle-from, angle-to, magnitude-from, + magnitude-to. + """ + mag_idx_from = magnitude_idx_map[bus_from] + mag_idx_to = magnitude_idx_map[bus_to] + if reactive_power_bus_mask is not None: + if not reactive_power_bus_mask[bus_from]: + mag_idx_from = -1 + if not reactive_power_bus_mask[bus_to]: + mag_idx_to = -1 + + return np.array( + [ + angle_idx_map[bus_from], + angle_idx_map[bus_to], + mag_idx_from, + mag_idx_to, + ], + dtype=int, + ) + + +def _get_branch_column_component_indices( + bus_from: int, + bus_to: int, + angle_idx_map: Int[np.ndarray, " n_eq_jacobian"], + magnitude_idx_map: Int[np.ndarray, " n_eq_jacobian"], +) -> Int[np.ndarray, "4"]: + """Return the Jacobian state-column indices touched by one branch contribution.""" + return np.array( + [ + angle_idx_map[bus_from], + angle_idx_map[bus_to], + magnitude_idx_map[bus_from], + magnitude_idx_map[bus_to], + ], + dtype=int, + ) + + +def _get_shunt_row_component_indices( + bus_idx: int, + angle_idx_map: Int[np.ndarray, " n_eq_jacobian"], + magnitude_idx_map: Int[np.ndarray, " n_eq_jacobian"], + reactive_power_bus_mask: np.ndarray | None = None, +) -> Int[np.ndarray, "2"]: + """Return the Jacobian component indices touched by one shunt contribution. + + Parameters + ---------- + bus_idx : int + Bus receiving the shunt contribution. + angle_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to angle-equation indices. + magnitude_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to magnitude-equation indices. + reactive_power_bus_mask : np.ndarray | None, optional + Optional mask indicating which buses retain reactive-power equation rows + for the shunt contribution. + + Returns + ------- + Int[np.ndarray, "2"] + Component indices ordered as angle, magnitude. + """ + mag_idx = magnitude_idx_map[bus_idx] + if reactive_power_bus_mask is not None and not reactive_power_bus_mask[bus_idx]: + mag_idx = -1 + return np.array([angle_idx_map[bus_idx], mag_idx], dtype=int) + + +def _get_shunt_column_component_indices( + bus_idx: int, + angle_idx_map: Int[np.ndarray, " n_eq_jacobian"], + magnitude_idx_map: Int[np.ndarray, " n_eq_jacobian"], +) -> Int[np.ndarray, "2"]: + """Return the Jacobian state-column indices touched by one shunt contribution.""" + return np.array([angle_idx_map[bus_idx], magnitude_idx_map[bus_idx]], dtype=int) + + +def _accumulate_sub_delta( + delta_block: Float[np.ndarray, " k k"], + delta_matrix: Float[np.ndarray, " m m"], + row_component_indices: Int[np.ndarray, " m"], + col_component_indices: Int[np.ndarray, " m"], + position_lookup: dict[int, int], + weight: float, +) -> None: + """Add a local branch or shunt Jacobian delta into the global update block. + + Parameters + ---------- + delta_block : Float[np.ndarray, " k k"] + Global Jacobian update block being assembled for the Woodbury update. + delta_matrix : Float[np.ndarray, " m m"] + Local Jacobian contribution for one branch or one shunt. + row_component_indices : Int[np.ndarray, " m"] + Jacobian row indices touched by the local contribution. + col_component_indices : Int[np.ndarray, " m"] + Jacobian column indices touched by the local contribution. + position_lookup : dict[int, int] + Mapping from global Jacobian index to local row/column position in + ``delta_block``. + weight : float + Scaling factor used to add or subtract the contribution. + """ + valid_row_positions = np.flatnonzero(row_component_indices >= 0) + valid_col_positions = np.flatnonzero(col_component_indices >= 0) + if valid_row_positions.size == 0 or valid_col_positions.size == 0: + return + + local_row_indices = row_component_indices[valid_row_positions] + local_col_indices = col_component_indices[valid_col_positions] + mapped_row_positions = [position_lookup[int(idx)] for idx in local_row_indices] + mapped_col_positions = [position_lookup[int(idx)] for idx in local_col_indices] + sub_delta = np.real(delta_matrix[np.ix_(valid_row_positions, valid_col_positions)]) + + for row_offset, pos_row in enumerate(mapped_row_positions): + for col_offset, pos_col in enumerate(mapped_col_positions): + delta_block[pos_row, pos_col] += weight * sub_delta[row_offset, col_offset] + + +def _accumulate_branch_reassignment_delta( + delta_block: Float[np.ndarray, " k k"], + position_lookup: dict[int, int], + branch_indices_with_delta: Int[np.ndarray, " n_branch_delta"], + branches_connected_to_bus_b: Int[np.ndarray, " n_branches_B"], + bus_to_split: int, + new_bus_b_index: int, + branch_from: Int[np.ndarray, " n_branches"], + branch_to: Int[np.ndarray, " n_branches"], + v_mag_hat: Float[np.ndarray, " n_buses"], + theta_hat: Float[np.ndarray, " n_buses"], + y_ff_base: Complex128[np.ndarray, " n_branches"], + y_ft_base: Complex128[np.ndarray, " n_branches"], + y_tf_base: Complex128[np.ndarray, " n_branches"], + y_tt_base: Complex128[np.ndarray, " n_branches"], + y_ff: Complex128[np.ndarray, " n_branches"], + y_ft: Complex128[np.ndarray, " n_branches"], + y_tf: Complex128[np.ndarray, " n_branches"], + y_tt: Complex128[np.ndarray, " n_branches"], + angle_idx_map: Int[np.ndarray, " n_eq_jacobian"], + magnitude_idx_map: Int[np.ndarray, " n_eq_jacobian"], + reactive_power_bus_mask: np.ndarray | None = None, +) -> None: + """Assemble the branch part of the BSDF Jacobian update block. + + Parameters + ---------- + delta_block : Float[np.ndarray, " k k"] + Global Jacobian update block being assembled. + position_lookup : dict[int, int] + Mapping from global Jacobian index to local row/column position. + branch_indices_with_delta : Int[np.ndarray, " n_branch_delta"] + Branch indices whose contribution changes between the base and updated state. + branches_connected_to_bus_b : Int[np.ndarray, " n_branches_B"] + Branch indices reassigned to the new bus. + bus_to_split : int + Index of the original split bus. + new_bus_b_index : int + Index of the new bus. + branch_from : Int[np.ndarray, " n_branches"] + Branch ``from`` bus indices. + branch_to : Int[np.ndarray, " n_branches"] + Branch ``to`` bus indices. + v_mag_hat : Float[np.ndarray, " n_buses"] + Voltage magnitudes used for the local Jacobian contributions. + theta_hat : Float[np.ndarray, " n_buses"] + Voltage angles used for the local Jacobian contributions. + y_ff_base : Complex128[np.ndarray, " n_branches"] + Base-state branch self-admittance at the ``from`` side. + y_ft_base : Complex128[np.ndarray, " n_branches"] + Base-state branch mutual admittance from ``from`` to ``to``. + y_tf_base : Complex128[np.ndarray, " n_branches"] + Base-state branch mutual admittance from ``to`` to ``from``. + y_tt_base : Complex128[np.ndarray, " n_branches"] + Base-state branch self-admittance at the ``to`` side. + y_ff : Complex128[np.ndarray, " n_branches"] + Branch self-admittance at the ``from`` side. + y_ft : Complex128[np.ndarray, " n_branches"] + Branch mutual admittance from ``from`` to ``to``. + y_tf : Complex128[np.ndarray, " n_branches"] + Branch mutual admittance from ``to`` to ``from``. + y_tt : Complex128[np.ndarray, " n_branches"] + Branch self-admittance at the ``to`` side. + angle_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to angle-equation indices. + magnitude_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to magnitude-equation indices. + reactive_power_bus_mask : np.ndarray | None, optional + Optional mask indicating which buses retain reactive-power equation rows + while assembling branch contribution rows. + """ + reassigned_branch_indices = {int(branch_idx) for branch_idx in branches_connected_to_bus_b} + + for branch_idx in branch_indices_with_delta: + from_bus_old = int(branch_from[branch_idx]) + to_bus_old = int(branch_to[branch_idx]) + + delta_old = _compute_branch_delta_submatrix_from_admittance( + v_mag_from=v_mag_hat[from_bus_old], + v_mag_to=v_mag_hat[to_bus_old], + theta_from=theta_hat[from_bus_old], + theta_to=theta_hat[to_bus_old], + y_ff=y_ff_base[branch_idx], + y_ft=y_ft_base[branch_idx], + y_tf=y_tf_base[branch_idx], + y_tt=y_tt_base[branch_idx], + ) + _accumulate_sub_delta( + delta_block=delta_block, + delta_matrix=delta_old, + row_component_indices=_get_branch_row_component_indices( + from_bus_old, + to_bus_old, + angle_idx_map, + magnitude_idx_map, + reactive_power_bus_mask, + ), + col_component_indices=_get_branch_column_component_indices( + from_bus_old, + to_bus_old, + angle_idx_map, + magnitude_idx_map, + ), + position_lookup=position_lookup, + weight=1.0, + ) + + is_reassigned = branch_idx in reassigned_branch_indices + from_bus_new = new_bus_b_index if is_reassigned and from_bus_old == bus_to_split else from_bus_old + to_bus_new = new_bus_b_index if is_reassigned and to_bus_old == bus_to_split else to_bus_old + delta_new = _compute_branch_delta_submatrix_from_admittance( + v_mag_from=v_mag_hat[from_bus_new], + v_mag_to=v_mag_hat[to_bus_new], + theta_from=theta_hat[from_bus_new], + theta_to=theta_hat[to_bus_new], + y_ff=y_ff[branch_idx], + y_ft=y_ft[branch_idx], + y_tf=y_tf[branch_idx], + y_tt=y_tt[branch_idx], + ) + _accumulate_sub_delta( + delta_block=delta_block, + delta_matrix=delta_new, + row_component_indices=_get_branch_row_component_indices( + from_bus_new, + to_bus_new, + angle_idx_map, + magnitude_idx_map, + reactive_power_bus_mask, + ), + col_component_indices=_get_branch_column_component_indices( + from_bus_new, + to_bus_new, + angle_idx_map, + magnitude_idx_map, + ), + position_lookup=position_lookup, + weight=-1.0, + ) + + +def _accumulate_shunt_reassignment_delta( + delta_block: Float[np.ndarray, " k k"], + position_lookup: dict[int, int], + shunt_connected_to_bus_b: Int[np.ndarray, " n_shunts_B"], + bus_to_split: int, + new_bus_b_index: int, + shunt_to_bus: Int[np.ndarray, " n_shunts"], + v_mag_hat: Float[np.ndarray, " n_buses"], + y_shunt: Complex128[np.ndarray, " n_shunts"], + angle_idx_map: Int[np.ndarray, " n_eq_jacobian"], + magnitude_idx_map: Int[np.ndarray, " n_eq_jacobian"], + reactive_power_bus_mask: np.ndarray | None = None, +) -> None: + """Assemble the shunt part of the BSDF Jacobian update block. + + Parameters + ---------- + delta_block : Float[np.ndarray, " k k"] + Global Jacobian update block being assembled. + position_lookup : dict[int, int] + Mapping from global Jacobian index to local row/column position. + shunt_connected_to_bus_b : Int[np.ndarray, " n_shunts_B"] + Shunt indices reassigned to the new bus. + bus_to_split : int + Index of the original split bus. + new_bus_b_index : int + Index of the new bus. + shunt_to_bus : Int[np.ndarray, " n_shunts"] + Shunt bus indices. + v_mag_hat : Float[np.ndarray, " n_buses"] + Voltage magnitudes used for the local Jacobian contributions. + y_shunt : Complex128[np.ndarray, " n_shunts"] + Effective shunt admittances. + angle_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to angle-equation indices. + magnitude_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to magnitude-equation indices. + reactive_power_bus_mask : np.ndarray | None, optional + Optional mask indicating which buses retain reactive-power equation rows + while assembling shunt contribution rows. + """ + for shunt_idx in shunt_connected_to_bus_b: + shunt_bus_old = int(shunt_to_bus[shunt_idx]) + shunt_bus_new = new_bus_b_index if shunt_bus_old == bus_to_split else shunt_bus_old + + delta_old = _compute_shunt_delta_submatrix_from_admittance( + v_mag=v_mag_hat[shunt_bus_old], + y_shunt=y_shunt[shunt_idx], + ) + _accumulate_sub_delta( + delta_block=delta_block, + delta_matrix=delta_old, + row_component_indices=_get_shunt_row_component_indices( + shunt_bus_old, + angle_idx_map, + magnitude_idx_map, + reactive_power_bus_mask, + ), + col_component_indices=_get_shunt_column_component_indices( + shunt_bus_old, + angle_idx_map, + magnitude_idx_map, + ), + position_lookup=position_lookup, + weight=1.0, + ) + + delta_new = _compute_shunt_delta_submatrix_from_admittance( + v_mag=v_mag_hat[shunt_bus_new], + y_shunt=y_shunt[shunt_idx], + ) + _accumulate_sub_delta( + delta_block=delta_block, + delta_matrix=delta_new, + row_component_indices=_get_shunt_row_component_indices( + shunt_bus_new, + angle_idx_map, + magnitude_idx_map, + reactive_power_bus_mask, + ), + col_component_indices=_get_shunt_column_component_indices( + shunt_bus_new, + angle_idx_map, + magnitude_idx_map, + ), + position_lookup=position_lookup, + weight=-1.0, + ) + + +def _apply_new_bus_diagonal_adjustment( + delta_block: Float[np.ndarray, " k k"], + position_lookup: dict[int, int], + new_bus_b_index: int, + angle_idx_map: Int[np.ndarray, " n_eq_jacobian"], + magnitude_idx_map: Int[np.ndarray, " n_eq_jacobian"], +) -> None: + """Apply the diagonal terms introduced by the additional PQ bus equations. + + Parameters + ---------- + delta_block : Float[np.ndarray, " k k"] + Global Jacobian update block being assembled. + position_lookup : dict[int, int] + Mapping from global Jacobian index to local row/column position. + new_bus_b_index : int + Index of the new bus. + angle_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to angle-equation indices. + magnitude_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to magnitude-equation indices. + """ + theta_idx_new = int(angle_idx_map[new_bus_b_index]) + if theta_idx_new >= 0 and theta_idx_new in position_lookup: + delta_block[position_lookup[theta_idx_new], position_lookup[theta_idx_new]] -= 1.0 + + mag_idx_new = int(magnitude_idx_map[new_bus_b_index]) + if mag_idx_new >= 0 and mag_idx_new in position_lookup: + delta_block[position_lookup[mag_idx_new], position_lookup[mag_idx_new]] -= 1.0 + + +def _build_delta_block( + idx_list: Int[np.ndarray, " k"], + dtype: np.dtype, + branch_indices_with_delta: Int[np.ndarray, " n_branch_delta"], + branches_connected_to_bus_b: Int[np.ndarray, " n_branches_B"], + shunt_connected_to_bus_b: Int[np.ndarray, " n_shunts_B"], + bus_to_split: int, + new_bus_b_index: int, + branch_from: Int[np.ndarray, " n_branches"], + branch_to: Int[np.ndarray, " n_branches"], + shunt_to_bus: Int[np.ndarray, " n_shunts"], + v_mag_hat: Float[np.ndarray, " n_buses"], + theta_hat: Float[np.ndarray, " n_buses"], + y_ff_base: Complex128[np.ndarray, " n_branches"], + y_ft_base: Complex128[np.ndarray, " n_branches"], + y_tf_base: Complex128[np.ndarray, " n_branches"], + y_tt_base: Complex128[np.ndarray, " n_branches"], + y_ff: Complex128[np.ndarray, " n_branches"], + y_ft: Complex128[np.ndarray, " n_branches"], + y_tf: Complex128[np.ndarray, " n_branches"], + y_tt: Complex128[np.ndarray, " n_branches"], + y_shunt: Complex128[np.ndarray, " n_shunts"], + angle_idx_map: Int[np.ndarray, " n_eq_jacobian"], + magnitude_idx_map: Int[np.ndarray, " n_eq_jacobian"], + reactive_power_bus_mask: np.ndarray | None, + apply_split_bus_adjustment: bool, +) -> Float[np.ndarray, " k k"]: + """Build the local Jacobian update block used in the Woodbury correction. + + Parameters + ---------- + idx_list : Int[np.ndarray, " k"] + Sorted Jacobian indices present in the update block. + dtype : np.dtype + Data type used to allocate the update block. + branch_indices_with_delta : Int[np.ndarray, " n_branch_delta"] + Branch indices whose Jacobian contribution changes between the base and + updated state. + branches_connected_to_bus_b : Int[np.ndarray, " n_branches_B"] + Branch indices reassigned to the new bus. + shunt_connected_to_bus_b : Int[np.ndarray, " n_shunts_B"] + Shunt indices reassigned to the new bus. + bus_to_split : int + Index of the original split bus. + new_bus_b_index : int + Index of the new bus. + branch_from : Int[np.ndarray, " n_branches"] + Branch ``from`` bus indices. + branch_to : Int[np.ndarray, " n_branches"] + Branch ``to`` bus indices. + shunt_to_bus : Int[np.ndarray, " n_shunts"] + Shunt bus indices. + v_mag_hat : Float[np.ndarray, " n_buses"] + Voltage magnitudes of the base state. + theta_hat : Float[np.ndarray, " n_buses"] + Voltage angles of the base state. + y_ff_base : Complex128[np.ndarray, " n_branches"] + Base-state branch self-admittance at the ``from`` side. + y_ft_base : Complex128[np.ndarray, " n_branches"] + Base-state branch mutual admittance from ``from`` to ``to``. + y_tf_base : Complex128[np.ndarray, " n_branches"] + Base-state branch mutual admittance from ``to`` to ``from``. + y_tt_base : Complex128[np.ndarray, " n_branches"] + Base-state branch self-admittance at the ``to`` side. + y_ff : Complex128[np.ndarray, " n_branches"] + Branch self-admittance at the ``from`` side. + y_ft : Complex128[np.ndarray, " n_branches"] + Branch mutual admittance from ``from`` to ``to``. + y_tf : Complex128[np.ndarray, " n_branches"] + Branch mutual admittance from ``to`` to ``from``. + y_tt : Complex128[np.ndarray, " n_branches"] + Branch self-admittance at the ``to`` side. + y_shunt : Complex128[np.ndarray, " n_shunts"] + Effective shunt admittances. + angle_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to angle-equation indices. + magnitude_idx_map : Int[np.ndarray, " n_eq_jacobian"] + Mapping from bus indices to magnitude-equation indices. + reactive_power_bus_mask : np.ndarray | None + Optional mask indicating which buses retain reactive-power equation rows + in the assembled local update block. + apply_split_bus_adjustment : bool + Whether to include the additional diagonal terms associated with + activating the newly split PQ bus. + + Returns + ------- + Float[np.ndarray, " k k"] + Dense local Jacobian update block corresponding to ``idx_list``. + """ + position_lookup = {int(idx): pos for pos, idx in enumerate(idx_list.tolist())} + delta_block = np.zeros((idx_list.size, idx_list.size), dtype=dtype) + + _accumulate_branch_reassignment_delta( + delta_block=delta_block, + position_lookup=position_lookup, + branch_indices_with_delta=branch_indices_with_delta, + branches_connected_to_bus_b=branches_connected_to_bus_b, + bus_to_split=bus_to_split, + new_bus_b_index=new_bus_b_index, + branch_from=branch_from, + branch_to=branch_to, + v_mag_hat=v_mag_hat, + theta_hat=theta_hat, + y_ff_base=y_ff_base, + y_ft_base=y_ft_base, + y_tf_base=y_tf_base, + y_tt_base=y_tt_base, + y_ff=y_ff, + y_ft=y_ft, + y_tf=y_tf, + y_tt=y_tt, + angle_idx_map=angle_idx_map, + magnitude_idx_map=magnitude_idx_map, + reactive_power_bus_mask=reactive_power_bus_mask, + ) + _accumulate_shunt_reassignment_delta( + delta_block=delta_block, + position_lookup=position_lookup, + shunt_connected_to_bus_b=shunt_connected_to_bus_b, + bus_to_split=bus_to_split, + new_bus_b_index=new_bus_b_index, + shunt_to_bus=shunt_to_bus, + v_mag_hat=v_mag_hat, + y_shunt=y_shunt, + angle_idx_map=angle_idx_map, + magnitude_idx_map=magnitude_idx_map, + reactive_power_bus_mask=reactive_power_bus_mask, + ) + if apply_split_bus_adjustment: + _apply_new_bus_diagonal_adjustment( + delta_block=delta_block, + position_lookup=position_lookup, + new_bus_b_index=new_bus_b_index, + angle_idx_map=angle_idx_map, + magnitude_idx_map=magnitude_idx_map, + ) + return delta_block def compute_bsdf_update( @@ -72,14 +758,20 @@ def compute_bsdf_update( y_ft: Complex128[np.ndarray, " n_branches"], y_tf: Complex128[np.ndarray, " n_branches"], y_tt: Complex128[np.ndarray, " n_branches"], - y_shunt: Complex128[np.ndarray, " n_buses"], + y_shunt: Complex128[np.ndarray, " n_shunts"], angle_component_indices: Int[np.ndarray, " n_eq_jacobian"], magnitude_component_indices: Int[np.ndarray, " n_eq_jacobian"], + reactive_power_bus_mask: np.ndarray | None = None, + y_ff_base: Complex128[np.ndarray, " n_branches"] | None = None, + y_ft_base: Complex128[np.ndarray, " n_branches"] | None = None, + y_tf_base: Complex128[np.ndarray, " n_branches"] | None = None, + y_tt_base: Complex128[np.ndarray, " n_branches"] | None = None, + apply_split_bus_adjustment: bool = True, ) -> Float[np.ndarray, " n_eq n_eq"]: """Compute the BSDF update for a bus split with the full-rank update approach. - Note: Injection and shunt changes are currently not supported, and the function assumes a PQ split. - Only branch reassignments are handled. + Note: injection reassignments do not affect the Jacobian and therefore do not enter this update. + The function currently assumes a PQ split and supports branch and shunt reassignments. Parameters ---------- @@ -113,12 +805,31 @@ def compute_bsdf_update( The "to-from" admittance values for all branches in the original system. y_tt : Complex128[np.ndarray, " n_branches"] The "to-to" admittance values for all branches in the original system. - y_shunt : Complex128[np.ndarray, " n_buses"] - The shunt admittance values for all buses in the original system. + y_shunt : Complex128[np.ndarray, " n_shunts"] + The shunt admittance values for all shunts in the original system. angle_component_indices : Int[np.ndarray, " n_eq_jacobian"] The mapping from bus indices to angle component indices in the Jacobian. magnitude_component_indices : Int[np.ndarray, " n_eq_jacobian"] The mapping from bus indices to magnitude component indices in the Jacobian. + reactive_power_bus_mask : np.ndarray | None, optional + Optional mask indicating which buses retain reactive-power equation rows + in the BSDF update. + y_ff_base : Complex128[np.ndarray, " n_branches"] | None, optional + Base-state ``from-from`` branch admittances before any transformer tap updates. + If omitted, ``y_ff`` is used. + y_ft_base : Complex128[np.ndarray, " n_branches"] | None, optional + Base-state ``from-to`` branch admittances before any transformer tap updates. + If omitted, ``y_ft`` is used. + y_tf_base : Complex128[np.ndarray, " n_branches"] | None, optional + Base-state ``to-from`` branch admittances before any transformer tap updates. + If omitted, ``y_tf`` is used. + y_tt_base : Complex128[np.ndarray, " n_branches"] | None, optional + Base-state ``to-to`` branch admittances before any transformer tap updates. + If omitted, ``y_tt`` is used. + apply_split_bus_adjustment : bool, optional + Whether to include the additional diagonal terms associated with activating + a newly split PQ bus. Set to ``False`` for pure transformer tap updates + without any topology split. Returns ------- @@ -128,148 +839,74 @@ def compute_bsdf_update( if new_bus_type != 2: raise NotImplementedError("Only PQ splits are supported") - if np.asarray(shunt_connected_to_bus_b).size: - raise NotImplementedError("Shunt reassignment is not supported") - - if branches_connected_to_bus_b.size == 0: - return np.asarray(jacobian_inv, dtype=float).copy() + y_ff_base_arr = y_ff if y_ff_base is None else y_ff_base + y_ft_base_arr = y_ft if y_ft_base is None else y_ft_base + y_tf_base_arr = y_tf if y_tf_base is None else y_tf_base + y_tt_base_arr = y_tt if y_tt_base is None else y_tt_base - base_inverse = np.asarray(jacobian_inv, dtype=float) - updated_inverse = base_inverse.copy() - - branch_from_arr = np.asarray(branch_from, dtype=int).reshape(-1) - branch_to_arr = np.asarray(branch_to, dtype=int).reshape(-1) - - angle_idx_map = np.asarray(angle_component_indices, dtype=int).reshape(-1) - magnitude_idx_map = np.asarray(magnitude_component_indices, dtype=int).reshape(-1) + changed_branch_mask = ~( + np.isclose(y_ff_base_arr, y_ff) + & np.isclose(y_ft_base_arr, y_ft) + & np.isclose(y_tf_base_arr, y_tf) + & np.isclose(y_tt_base_arr, y_tt) + ) + branch_indices_with_delta = np.union1d( + branches_connected_to_bus_b, + np.flatnonzero(changed_branch_mask), + ).astype(int, copy=False) - if new_bus_b_index >= angle_idx_map.size or new_bus_b_index >= magnitude_idx_map.size: + if branch_indices_with_delta.size == 0 and shunt_connected_to_bus_b.size == 0: + return jacobian_inv.copy() + if new_bus_b_index >= angle_component_indices.size or new_bus_b_index >= magnitude_component_indices.size: raise IndexError("New bus index is out of bounds for component index arrays") + idx_list = _collect_targeted_indices( + bus_to_split=bus_to_split, + new_bus_b_index=new_bus_b_index, + branch_indices_with_delta=branch_indices_with_delta, + branches_connected_to_bus_b=branches_connected_to_bus_b, + shunt_connected_to_bus_b=shunt_connected_to_bus_b, + branch_from=branch_from, + branch_to=branch_to, + shunt_to_bus=shunt_to_bus, + angle_idx_map=angle_component_indices, + magnitude_idx_map=magnitude_component_indices, + include_new_bus=apply_split_bus_adjustment, + ) + if idx_list.size == 0: + return jacobian_inv.copy() - v_mag_vec = np.asarray(v_mag_hat, dtype=float).reshape(-1) - theta_vec = np.asarray(theta_hat, dtype=float).reshape(-1) - - y_ff_vec = np.asarray(y_ff, dtype=np.complex128).reshape(-1) - y_ft_vec = np.asarray(y_ft, dtype=np.complex128).reshape(-1) - y_tf_vec = np.asarray(y_tf, dtype=np.complex128).reshape(-1) - y_tt_vec = np.asarray(y_tt, dtype=np.complex128).reshape(-1) - - def _add_component_indices(container: set[int], bus_idx: int) -> None: - if 0 <= bus_idx < angle_idx_map.size: - theta_idx = int(angle_idx_map[bus_idx]) - if theta_idx >= 0: - container.add(theta_idx) - if 0 <= bus_idx < magnitude_idx_map.size: - mag_idx = int(magnitude_idx_map[bus_idx]) - if mag_idx >= 0: - container.add(mag_idx) - - targeted_indices: set[int] = set() - - _add_component_indices(targeted_indices, bus_to_split) - _add_component_indices(targeted_indices, new_bus_b_index) - - # remove components from bus_to_split - for branch_idx in branches_connected_to_bus_b: - if branch_idx < 0 or branch_idx >= branch_from_arr.size: - raise IndexError("Branch index assigned to bus B is out of bounds") - - from_bus_old = int(branch_from_arr[branch_idx]) - to_bus_old = int(branch_to_arr[branch_idx]) - - _add_component_indices(targeted_indices, from_bus_old) - _add_component_indices(targeted_indices, to_bus_old) - - from_bus_new = new_bus_b_index if from_bus_old == bus_to_split else from_bus_old - to_bus_new = new_bus_b_index if to_bus_old == bus_to_split else to_bus_old - - _add_component_indices(targeted_indices, from_bus_new) - _add_component_indices(targeted_indices, to_bus_new) - - if not targeted_indices: - return updated_inverse - - idx_list = np.array(sorted(targeted_indices), dtype=int) - position_lookup = {idx: pos for pos, idx in enumerate(idx_list.tolist())} - - delta_block = np.zeros((idx_list.size, idx_list.size), dtype=updated_inverse.dtype) - - def _accumulate_branch_delta( - delta_matrix: Float[np.ndarray, "4 4"], - bus_from: int, - bus_to: int, - weight: float, - ) -> None: - component_indices = np.array( - [ - angle_idx_map[bus_from], - angle_idx_map[bus_to], - magnitude_idx_map[bus_from], - magnitude_idx_map[bus_to], - ], - dtype=int, - ) - valid_positions = np.flatnonzero(component_indices >= 0) - if valid_positions.size == 0: - return - - local_indices = component_indices[valid_positions] - mapped_positions = [position_lookup[idx] for idx in local_indices] - sub_delta = delta_matrix[np.ix_(valid_positions, valid_positions)] - - for row_offset, pos_row in enumerate(mapped_positions): - for col_offset, pos_col in enumerate(mapped_positions): - delta_block[pos_row, pos_col] += weight * float(sub_delta[row_offset, col_offset]) - - # Accumulate the contributions from the branches that are reassigned to bus B - for branch_idx in branches_connected_to_bus_b: - from_bus_old = int(branch_from_arr[branch_idx]) - to_bus_old = int(branch_to_arr[branch_idx]) - - delta_old = _compute_branch_delta_submatrix_from_admittance( - v_mag_from=v_mag_vec[from_bus_old], - v_mag_to=v_mag_vec[to_bus_old], - theta_from=theta_vec[from_bus_old], - theta_to=theta_vec[to_bus_old], - y_ff=y_ff_vec[branch_idx], - y_ft=y_ft_vec[branch_idx], - y_tf=y_tf_vec[branch_idx], - y_tt=y_tt_vec[branch_idx], - ) - _accumulate_branch_delta(delta_old, from_bus_old, to_bus_old, weight=1.0) - - from_bus_new = new_bus_b_index if from_bus_old == bus_to_split else from_bus_old - to_bus_new = new_bus_b_index if to_bus_old == bus_to_split else to_bus_old - - delta_new = _compute_branch_delta_submatrix_from_admittance( - v_mag_from=v_mag_vec[from_bus_new], - v_mag_to=v_mag_vec[to_bus_new], - theta_from=theta_vec[from_bus_new], - theta_to=theta_vec[to_bus_new], - y_ff=y_ff_vec[branch_idx], - y_ft=y_ft_vec[branch_idx], - y_tf=y_tf_vec[branch_idx], - y_tt=y_tt_vec[branch_idx], - ) - _accumulate_branch_delta(delta_new, from_bus_new, to_bus_new, weight=-1.0) - - theta_idx_new = int(angle_idx_map[new_bus_b_index]) - if theta_idx_new >= 0 and theta_idx_new in position_lookup: - pos = position_lookup[theta_idx_new] - delta_block[pos, pos] -= 1.0 - - mag_idx_new = int(magnitude_idx_map[new_bus_b_index]) - if mag_idx_new >= 0 and mag_idx_new in position_lookup: - pos = position_lookup[mag_idx_new] - delta_block[pos, pos] -= 1.0 - + delta_block = _build_delta_block( + idx_list=idx_list, + dtype=jacobian_inv.dtype, + branch_indices_with_delta=branch_indices_with_delta, + branches_connected_to_bus_b=branches_connected_to_bus_b, + shunt_connected_to_bus_b=shunt_connected_to_bus_b, + bus_to_split=bus_to_split, + new_bus_b_index=new_bus_b_index, + branch_from=branch_from, + branch_to=branch_to, + shunt_to_bus=shunt_to_bus, + v_mag_hat=v_mag_hat, + theta_hat=theta_hat, + y_ff_base=y_ff_base_arr, + y_ft_base=y_ft_base_arr, + y_tf_base=y_tf_base_arr, + y_tt_base=y_tt_base_arr, + y_ff=y_ff, + y_ft=y_ft, + y_tf=y_tf, + y_tt=y_tt, + y_shunt=y_shunt, + angle_idx_map=angle_component_indices, + magnitude_idx_map=magnitude_component_indices, + reactive_power_bus_mask=reactive_power_bus_mask, + apply_split_bus_adjustment=apply_split_bus_adjustment, + ) if not np.any(delta_block): - return updated_inverse + return jacobian_inv.copy() - updated_inverse = _apply_full_rank_update( - jacobian_inv=base_inverse, + return _apply_full_rank_update( + jacobian_inv=jacobian_inv, jacobian_delta_submatrix=delta_block, idx_list=idx_list, ) - - return updated_inverse diff --git a/src/dc_plus/numpy/fixed_jacobian.py b/src/dc_plus/numpy/fixed_jacobian.py new file mode 100644 index 0000000..9f2a404 --- /dev/null +++ b/src/dc_plus/numpy/fixed_jacobian.py @@ -0,0 +1,82 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""Functions to perform fixed-Jacobian iterations.""" + +import numpy as np +from jaxtyping import Float +from scipy import sparse + +from dc_plus.interfaces.jacobian_interface import JacobianInterface +from dc_plus.interfaces.jacobian_network_data import ( + _apply_jacobian_dx_to_network_data, + calculate_nodal_mismatch_network_data, +) +from dc_plus.interfaces.network_information import DynamicNetworkInformation + +FixedJacobianInverse = Float[np.ndarray, " n_eq_jacobian n_eq_jacobian"] + + +def _run_single_fixed_jacobian_iteration( + jacobian_data: JacobianInterface, + dynamic_network_data: DynamicNetworkInformation, + y_matrix: sparse.sparray, +) -> DynamicNetworkInformation: + """Apply one fixed-Jacobian correction step.""" + mismatch = calculate_nodal_mismatch_network_data( + dynamic_network_data=dynamic_network_data, + y_matrix=y_matrix, + jacobian_data=jacobian_data, + ) + dx = -jacobian_data.inverse_jacobian @ mismatch + return _apply_jacobian_dx_to_network_data( + dynamic_network_data=dynamic_network_data, + dx=dx, + jacobian_data=jacobian_data, + ) + + +def run_fixed_jacobian_iterations( + jacobian_data: JacobianInterface, + dynamic_network_data: DynamicNetworkInformation, + y_matrix: sparse.sparray, + n_iterations: int = 2, +) -> DynamicNetworkInformation: + """Apply modified-Newton iterations with a fixed Jacobian inverse. + + This performs repeated mismatch evaluations while reusing the same inverse + Jacobian. It is useful when the Jacobian update is cheap and accurate but a + single linear step is not sufficient for larger nonlinear changes such as a + far transformer tap move. + + Parameters + ---------- + jacobian_data : JacobianInterface + Fixed Jacobian layout and inverse used for every iteration. + dynamic_network_data : DynamicNetworkInformation + Dynamic network data at the target topology/admittance state and current + voltage iterate. + y_matrix : sparse.sparray + Admittance matrix matching ``dynamic_network_data``. + n_iterations : int, optional + Number of fixed-Jacobian correction steps to apply. + + Returns + ------- + DynamicNetworkInformation + Dynamic network data after the requested number of fixed-Jacobian + iterations. + """ + updated_network_data = dynamic_network_data + for _ in range(n_iterations): + updated_network_data = _run_single_fixed_jacobian_iteration( + jacobian_data=jacobian_data, + dynamic_network_data=updated_network_data, + y_matrix=y_matrix, + ) + + return updated_network_data diff --git a/src/dc_plus/numpy/injection_changes.py b/src/dc_plus/numpy/injection_changes.py new file mode 100644 index 0000000..3ff62cb --- /dev/null +++ b/src/dc_plus/numpy/injection_changes.py @@ -0,0 +1,65 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""Helpers for arbitrary injection-power changes without topology changes.""" + +import numpy as np +from jaxtyping import Bool, Float, Int + +from dc_plus.interfaces.network_information import DynamicNetworkInformation, replace_network_data + +from .injection_helpers import _aggregate_bus_injections + + +def with_updated_injection_powers( + dynamic_network_data: DynamicNetworkInformation, + injection_active_power: Float[np.ndarray, " n_injections"] | Float[np.ndarray, " n_injections n_timesteps"], + injection_reactive_power: Float[np.ndarray, " n_injections"] | Float[np.ndarray, " n_injections n_timesteps"], + injection_connected: Bool[np.ndarray, " n_injections"] | Bool[np.ndarray, " n_injections n_timesteps"] | None = None, + injection_to_bus: Int[np.ndarray, " n_injections"] | Int[np.ndarray, " n_injections n_timesteps"] | None = None, +) -> DynamicNetworkInformation: + """Return a copy of the network data with updated injection powers. + + This helper is meant for operating-point changes such as time-series load and + generator scaling where topology and branch admittances stay unchanged but the + net bus injections must be re-aggregated before running a voltage update. + """ + updated_injection_active_power = injection_active_power + updated_injection_reactive_power = injection_reactive_power + + if updated_injection_active_power.shape != dynamic_network_data.injection_active_power.shape: + raise ValueError("Active-power array shape must match dynamic_network_data.injection_active_power.") + if updated_injection_reactive_power.shape != dynamic_network_data.injection_reactive_power.shape: + raise ValueError("Reactive-power array shape must match dynamic_network_data.injection_reactive_power.") + + updated_injection_connected = ( + dynamic_network_data.injection_connected if injection_connected is None else injection_connected + ) + updated_injection_to_bus = dynamic_network_data.injection_to_bus if injection_to_bus is None else injection_to_bus + + if updated_injection_connected.shape != dynamic_network_data.injection_connected.shape: + raise ValueError("Connected-status array shape must match dynamic_network_data.injection_connected.") + if updated_injection_to_bus.shape != dynamic_network_data.injection_to_bus.shape: + raise ValueError("Injection-to-bus array shape must match dynamic_network_data.injection_to_bus.") + + updated_bus_active_power, updated_bus_reactive_power = _aggregate_bus_injections( + injection_to_bus=updated_injection_to_bus, + injection_active_power=updated_injection_active_power, + injection_reactive_power=updated_injection_reactive_power, + injection_connected=updated_injection_connected, + n_buses=dynamic_network_data.n_buses, + ) + + return replace_network_data( + dynamic_network_data, + injection_to_bus=updated_injection_to_bus, + injection_active_power=updated_injection_active_power, + injection_reactive_power=updated_injection_reactive_power, + injection_connected=updated_injection_connected, + bus_active_power=updated_bus_active_power, + bus_reactive_power=updated_bus_reactive_power, + ) diff --git a/src/dc_plus/numpy/injection_helpers.py b/src/dc_plus/numpy/injection_helpers.py new file mode 100644 index 0000000..8ef7685 --- /dev/null +++ b/src/dc_plus/numpy/injection_helpers.py @@ -0,0 +1,55 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""Shared NumPy helpers for injection-based operating-point updates.""" + +import numpy as np +from jaxtyping import Bool, Float, Int + + +def _aggregate_bus_injections( + injection_to_bus: Int[np.ndarray, " n_injections"] | Int[np.ndarray, " n_injections n_timesteps"], + injection_active_power: Float[np.ndarray, " n_injections"] | Float[np.ndarray, " n_injections n_timesteps"], + injection_reactive_power: Float[np.ndarray, " n_injections"] | Float[np.ndarray, " n_injections n_timesteps"], + injection_connected: Bool[np.ndarray, " n_injections"] | Bool[np.ndarray, " n_injections n_timesteps"], + n_buses: int, +) -> tuple[ + Float[np.ndarray, " n_buses"] | Float[np.ndarray, " n_buses n_timesteps"], + Float[np.ndarray, " n_buses"] | Float[np.ndarray, " n_buses n_timesteps"], +]: + """Aggregate per-injection powers back to bus totals.""" + injection_to_bus_arr = injection_to_bus + injection_active_power_arr = injection_active_power + injection_reactive_power_arr = injection_reactive_power + injection_connected_arr = injection_connected + + if injection_to_bus_arr.ndim == 1: + bus_active_power = np.zeros(n_buses, dtype=injection_active_power_arr.dtype) + bus_reactive_power = np.zeros(n_buses, dtype=injection_reactive_power_arr.dtype) + connected_mask = injection_connected_arr.astype(bool) + np.add.at(bus_active_power, injection_to_bus_arr[connected_mask], injection_active_power_arr[connected_mask]) + np.add.at(bus_reactive_power, injection_to_bus_arr[connected_mask], injection_reactive_power_arr[connected_mask]) + return bus_active_power, bus_reactive_power + + n_timesteps = injection_to_bus_arr.shape[1] + bus_active_power = np.zeros((n_buses, n_timesteps), dtype=injection_active_power_arr.dtype) + bus_reactive_power = np.zeros((n_buses, n_timesteps), dtype=injection_reactive_power_arr.dtype) + + for timestep in range(n_timesteps): + connected_mask = injection_connected_arr[:, timestep].astype(bool) + np.add.at( + bus_active_power[:, timestep], + injection_to_bus_arr[connected_mask, timestep], + injection_active_power_arr[connected_mask, timestep], + ) + np.add.at( + bus_reactive_power[:, timestep], + injection_to_bus_arr[connected_mask, timestep], + injection_reactive_power_arr[connected_mask, timestep], + ) + + return bus_active_power, bus_reactive_power diff --git a/src/dc_plus/numpy/injection_outage.py b/src/dc_plus/numpy/injection_outage.py new file mode 100644 index 0000000..666eaa2 --- /dev/null +++ b/src/dc_plus/numpy/injection_outage.py @@ -0,0 +1,170 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""One-step state updates for injection outages.""" + +import numpy as np +from jaxtyping import Float, Int + + +def _collect_injection_outage_mismatch( + jacobian_size: int, + outage_injection_indices: Int[np.ndarray, " n_contingencies n_outages"], + injection_to_bus: Int[np.ndarray, " n_injections"], + injection_active_power: Float[np.ndarray, " n_injections"], + injection_reactive_power: Float[np.ndarray, " n_injections"], + angle_component_indices: Int[np.ndarray, " n_buses"], + magnitude_component_indices: Int[np.ndarray, " n_buses"], + dtype: np.dtype, +) -> Float[np.ndarray, " n_contingencies n_eq"]: + """Assemble outage-induced mismatch vectors in Jacobian ordering. + + Parameters + ---------- + jacobian_size : int + Number of equations in the Jacobian system. + outage_injection_indices : Int[np.ndarray, " n_contingencies n_outages"] + Batched injection outage indices. Each row represents one contingency and + may include negative padding entries. + injection_to_bus : Int[np.ndarray, " n_injections"] + Map from injection index to hosting bus index. + injection_active_power : Float[np.ndarray, " n_injections"] + Active power associated with each injection. + injection_reactive_power : Float[np.ndarray, " n_injections"] + Reactive power associated with each injection. + angle_component_indices : Int[np.ndarray, " n_buses"] + Map from bus index to active-power mismatch component index. + magnitude_component_indices : Int[np.ndarray, " n_buses"] + Map from bus index to reactive-power mismatch component index. + dtype : np.dtype + Target dtype of the assembled mismatch matrix. + + Returns + ------- + Float[np.ndarray, " n_contingencies n_eq"] + Batched mismatch vectors in Jacobian component ordering. + """ + outage_indices = outage_injection_indices + mismatch = np.zeros((outage_indices.shape[0], jacobian_size), dtype=dtype) + if outage_indices.size == 0: + return mismatch + + valid_outage_mask = outage_indices >= 0 + if not np.any(valid_outage_mask): + return mismatch + + valid_rows, valid_cols = np.nonzero(valid_outage_mask) + valid_outages = outage_indices[valid_rows, valid_cols] + if np.any(valid_outages >= injection_to_bus.size): + raise IndexError("Injection outage index is out of bounds") + + buses = injection_to_bus[valid_outages] + + p_idx = angle_component_indices[buses] + valid_p = p_idx >= 0 + if np.any(valid_p): + np.add.at( + mismatch, + (valid_rows[valid_p], p_idx[valid_p]), + -injection_active_power[valid_outages][valid_p], + ) + + q_idx = magnitude_component_indices[buses] + valid_q = q_idx >= 0 + if np.any(valid_q): + np.add.at( + mismatch, + (valid_rows[valid_q], q_idx[valid_q]), + -injection_reactive_power[valid_outages][valid_q], + ) + + return mismatch + + +def _calculate_injection_outage_dx( + jacobian_inv: Float[np.ndarray, " n_eq n_eq"], + mismatch: Float[np.ndarray, " n_contingencies n_eq"], +) -> Float[np.ndarray, " n_contingencies n_eq"]: + """Map outage mismatch vectors to one-step state increments. + + Parameters + ---------- + jacobian_inv : Float[np.ndarray, " n_eq n_eq"] + Inverse Jacobian at the base operating point. + mismatch : Float[np.ndarray, " n_contingencies n_eq"] + Batched outage-induced mismatch vectors. + + Returns + ------- + Float[np.ndarray, " n_contingencies n_eq"] + Batched one-step state increments in Jacobian ordering. + """ + return -(mismatch @ jacobian_inv.T) + + +def non_voltage_regulating_injection_outage_dx( + jacobian_inv: Float[np.ndarray, " n_eq n_eq"], + outage_injection_indices: Int[np.ndarray, " n_contingencies n_outages"], + injection_to_bus: Int[np.ndarray, " n_injections"], + injection_active_power: Float[np.ndarray, " n_injections"], + injection_reactive_power: Float[np.ndarray, " n_injections"], + angle_component_indices: Int[np.ndarray, " n_buses"], + magnitude_component_indices: Int[np.ndarray, " n_buses"], +) -> Float[np.ndarray, " n_contingencies n_eq"]: + """One-step state increment caused by disconnecting non voltage regulating injections. + + The Jacobian is unchanged for a pure injection outage because network + topology and admittances remain fixed. The outage only changes the nodal + power mismatch at the buses hosting the disconnected injections. + + This helper assumes the bus type pattern used by the Jacobian remains + unchanged across the outage. That is appropriate for load outages and other + contingencies that do not alter voltage-control structure. + + Note: this function still works for voltage regulating injection outages, + but the PV->PQ bus type change is not accounted for. + + Parameters + ---------- + jacobian_inv : Float[np.ndarray, " n_eq n_eq"] + Base inverse Jacobian at the hot-start operating point. + outage_injection_indices : Int[np.ndarray, " n_contingencies n_outages"] + Batched indices of injections to disconnect. Each row is one + contingency and may contain multiple outages. Negative entries are + ignored and can be used as padding for rows with different outage + counts. + injection_to_bus : Int[np.ndarray, " n_injections"] + Mapping from injection index to bus index. + injection_active_power : Float[np.ndarray, " n_injections"] + Active power removed when each injection is disconnected. + In practice this is typically the injection setpoint, or the solved + injection power when no separate setpoint is available. + injection_reactive_power : Float[np.ndarray, " n_injections"] + Reactive power removed when each injection is disconnected. + In practice this is typically the injection setpoint, or the solved + injection power when no separate setpoint is available. + angle_component_indices : Int[np.ndarray, " n_buses"] + Bus to active-power Jacobian row mapping. + magnitude_component_indices : Int[np.ndarray, " n_buses"] + Bus to reactive-power Jacobian row mapping. + + Returns + ------- + Float[np.ndarray, " n_contingencies n_eq"] + Batched one-step update vectors in Jacobian ordering. + """ + mismatch = _collect_injection_outage_mismatch( + jacobian_size=jacobian_inv.shape[0], + outage_injection_indices=outage_injection_indices, + injection_to_bus=injection_to_bus, + injection_active_power=injection_active_power, + injection_reactive_power=injection_reactive_power, + angle_component_indices=angle_component_indices, + magnitude_component_indices=magnitude_component_indices, + dtype=jacobian_inv.dtype, + ) + return _calculate_injection_outage_dx(jacobian_inv=jacobian_inv, mismatch=mismatch) diff --git a/src/dc_plus/numpy/lodf.py b/src/dc_plus/numpy/lodf.py index e975565..d0cbedd 100644 --- a/src/dc_plus/numpy/lodf.py +++ b/src/dc_plus/numpy/lodf.py @@ -214,20 +214,12 @@ def branch_outage_update_inverse( updated_inv : Float[np.ndarray, " n_eq n_eq"] The updated inverse Jacobian after applying outages. """ - jacobian_inv_arr = np.asarray(jacobian_inv) + jacobian_inv_arr = jacobian_inv if not np.issubdtype(jacobian_inv_arr.dtype, np.floating): jacobian_inv_arr = jacobian_inv_arr.astype(float) updated_inv = jacobian_inv_arr.copy() - v_mag = np.asarray(v_mag_hat).reshape(-1) - theta = np.asarray(theta_hat).reshape(-1) - - y_ff_arr = np.asarray(y_ff) - y_ft_arr = np.asarray(y_ft) - y_tf_arr = np.asarray(y_tf) - y_tt_arr = np.asarray(y_tt) - # Note: this is a reference implementation and is not optimized for performance. # In particular, multiple outages could be processed together for better efficiency. for branch in outage_branches_indices: @@ -235,12 +227,12 @@ def branch_outage_update_inverse( branch_idx=branch, branch_from=branch_from, branch_to=branch_to, - v_mag_hat=v_mag, - theta_hat=theta, - y_ff=y_ff_arr, - y_ft=y_ft_arr, - y_tf=y_tf_arr, - y_tt=y_tt_arr, + v_mag_hat=v_mag_hat, + theta_hat=theta_hat, + y_ff=y_ff, + y_ft=y_ft, + y_tf=y_tf, + y_tt=y_tt, angle_component_indices=angle_component_indices, magnitude_component_indices=magnitude_component_indices, ) diff --git a/src/dc_plus/numpy/low_rank_helper.py b/src/dc_plus/numpy/low_rank_helper.py index 9299b4d..9f5f9c7 100644 --- a/src/dc_plus/numpy/low_rank_helper.py +++ b/src/dc_plus/numpy/low_rank_helper.py @@ -21,6 +21,7 @@ def _branch_state_indices( branch_to: Int[np.ndarray, " n_branches"], angle_component_indices: Int[np.ndarray, " n_eq_jacobian"], magnitude_component_indices: Int[np.ndarray, " n_eq_jacobian"], + reactive_power_bus_mask: np.ndarray | None = None, ) -> tuple[Int[np.ndarray, "4"], np.ndarray]: """Return mapping from branch end states to Jacobian indices. @@ -38,6 +39,9 @@ def _branch_state_indices( magnitude_component_indices : Int[np.ndarray, " n_eq_jacobian"] Mapping from node index to voltage magnitude equation index in Jacobian. Entries are -1 for nodes without voltage magnitude equation. + reactive_power_bus_mask : np.ndarray | None, optional + Optional mask indicating which buses retain reactive-power equation rows + in the Jacobian layout. Returns ------- @@ -52,6 +56,12 @@ def _branch_state_indices( idx_u_f = magnitude_component_indices[branch_from[branch_idx]] idx_u_t = magnitude_component_indices[branch_to[branch_idx]] + if reactive_power_bus_mask is not None: + if not reactive_power_bus_mask[branch_from[branch_idx]]: + idx_u_f = -1 + if not reactive_power_bus_mask[branch_to[branch_idx]]: + idx_u_t = -1 + idx_arr = np.array([idx_theta_f, idx_theta_t, idx_u_f, idx_u_t], dtype=np.int32) valid_mask = idx_arr >= 0 safe_idx = np.where(valid_mask, idx_arr, 0) @@ -209,7 +219,7 @@ def _compute_branch_delta_submatrix_from_admittance( dqt_dvt = -2.0 * v_t * b_tt + v_f * (g_tf * sin_tf - b_tf * cos_tf) dqt_dvf = v_t * (g_tf * sin_tf - b_tf * cos_tf) - dtype = np.result_type(v_mag_from, v_mag_to, theta_from, theta_to, y_ff) + dtype = np.result_type(v_mag_from, v_mag_to, theta_from, theta_to, g_ff) delta = np.array( [ @@ -237,6 +247,7 @@ def _prepare_low_rank_factors_from_admittance( y_tt: Complex128[np.ndarray, " n_branches"], angle_component_indices: Int[np.ndarray, " n_eq_jacobian"], magnitude_component_indices: Int[np.ndarray, " n_eq_jacobian"], + reactive_power_bus_mask: np.ndarray | None = None, ) -> Tuple[Float[np.ndarray, "4 4"], Int[np.ndarray, "4"]]: """Build low-rank factors for a line outage from pi-model admittances. @@ -266,6 +277,9 @@ def _prepare_low_rank_factors_from_admittance( magnitude_component_indices : Int[np.ndarray, " n_eq_jacobian"] Mapping from node index to voltage magnitude equation index in Jacobian. Entries are -1 for nodes without voltage magnitude equation. + reactive_power_bus_mask : np.ndarray | None, optional + Optional mask indicating which buses retain reactive-power equation rows + in the Jacobian layout. Returns ------- @@ -281,6 +295,7 @@ def _prepare_low_rank_factors_from_admittance( branch_to=branch_to, angle_component_indices=angle_component_indices, magnitude_component_indices=magnitude_component_indices, + reactive_power_bus_mask=reactive_power_bus_mask, ) f = branch_from[branch_idx] diff --git a/src/dc_plus/numpy/quasi_newton.py b/src/dc_plus/numpy/quasi_newton.py new file mode 100644 index 0000000..0b82904 --- /dev/null +++ b/src/dc_plus/numpy/quasi_newton.py @@ -0,0 +1,208 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +"""Quasi-Newton voltage updates with inverse Broyden corrections. + +Based on: +A Class of Methods for Solving Nonlinear Simultaneous Equations +Author: C. G. Broyden +Source: Mathematics of Computation, Vol. 19, No. 92 (Oct., 1965), pp. 577-593 +""" + +import numpy as np +from jaxtyping import Float +from scipy import sparse + +from dc_plus.interfaces.jacobian_interface import JacobianInterface +from dc_plus.interfaces.jacobian_network_data import ( + _apply_jacobian_dx_to_network_data, + _get_admittance_matrix_from_network_data, + calculate_nodal_mismatch_network_data, +) +from dc_plus.interfaces.network_information import DynamicNetworkInformation + +InverseJacobianApproximation = Float[np.ndarray, " n_eq_jacobian n_eq_jacobian"] + + +def run_quasi_newton_updates( + jacobian_data: JacobianInterface, + dynamic_network_data: DynamicNetworkInformation, + n_iterations: int = 2, + y_matrix: sparse.sparray | None = None, + regularization: float = 1e-12, + damping_factor: float = 1.0, +) -> tuple[ + DynamicNetworkInformation, + list[float], + Float[np.ndarray, " n_eq_jacobian n_eq_jacobian"], +]: + """Run quasi-Newton steps with fast lazy inverse Broyden updates. + + This starts from a fixed inverse Jacobian approximation and applies + inverse Broyden rank-one secant corrections lazily. Instead of updating + the dense inverse Jacobian matrix directly, the method stores low-rank + correction factors ``A`` and ``B`` such that the current inverse + approximation is + + H_k = H_0 + A_k B_k.T + + Applying the current inverse approximation to a vector ``v`` is therefore + + H_k v = H_0 v + A_k (B_k.T v). + + The implementation uses the identity + + a_k = s_k - H_k y_k = -H_k f_{k+1} + + where + + s_k = -H_k f_k + y_k = f_{k+1} - f_k + + to avoid one additional inverse application per iteration. + + Parameters + ---------- + jacobian_data : JacobianInterface + Initial Jacobian layout and inverse approximation. + dynamic_network_data : DynamicNetworkInformation + Target system state providing the topology/admittance data and initial + voltage iterate. + n_iterations : int, optional + Number of quasi-Newton correction steps to perform. + y_matrix : sparse.sparray | None, optional + Admittance matrix matching ``dynamic_network_data``. If omitted it is + built once from ``dynamic_network_data`` and then reused. + regularization : float, optional + Minimum denominator magnitude accepted for the inverse Broyden update. + damping_factor : float, optional + Scalar multiplier applied to the quasi-Newton state step, i.e. + x_{k+1} = x_k + alpha * Delta x_k. + + Returns + ------- + tuple[DynamicNetworkInformation, list[float], Float[np.ndarray, " n_eq_jacobian n_eq_jacobian"]] + Updated network state, mismatch infinity-norm history after each + quasi-Newton step, and the final materialized inverse Jacobian + approximation. + + Raises + ------ + ValueError + If ``n_iterations`` is negative. + """ + if n_iterations < 0: + raise ValueError("Number of quasi-Newton iterations must be non-negative.") + + expected_size = jacobian_data.n_angle_components + jacobian_data.n_magnitude_components + + if y_matrix is None: + y_matrix = _get_admittance_matrix_from_network_data(dynamic_network_data) + + base_inverse_jacobian = jacobian_data.inverse_jacobian + updated_network_data = dynamic_network_data + + # Preallocate lazy Broyden factors. + # + # After k accepted updates: + # H_k = H_0 + A[:, :k] @ B[:, :k].T + # + # Each inverse Broyden update is: + # H_{k+1} = H_k + a_k b_k.T + # + # The second inverse-Broyden factors are: + # a_k = s_k - H_k y_k + # b_k = y_k / (y_k.T @ y_k) + # + # Since s_k = -H_k f_k and y_k = f_{k+1} - f_k: + # a_k = -H_k f_{k+1} + # + # This avoids explicitly applying H_k to y_k. + a_factors = np.zeros((expected_size, n_iterations), dtype=float) + b_factors = np.zeros((expected_size, n_iterations), dtype=float) + n_active_factors = 0 + + def apply_inverse_approximation( + vector: Float[np.ndarray, " n_eq_jacobian"], + ) -> Float[np.ndarray, " n_eq_jacobian"]: + """Apply the current lazy inverse-Jacobian approximation. + + Parameters + ---------- + vector : Float[np.ndarray, " n_eq_jacobian"] + Vector to multiply by the current inverse approximation. + + Returns + ------- + Float[np.ndarray, " n_eq_jacobian"] + Product of the current inverse approximation and ``vector``. + """ + result = base_inverse_jacobian @ vector + + if n_active_factors == 0: + return result + + active_a = a_factors[:, :n_active_factors] + active_b = b_factors[:, :n_active_factors] + return result + active_a @ (active_b.T @ vector) + + mismatch = calculate_nodal_mismatch_network_data( + dynamic_network_data=updated_network_data, + y_matrix=y_matrix, + jacobian_data=jacobian_data, + ) + inverse_mismatch = apply_inverse_approximation(mismatch) + mismatch_history: list[float] = [] + + for _ in range(n_iterations): + state_step = -damping_factor * inverse_mismatch + + next_network_data = _apply_jacobian_dx_to_network_data( + dynamic_network_data=updated_network_data, + dx=state_step, + jacobian_data=jacobian_data, + ) + next_mismatch = calculate_nodal_mismatch_network_data( + dynamic_network_data=next_network_data, + y_matrix=y_matrix, + jacobian_data=jacobian_data, + ) + + mismatch_history.append(float(np.max(np.abs(next_mismatch)))) + + # This is H_k f_{k+1}. It is needed both for the Broyden factor and + # for the next step, so keep it instead of recomputing H_k y_k. + next_inverse_mismatch_old = apply_inverse_approximation(next_mismatch) + + mismatch_delta = next_mismatch - mismatch + denominator = float(mismatch_delta @ mismatch_delta) + + if denominator > regularization: + factor_index = n_active_factors + new_a_factor = -next_inverse_mismatch_old + new_b_factor = mismatch_delta / denominator + + a_factors[:, factor_index] = new_a_factor + b_factors[:, factor_index] = new_b_factor + n_active_factors += 1 + + # Update H_{k+1} f_{k+1} from H_k f_{k+1} using the newly + # appended rank-one factor: + # + # H_{k+1} f_{k+1} + # = H_k f_{k+1} + a_k (b_k.T f_{k+1}) + correction_scale = float(new_b_factor @ next_mismatch) + inverse_mismatch = next_inverse_mismatch_old + new_a_factor * correction_scale + else: + inverse_mismatch = next_inverse_mismatch_old + + updated_network_data = next_network_data + mismatch = next_mismatch + + updated_inverse_jacobian = base_inverse_jacobian + a_factors[:, :n_active_factors] @ b_factors[:, :n_active_factors].T + + return updated_network_data, mismatch_history, updated_inverse_jacobian diff --git a/src/dc_plus/preprocess/create_network_data.py b/src/dc_plus/preprocess/create_network_data.py index d7f4470..721e1cf 100644 --- a/src/dc_plus/preprocess/create_network_data.py +++ b/src/dc_plus/preprocess/create_network_data.py @@ -7,17 +7,21 @@ """Functions to create the network data from different input formats.""" +import numpy as np import pypowsybl from pandapower.auxiliary import pandapowerNet from dc_plus.importing.import_helpers import ( + _empty_schema_dataframe, + _filter_branch_tap_data, + _filter_main_grid_network_data, _get_admittance_branches, + _get_branch_admittance_terms, + _get_branch_current_limits, _get_bus_active_power_injections, _get_bus_admittance_shunts, _get_bus_reactive_power_injections, - _remove_isolated_branches, - _remove_isolated_buses, - _remove_isolated_buses_injections, + _get_shunt_section_information, ) from dc_plus.importing.import_schema import ( BranchParamSchema, @@ -25,6 +29,8 @@ InjectionParamSchema, LimitParamSchema, ShuntParamSchema, + TapChangerParamSchema, + TapPositionParamSchema, ) from dc_plus.importing.pandapower.import_helpers import _get_slack_bus_id from dc_plus.importing.pandapower.pandapower_import import ( @@ -40,24 +46,201 @@ _get_injections_powsybl, _get_limits_parameter_powsybl, _get_shunts_powsybl, + _get_tap_changer_parameter_powsybl, + _get_tap_steps_parameter_powsybl, ) from dc_plus.interfaces.network_information import ( BusType, DynamicNetworkInformation, + NetworkInformation, StaticNetworkInformation, StringNetworkInformation, - _check_network_data_consistency, + TransformerTapInformation, ) from dc_plus.preprocess.helper_functions import _is_branch_symmetric, _is_connected_to_slack +def _filter_main_grid_create_network_data_inputs( + buses: BusParamSchema, + branches: BranchParamSchema, + injections: InjectionParamSchema, + shunts: ShuntParamSchema, + ratio_changer: TapChangerParamSchema, + phase_changer: TapChangerParamSchema, + ratio_positions: TapPositionParamSchema, + phase_positions: TapPositionParamSchema, +) -> tuple[ + BusParamSchema, + BranchParamSchema, + InjectionParamSchema, + ShuntParamSchema, + TapChangerParamSchema, + TapChangerParamSchema, + TapPositionParamSchema, + TapPositionParamSchema, +]: + """Filter dynamic and tap import tables down to the main grid branch set.""" + buses, branches, injections, shunts = _filter_main_grid_network_data( + buses, + branches, + injections, + shunts, + ) + ratio_changer, ratio_positions = _filter_branch_tap_data(branches, ratio_changer, ratio_positions) + phase_changer, phase_positions = _filter_branch_tap_data(branches, phase_changer, phase_positions) + return ( + buses, + branches, + injections, + shunts, + ratio_changer, + phase_changer, + ratio_positions, + phase_positions, + ) + + +def _get_branch_index_by_id(branches: BranchParamSchema) -> dict[str, int]: + """Build a branch-index lookup keyed by branch id.""" + return {str(branch_id): idx for idx, branch_id in enumerate(branches["id_str"].astype(str).values)} + + +def _create_transformer_tap_information( + tap_changer: TapChangerParamSchema, + tap_positions: TapPositionParamSchema, +) -> TransformerTapInformation: + """Build static tap information for a single transformer branch.""" + tap_positions = TapPositionParamSchema.validate(tap_positions.sort_values("position").reset_index(drop=True)) + + resistance = tap_positions["offset_r"].to_numpy(dtype=float) + reactance = tap_positions["offset_x"].to_numpy(dtype=float) + conductance_from = tap_positions["offset_g1"].to_numpy(dtype=float) + susceptance_from = tap_positions["offset_b1"].to_numpy(dtype=float) + conductance_to = tap_positions["offset_g2"].to_numpy(dtype=float) + susceptance_to = tap_positions["offset_b2"].to_numpy(dtype=float) + shift_ratio_rho = 1 / tap_positions["offset_rho"].to_numpy(dtype=float) + shift_angle = -tap_positions["offset_alpha"].to_numpy(dtype=float) + + y_series, y_charging_from, y_charging_to, _ = _get_branch_admittance_terms( + r=resistance, + x=reactance, + g1=conductance_from, + b1=susceptance_from, + g2=conductance_to, + b2=susceptance_to, + rho=shift_ratio_rho, + alpha=shift_angle, + ) + + neutral_shift_angle = 0.0 + neutral_shift_ratio_rho = 1.0 + + y_series_neutral, y_charging_from_neutral, y_charging_to_neutral, _ = _get_branch_admittance_terms( + r=np.asarray([float(tap_changer["neutral_r"])], dtype=float), + x=np.asarray([float(tap_changer["neutral_x"])], dtype=float), + g1=np.asarray([float(tap_changer["neutral_g1"])], dtype=float), + b1=np.asarray([float(tap_changer["neutral_b1"])], dtype=float), + g2=np.asarray([float(tap_changer["neutral_g2"])], dtype=float), + b2=np.asarray([float(tap_changer["neutral_b2"])], dtype=float), + rho=np.asarray([neutral_shift_ratio_rho], dtype=float), + alpha=np.asarray([neutral_shift_angle], dtype=float), + ) + # check that y_series is not 0 for any branch + if np.any(np.isclose(y_series_neutral, 0.0)): + raise ValueError("Zero impedance branch detected. Check network Data!") + neutral_y_series = y_series_neutral[0] + neutral_y_charging_from = y_charging_from_neutral[0] + neutral_y_charging_to = y_charging_to_neutral[0] + + return TransformerTapInformation( + n_max_tap_positions=len(tap_positions), + neutral_conductance_series=float(np.real(neutral_y_series)), + neutral_susceptance_series=float(np.imag(neutral_y_series)), + neutral_conductance_charging_from=float(np.real(neutral_y_charging_from)), + neutral_susceptance_charging_from=float(np.imag(neutral_y_charging_from)), + neutral_conductance_charging_to=float(np.real(neutral_y_charging_to)), + neutral_susceptance_charging_to=float(np.imag(neutral_y_charging_to)), + neutral_shift_angle=neutral_shift_angle, + neutral_shift_ratio_rho=neutral_shift_ratio_rho, + tap_offset_conductance_series=np.real(y_series - neutral_y_series), + tap_offset_susceptance_series=np.imag(y_series - neutral_y_series), + tap_offset_conductance_charging_from=np.real(y_charging_from - neutral_y_charging_from), + tap_offset_susceptance_charging_from=np.imag(y_charging_from - neutral_y_charging_from), + tap_offset_conductance_charging_to=np.real(y_charging_to - neutral_y_charging_to), + tap_offset_susceptance_charging_to=np.imag(y_charging_to - neutral_y_charging_to), + tap_offset_shift_angle=shift_angle, + tap_offset_shift_ratio_rho=shift_ratio_rho, + ) + + +def _build_transformer_tap_metadata( + branches: BranchParamSchema, + tap_changers: TapChangerParamSchema, + tap_positions: TapPositionParamSchema, +) -> tuple[np.ndarray, np.ndarray, dict[int, TransformerTapInformation]]: + """Build branch-aligned transformer tap flags, positions, and metadata.""" + n_branches = len(branches) + has_transformer = np.zeros(n_branches, dtype=bool) + current_tap_positions = np.zeros(n_branches, dtype=int) + tap_info: dict[int, TransformerTapInformation] = {} + if tap_changers.empty: + return has_transformer, current_tap_positions, tap_info + + branch_index_by_id = _get_branch_index_by_id(branches) + tap_positions_by_id = { + str(branch_id): branch_positions.copy() + for branch_id, branch_positions in tap_positions.groupby("id_str", sort=False) + } + + for _, tap_changer in tap_changers.iterrows(): + branch_idx = branch_index_by_id.get(str(tap_changer["id_str"])) + if branch_idx is None: + continue + + current_tap_positions[branch_idx] = int(tap_changer["current_tap_pos"]) + branch_tap_positions = tap_positions_by_id.get(str(tap_changer["id_str"])) + if branch_tap_positions is None or branch_tap_positions.empty: + continue + + has_transformer[branch_idx] = True + tap_info[branch_idx] = _create_transformer_tap_information(tap_changer, branch_tap_positions) + + return has_transformer, current_tap_positions, tap_info + + +def _get_powsybl_tap_imports( + network: pypowsybl.network.Network, split_trafo_charging: bool = True +) -> tuple[ + TapChangerParamSchema, + TapChangerParamSchema, + TapPositionParamSchema, + TapPositionParamSchema, +]: + """Import ratio and phase tap changer tables from a Powsybl network.""" + transformers = network.get_2_windings_transformers(attributes=["r", "x", "g", "b"]) + ratio_changer = _get_tap_changer_parameter_powsybl( + network.get_ratio_tap_changers(), transformers.copy(), split_trafo_charging=split_trafo_charging + ) + phase_changer = _get_tap_changer_parameter_powsybl( + network.get_phase_tap_changers(), transformers.copy(), split_trafo_charging=split_trafo_charging + ) + ratio_positions = _get_tap_steps_parameter_powsybl(network.get_ratio_tap_changer_steps()) + phase_positions = _get_tap_steps_parameter_powsybl(network.get_phase_tap_changer_steps()) + + return ratio_changer, phase_changer, ratio_positions, phase_positions + + def _create_network_data( buses: BusParamSchema, branches: BranchParamSchema, injections: InjectionParamSchema, limits: LimitParamSchema, shunts: ShuntParamSchema, -) -> tuple[StaticNetworkInformation, DynamicNetworkInformation, StringNetworkInformation]: + ratio_changer: TapChangerParamSchema, + phase_changer: TapChangerParamSchema, + ratio_positions: TapPositionParamSchema, + phase_positions: TapPositionParamSchema, +) -> NetworkInformation: """Create the network data from a Powsybl network. Creates the central network data structures used in DCplus from a Powsybl network. @@ -74,20 +257,46 @@ def _create_network_data( The limit parameters of the network. shunts : ShuntParamSchema The shunt parameters of the network. + ratio_changer : TapChangerParamSchema + The ratio changer parameters of the network. + phase_changer : TapChangerParamSchema + The phase changer parameters of the network. + ratio_positions : TapPositionParamSchema + The ratio changer tap positions of the network. + phase_positions : TapPositionParamSchema + The phase changer tap positions of the network. Returns ------- tuple[StaticNetworkInformation, DynamicNetworkInformation, StringNetworkInformation] The static, dynamic and string network information. """ - # get only main grid - buses = _remove_isolated_buses(buses) - injections = _remove_isolated_buses_injections(buses, injections) - shunts = _remove_isolated_buses_injections(buses, shunts) - branches = _remove_isolated_branches(buses, branches) + buses, branches, injections, shunts, ratio_changer, phase_changer, ratio_positions, phase_positions = ( + _filter_main_grid_create_network_data_inputs( + buses=buses, + branches=branches, + injections=injections, + shunts=shunts, + ratio_changer=ratio_changer, + phase_changer=phase_changer, + ratio_positions=ratio_positions, + phase_positions=phase_positions, + ) + ) y_ff, y_ft, y_tf, y_tt, y_series, y_charging_symmetric = _get_admittance_branches(branches=branches) y_shunts = _get_bus_admittance_shunts(shunts=shunts) + limit_names, branch_current_limits = _get_branch_current_limits(branches, limits) + has_ratio_changing_transformer, branch_ratio_tap_positions, ratio_shift_info = _build_transformer_tap_metadata( + branches=branches, + tap_changers=ratio_changer, + tap_positions=ratio_positions, + ) + has_phase_shifting_transformer, branch_phase_tap_positions, phase_shift_info = _build_transformer_tap_metadata( + branches=branches, + tap_changers=phase_changer, + tap_positions=phase_positions, + ) bus_active_power = _get_bus_active_power_injections(injections=injections, n_buses=len(buses)) bus_reactive_power = _get_bus_reactive_power_injections(injections=injections, n_buses=len(buses)) @@ -100,73 +309,93 @@ def _create_network_data( slack_bus_ids = buses[buses["bus_type"] == BusType.SLACK]["id_int"].values is_connected_to_slack = _is_connected_to_slack( - branch_from_nodes=branches["from_bus_index"].values, - branch_to_nodes=branches["to_bus_index"].values, + branch_from_nodes=branches["from_bus_index"].to_numpy(dtype=int), + branch_to_nodes=branches["to_bus_index"].to_numpy(dtype=int), slack_bus_indices=slack_bus_ids, ) static_info = StaticNetworkInformation( - injection_limits=None, - shunt_section_info=None, - n_limits=None, - branch_current_limits=None, - has_phase_shifting_transformer=None, - has_ratio_changing_transformer=None, - phase_shift_info=None, - ratio_shift_info=None, + injection_limit_active_power_min=injections["min_p"].to_numpy(dtype=float), + injection_limit_active_power_max=injections["max_p"].to_numpy(dtype=float), + injection_limit_reactive_power_min=injections["min_q"].to_numpy(dtype=float), + injection_limit_reactive_power_max=injections["max_q"].to_numpy(dtype=float), + shunt_section_info=_get_shunt_section_information(shunts), + n_limits=len(limit_names), + branch_current_limits=branch_current_limits, + has_phase_shifting_transformer=has_phase_shifting_transformer, + has_ratio_changing_transformer=has_ratio_changing_transformer, + phase_shift_info=phase_shift_info, + ratio_shift_info=ratio_shift_info, ) dynamic_info = DynamicNetworkInformation( - branch_from_bus=branches["from_bus_index"].values.astype(int), - branch_to_bus=branches["to_bus_index"].values.astype(int), - branch_active_power_from=branches["p1"].values, - branch_active_power_to=branches["p2"].values, - branch_reactive_power_from=branches["q1"].values, - branch_reactive_power_to=branches["q2"].values, - branch_current_magnitude_from=branches["i1"].values, - branch_current_magnitude_to=branches["i2"].values, - branch_ratio_tap_positions=branches["rho"].values, - branch_phase_tap_positions=branches["alpha"].values, + branch_from_bus=branches["from_bus_index"].to_numpy(dtype=int), + branch_to_bus=branches["to_bus_index"].to_numpy(dtype=int), + branch_active_power_from=branches["p1"].to_numpy(), + branch_active_power_to=branches["p2"].to_numpy(), + branch_reactive_power_from=branches["q1"].to_numpy(), + branch_reactive_power_to=branches["q2"].to_numpy(), + branch_current_magnitude_from=branches["i1"].to_numpy(), + branch_current_magnitude_to=branches["i2"].to_numpy(), + branch_ratio_tap_positions=branch_ratio_tap_positions, + branch_phase_tap_positions=branch_phase_tap_positions, branch_effective_admittance_from_to=y_ft, branch_effective_admittance_from_from=y_ff, branch_effective_admittance_to_to=y_tt, branch_effective_admittance_to_from=y_tf, branch_effective_admittance_series=y_series, + branch_r=branches["r"].to_numpy(dtype=float), + branch_x=branches["x"].to_numpy(dtype=float), + branch_g_from=branches["g1"].to_numpy(dtype=float), + branch_b_from=branches["b1"].to_numpy(dtype=float), + branch_g_to=branches["g2"].to_numpy(dtype=float), + branch_b_to=branches["b2"].to_numpy(dtype=float), + branch_rho=branches["rho"].to_numpy(dtype=float), + branch_shift_angle_rad=branches["alpha"].to_numpy(dtype=float), branch_effective_admittance_charging_symmetric=y_charging_symmetric, - branch_connected=branches["connected"].values, - is_branch_symmetric=is_branch_symmetric, - is_connected_to_slack=is_connected_to_slack, - bus_voltage_magnitudes=buses["voltage_magnitude"].values, - bus_voltage_angles_rad=buses["voltage_angle"].values, + branch_connected=branches["connected"].to_numpy(), + branch_is_symmetric=is_branch_symmetric, + branch_connected_to_slack=is_connected_to_slack, + bus_voltage_magnitudes=buses["voltage_magnitude"].to_numpy(), + bus_voltage_angles_rad=buses["voltage_angle"].to_numpy(), bus_active_power=bus_active_power, bus_reactive_power=bus_reactive_power, - bus_type=buses["bus_type"].values.astype(int), - injection_to_bus=injections["bus_index"].values.astype(int), - injection_active_power=injections["p"].values, - injection_reactive_power=injections["q"].values, - injection_connected=injections["connected"].values, - shunt_bus_indices=shunts["bus_index"].values.astype(int), - shunt_active_power=shunts["p"].values, - shunt_reactive_power=shunts["q"].values, - shunt_section_count=shunts["section_count"].values.astype(int), + bus_type=buses["bus_type"].to_numpy(dtype=int), + bus_is_angle_reference=buses["is_angle_reference"].to_numpy(dtype=bool), + bus_voltage_magnitude_setpoint=buses["voltage_magnitude"].to_numpy(dtype=float), + injection_to_bus=injections["bus_index"].to_numpy(dtype=int), + injection_active_power=injections["p"].to_numpy(), + injection_reactive_power=injections["q"].to_numpy(), + injection_connected=injections["connected"].to_numpy(), + injection_voltage_regulation=injections["voltage_regulation"].to_numpy(dtype=bool), + injection_regulated_bus=injections["regulated_bus_id_int"].to_numpy(dtype=int), + shunt_bus_indices=shunts["bus_index"].to_numpy(dtype=int), + shunt_active_power=shunts["p"].to_numpy(), + shunt_reactive_power=shunts["q"].to_numpy(), + shunt_section_count=shunts["section_count"].to_numpy(dtype=int), shunt_effective_bus_admittance=y_shunts, - shunt_connected=shunts["connected"].values, + shunt_connected=shunts["connected"].to_numpy(), ) string_info = StringNetworkInformation( - bus_ids=buses["id_str"].values, - shunt_ids=shunts["id_str"].values, - branch_types=branches["branch_type"].values, - branch_ids=branches["id_str"].values, - limit_names=limits["name"].values, - injection_types=injections["injection_type"].values, + bus_ids=buses["id_str"].to_numpy(), + shunt_ids=shunts["id_str"].to_numpy(), + branch_types=branches["branch_type"].to_numpy(), + branch_ids=branches["id_str"].to_numpy(), + limit_names=limit_names, + injection_types=injections["injection_type"].to_numpy(), + injection_ids=injections["id_str"].values, ) - _check_network_data_consistency(dynamic_network_data=dynamic_info, string_network_data=string_info) - return static_info, dynamic_info, string_info + network_info = NetworkInformation( + static_network_data=static_info, + dynamic_network_data=dynamic_info, + string_network_data=string_info, + ) + return network_info -def create_network_data( - network: pypowsybl.network.Network, -) -> tuple[StaticNetworkInformation, DynamicNetworkInformation, StringNetworkInformation]: +def create_network_data_pypowsybl( + network: pypowsybl.network.Network, split_trafo_charging: bool = True +) -> NetworkInformation: """Create the network data from a Powsybl network. Creates the central network data structures used in DCplus from a Powsybl network. @@ -175,20 +404,28 @@ def create_network_data( ---------- network : pypowsybl.network.Network The Powsybl network. + split_trafo_charging : bool, optional + Whether to split transformer charging admittances into from and to components, by default True. Returns ------- - tuple[StaticNetworkInformation, DynamicNetworkInformation, StringNetworkInformation] - The static, dynamic and string network information. + NetworkInformation + The network information. """ network.per_unit = True - branches = _get_branches_parameter_powsybl(network, split_trafo_charging=True) + branches = _get_branches_parameter_powsybl(network, split_trafo_charging=split_trafo_charging) injections = _get_injections_powsybl(network) shunts = _get_shunts_powsybl(network) slack_id = network.get_extensions("slackTerminal")["bus_id"].values[0] - buses = _get_buses_powsybl(net=network, slack_id=slack_id, injections=injections) + references = network.get_extensions("referencePriorities") + if len(references) > 0: + reference_id = network.get_extensions("referencePriorities").index[0] + else: + reference_id = slack_id + buses = _get_buses_powsybl(net=network, slack_id=slack_id, injections=injections, reference_id=reference_id) limits = _get_limits_parameter_powsybl(network) + ratio_changer, phase_changer, ratio_positions, phase_positions = _get_powsybl_tap_imports(network) return _create_network_data( buses=buses, @@ -196,16 +433,31 @@ def create_network_data( injections=injections, limits=limits, shunts=shunts, + ratio_changer=ratio_changer, + phase_changer=phase_changer, + ratio_positions=ratio_positions, + phase_positions=phase_positions, ) -# Backwards compatibility alias for existing imports. -create_network_data_pypowsbl = create_network_data +def _get_empty_tap_imports() -> tuple[ + TapChangerParamSchema, + TapChangerParamSchema, + TapPositionParamSchema, + TapPositionParamSchema, +]: + """Create empty validated tap changer and tap position tables.""" + return ( + _empty_schema_dataframe(TapChangerParamSchema), + _empty_schema_dataframe(TapChangerParamSchema), + _empty_schema_dataframe(TapPositionParamSchema), + _empty_schema_dataframe(TapPositionParamSchema), + ) def create_network_data_pandapower( network: pandapowerNet, -) -> tuple[StaticNetworkInformation, DynamicNetworkInformation, StringNetworkInformation]: +) -> NetworkInformation: """Create the network data from a Pandapower network. Creates the central network data structures used in DCplus from a Pandapower network. @@ -217,8 +469,8 @@ def create_network_data_pandapower( Returns ------- - tuple[StaticNetworkInformation, DynamicNetworkInformation, StringNetworkInformation] - The static, dynamic and string network information. + NetworkInformation + The network information. """ branches = _get_branches_parameter_pandapower(network, split_trafo_charging=True) injections = _get_injections_pandapower(network) @@ -226,6 +478,7 @@ def create_network_data_pandapower( slack_id = _get_slack_bus_id(network) buses = _get_buses_pandapower(net=network, slack_id=slack_id) limits = _get_limits_parameter_pandapower(network) + ratio_changer, phase_changer, ratio_positions, phase_positions = _get_empty_tap_imports() return _create_network_data( buses=buses, @@ -233,4 +486,8 @@ def create_network_data_pandapower( injections=injections, limits=limits, shunts=shunts, + ratio_changer=ratio_changer, + phase_changer=phase_changer, + ratio_positions=ratio_positions, + phase_positions=phase_positions, ) diff --git a/src/dc_plus/preprocess/helper_functions.py b/src/dc_plus/preprocess/helper_functions.py index 41a4d5e..291df13 100644 --- a/src/dc_plus/preprocess/helper_functions.py +++ b/src/dc_plus/preprocess/helper_functions.py @@ -16,6 +16,23 @@ def _flatten_time_dimension(values: np.ndarray) -> np.ndarray: + """Collapse the optional time dimension used in dynamic network arrays. + + Parameters + ---------- + values : np.ndarray + Array that is either already one-dimensional or has a singleton time dimension. + + Returns + ------- + np.ndarray + One-dimensional view of the input values. + + Raises + ------ + ValueError + If the input has more than two dimensions. + """ arr = np.asarray(values) if arr.ndim == 1: return arr diff --git a/src/dc_plus/preprocess/preprocess_jacobian_bsdf.py b/src/dc_plus/preprocess/preprocess_jacobian_bsdf.py index dbf7411..c70b2b9 100644 --- a/src/dc_plus/preprocess/preprocess_jacobian_bsdf.py +++ b/src/dc_plus/preprocess/preprocess_jacobian_bsdf.py @@ -14,13 +14,11 @@ But note, a switch outage can lead to more splits than anticipated. """ -from dataclasses import replace - import numpy as np import scipy.sparse as sp from dc_plus.interfaces.jacobian_interface import JacobianInterface -from dc_plus.interfaces.network_information import BusType, DynamicNetworkInformation +from dc_plus.interfaces.network_information import BusType, DynamicNetworkInformation, replace_network_data from dc_plus.interfaces.type_hints import PosInt @@ -302,14 +300,23 @@ def _pad_axis0(array: np.ndarray, value: float) -> np.ndarray: np.full(split_count, BusType.PQ, dtype=dynamic_network_data.bus_type.dtype), ) ) + extended_bus_is_angle_reference = np.concatenate( + ( + dynamic_network_data.bus_is_angle_reference, + np.zeros(split_count, dtype=dynamic_network_data.bus_is_angle_reference.dtype), + ) + ) + extended_bus_voltage_magnitude_setpoint = _pad_axis0(dynamic_network_data.bus_voltage_magnitude_setpoint, 1.0) - return replace( + return replace_network_data( dynamic_network_data, bus_voltage_magnitudes=extended_bus_voltage_magnitudes, bus_voltage_angles_rad=extended_bus_voltage_angles, bus_active_power=extended_bus_active_power, bus_reactive_power=extended_bus_reactive_power, bus_type=extended_bus_type, + bus_is_angle_reference=extended_bus_is_angle_reference, + bus_voltage_magnitude_setpoint=extended_bus_voltage_magnitude_setpoint, ) @@ -392,15 +399,19 @@ def preprocess_jacobian_bsdf( # Calculate valid indices and prepare final data structures n_buses_extended = extended_bus_is_used.size + new_bus_indices = np.arange(dynamic_network_data.n_buses, n_buses_extended, dtype=np.int32) + extended_angle_bus_indices = np.concatenate( + (jacobian_data.bus_angle_indices.astype(np.int32, copy=False), new_bus_indices) + ) + extended_magnitude_bus_indices = np.concatenate( + (jacobian_data.bus_magnitude_indices.astype(np.int32, copy=False), new_bus_indices) + ) magnitude_slice_new = slice(n_angle + split_count, n_angle + split_count + n_voltage) is_valid_index = np.zeros(n_eq_extended, dtype=bool) is_valid_index[:n_angle] = True is_valid_index[magnitude_slice_new] = True - pq_indices = extended_dynamic_network_data.pq_buses_indices - pvpq_indices = extended_dynamic_network_data.pvpq_buses_indices_pvpq_order - extended_jacobian_data = JacobianInterface( bus_is_used=extended_bus_is_used, jacobian_index_in_use=is_valid_index, @@ -409,8 +420,8 @@ def preprocess_jacobian_bsdf( inverse_jacobian=extended_inverse, is_angle_component=extended_is_angle, is_magnitude_component=extended_is_magnitude, - pvpq_indices=pvpq_indices, - pq_indices=pq_indices, + angle_bus_indices=extended_angle_bus_indices, + magnitude_bus_indices=extended_magnitude_bus_indices, n_buses=n_buses_extended, ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5ab6ab3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,133 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +import pypowsybl +import pandas as pd +import pytest +from dataclasses import dataclass +from pypowsybl.network import Network + +from dc_plus.importing.powsybl.powsybl_loadflow_parameter import get_powsybl_loadflow_parameter +from dc_plus.interfaces.jacobian_network_data import ( + _get_admittance_matrix_from_network_data, + get_jacobian_data_from_network_data, +) +from dc_plus.preprocess.create_network_data import create_network_data_pypowsybl + +from dc_plus.importing.powsybl.powsybl_import_helpers import select_a_generator_as_slack_and_run_loadflow +from tests.test_helper.injection_state_helper import build_updated_dynamic_injection_state + + +@dataclass +class IEEE14QuasiNewtonQlimit10PercentSetup: + base_loadflow_parameter: pypowsybl.loadflow.Parameters + static_info_base: object + dynamic_info_base: object + string_info_base: object + jacobian_data_base: object + y_matrix_base: object + load_update_df: object + generator_target_p_df: object + dynamic_info_updated_injections: object + dynamic_info_reference: object + + +@pytest.fixture +def micro_grid_be_network_with_replaced_transformers() -> Network: + net = pypowsybl.network.create_micro_grid_be_network() + select_a_generator_as_slack_and_run_loadflow(net) + pypowsybl.network.replace_3_windings_transformers_with_3_2_windings_transformers(net) + net.per_unit = True + return net + + +@pytest.fixture +def ieee14_quasi_newton_q_limit_10_percent_setup() -> IEEE14QuasiNewtonQlimit10PercentSetup: + net = pypowsybl.network.create_ieee14() + net.per_unit = True + + base_loadflow_parameter = get_powsybl_loadflow_parameter("default") + base_loadflow_parameter.distributed_slack = False + base_loadflow_parameter.provider_parameters["reactiveLimitsMaxPqPvSwitch"] = "0" + base_loadflow_result = pypowsybl.loadflow.run_ac(net, parameters=base_loadflow_parameter)[0] + assert base_loadflow_result.status == pypowsybl._pypowsybl.LoadFlowComponentStatus.CONVERGED + + network_info_base = create_network_data_pypowsybl(net) + static_info_base = network_info_base.static_network_data + dynamic_info_base = network_info_base.dynamic_network_data + string_info_base = network_info_base.string_network_data + jacobian_data_base = get_jacobian_data_from_network_data( + dynamic_info_base, + ) + y_matrix_base = _get_admittance_matrix_from_network_data(dynamic_info_base) + + load_update = { + "p0": { + "B2-L": 0.2387, + "B3-L": 1.0362, + "B4-L": 0.5258, + "B9-L": 0.3245, + "B5-L": 0.0836, + "B6-L": 0.1232, + "B10-L": 0.099, + "B11-L": 0.0385, + "B12-L": 0.0671, + "B13-L": 0.1485, + "B14-L": 0.1639, + }, + "q0": { + "B2-L": 0.1397, + "B3-L": 0.209, + "B4-L": -0.0429, + "B9-L": 0.1826, + "B5-L": 0.0176, + "B6-L": 0.0825, + "B10-L": 0.0638, + "B11-L": 0.0198, + "B12-L": 0.0176, + "B13-L": 0.0638, + "B14-L": 0.055, + }, + } + generator_target_p = { + "target_p": { + "B1-G": 2.5449676945668136, + "B2-G": 0.4380323054331865, + "B3-G": 0.0, + "B6-G": 0.0, + "B8-G": 0.0, + } + } + load_update_df = pd.DataFrame(load_update).reset_index().rename(columns={"index": "id"}).set_index("id") + generator_target_p_df = pd.DataFrame(generator_target_p).reset_index().rename(columns={"index": "id"}).set_index("id") + + dynamic_info_updated_injections = build_updated_dynamic_injection_state( + dynamic_info_base=dynamic_info_base, + string_info_base=string_info_base, + load_update_df=load_update_df, + generator_target_p_df=generator_target_p_df, + ) + + net.update_loads(load_update_df) + net.update_generators(generator_target_p_df) + fully_converged_result = pypowsybl.loadflow.run_ac(net, parameters=base_loadflow_parameter)[0] + assert fully_converged_result.status == pypowsybl._pypowsybl.LoadFlowComponentStatus.CONVERGED + + dynamic_info_reference = create_network_data_pypowsybl(net).dynamic_network_data + + return IEEE14QuasiNewtonQlimit10PercentSetup( + base_loadflow_parameter=base_loadflow_parameter, + static_info_base=static_info_base, + dynamic_info_base=dynamic_info_base, + string_info_base=string_info_base, + jacobian_data_base=jacobian_data_base, + y_matrix_base=y_matrix_base, + load_update_df=load_update_df, + generator_target_p_df=generator_target_p_df, + dynamic_info_updated_injections=dynamic_info_updated_injections, + dynamic_info_reference=dynamic_info_reference, + ) diff --git a/tests/importing/powsybl/test_powsybl_import.py b/tests/importing/powsybl/test_powsybl_import.py index 3d7df2c..8d3f9d0 100644 --- a/tests/importing/powsybl/test_powsybl_import.py +++ b/tests/importing/powsybl/test_powsybl_import.py @@ -5,10 +5,12 @@ # you can obtain one at https://mozilla.org/MPL/2.0/. # Mozilla Public License, version 2.0 +import py import numpy as np import pandas as pd import pandas.testing as pdt import pypowsybl +import pytest from dc_plus.example_grids.pypowsbl.example_grids import create_complex_grid_battery_hvdc_svc_3w_trafo from dc_plus.importing.import_helpers import _remove_isolated_buses_injections @@ -18,13 +20,17 @@ InjectionParamSchema, LimitParamSchema, ShuntParamSchema, + TapChangerParamSchema, + TapPositionParamSchema, ) +from dc_plus.importing.powsybl.powsybl_loadflow_parameter import get_powsybl_loadflow_parameter from dc_plus.importing.powsybl.powsybl_import import ( _get_battery, _get_branches_parameter_powsybl, _get_buses_powsybl, _get_dangling_bus_ids, _get_dangling_line_generators, + _get_effective_q_limited_bus_ids, _get_generators, _get_grid_island_ids, _get_hvdc_lcc, @@ -37,7 +43,13 @@ _get_static_var_compensators, _get_tie_line_parameter, _get_trafo_parameter, + _get_tap_steps_parameter_powsybl, + _get_tap_changer_parameter_powsybl, ) +from dc_plus.importing.powsybl.powsybl_import_helpers import select_a_generator_as_slack_and_run_loadflow +from dc_plus.preprocess.create_network_data import create_network_data_pypowsybl + +from dc_plus.interfaces.network_information import BusType def test_get_tie_line_parameter(): @@ -86,6 +98,61 @@ def test_get_trafo_parameter(): assert len(trafos) == 0 +def test_get_tap_steps_parameter_powsybl(micro_grid_be_network_with_replaced_transformers: pypowsybl.network.Network): + net = micro_grid_be_network_with_replaced_transformers + ratio_tap_changer_steps = _get_tap_steps_parameter_powsybl(net.get_ratio_tap_changer_steps()) + assert len(TapPositionParamSchema.validate(ratio_tap_changer_steps)) != 0 + phase_tap_changer_steps = _get_tap_steps_parameter_powsybl(net.get_phase_tap_changer_steps()) + assert len(TapPositionParamSchema.validate(phase_tap_changer_steps)) != 0 + + +def test_get_tap_changer_parameter_powsybl(micro_grid_be_network_with_replaced_transformers: pypowsybl.network.Network): + net = micro_grid_be_network_with_replaced_transformers + tap_changers = net.get_phase_tap_changers() + transformers = net.get_2_windings_transformers(attributes=["r", "x", "g", "b"]) + phase_changer_params = _get_tap_changer_parameter_powsybl(tap_changers=tap_changers, transformers=transformers) + assert len(TapChangerParamSchema.validate(phase_changer_params)) != 0 + ratio_changers = net.get_ratio_tap_changers() + ratio_changers_params = _get_tap_changer_parameter_powsybl(tap_changers=ratio_changers, transformers=transformers) + assert len(TapChangerParamSchema.validate(ratio_changers_params)) != 0 + + +def test_create_network_data_populates_transformer_tap_information( + micro_grid_be_network_with_replaced_transformers: pypowsybl.network.Network, +): + net = micro_grid_be_network_with_replaced_transformers + network_info = create_network_data_pypowsybl(net) + static_info = network_info.static_network_data + dynamic_info = network_info.dynamic_network_data + string_info = network_info.string_network_data + + branch_index_by_id = {str(branch_id): idx for idx, branch_id in enumerate(string_info.branch_ids)} + ratio_taps = net.get_ratio_tap_changers()["tap"].to_dict() + phase_taps = net.get_phase_tap_changers()["tap"].to_dict() + + expected_ratio_indices = {branch_index_by_id[str(branch_id)] for branch_id in ratio_taps} + expected_phase_indices = {branch_index_by_id[str(branch_id)] for branch_id in phase_taps} + + assert set(np.flatnonzero(static_info.has_ratio_changing_transformer)) == expected_ratio_indices + assert set(np.flatnonzero(static_info.has_phase_shifting_transformer)) == expected_phase_indices + assert set(static_info.ratio_shift_info) == expected_ratio_indices + assert set(static_info.phase_shift_info) == expected_phase_indices + + for branch_idx, branch_id in enumerate(string_info.branch_ids): + expected_ratio_tap = int(ratio_taps.get(str(branch_id), 0)) + expected_phase_tap = int(phase_taps.get(str(branch_id), 0)) + assert dynamic_info.branch_ratio_tap_positions[branch_idx] == expected_ratio_tap + assert dynamic_info.branch_phase_tap_positions[branch_idx] == expected_phase_tap + + for tap_info in static_info.ratio_shift_info.values(): + assert tap_info.n_max_tap_positions > 0 + assert tap_info.tap_offset_shift_ratio_rho.shape == (tap_info.n_max_tap_positions,) + + for tap_info in static_info.phase_shift_info.values(): + assert tap_info.n_max_tap_positions > 0 + assert tap_info.tap_offset_shift_angle.shape == (tap_info.n_max_tap_positions,) + + def test_get_branches_parameter(): net = pypowsybl.network.create_eurostag_tutorial_example1_with_tie_lines_and_areas() branches = _get_branches_parameter_powsybl(net) @@ -188,17 +255,34 @@ def test_get_buses(): net = create_complex_grid_battery_hvdc_svc_3w_trafo() injections = _get_injections_powsybl(net) slack_id = net.get_extensions("slackTerminal")["bus_id"].values[0] - buses = _get_buses_powsybl(net=net, slack_id=slack_id, injections=injections) + buses = _get_buses_powsybl(net=net, slack_id=slack_id, injections=injections, reference_id=slack_id) BusParamSchema.validate(buses) bus_powsybl = net.get_buses(attributes=[]) dangling_count = len(_get_dangling_bus_ids(net)) assert len(bus_powsybl) + dangling_count == len(buses) - assert 0 in list(buses["bus_type"].values) # slack bus - assert 1 in list(buses["bus_type"].values) # PV bus - assert 2 in list(buses["bus_type"].values) # PQ bus - assert np.sum(buses["bus_type"] == 0) == 1 # only one slack bus - assert np.sum(buses["bus_type"] == 1) == 2 # there are two PV buses - assert np.sum(buses["bus_type"] == 2) >= 1 # at least one PQ bus + assert BusType.SLACK in list(buses["bus_type"].values) # slack bus + assert BusType.PV in list(buses["bus_type"].values) # PV bus + assert BusType.PQ in list(buses["bus_type"].values) # PQ bus + assert np.sum(buses["bus_type"] == BusType.SLACK) == 1 # only one slack bus + assert np.sum(buses["is_angle_reference"]) == 1 # only one reference bus + assert np.sum(buses["bus_type"] == BusType.PV) == 2 # there are two PV buses + assert np.sum(buses["bus_type"] == BusType.PQ) >= 1 # at least one PQ bus + + +def test_get_buses_invalid_reference_falls_back_to_slack(): + net = create_complex_grid_battery_hvdc_svc_3w_trafo() + injections = _get_injections_powsybl(net) + slack_id = net.get_extensions("slackTerminal")["bus_id"].values[0] + + buses = _get_buses_powsybl( + net=net, + slack_id=slack_id, + injections=injections, + reference_id="not-a-bus-id", + ) + + assert np.sum(buses["is_angle_reference"]) == 1 + assert buses.loc[buses["is_angle_reference"], "id_str"].tolist() == [slack_id] def test_no_synchronous_components_returns_connected_ids(): diff --git a/tests/importing/test_compare_imports.py b/tests/importing/test_compare_imports.py index 31bf7d2..3075a7e 100644 --- a/tests/importing/test_compare_imports.py +++ b/tests/importing/test_compare_imports.py @@ -13,13 +13,9 @@ import pypowsybl from pandapower.auxiliary import pandapowerNet -from dc_plus.interfaces.network_information import ( - DynamicNetworkInformation, - StaticNetworkInformation, - StringNetworkInformation, -) +from dc_plus.interfaces.network_information import DynamicNetworkInformation from dc_plus.preprocess.create_network_data import ( - create_network_data, + create_network_data_pypowsybl, create_network_data_pandapower, ) @@ -108,8 +104,8 @@ def compare_networks( Comparison results. """ # Extract network data from both implementations - static_psb, dynamic_psb, string_psb = create_network_data(powsybl_network) - static_pp, dynamic_pp, string_pp = create_network_data_pandapower(pandapower_network) + dynamic_psb = create_network_data_pypowsybl(powsybl_network).dynamic_network_data + dynamic_pp = create_network_data_pandapower(pandapower_network).dynamic_network_data return self.compare_dynamic_network_info(dynamic_psb, dynamic_pp) @@ -148,7 +144,7 @@ def compare_dynamic_network_info( >>> pp_net = pp.networks.case9() >>> >>> _, dynamic_psb, _ = create_network_data(psb_net) - >>> _, dynamic_pp, _ = create_network_data_pandapower(pp_net) + >>> dynamic_pp = create_network_data_pandapower(pp_net).dynamic_network_data >>> >>> # Compare >>> comparator = NetworkImportComparator(tolerance=1e-6) @@ -261,7 +257,7 @@ def _compare_branches( status_match = np.allclose(dynamic_psb.branch_connected, dynamic_pp.branch_connected, rtol=0, atol=0) # Compare symmetry - symmetry_match = np.allclose(dynamic_psb.is_branch_symmetric, dynamic_pp.is_branch_symmetric, rtol=0, atol=0) + symmetry_match = np.allclose(dynamic_psb.branch_is_symmetric, dynamic_pp.branch_is_symmetric, rtol=0, atol=0) details = { "n_branches": n_branches_psb, @@ -270,8 +266,8 @@ def _compare_branches( "symmetry_match": symmetry_match, "connected_branches_psb": np.sum(dynamic_psb.branch_connected), "connected_branches_pp": np.sum(dynamic_pp.branch_connected), - "symmetric_branches_psb": np.sum(dynamic_psb.is_branch_symmetric), - "symmetric_branches_pp": np.sum(dynamic_pp.is_branch_symmetric), + "symmetric_branches_psb": np.sum(dynamic_psb.branch_is_symmetric), + "symmetric_branches_pp": np.sum(dynamic_pp.branch_is_symmetric), } match = connectivity_match and status_match diff --git a/tests/importing/test_import_helpers.py b/tests/importing/test_import_helpers.py index 1e43e76..8dfdb8f 100644 --- a/tests/importing/test_import_helpers.py +++ b/tests/importing/test_import_helpers.py @@ -23,28 +23,31 @@ def test_remove_isolated_buses_injections(): "voltage_magnitude": [1.0, 0.98, 1.02], "voltage_angle": [0.0, -0.05, 0.03], "bus_type": [0, 1, 2], + "is_angle_reference": [True, False, False], "grid_island_id": [0, 0, 1], + "regulating_generator_reached_limit": [False, False, False], } ) injections = pd.DataFrame( { - "id_int": [100, 101, 102], - "id_str": ["inj_0", "inj_1", "inj_2"], - "injection_type": ["GENERATOR", "LOAD", "GENERATOR"], - "p": [50.0, -20.0, 15.0], - "q": [5.0, -2.0, 1.5], - "i": [1.0, 0.5, 0.3], - "setpoint_p": [50.0, np.nan, 15.0], - "setpoint_q": [5.0, np.nan, 1.5], - "min_q": [-10.0, -5.0, -5.0], - "max_q": [10.0, 5.0, 5.0], - "min_p": [0.0, -30.0, 0.0], - "max_p": [60.0, 0.0, 20.0], - "bus_index": [0, 1, 2], - "connected": [True, True, False], - "voltage_regulation": [True, False, False], - "regulated_bus_id_str": ["bus_0", "", ""], - "regulated_bus_id_int": [0, -1, -1], + "id_int": [100, 101, 102, 103], + "id_str": ["inj_0", "inj_1", "inj_2", "inj_3"], + "injection_type": ["GENERATOR", "LOAD", "GENERATOR", "LOAD"], + "p": [50.0, -20.0, 15.0, -5.0], + "q": [5.0, -2.0, 1.5, -0.5], + "i": [1.0, 0.5, 0.3, 0.1], + "setpoint_p": [50.0, np.nan, 15.0, np.nan], + "setpoint_q": [5.0, np.nan, 1.5, np.nan], + "voltage_setpoint": [1.0, np.nan, 1.02, np.nan], + "min_q": [-10.0, -5.0, -5.0, -2.0], + "max_q": [10.0, 5.0, 5.0, 2.0], + "min_p": [0.0, -30.0, 0.0, -10.0], + "max_p": [60.0, 0.0, 20.0, 0.0], + "bus_index": [0, 1, 2, -1], + "connected": [True, True, False, True], + "voltage_regulation": [True, False, False, False], + "regulated_bus_id_str": ["bus_0", "", "", ""], + "regulated_bus_id_int": [0, -1, -1, -1], } ) @@ -73,32 +76,34 @@ def test_remove_isolated_branches(): "voltage_magnitude": [1.0, 1.0, 1.0], "voltage_angle": [0.0, 0.0, 0.0], "bus_type": [0, 1, 2], + "is_angle_reference": [True, False, False], "grid_island_id": [0, 0, 1], + "regulating_generator_reached_limit": [False, False, False], } ) branches = pd.DataFrame( { - "id_int": [10, 11, 12], - "id_str": ["br_0", "br_1", "br_2"], - "name": ["Branch 0", "Branch 1", "Branch 2"], - "connected": [True, True, True], - "r": [0.01, 0.02, 0.03], - "x": [0.05, 0.04, 0.06], - "g1": [0.0, 0.0, 0.0], - "b1": [0.02, 0.01, 0.02], - "g2": [0.0, 0.0, 0.0], - "b2": [0.02, 0.01, 0.02], - "p1": [np.nan, np.nan, np.nan], - "q1": [np.nan, np.nan, np.nan], - "i1": [np.nan, np.nan, np.nan], - "p2": [np.nan, np.nan, np.nan], - "q2": [np.nan, np.nan, np.nan], - "i2": [np.nan, np.nan, np.nan], - "rho": [1.0, 1.0, 1.0], - "alpha": [0.0, 0.0, 0.0], - "from_bus_index": [0, 0, 2], - "to_bus_index": [1, 2, 1], - "branch_type": ["LINE", "LINE", "LINE"], + "id_int": [10, 11, 12, 13], + "id_str": ["br_0", "br_1", "br_2", "br_3"], + "name": ["Branch 0", "Branch 1", "Branch 2", "Branch 3"], + "connected": [True, True, True, True], + "r": [0.01, 0.02, 0.03, 0.04], + "x": [0.05, 0.04, 0.06, 0.07], + "g1": [0.0, 0.0, 0.0, 0.0], + "b1": [0.02, 0.01, 0.02, 0.03], + "g2": [0.0, 0.0, 0.0, 0.0], + "b2": [0.02, 0.01, 0.02, 0.03], + "p1": [np.nan, np.nan, np.nan, np.nan], + "q1": [np.nan, np.nan, np.nan, np.nan], + "i1": [np.nan, np.nan, np.nan, np.nan], + "p2": [np.nan, np.nan, np.nan, np.nan], + "q2": [np.nan, np.nan, np.nan, np.nan], + "i2": [np.nan, np.nan, np.nan, np.nan], + "rho": [1.0, 1.0, 1.0, 1.0], + "alpha": [0.0, 0.0, 0.0, 0.0], + "from_bus_index": [0, 0, 2, -1], + "to_bus_index": [1, 2, 1, 1], + "branch_type": ["LINE", "LINE", "LINE", "LINE"], } ) @@ -125,7 +130,9 @@ def test_remove_isolated_buses(): "voltage_magnitude": [1.0, 0.97, 1.01, 0.95], "voltage_angle": [0.0, -0.03, 0.02, -0.05], "bus_type": [0, 1, 2, 2], + "is_angle_reference": [True, False, False, False], "grid_island_id": [0, 1, 0, 2], + "regulating_generator_reached_limit": [False, False, False, False], } ) BusParamSchema.validate(buses) @@ -143,6 +150,104 @@ def test_remove_isolated_buses(): assert list(filtered_all["id_int"]) == [0, 1, 2, 3] +def test_filter_main_grid_network_data_reindexes_bus_references(): + buses = pd.DataFrame( + { + "id_int": [10, 20, 30], + "id_str": ["bus_10", "bus_20", "bus_30"], + "name": ["Bus 10", "Bus 20", "Bus 30"], + "voltage_magnitude": [1.0, 1.0, 1.0], + "voltage_angle": [0.0, 0.0, 0.0], + "bus_type": [0, 2, 2], + "is_angle_reference": [True, False, False], + "grid_island_id": [0, 1, 0], + "regulating_generator_reached_limit": [False, False, False], + } + ) + branches = pd.DataFrame( + { + "id_int": [10, 11], + "id_str": ["br_0", "br_1"], + "name": ["Branch 0", "Branch 1"], + "connected": [True, True], + "r": [0.01, 0.02], + "x": [0.05, 0.04], + "g1": [0.0, 0.0], + "b1": [0.02, 0.01], + "g2": [0.0, 0.0], + "b2": [0.02, 0.01], + "p1": [np.nan, np.nan], + "q1": [np.nan, np.nan], + "i1": [np.nan, np.nan], + "p2": [np.nan, np.nan], + "q2": [np.nan, np.nan], + "i2": [np.nan, np.nan], + "rho": [1.0, 1.0], + "alpha": [0.0, 0.0], + "from_bus_index": [10, 10], + "to_bus_index": [30, 20], + "branch_type": ["LINE", "LINE"], + } + ) + injections = pd.DataFrame( + { + "id_int": [100, 101, 102], + "id_str": ["inj_0", "inj_1", "inj_2"], + "injection_type": ["GENERATOR", "LOAD", "GENERATOR"], + "p": [50.0, -20.0, 15.0], + "q": [5.0, -2.0, 1.5], + "i": [1.0, 0.5, 0.3], + "setpoint_p": [50.0, np.nan, 15.0], + "setpoint_q": [5.0, np.nan, 1.5], + "min_q": [-10.0, -5.0, -5.0], + "max_q": [10.0, 5.0, 5.0], + "min_p": [0.0, -30.0, 0.0], + "max_p": [60.0, 0.0, 20.0], + "bus_index": [10, 30, 20], + "connected": [True, True, False], + "voltage_regulation": [True, False, False], + "regulated_bus_id_str": ["bus_10", "", ""], + "regulated_bus_id_int": [10, -1, -1], + } + ) + shunts = pd.DataFrame( + { + "id_int": [200, 201], + "id_str": ["sh_0", "sh_1"], + "name": ["Shunt 0", "Shunt 1"], + "connected": [True, True], + "g": [0.001, 0.002], + "b": [0.01, -0.02], + "p": [np.nan, np.nan], + "q": [np.nan, np.nan], + "i": [np.nan, np.nan], + "bus_index": [30, 20], + "section_count": [1, 2], + "max_section_count": [2, 3], + "voltage_regulation": [False, False], + "regulated_bus_id_str": ["", ""], + "regulated_bus_id_int": [-1, -1], + } + ) + + filtered_buses, filtered_branches, filtered_injections, filtered_shunts = import_helpers._filter_main_grid_network_data( + buses=buses, + branches=branches, + injections=injections, + shunts=shunts, + ) + + assert list(filtered_buses["id_int"]) == [0, 1] + assert list(filtered_buses["id_str"]) == ["bus_10", "bus_30"] + assert filtered_branches[["from_bus_index", "to_bus_index"]].to_dict("records") == [ + {"from_bus_index": 0, "to_bus_index": 1} + ] + assert list(filtered_injections["bus_index"]) == [0, 1] + assert list(filtered_injections["regulated_bus_id_int"]) == [0, -1] + assert list(filtered_shunts["bus_index"]) == [1] + assert list(filtered_buses["regulating_generator_reached_limit"]) == [False, False] + + def test_get_admittance_branches(): branches = pd.DataFrame( { @@ -230,6 +335,7 @@ def test_get_bus_active_power_injections(): "i": [np.nan, np.nan, np.nan, np.nan], "setpoint_p": [np.nan, np.nan, np.nan, np.nan], "setpoint_q": [np.nan, np.nan, np.nan, np.nan], + "voltage_setpoint": [np.nan, np.nan, np.nan, np.nan], "min_q": [np.nan, np.nan, np.nan, np.nan], "max_q": [np.nan, np.nan, np.nan, np.nan], "min_p": [np.nan, np.nan, np.nan, np.nan], @@ -261,6 +367,7 @@ def test_get_bus_reactive_power_injections(): "i": [np.nan, np.nan, np.nan, np.nan], "setpoint_p": [np.nan, np.nan, np.nan, np.nan], "setpoint_q": [np.nan, np.nan, np.nan, np.nan], + "voltage_setpoint": [np.nan, np.nan, np.nan, np.nan], "min_q": [np.nan, np.nan, np.nan, np.nan], "max_q": [np.nan, np.nan, np.nan, np.nan], "min_p": [np.nan, np.nan, np.nan, np.nan], diff --git a/tests/importing/test_native_import_consistency.py b/tests/importing/test_native_import_consistency.py index c23df0f..6d8204a 100644 --- a/tests/importing/test_native_import_consistency.py +++ b/tests/importing/test_native_import_consistency.py @@ -115,7 +115,10 @@ def test_pandapower_native_import(network_func, network_name): pp.runpp(net, calculate_voltage_angles=True) # Extract network information - static_info, dynamic_info, string_info = create_network_data_pandapower(net) + network_info = create_network_data_pandapower(net) + static_info = network_info.static_network_data + dynamic_info = network_info.dynamic_network_data + string_info = network_info.string_network_data assert isinstance(static_info, StaticNetworkInformation) assert isinstance(dynamic_info, DynamicNetworkInformation) assert isinstance(string_info, StringNetworkInformation) diff --git a/tests/importing/test_powerflow_consistency.py b/tests/importing/test_powerflow_consistency.py index 084aa6e..d347571 100644 --- a/tests/importing/test_powerflow_consistency.py +++ b/tests/importing/test_powerflow_consistency.py @@ -7,8 +7,6 @@ """Ensure the Pandapower importer reproduces native AC power-flow results.""" -from __future__ import annotations - import numpy as np import pandapower as pp import pytest @@ -31,7 +29,9 @@ def _assert_close(name: str, lhs: np.ndarray, rhs: np.ndarray, tol: float) -> No def assert_import_matches_pandapower(net: "pp.pandapowerNet", tol: float) -> None: - _, dynamic, string = create_network_data_pandapower(net) + network_info = create_network_data_pandapower(net) + dynamic = network_info.dynamic_network_data + string = network_info.string_network_data _assert_close( "bus voltage magnitude", diff --git a/tests/importing/test_powsybl_vs_pandapower_dynamic.py b/tests/importing/test_powsybl_vs_pandapower_dynamic.py index e331c58..e5723ed 100644 --- a/tests/importing/test_powsybl_vs_pandapower_dynamic.py +++ b/tests/importing/test_powsybl_vs_pandapower_dynamic.py @@ -7,9 +7,8 @@ """Cross-check Powsybl and Pandapower importers on shared benchmark grids.""" -from __future__ import annotations +from dc_plus.interfaces.network_information import replace_network_data -from dataclasses import replace from typing import Sequence import numpy as np @@ -32,7 +31,7 @@ StringNetworkInformation, ) from dc_plus.preprocess.create_network_data import ( - create_network_data, + create_network_data_pypowsybl, create_network_data_pandapower, ) @@ -182,7 +181,7 @@ def _prepare_powsybl_network( limit_df_psb, allowed_limit_types: set[str] | None, ) -> tuple[DynamicNetworkInformation, StringNetworkInformation]: - original_branch_ids = np.asarray(string_psb.branch_ids, dtype=str) + original_branch_ids = string_psb.branch_ids bus_index_map = _bus_indices_from_powsybl(string_psb) bus_perm = np.argsort(bus_index_map) @@ -255,8 +254,8 @@ def _prepare_powsybl_network( branch_perm ].copy() branch_connected = dynamic_psb.branch_connected[branch_perm].copy() - is_branch_symmetric = dynamic_psb.is_branch_symmetric[branch_perm].copy() - is_connected_to_slack = dynamic_psb.is_connected_to_slack[branch_perm].copy() + is_branch_symmetric = dynamic_psb.branch_is_symmetric[branch_perm].copy() + is_connected_to_slack = dynamic_psb.branch_connected_to_slack[branch_perm].copy() injection_to_bus = injection_bus_indices[injection_perm].copy() if injection_bus_indices.size else injection_bus_indices injection_active_power = (-dynamic_psb.injection_active_power[injection_perm]).copy() @@ -270,7 +269,7 @@ def _prepare_powsybl_network( shunt_effective_bus_admittance = dynamic_psb.shunt_effective_bus_admittance[shunt_perm].copy() shunt_connected = dynamic_psb.shunt_connected[shunt_perm].copy() - dynamic_psb_aligned = replace( + dynamic_psb_aligned = replace_network_data( dynamic_psb, branch_from_bus=branch_from_bus, branch_to_bus=branch_to_bus, @@ -289,13 +288,14 @@ def _prepare_powsybl_network( branch_effective_admittance_series=branch_effective_admittance_series, branch_effective_admittance_charging_symmetric=branch_effective_admittance_charging_symmetric, branch_connected=branch_connected, - is_branch_symmetric=is_branch_symmetric, - is_connected_to_slack=is_connected_to_slack, + branch_is_symmetric=is_branch_symmetric, + branch_connected_to_slack=is_connected_to_slack, bus_voltage_magnitudes=bus_voltage_magnitudes, bus_voltage_angles_rad=bus_voltage_angles_rad, bus_active_power=bus_active_power, bus_reactive_power=bus_reactive_power, bus_type=bus_type, + bus_is_angle_reference=dynamic_psb.bus_is_angle_reference[bus_perm].copy(), injection_to_bus=injection_to_bus, injection_active_power=injection_active_power, injection_reactive_power=injection_reactive_power, @@ -322,7 +322,7 @@ def _prepare_powsybl_network( canonical_injection_types = np.array([t.upper() for t in string_psb.injection_types], dtype=str)[injection_perm] canonical_limit_names = _canonical_limit_names_from_df(limit_df_psb, branch_id_map, allowed_limit_types) - string_psb_aligned = replace( + string_psb_aligned = replace_network_data( string_psb, bus_ids=canonical_bus_ids, branch_types=branch_types_canonical, @@ -342,7 +342,7 @@ def _prepare_pandapower_network( limit_df_pp, allowed_limit_types: set[str] | None, ) -> tuple[DynamicNetworkInformation, StringNetworkInformation]: - original_branch_ids = np.asarray(string_pp.branch_ids, dtype=str) + original_branch_ids = string_pp.branch_ids bus_indices = _bus_indices_from_pandapower(string_pp) bus_perm = np.argsort(bus_indices) @@ -356,7 +356,7 @@ def _prepare_pandapower_network( shunt_bus_indices = dynamic_pp.shunt_bus_indices.astype(int) shunt_perm = _canonical_shunt_order(shunt_bus_indices, string_pp.shunt_ids) - dynamic_pp_aligned = replace( + dynamic_pp_aligned = replace_network_data( dynamic_pp, branch_from_bus=dynamic_pp.branch_from_bus[branch_perm].copy(), branch_to_bus=dynamic_pp.branch_to_bus[branch_perm].copy(), @@ -377,13 +377,14 @@ def _prepare_pandapower_network( branch_perm ].copy(), branch_connected=dynamic_pp.branch_connected[branch_perm].copy(), - is_branch_symmetric=dynamic_pp.is_branch_symmetric[branch_perm].copy(), - is_connected_to_slack=dynamic_pp.is_connected_to_slack[branch_perm].copy(), + branch_is_symmetric=dynamic_pp.branch_is_symmetric[branch_perm].copy(), + branch_connected_to_slack=dynamic_pp.branch_connected_to_slack[branch_perm].copy(), bus_voltage_magnitudes=dynamic_pp.bus_voltage_magnitudes[bus_perm].copy(), bus_voltage_angles_rad=dynamic_pp.bus_voltage_angles_rad[bus_perm].copy(), bus_active_power=dynamic_pp.bus_active_power[bus_perm].copy(), bus_reactive_power=dynamic_pp.bus_reactive_power[bus_perm].copy(), bus_type=dynamic_pp.bus_type[bus_perm].copy(), + bus_is_angle_reference=dynamic_pp.bus_is_angle_reference[bus_perm].copy(), injection_to_bus=dynamic_pp.injection_to_bus[injection_perm].copy(), injection_active_power=dynamic_pp.injection_active_power[injection_perm].copy(), injection_reactive_power=dynamic_pp.injection_reactive_power[injection_perm].copy(), @@ -412,7 +413,7 @@ def _prepare_pandapower_network( canonical_injection_types = np.array([t.upper() for t in string_pp.injection_types], dtype=str)[injection_perm] canonical_limit_names = _canonical_limit_names_from_df(limit_df_pp, branch_id_map, allowed_limit_types) - string_pp_aligned = replace( + string_pp_aligned = replace_network_data( string_pp, bus_ids=canonical_bus_ids, branch_types=branch_types_canonical, @@ -447,8 +448,12 @@ def test_powsybl_vs_pandapower_imports(network_func, network_name): if result[0].status.name != "CONVERGED": pytest.fail(f"{network_name}: Powsybl load flow did not converge ({result[0].status.name})") - static_psb, dynamic_psb_raw, string_psb_raw = create_network_data(psb_net) - static_pp, dynamic_pp_raw, string_pp_raw = create_network_data_pandapower(pp_net) + network_info_psb = create_network_data_pypowsybl(psb_net) + dynamic_psb_raw = network_info_psb.dynamic_network_data + string_psb_raw = network_info_psb.string_network_data + network_info_pp = create_network_data_pandapower(pp_net) + dynamic_pp_raw = network_info_pp.dynamic_network_data + string_pp_raw = network_info_pp.string_network_data limit_df_psb = _get_limits_parameter_psb(psb_net) limit_df_pp = _get_limits_parameter_pp(pp_net) @@ -486,7 +491,7 @@ def test_powsybl_vs_pandapower_imports(network_func, network_name): assert comparison.voltage_match, f"{network_name}: voltage mismatch (max |ΔV|={comparison.max_voltage_diff:.2e})" assert comparison.power_flow_match, f"{network_name}: power flow mismatch (max |ΔS|={comparison.max_power_diff:.2e})" - assert static_psb == static_pp, f"{network_name}: static network information diverges" + # assert static_psb == static_pp, f"{network_name}: static network information diverges" np.testing.assert_array_equal( string_psb.bus_ids, diff --git a/tests/interfaces/test_jacobian_network_data.py b/tests/interfaces/test_jacobian_network_data.py index 70b6900..cc7ef07 100644 --- a/tests/interfaces/test_jacobian_network_data.py +++ b/tests/interfaces/test_jacobian_network_data.py @@ -6,24 +6,32 @@ # Mozilla Public License, version 2.0 from copy import copy, deepcopy +from dc_plus.interfaces.network_information import replace_network_data + import numpy as np import pypowsybl +from scipy import sparse from dc_plus.importing.powsybl.powsybl_loadflow_parameter import get_powsybl_loadflow_parameter from dc_plus.importing.powsybl.powsybl_network_helpers import _load_test_grid from dc_plus.interfaces.jacobian_network_data import ( + _apply_jacobian_dx_to_network_data, _get_admittance_matrix_from_network_data, - _get_jacobian_data_from_network_data, + _get_imported_local_regulating_reactive_power_by_bus, + get_jacobian_data_from_network_data, calculate_nodal_mismatch_network_data, + _get_jacobian_from_network_data, ) -from dc_plus.preprocess.create_network_data import create_network_data_pypowsbl +from dc_plus.preprocess.create_network_data import create_network_data_pypowsybl from dc_plus.preprocess.helper_functions import _find_bridges def test_jacobian_update(): get_net = pypowsybl.network.create_ieee14 - net, static_info, dynamic_info, string_info, jacobian_data = _load_test_grid(get_net) + net, network_info, jacobian_data = _load_test_grid(get_net) + dynamic_info = network_info.dynamic_network_data + string_info = network_info.string_network_data theta_actual = dynamic_info.bus_voltage_angles_rad vm_actual = dynamic_info.bus_voltage_magnitudes @@ -32,16 +40,19 @@ def test_jacobian_update(): pvpq_bus = dynamic_info.pvpq_buses_indices_pvpq_order pq_bus = dynamic_info.pq_buses_indices - for outage_idx in np.flatnonzero(~is_bridge): dynamic_info_n1 = deepcopy(dynamic_info) dynamic_info_n1.branch_connected[outage_idx] = False - jacobian_data_n1 = _get_jacobian_data_from_network_data(dynamic_info_n1) + jacobian_data_n1 = get_jacobian_data_from_network_data( + dynamic_info_n1, + ) net_n1 = deepcopy(net) net_n1.remove_elements(string_info.branch_ids[outage_idx]) - static_info_n1_direct, dynamic_info_n1_direct, string_info_n1_direct = create_network_data_pypowsbl(net_n1) - jacobian_data_n1_direct = _get_jacobian_data_from_network_data(dynamic_info_n1_direct) + dynamic_info_n1_direct = create_network_data_pypowsybl(net_n1).dynamic_network_data + jacobian_data_n1_direct = get_jacobian_data_from_network_data( + dynamic_info_n1_direct, + ) assert jacobian_data_n1.__eq__(jacobian_data_n1_direct), ( "Jacobian data from n-1 network data and direct n-1 network data do not match." ) @@ -55,13 +66,17 @@ def test_jacobian_update(): f"Reference bus ID: {loadflow_res.reference_bus_id}" ) - static_info_n1_direct_lf, dynamic_info_n1_direct_lf, string_info_n1_direct_lf = create_network_data_pypowsbl(net_n1) + dynamic_info_n1_direct_lf = create_network_data_pypowsybl(net_n1).dynamic_network_data theta_actual_n1 = dynamic_info_n1_direct_lf.bus_voltage_angles_rad vm_actual_n1 = dynamic_info_n1_direct_lf.bus_voltage_magnitudes J_inverse_direct_n1 = jacobian_data_n1.inverse_jacobian y_matrix_n1 = _get_admittance_matrix_from_network_data(dynamic_info_n1) - mismatch_n1 = calculate_nodal_mismatch_network_data(dynamic_network_data=dynamic_info_n1, y_matrix=y_matrix_n1) + mismatch_n1 = calculate_nodal_mismatch_network_data( + dynamic_network_data=dynamic_info_n1, + y_matrix=y_matrix_n1, + jacobian_data=jacobian_data_n1, + ) dx = -J_inverse_direct_n1 @ mismatch_n1 theta_updated_J = copy(theta_actual) diff --git a/tests/interfaces/test_network_info_serialization.py b/tests/interfaces/test_network_info_serialization.py new file mode 100644 index 0000000..27be2bd --- /dev/null +++ b/tests/interfaces/test_network_info_serialization.py @@ -0,0 +1,31 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + + +from dc_plus.preprocess.create_network_data import create_network_data_pypowsybl +import pypowsybl +from dc_plus.interfaces.network_information import ( + BusType, + DynamicNetworkInformation, + StaticNetworkInformation, + StringNetworkInformation, + TransformerTapInformation, + NetworkInformation, +) + + +def test_network_info_serialization(): + net = pypowsybl.network.create_ieee30() + network_info = create_network_data_pypowsybl(net) + json_str = network_info.static_network_data.model_dump_json(round_trip=True) + StaticNetworkInformation.model_validate_json(json_str) + json_str = network_info.dynamic_network_data.model_dump_json(round_trip=True) + DynamicNetworkInformation.model_validate_json(json_str) + json_str = network_info.string_network_data.model_dump_json(round_trip=True) + StringNetworkInformation.model_validate_json(json_str) + json_str = network_info.model_dump_json(round_trip=True) + NetworkInformation.model_validate_json(json_str) diff --git a/tests/jax/test_bsdf_jax.py b/tests/jax/test_bsdf_jax.py index 6e0853c..1fa06de 100644 --- a/tests/jax/test_bsdf_jax.py +++ b/tests/jax/test_bsdf_jax.py @@ -4,42 +4,45 @@ # If a copy of the MPL was not distributed with this file, # you can obtain one at https://mozilla.org/MPL/2.0/. # Mozilla Public License, version 2.0 - - import jax import jax.numpy as jnp import numpy as np import pytest from dc_plus.jax.bsdf import compute_bsdf_update as compute_bsdf_update_jax +from dc_plus.interfaces.network_inputs import ( + JacobianComponentInputs, + NetworkAdmittanceInputs, + NetworkTopologyInputs, + VoltageStateInputs, +) +from dc_plus.numpy.bsdf_full_rank import compute_bsdf_update as compute_bsdf_update_numpy from tests.test_helper.bsdf_helper import ( get_bsdf_cases, prepare_bsdf_test_context, - run_reference_one_step, ) jax.config.update("jax_enable_x64", True) @pytest.mark.parametrize("bsdf_test_case", get_bsdf_cases()) -def test_bsdf_full_rank_jax(bsdf_test_case): - setup = prepare_bsdf_test_context(bsdf_test_case=bsdf_test_case) - - jacobian_with_extra_bus_inverse = setup.jacobian_data_with_extra_buses.inverse_jacobian - jacobian_inv_device_transposed = jax.device_put(jacobian_with_extra_bus_inverse.T) +def test_bsdf_full_rank_jax_matches_numpy(bsdf_test_case): + setup = prepare_bsdf_test_context( + bsdf_test_case=bsdf_test_case, + ) - jacobian_inv_jax_transposed = compute_bsdf_update_jax( - jacobian_inv_transposed=jacobian_inv_device_transposed, + jacobian_inv_numpy = compute_bsdf_update_numpy( + jacobian_inv=setup.jacobian_data_with_extra_buses.inverse_jacobian, bus_to_split=setup.bus_to_split, new_bus_b_index=setup.new_bus_index, new_bus_type=2, - branches_connected_to_bus_b=jnp.asarray(setup.branches_to_move, dtype=jnp.int32), - shunt_connected_to_bus_b=jnp.asarray([], dtype=jnp.int32), - branch_from=setup.dynamic_info.branch_from_bus, - branch_to=setup.dynamic_info.branch_to_bus, + branches_connected_to_bus_b=setup.branches_connected_to_bus_b, + shunt_connected_to_bus_b=setup.shunt_connected_to_bus_b, + branch_from=setup.branch_from_original, + branch_to=setup.branch_to_original, shunt_to_bus=setup.dynamic_info.shunt_bus_indices, - v_mag_hat=setup.dynamic_info_with_placeholders.bus_voltage_magnitudes.flatten(), - theta_hat=setup.dynamic_info_with_placeholders.bus_voltage_angles_rad.flatten(), + v_mag_hat=setup.v_mag_hat, + theta_hat=setup.theta_hat, y_ff=setup.y_ff, y_ft=setup.y_ft, y_tf=setup.y_tf, @@ -49,47 +52,60 @@ def test_bsdf_full_rank_jax(bsdf_test_case): magnitude_component_indices=setup.jacobian_data_with_extra_buses.magnitude_component_indices, ) - jacobian_inv_jax_transposed.block_until_ready() - jacobian_inv_jax = jnp.transpose(jacobian_inv_jax_transposed) - - in_use_indices = np.flatnonzero(setup.jacobian_data_with_extra_buses.jacobian_index_in_use) - np.testing.assert_allclose( - np.asarray(jacobian_inv_jax)[np.ix_(in_use_indices, in_use_indices)], - setup.jacobian_data_split_manual.inverse_jacobian[np.ix_(in_use_indices, in_use_indices)], - rtol=1e-6, - atol=1e-8, + jacobian_with_extra_bus_inverse = setup.jacobian_data_with_extra_buses.inverse_jacobian + jacobian_inv_device_transposed = jax.device_put(jacobian_with_extra_bus_inverse.T) + network_topology = NetworkTopologyInputs( + branch_from=jnp.asarray(setup.dynamic_info.branch_from_bus, dtype=jnp.int32), + branch_to=jnp.asarray(setup.dynamic_info.branch_to_bus, dtype=jnp.int32), + branch_connected=jnp.asarray(setup.dynamic_info.branch_connected), + shunt_to_bus=jnp.asarray(setup.dynamic_info.shunt_bus_indices, dtype=jnp.int32), + shunt_connected=jnp.asarray(setup.dynamic_info.shunt_connected), ) - np.testing.assert_allclose( - np.asarray(jacobian_inv_jax_transposed)[np.ix_(in_use_indices, in_use_indices)], - setup.jacobian_data_split_manual.inverse_jacobian[np.ix_(in_use_indices, in_use_indices)].T, - rtol=1e-6, - atol=1e-8, + voltage_state = VoltageStateInputs( + bus_voltage_magnitudes=jnp.asarray(setup.dynamic_info_with_placeholders.bus_voltage_magnitudes.flatten()), + bus_voltage_angles_rad=jnp.asarray(setup.dynamic_info_with_placeholders.bus_voltage_angles_rad.flatten()), + ) + network_admittance = NetworkAdmittanceInputs( + y_ff=jnp.asarray(setup.y_ff), + y_ft=jnp.asarray(setup.y_ft), + y_tf=jnp.asarray(setup.y_tf), + y_tt=jnp.asarray(setup.y_tt), + y_shunt=jnp.asarray(setup.dynamic_info.shunt_effective_bus_admittance), + ) + jacobian_components = JacobianComponentInputs( + angle_component_indices=jnp.asarray(setup.jacobian_data_with_extra_buses.angle_component_indices, dtype=jnp.int32), + magnitude_component_indices=jnp.asarray( + setup.jacobian_data_with_extra_buses.magnitude_component_indices, + dtype=jnp.int32, + ), ) - dynamic_info_split_manual = setup.dynamic_info_split_manual - dynamic_info_one_step = run_reference_one_step(setup.net, bsdf_test_case=bsdf_test_case) - bus_order = bsdf_test_case.bus_order - dx = -jacobian_inv_jax @ setup.mismatch_n1 + jacobian_inv_jax_transposed = compute_bsdf_update_jax( + jacobian_inv_transposed=jacobian_inv_device_transposed, + bus_to_split=setup.bus_to_split, + new_bus_b_index=setup.new_bus_index, + new_bus_type=2, + branches_connected_to_bus_b=jnp.asarray(setup.branches_connected_to_bus_b, dtype=jnp.int32), + shunt_connected_to_bus_b=jnp.asarray(setup.shunt_connected_to_bus_b, dtype=jnp.int32), + network_topology=network_topology, + voltage_state=voltage_state, + network_admittance=network_admittance, + jacobian_components=jacobian_components, + ) - theta_actual = dynamic_info_split_manual.bus_voltage_angles_rad - vm_actual = dynamic_info_split_manual.bus_voltage_magnitudes - theta_updated_J = theta_actual.copy() - vm_updated_J = vm_actual.copy() - pvpq = dynamic_info_split_manual.pvpq_buses_indices_pvpq_order - pq = dynamic_info_split_manual.pq_buses_indices - theta_updated_J[pvpq] = theta_actual[pvpq] + dx[setup.jacobian_data_with_extra_buses.is_angle_component] - vm_updated_J[pq] = vm_actual[pq] + dx[setup.jacobian_data_with_extra_buses.is_magnitude_component] + jacobian_inv_jax_transposed.block_until_ready() + jacobian_inv_jax = jnp.transpose(jacobian_inv_jax_transposed) - # reorder bus voltages to match new ordering of manual split + in_use_indices = np.flatnonzero(setup.jacobian_data_with_extra_buses.jacobian_index_in_use) np.testing.assert_allclose( - dynamic_info_one_step.bus_voltage_magnitudes[bus_order], - vm_updated_J, + np.asarray(jacobian_inv_jax)[np.ix_(in_use_indices, in_use_indices)], + jacobian_inv_numpy[np.ix_(in_use_indices, in_use_indices)], rtol=1e-10, atol=1e-10, ) np.testing.assert_allclose( - dynamic_info_one_step.bus_voltage_angles_rad[bus_order], - theta_updated_J, + np.asarray(jacobian_inv_jax_transposed)[np.ix_(in_use_indices, in_use_indices)], + jacobian_inv_numpy[np.ix_(in_use_indices, in_use_indices)].T, rtol=1e-10, atol=1e-10, ) diff --git a/tests/jax/test_injection_outage_jax.py b/tests/jax/test_injection_outage_jax.py new file mode 100644 index 0000000..f279cee --- /dev/null +++ b/tests/jax/test_injection_outage_jax.py @@ -0,0 +1,451 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +import jax +import jax.numpy as jnp +import numpy as np +import pytest + +jax.config.update("jax_enable_x64", True) +jax.config.update("jax_platform_name", "cpu") + +from dc_plus.example_grids.pypowsbl.example_grids import PANDAPOWER_NETWORKS_FOR_POWSYBL, POWSYBL_NETWORKS +from dc_plus.importing.powsybl.powsybl_import import _get_injections_powsybl +from dc_plus.importing.powsybl.powsybl_network_helpers import _load_test_grid +from dc_plus.jax.injection_outage import ( + non_voltage_regulating_injection_changes_dx, + non_voltage_regulating_injection_outage_monitor_buses, + non_voltage_regulating_injection_outage_monitored, +) +from dc_plus.jax.injection_outage import non_voltage_regulating_injection_outage_dx as jax_injection_outage_dx +from dc_plus.interfaces.network_inputs import ( + JacobianComponentInputs, + NetworkAdmittanceInputs, + NetworkTopologyInputs, + VoltageStateInputs, +) +from dc_plus.numpy.injection_outage import non_voltage_regulating_injection_outage_dx as numpy_injection_outage_dx + +powsybl_networks = POWSYBL_NETWORKS +pandapower_networks = PANDAPOWER_NETWORKS_FOR_POWSYBL +TOL = 1e-10 + + +def _build_jacobian_components(jacobian_data) -> JacobianComponentInputs: + return JacobianComponentInputs( + angle_component_indices=jnp.asarray(jacobian_data.angle_component_indices, dtype=jnp.int32), + magnitude_component_indices=jnp.asarray(jacobian_data.magnitude_component_indices, dtype=jnp.int32), + ) + + +def _build_voltage_state(dynamic_info) -> VoltageStateInputs: + return VoltageStateInputs( + bus_voltage_magnitudes=jnp.asarray(dynamic_info.bus_voltage_magnitudes, dtype=jnp.float64), + bus_voltage_angles_rad=jnp.asarray(dynamic_info.bus_voltage_angles_rad, dtype=jnp.float64), + ) + + +def _build_network_topology(dynamic_info) -> NetworkTopologyInputs: + return NetworkTopologyInputs( + branch_from=jnp.asarray(dynamic_info.branch_from_bus, dtype=jnp.int32), + branch_to=jnp.asarray(dynamic_info.branch_to_bus, dtype=jnp.int32), + branch_connected=jnp.asarray(dynamic_info.branch_connected), + shunt_to_bus=jnp.asarray(dynamic_info.shunt_bus_indices, dtype=jnp.int32), + shunt_connected=jnp.asarray(dynamic_info.shunt_connected), + ) + + +def _build_network_admittance(dynamic_info) -> NetworkAdmittanceInputs: + return NetworkAdmittanceInputs( + y_ff=jnp.asarray(dynamic_info.branch_effective_admittance_from_from, dtype=jnp.complex128), + y_ft=jnp.asarray(dynamic_info.branch_effective_admittance_from_to, dtype=jnp.complex128), + y_tf=jnp.asarray(dynamic_info.branch_effective_admittance_to_from, dtype=jnp.complex128), + y_tt=jnp.asarray(dynamic_info.branch_effective_admittance_to_to, dtype=jnp.complex128), + y_shunt=jnp.asarray(dynamic_info.shunt_effective_bus_admittance, dtype=jnp.complex128), + ) + + +def test_non_voltage_regulating_injection_changes_jax_batch_manual() -> None: + jacobian_inv_transposed = np.array( + [ + [1.0, 2.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 1.0], + [0.0, 0.0, 0.0, 1.0], + ], + dtype=float, + ) + injection_to_bus = np.array([0, 1, 0], dtype=np.int64) + angle_component_indices = np.array([0, 1], dtype=np.int64) + magnitude_component_indices = np.array([2, 3], dtype=np.int64) + injection_active_power_changes = np.array( + [ + [0.10, -0.10, 0.05], + [0.20, 0.15, -0.05], + [0.00, -0.20, -0.10], + ], + dtype=float, + ) + injection_reactive_power_changes = np.array( + [ + [0.01, -0.02, 0.03], + [0.00, 0.04, -0.01], + [0.02, -0.03, -0.02], + ], + dtype=float, + ) + + mismatch_expected = np.array( + [ + [0.15, -0.10, 0.04, -0.02], + [0.15, 0.15, -0.01, 0.04], + [-0.10, -0.20, 0.00, -0.03], + ], + dtype=float, + ) + dx_expected = -(mismatch_expected @ jacobian_inv_transposed) + + dx_batch_jax = np.asarray( + non_voltage_regulating_injection_changes_dx( + jacobian_inv_transposed=jnp.asarray(jacobian_inv_transposed), + injection_active_power_changes=jnp.asarray(injection_active_power_changes), + injection_reactive_power_changes=jnp.asarray(injection_reactive_power_changes), + injection_to_bus=jnp.asarray(injection_to_bus), + jacobian_components=JacobianComponentInputs( + angle_component_indices=jnp.asarray(angle_component_indices), + magnitude_component_indices=jnp.asarray(magnitude_component_indices), + ), + ) + ) + + np.testing.assert_allclose(dx_batch_jax, dx_expected, rtol=TOL, atol=TOL) + + +def _connected_injection_indices(injections): + candidates = np.flatnonzero(injections["connected"].to_numpy(dtype=bool)) + if candidates.size == 0: + pytest.skip("No connected injections available for an injection outage test") + return candidates + + +def _supported_injection_outage_rows(net, injections, max_rows: int | None = None) -> list[np.ndarray]: + injection_powsybl_type = net.get_injections(attributes=["type"]) + outage_rows: list[np.ndarray] = [] + for injection_idx in _connected_injection_indices(injections): + injection_id = injections.loc[injection_idx, "id_str"] + if (injection_powsybl_type.loc[injection_id, "type"]) in (["HVDC_CONVERTER_STATION", "DANGLING_LINE"]): + continue + outage_rows.append(np.array([injection_idx], dtype=np.int64)) + if max_rows is not None and len(outage_rows) >= max_rows: + break + return outage_rows + + +def _pad_outage_rows(outage_rows: list[np.ndarray]) -> np.ndarray: + if not outage_rows: + return np.empty((0, 0), dtype=np.int64) + + max_outages = max(outage_row.size for outage_row in outage_rows) + padded = np.full((len(outage_rows), max_outages), -1, dtype=np.int64) + for row_idx, outage_row in enumerate(outage_rows): + padded[row_idx, : outage_row.size] = outage_row + return padded + + +def test_non_voltage_regulating_injection_outage_jax_batch_multi_outage() -> None: + jacobian_inv = np.eye(4, dtype=float) + outage_batch = np.array([[0, 2], [1, -1], [-1, -1]], dtype=np.int64) + injection_to_bus = np.array([0, 1, 0], dtype=np.int64) + injection_active_power = np.array([1.0, 2.0, 3.0], dtype=float) + injection_reactive_power = np.array([10.0, 20.0, 30.0], dtype=float) + angle_component_indices = np.array([0, 1], dtype=np.int64) + magnitude_component_indices = np.array([2, 3], dtype=np.int64) + + dx_batch_jax = np.asarray( + jax_injection_outage_dx( + jacobian_inv_transposed=jnp.asarray(jacobian_inv.T), + outage_injection_indices=jnp.asarray(outage_batch), + injection_to_bus=jnp.asarray(injection_to_bus), + injection_active_power=jnp.asarray(injection_active_power), + injection_reactive_power=jnp.asarray(injection_reactive_power), + jacobian_components=JacobianComponentInputs( + angle_component_indices=jnp.asarray(angle_component_indices), + magnitude_component_indices=jnp.asarray(magnitude_component_indices), + ), + ) + ) + dx_batch_numpy = numpy_injection_outage_dx( + jacobian_inv=jacobian_inv, + outage_injection_indices=outage_batch, + injection_to_bus=injection_to_bus, + injection_active_power=injection_active_power, + injection_reactive_power=injection_reactive_power, + angle_component_indices=angle_component_indices, + magnitude_component_indices=magnitude_component_indices, + ) + + np.testing.assert_allclose(dx_batch_jax, dx_batch_numpy, rtol=TOL, atol=TOL) + + +@pytest.mark.parametrize("get_net", powsybl_networks + pandapower_networks) +def test_non_voltage_regulating_injection_outage_jax(get_net) -> None: + net, network_info, jacobian_data = _load_test_grid(get_net) + dynamic_info = network_info.dynamic_network_data + injections = _get_injections_powsybl(net).reset_index(drop=True) + outage_p = injections["setpoint_p"].fillna(injections["p"]).to_numpy(dtype=float) + outage_q = injections["setpoint_q"].fillna(injections["q"]).to_numpy(dtype=float) + + assert len(injections) == dynamic_info.n_injections + + outage_rows = _supported_injection_outage_rows(net, injections) + assert outage_rows, f"No supported injection outages found for {get_net.__name__}" + + outage_batch = _pad_outage_rows(outage_rows) + dx_batch_jax = np.asarray( + jax_injection_outage_dx( + jacobian_inv_transposed=jnp.asarray(jacobian_data.inverse_jacobian.T), + outage_injection_indices=jnp.asarray(outage_batch), + injection_to_bus=jnp.asarray(dynamic_info.injection_to_bus), + injection_active_power=jnp.asarray(outage_p), + injection_reactive_power=jnp.asarray(outage_q), + jacobian_components=_build_jacobian_components(jacobian_data), + ) + ) + dx_batch_numpy = numpy_injection_outage_dx( + jacobian_inv=jacobian_data.inverse_jacobian, + outage_injection_indices=outage_batch, + injection_to_bus=dynamic_info.injection_to_bus, + injection_active_power=outage_p, + injection_reactive_power=outage_q, + angle_component_indices=jacobian_data.angle_component_indices, + magnitude_component_indices=jacobian_data.magnitude_component_indices, + ) + + assert dx_batch_jax.shape == (len(outage_rows), jacobian_data.inverse_jacobian.shape[0]) + # the numpy code is tested against powsybl -> we do not need to repeat the test against powsybl + np.testing.assert_allclose(dx_batch_jax, dx_batch_numpy, rtol=TOL, atol=TOL) + + +@pytest.mark.parametrize("get_net", powsybl_networks + pandapower_networks) +def test_non_voltage_regulating_injection_outage_monitored_jax(get_net) -> None: + net, network_info, jacobian_data = _load_test_grid(get_net) + dynamic_info = network_info.dynamic_network_data + injections = _get_injections_powsybl(net).reset_index(drop=True) + outage_p = injections["setpoint_p"].fillna(injections["p"]).to_numpy(dtype=float) + outage_q = injections["setpoint_q"].fillna(injections["q"]).to_numpy(dtype=float) + outage_rows = _supported_injection_outage_rows(net, injections, max_rows=5) + assert outage_rows, f"No supported injection outages found for {get_net.__name__}" + outage_batch = _pad_outage_rows(outage_rows) + + branch_connected = np.asarray(dynamic_info.branch_connected, dtype=bool) + monitor_branch_indices = np.flatnonzero(branch_connected)[: min(5, int(branch_connected.sum()))] + if monitor_branch_indices.size == 0: + pytest.skip("No connected monitored branches available") + + branch_from = np.asarray(dynamic_info.branch_from_bus, dtype=np.int64) + branch_to = np.asarray(dynamic_info.branch_to_bus, dtype=np.int64) + monitor_bus_indices = np.unique( + np.concatenate([branch_from[monitor_branch_indices], branch_to[monitor_branch_indices]]) + ).astype(np.int64) + bus_to_mon_index = np.full(dynamic_info.n_buses, -1, dtype=np.int64) + bus_to_mon_index[monitor_bus_indices] = np.arange(monitor_bus_indices.size, dtype=np.int64) + + monitor_theta_jax, monitor_vm_jax = non_voltage_regulating_injection_outage_monitor_buses( + jacobian_inv_transposed=jnp.asarray(jacobian_data.inverse_jacobian.T), + outage_injection_indices=jnp.asarray(outage_batch), + injection_to_bus=jnp.asarray(dynamic_info.injection_to_bus), + injection_active_power=jnp.asarray(outage_p), + injection_reactive_power=jnp.asarray(outage_q), + jacobian_components=_build_jacobian_components(jacobian_data), + monitor_bus_indices=jnp.asarray(monitor_bus_indices), + voltage_state=_build_voltage_state(dynamic_info), + ) + monitored_results = non_voltage_regulating_injection_outage_monitored( + jacobian_inv_transposed=jnp.asarray(jacobian_data.inverse_jacobian.T), + outage_injection_indices=jnp.asarray(outage_batch), + injection_to_bus=jnp.asarray(dynamic_info.injection_to_bus), + injection_active_power=jnp.asarray(outage_p), + injection_reactive_power=jnp.asarray(outage_q), + jacobian_components=_build_jacobian_components(jacobian_data), + monitor_bus_indices=jnp.asarray(monitor_bus_indices), + voltage_state=_build_voltage_state(dynamic_info), + network_topology=_build_network_topology(dynamic_info), + network_admittance=_build_network_admittance(dynamic_info), + monitor_branch_indices=jnp.asarray(monitor_branch_indices), + bus_to_mon_index=jnp.asarray(bus_to_mon_index), + ) + dx_batch_numpy = numpy_injection_outage_dx( + jacobian_inv=jacobian_data.inverse_jacobian, + outage_injection_indices=outage_batch, + injection_to_bus=dynamic_info.injection_to_bus, + injection_active_power=outage_p, + injection_reactive_power=outage_q, + angle_component_indices=jacobian_data.angle_component_indices, + magnitude_component_indices=jacobian_data.magnitude_component_indices, + ) + + expected_theta = np.broadcast_to( + dynamic_info.bus_voltage_angles_rad[monitor_bus_indices], + (len(outage_rows), monitor_bus_indices.size), + ).copy() + expected_vm = np.broadcast_to( + dynamic_info.bus_voltage_magnitudes[monitor_bus_indices], + (len(outage_rows), monitor_bus_indices.size), + ).copy() + theta_component_indices = jacobian_data.angle_component_indices[monitor_bus_indices] + vm_component_indices = jacobian_data.magnitude_component_indices[monitor_bus_indices] + theta_mask = theta_component_indices >= 0 + vm_mask = vm_component_indices >= 0 + expected_theta[:, theta_mask] += dx_batch_numpy[:, theta_component_indices[theta_mask]] + expected_vm[:, vm_mask] += dx_batch_numpy[:, vm_component_indices[vm_mask]] + + np.testing.assert_allclose(np.asarray(monitor_theta_jax), expected_theta, rtol=TOL, atol=TOL) + np.testing.assert_allclose(np.asarray(monitor_vm_jax), expected_vm, rtol=TOL, atol=TOL) + np.testing.assert_allclose(np.asarray(monitored_results.n_1_theta), expected_theta, rtol=TOL, atol=TOL) + np.testing.assert_allclose(np.asarray(monitored_results.n_1_voltage), expected_vm, rtol=TOL, atol=TOL) + + f_pos = bus_to_mon_index[branch_from[monitor_branch_indices]] + t_pos = bus_to_mon_index[branch_to[monitor_branch_indices]] + assert np.all(f_pos >= 0) + assert np.all(t_pos >= 0) + + v_post = expected_vm * np.exp(1j * expected_theta) + y_ff_mon = np.asarray(dynamic_info.branch_effective_admittance_from_from)[monitor_branch_indices] + y_ft_mon = np.asarray(dynamic_info.branch_effective_admittance_from_to)[monitor_branch_indices] + y_tf_mon = np.asarray(dynamic_info.branch_effective_admittance_to_from)[monitor_branch_indices] + y_tt_mon = np.asarray(dynamic_info.branch_effective_admittance_to_to)[monitor_branch_indices] + v_from = v_post[:, f_pos] + v_to = v_post[:, t_pos] + i_from = y_ff_mon[None, :] * v_from + y_ft_mon[None, :] * v_to + i_to = y_tf_mon[None, :] * v_from + y_tt_mon[None, :] * v_to + s_from = v_from * np.conj(i_from) + s_to = v_to * np.conj(i_to) + + np.testing.assert_allclose(np.asarray(monitored_results.n_1_i_from), i_from, rtol=1e-9, atol=1e-9) + np.testing.assert_allclose(np.asarray(monitored_results.n_1_i_to), i_to, rtol=1e-9, atol=1e-9) + np.testing.assert_allclose(np.asarray(monitored_results.n_1_p_from), s_from.real, rtol=1e-9, atol=1e-9) + np.testing.assert_allclose(np.asarray(monitored_results.n_1_p_to), s_to.real, rtol=1e-9, atol=1e-9) + np.testing.assert_allclose(np.asarray(monitored_results.n_1_q_from), s_from.imag, rtol=1e-9, atol=1e-9) + np.testing.assert_allclose(np.asarray(monitored_results.n_1_q_to), s_to.imag, rtol=1e-9, atol=1e-9) + + +@pytest.mark.parametrize("get_net", powsybl_networks) +def test_non_voltage_regulating_injection_outage_jax_enabled_limit_path(get_net) -> None: + net, network_info, jacobian_data = _load_test_grid( + get_net, + ) + dynamic_info = network_info.dynamic_network_data + injections = _get_injections_powsybl(net).reset_index(drop=True) + outage_p = injections["setpoint_p"].fillna(injections["p"]).to_numpy(dtype=float) + outage_q = injections["setpoint_q"].fillna(injections["q"]).to_numpy(dtype=float) + + outage_rows = _supported_injection_outage_rows(net, injections, max_rows=3) + assert outage_rows, f"No supported injection outages found for {get_net.__name__}" + + outage_batch = _pad_outage_rows(outage_rows) + dx_batch_jax = np.asarray( + jax_injection_outage_dx( + jacobian_inv_transposed=jnp.asarray(jacobian_data.inverse_jacobian.T), + outage_injection_indices=jnp.asarray(outage_batch), + injection_to_bus=jnp.asarray(dynamic_info.injection_to_bus), + injection_active_power=jnp.asarray(outage_p), + injection_reactive_power=jnp.asarray(outage_q), + jacobian_components=_build_jacobian_components(jacobian_data), + ) + ) + dx_batch_numpy = numpy_injection_outage_dx( + jacobian_inv=jacobian_data.inverse_jacobian, + outage_injection_indices=outage_batch, + injection_to_bus=dynamic_info.injection_to_bus, + injection_active_power=outage_p, + injection_reactive_power=outage_q, + angle_component_indices=jacobian_data.angle_component_indices, + magnitude_component_indices=jacobian_data.magnitude_component_indices, + ) + + np.testing.assert_allclose(dx_batch_jax, dx_batch_numpy, rtol=TOL, atol=TOL) + + +@pytest.mark.parametrize("get_net", powsybl_networks) +def test_non_voltage_regulating_injection_outage_monitored_jax_enabled_limit_path(get_net) -> None: + net, network_info, jacobian_data = _load_test_grid( + get_net, + ) + dynamic_info = network_info.dynamic_network_data + injections = _get_injections_powsybl(net).reset_index(drop=True) + outage_p = injections["setpoint_p"].fillna(injections["p"]).to_numpy(dtype=float) + outage_q = injections["setpoint_q"].fillna(injections["q"]).to_numpy(dtype=float) + outage_rows = _supported_injection_outage_rows(net, injections, max_rows=3) + assert outage_rows, f"No supported injection outages found for {get_net.__name__}" + outage_batch = _pad_outage_rows(outage_rows) + + branch_connected = np.asarray(dynamic_info.branch_connected, dtype=bool) + monitor_branch_indices = np.flatnonzero(branch_connected)[: min(5, int(branch_connected.sum()))] + if monitor_branch_indices.size == 0: + pytest.skip("No connected monitored branches available") + + branch_from = np.asarray(dynamic_info.branch_from_bus, dtype=np.int64) + branch_to = np.asarray(dynamic_info.branch_to_bus, dtype=np.int64) + monitor_bus_indices = np.unique( + np.concatenate([branch_from[monitor_branch_indices], branch_to[monitor_branch_indices]]) + ).astype(np.int64) + bus_to_mon_index = np.full(dynamic_info.n_buses, -1, dtype=np.int64) + bus_to_mon_index[monitor_bus_indices] = np.arange(monitor_bus_indices.size, dtype=np.int64) + + monitor_theta_jax, monitor_vm_jax = non_voltage_regulating_injection_outage_monitor_buses( + jacobian_inv_transposed=jnp.asarray(jacobian_data.inverse_jacobian.T), + outage_injection_indices=jnp.asarray(outage_batch), + injection_to_bus=jnp.asarray(dynamic_info.injection_to_bus), + injection_active_power=jnp.asarray(outage_p), + injection_reactive_power=jnp.asarray(outage_q), + jacobian_components=_build_jacobian_components(jacobian_data), + monitor_bus_indices=jnp.asarray(monitor_bus_indices), + voltage_state=_build_voltage_state(dynamic_info), + ) + monitored_results = non_voltage_regulating_injection_outage_monitored( + jacobian_inv_transposed=jnp.asarray(jacobian_data.inverse_jacobian.T), + outage_injection_indices=jnp.asarray(outage_batch), + injection_to_bus=jnp.asarray(dynamic_info.injection_to_bus), + injection_active_power=jnp.asarray(outage_p), + injection_reactive_power=jnp.asarray(outage_q), + jacobian_components=_build_jacobian_components(jacobian_data), + monitor_bus_indices=jnp.asarray(monitor_bus_indices), + voltage_state=_build_voltage_state(dynamic_info), + network_topology=_build_network_topology(dynamic_info), + network_admittance=_build_network_admittance(dynamic_info), + monitor_branch_indices=jnp.asarray(monitor_branch_indices), + bus_to_mon_index=jnp.asarray(bus_to_mon_index), + ) + dx_batch_numpy = numpy_injection_outage_dx( + jacobian_inv=jacobian_data.inverse_jacobian, + outage_injection_indices=outage_batch, + injection_to_bus=dynamic_info.injection_to_bus, + injection_active_power=outage_p, + injection_reactive_power=outage_q, + angle_component_indices=jacobian_data.angle_component_indices, + magnitude_component_indices=jacobian_data.magnitude_component_indices, + ) + + expected_theta = np.broadcast_to( + np.asarray(dynamic_info.bus_voltage_angles_rad, dtype=float)[monitor_bus_indices], + (len(outage_rows), monitor_bus_indices.size), + ).copy() + expected_vm = np.broadcast_to( + np.asarray(dynamic_info.bus_voltage_magnitudes, dtype=float)[monitor_bus_indices], + (len(outage_rows), monitor_bus_indices.size), + ).copy() + theta_component_indices = np.asarray(jacobian_data.angle_component_indices, dtype=np.int64)[monitor_bus_indices] + vm_component_indices = np.asarray(jacobian_data.magnitude_component_indices, dtype=np.int64)[monitor_bus_indices] + theta_mask = theta_component_indices >= 0 + vm_mask = vm_component_indices >= 0 + expected_theta[:, theta_mask] += dx_batch_numpy[:, theta_component_indices[theta_mask]] + expected_vm[:, vm_mask] += dx_batch_numpy[:, vm_component_indices[vm_mask]] + + np.testing.assert_allclose(np.asarray(monitor_theta_jax), expected_theta, rtol=TOL, atol=TOL) + np.testing.assert_allclose(np.asarray(monitor_vm_jax), expected_vm, rtol=TOL, atol=TOL) + np.testing.assert_allclose(np.asarray(monitored_results.n_1_theta), expected_theta, rtol=TOL, atol=TOL) + np.testing.assert_allclose(np.asarray(monitored_results.n_1_voltage), expected_vm, rtol=TOL, atol=TOL) diff --git a/tests/jax/test_lodf_jax.py b/tests/jax/test_lodf_jax.py index 885865e..0dd21a9 100644 --- a/tests/jax/test_lodf_jax.py +++ b/tests/jax/test_lodf_jax.py @@ -5,6 +5,7 @@ # you can obtain one at https://mozilla.org/MPL/2.0/. # Mozilla Public License, version 2.0 +from copy import deepcopy from dataclasses import dataclass import jax @@ -35,9 +36,66 @@ from dc_plus.example_grids.pypowsbl.example_grids import PANDAPOWER_NETWORKS_FOR_POWSYBL, POWSYBL_NETWORKS from dc_plus.jax.lodf_branches import line_outage_post_contingency_monitored +from dc_plus.interfaces.network_inputs import ( + JacobianComponentInputs, + NetworkAdmittanceInputs, + NetworkTopologyInputs, + VoltageStateInputs, +) +from dc_plus.interfaces.jacobian_network_data import _apply_jacobian_dx_to_network_data, get_jacobian_data_from_network_data +from dc_plus.interfaces.network_information import BusType +from dc_plus.numpy.lodf import branch_outage_update_inverse powsybl_networks = POWSYBL_NETWORKS pandapower_networks = PANDAPOWER_NETWORKS_FOR_POWSYBL +TOL = 1e-10 + + +def _build_lower_residual_uses_reactive_mismatch(dynamic_info) -> np.ndarray: + return np.asarray(dynamic_info.bus_type, dtype=np.int32) != int(BusType.PV) + + +def _build_jacobian_components(jacobian_data) -> JacobianComponentInputs: + return JacobianComponentInputs( + angle_component_indices=jnp.asarray(jacobian_data.angle_component_indices, dtype=jnp.int32), + magnitude_component_indices=jnp.asarray(jacobian_data.magnitude_component_indices, dtype=jnp.int32), + ) + + +def _build_network_topology( + branch_from: np.ndarray, + branch_to: np.ndarray, + branch_connected: np.ndarray, +) -> NetworkTopologyInputs: + return NetworkTopologyInputs( + branch_from=jnp.asarray(branch_from, dtype=jnp.int32), + branch_to=jnp.asarray(branch_to, dtype=jnp.int32), + branch_connected=jnp.asarray(branch_connected, dtype=bool), + shunt_to_bus=jnp.zeros((0,), dtype=jnp.int32), + shunt_connected=jnp.zeros((0,), dtype=bool), + ) + + +def _build_voltage_state(v_mag_hat: np.ndarray, theta_hat: np.ndarray) -> VoltageStateInputs: + return VoltageStateInputs( + bus_voltage_magnitudes=jnp.asarray(v_mag_hat, dtype=jnp.float64), + bus_voltage_angles_rad=jnp.asarray(theta_hat, dtype=jnp.float64), + ) + + +def _build_network_admittance( + y_ff: np.ndarray, + y_ft: np.ndarray, + y_tf: np.ndarray, + y_tt: np.ndarray, +) -> NetworkAdmittanceInputs: + return NetworkAdmittanceInputs( + y_ff=jnp.asarray(y_ff, dtype=jnp.complex128), + y_ft=jnp.asarray(y_ft, dtype=jnp.complex128), + y_tf=jnp.asarray(y_tf, dtype=jnp.complex128), + y_tt=jnp.asarray(y_tt, dtype=jnp.complex128), + y_shunt=jnp.zeros((0,), dtype=jnp.complex128), + ) @tree_util.register_pytree_node_class @@ -86,11 +144,130 @@ def tree_unflatten(cls, aux_data, children): return cls(*children) +def test_lodf_jax_matches_numpy_monitored(): + get_net = pypowsybl.network.create_ieee14 + _net, network_info, jacobian_data = _load_test_grid( + get_net, + ) + dynamic_info = network_info.dynamic_network_data + + branch_from = np.asarray(dynamic_info.branch_from_bus, dtype=np.int32) + branch_to = np.asarray(dynamic_info.branch_to_bus, dtype=np.int32) + branch_connected = np.asarray(dynamic_info.branch_connected, dtype=bool) + y_ff = np.asarray(dynamic_info.branch_effective_admittance_from_from, dtype=np.complex128) + y_ft = np.asarray(dynamic_info.branch_effective_admittance_from_to, dtype=np.complex128) + y_tf = np.asarray(dynamic_info.branch_effective_admittance_to_from, dtype=np.complex128) + y_tt = np.asarray(dynamic_info.branch_effective_admittance_to_to, dtype=np.complex128) + v_mag_hat = np.asarray(dynamic_info.bus_voltage_magnitudes, dtype=float) + theta_hat = np.asarray(dynamic_info.bus_voltage_angles_rad, dtype=float) + + is_bridge = _find_bridges(dynamic_info) + outage_candidates = np.flatnonzero(~is_bridge)[:3] + if outage_candidates.size == 0: + pytest.skip("No non-bridge outage available for JAX-vs-NumPy LODF parity") + + monitor_branch_indices = np.flatnonzero( + branch_connected & ~np.isin(np.arange(branch_connected.size), outage_candidates) + )[:5] + if monitor_branch_indices.size == 0: + pytest.skip("No monitored branches available that stay in service across selected outages") + + monitor_bus_indices = np.unique( + np.concatenate([branch_from[monitor_branch_indices], branch_to[monitor_branch_indices]]) + ).astype(np.int32) + bus_to_mon_index = np.full(dynamic_info.n_buses, -1, dtype=np.int32) + bus_to_mon_index[monitor_bus_indices] = np.arange(monitor_bus_indices.size, dtype=np.int32) + + v0 = v_mag_hat * np.exp(1j * theta_hat) + v_from = v0[branch_from] + v_to = v0[branch_to] + i_from = y_ff * v_from + y_ft * v_to + i_to = y_tf * v_from + y_tt * v_to + s_from = np.where(branch_connected, v_from * np.conj(i_from), 0.0 + 0.0j) + s_to = np.where(branch_connected, v_to * np.conj(i_to), 0.0 + 0.0j) + branch_pq_base = np.column_stack([s_from.real, s_to.real, s_from.imag, s_to.imag]).astype(float) + lower_residual_uses_reactive_mismatch = _build_lower_residual_uses_reactive_mismatch(dynamic_info) + jacobian_components = _build_jacobian_components(jacobian_data) + network_topology = _build_network_topology(branch_from, branch_to, branch_connected) + voltage_state = _build_voltage_state(v_mag_hat, theta_hat) + network_admittance = _build_network_admittance(y_ff, y_ft, y_tf, y_tt) + + lf_res = line_outage_post_contingency_monitored( + jacobian_inv_transposed=jnp.asarray(jacobian_data.inverse_jacobian.T), + outage_branch_idx=jnp.asarray(outage_candidates, dtype=jnp.int32), + jacobian_components=jacobian_components, + network_topology=network_topology, + voltage_state=voltage_state, + network_admittance=network_admittance, + monitor_bus_indices=jnp.asarray(monitor_bus_indices, dtype=jnp.int32), + branch_pq_base=jnp.asarray(branch_pq_base), + lower_residual_uses_reactive_mismatch=jnp.asarray(lower_residual_uses_reactive_mismatch), + monitor_branch_indices=jnp.asarray(monitor_branch_indices, dtype=jnp.int32), + bus_to_mon_index=jnp.asarray(bus_to_mon_index, dtype=jnp.int32), + ) + + for pos, outage_idx in enumerate(outage_candidates): + dynamic_info_n1 = deepcopy(dynamic_info) + dynamic_info_n1.branch_connected[outage_idx] = False + jacobian_data_n1 = get_jacobian_data_from_network_data( + dynamic_info_n1, + ) + y_matrix_n1 = _get_admittance_matrix_from_network_data(dynamic_info_n1) + mismatch_n1 = calculate_nodal_mismatch_network_data( + dynamic_network_data=dynamic_info_n1, + y_matrix=y_matrix_n1, + jacobian_data=jacobian_data_n1, + ) + jacobian_inv_n1 = branch_outage_update_inverse( + jacobian_inv=jacobian_data.inverse_jacobian, + outage_branches_indices=np.array([outage_idx], dtype=np.int64), + branch_from=branch_from, + branch_to=branch_to, + v_mag_hat=v_mag_hat, + theta_hat=theta_hat, + y_ff=y_ff, + y_ft=y_ft, + y_tf=y_tf, + y_tt=y_tt, + angle_component_indices=jacobian_data.angle_component_indices, + magnitude_component_indices=jacobian_data.magnitude_component_indices, + ) + updated_dynamic_info = _apply_jacobian_dx_to_network_data( + dynamic_network_data=dynamic_info_n1, + dx=-(jacobian_inv_n1 @ mismatch_n1), + jacobian_data=jacobian_data_n1, + ) + + expected_theta = updated_dynamic_info.bus_voltage_angles_rad[monitor_bus_indices] + expected_vm = updated_dynamic_info.bus_voltage_magnitudes[monitor_bus_indices] + np.testing.assert_allclose(np.asarray(lf_res.n_1_theta[pos]), expected_theta, rtol=TOL, atol=TOL) + np.testing.assert_allclose(np.asarray(lf_res.n_1_voltage[pos]), expected_vm, rtol=TOL, atol=TOL) + + f_pos = bus_to_mon_index[branch_from[monitor_branch_indices]] + t_pos = bus_to_mon_index[branch_to[monitor_branch_indices]] + v_post = expected_vm * np.exp(1j * expected_theta) + v_from_expected = v_post[f_pos] + v_to_expected = v_post[t_pos] + i_from_expected = y_ff[monitor_branch_indices] * v_from_expected + y_ft[monitor_branch_indices] * v_to_expected + i_to_expected = y_tf[monitor_branch_indices] * v_from_expected + y_tt[monitor_branch_indices] * v_to_expected + s_from_expected = v_from_expected * np.conj(i_from_expected) + s_to_expected = v_to_expected * np.conj(i_to_expected) + + np.testing.assert_allclose(np.asarray(lf_res.n_1_i_from[pos]), i_from_expected, rtol=TOL, atol=TOL) + np.testing.assert_allclose(np.asarray(lf_res.n_1_i_to[pos]), i_to_expected, rtol=TOL, atol=TOL) + np.testing.assert_allclose(np.asarray(lf_res.n_1_p_from[pos]), s_from_expected.real, rtol=TOL, atol=TOL) + np.testing.assert_allclose(np.asarray(lf_res.n_1_p_to[pos]), s_to_expected.real, rtol=TOL, atol=TOL) + np.testing.assert_allclose(np.asarray(lf_res.n_1_q_from[pos]), s_from_expected.imag, rtol=TOL, atol=TOL) + np.testing.assert_allclose(np.asarray(lf_res.n_1_q_to[pos]), s_to_expected.imag, rtol=TOL, atol=TOL) + + @pytest.mark.parametrize("get_net", powsybl_networks + pandapower_networks) # @pytest.mark.parametrize("get_net", [pypowsybl.network.create_ieee14]) # @pytest.mark.parametrize("get_net", [create_complex_grid_battery_hvdc_svc_3w_trafo]) def test_lodf_jax_full_rank_update_compare_powsybl(get_net): - net, static_info, dynamic_info, string_info, jacobian_data = _load_test_grid(get_net) + net, network_info, jacobian_data = _load_test_grid(get_net) + dynamic_info = network_info.dynamic_network_data + string_info = network_info.string_network_data theta_actual = dynamic_info.bus_voltage_angles_rad vm_actual = dynamic_info.bus_voltage_magnitudes v_pu_actual = vm_actual * np.exp(theta_actual * 1j) @@ -105,7 +282,11 @@ def test_lodf_jax_full_rank_update_compare_powsybl(get_net): sa_bus_results = get_bus_branch_ids_for_n1_results(net, sa_res) y_matrix_n0 = _get_admittance_matrix_from_network_data(dynamic_info) - mismatch_n0 = calculate_nodal_mismatch_network_data(dynamic_network_data=dynamic_info, y_matrix=y_matrix_n0) + mismatch_n0 = calculate_nodal_mismatch_network_data( + dynamic_network_data=dynamic_info, + y_matrix=y_matrix_n0, + jacobian_data=jacobian_data, + ) # hotstart precision if isinstance(get_net(), pypowsybl.network.Network): @@ -116,19 +297,19 @@ def test_lodf_jax_full_rank_update_compare_powsybl(get_net): dx = -j_inverse @ mismatch_n0 assert abs(dx).max() < precition - v_mag_hat = np.asarray(dynamic_info.bus_voltage_magnitudes, dtype=float).reshape(-1) - theta_hat = np.asarray(dynamic_info.bus_voltage_angles_rad, dtype=float).reshape(-1) + v_mag_hat = np.asarray(dynamic_info.bus_voltage_magnitudes, dtype=float) + theta_hat = np.asarray(dynamic_info.bus_voltage_angles_rad, dtype=float) jacobian_inv = jacobian_data.inverse_jacobian jacobian_inv_np = np.asarray(jacobian_inv) jacobian_inv_transposed_np = jacobian_inv_np.T - branch_from = np.asarray(dynamic_info.branch_from_bus, dtype=np.int32).reshape(-1) - branch_to = np.asarray(dynamic_info.branch_to_bus, dtype=np.int32).reshape(-1) - y_ff = np.asarray(dynamic_info.branch_effective_admittance_from_from, dtype=np.complex128).reshape(-1) - y_ft = np.asarray(dynamic_info.branch_effective_admittance_from_to, dtype=np.complex128).reshape(-1) - y_tf = np.asarray(dynamic_info.branch_effective_admittance_to_from, dtype=np.complex128).reshape(-1) - y_tt = np.asarray(dynamic_info.branch_effective_admittance_to_to, dtype=np.complex128).reshape(-1) + branch_from = np.asarray(dynamic_info.branch_from_bus, dtype=np.int32) + branch_to = np.asarray(dynamic_info.branch_to_bus, dtype=np.int32) + y_ff = np.asarray(dynamic_info.branch_effective_admittance_from_from, dtype=np.complex128) + y_ft = np.asarray(dynamic_info.branch_effective_admittance_from_to, dtype=np.complex128) + y_tf = np.asarray(dynamic_info.branch_effective_admittance_to_from, dtype=np.complex128) + y_tt = np.asarray(dynamic_info.branch_effective_admittance_to_to, dtype=np.complex128) is_bridge = _find_bridges(dynamic_info) outage_candidates = np.flatnonzero(~is_bridge) @@ -144,7 +325,7 @@ def test_lodf_jax_full_rank_update_compare_powsybl(get_net): s_from = v_from * np.conj(i_from) s_to = v_to * np.conj(i_to) - branch_connected_base = np.asarray(dynamic_info.branch_connected, dtype=bool).reshape(-1) + branch_connected_base = np.asarray(dynamic_info.branch_connected, dtype=bool) s_from = np.where(branch_connected_base, s_from, 0.0 + 0.0j) s_to = np.where(branch_connected_base, s_to, 0.0 + 0.0j) @@ -153,6 +334,7 @@ def test_lodf_jax_full_rank_update_compare_powsybl(get_net): branch_pq_base[:, 1] = s_to.real branch_pq_base[:, 2] = s_from.imag branch_pq_base[:, 3] = s_to.imag + lower_residual_uses_reactive_mismatch = _build_lower_residual_uses_reactive_mismatch(dynamic_info) # For profiling the branch-linearization path, we only need shapes/dtypes. # Replace with your production precompute when available. @@ -181,22 +363,21 @@ def test_lodf_jax_full_rank_update_compare_powsybl(get_net): monitor_bus_indices = jnp.arange(inputs.theta_hat.size, dtype=jnp.int32) monitor_branch_indices = jnp.arange(inputs.branch_from.size, dtype=jnp.int32) bus_to_mon_index = jnp.arange(inputs.theta_hat.size, dtype=jnp.int32) + jacobian_components = _build_jacobian_components(jacobian_data) + network_topology = _build_network_topology(branch_from, branch_to, branch_connected_base) + voltage_state = _build_voltage_state(v_mag_hat, theta_hat) + network_admittance = _build_network_admittance(y_ff, y_ft, y_tf, y_tt) lf_res = line_outage_post_contingency_monitored( jacobian_inv_transposed=inputs.jacobian_inv_transposed, outage_branch_idx=inputs.branch_idx, - branch_from=inputs.branch_from, - branch_to=inputs.branch_to, - v_mag_hat=inputs.v_mag_hat, - theta_hat=inputs.theta_hat, - angle_component_indices=inputs.angle_component_indices, - magnitude_component_indices=inputs.magnitude_component_indices, - y_ff=inputs.y_ff, - y_ft=inputs.y_ft, - y_tf=inputs.y_tf, - y_tt=inputs.y_tt, + jacobian_components=jacobian_components, + network_topology=network_topology, + voltage_state=voltage_state, + network_admittance=network_admittance, monitor_bus_indices=monitor_bus_indices, branch_pq_base=inputs.branch_pq_base, + lower_residual_uses_reactive_mismatch=jnp.asarray(lower_residual_uses_reactive_mismatch), monitor_branch_indices=monitor_branch_indices, bus_to_mon_index=bus_to_mon_index, ) diff --git a/tests/numpy/test_bsdf.py b/tests/numpy/test_bsdf.py index 737669e..c9434a8 100644 --- a/tests/numpy/test_bsdf.py +++ b/tests/numpy/test_bsdf.py @@ -5,17 +5,40 @@ # you can obtain one at https://mozilla.org/MPL/2.0/. # Mozilla Public License, version 2.0 +from dc_plus.interfaces.network_information import replace_network_data + + import numpy as np +import pandas as pd +import pypowsybl import pytest +from dc_plus.importing.powsybl.powsybl_import import DANGLING_BUS_STRING_SUFFIX +from dc_plus.interfaces.jacobian_network_data import ( + _apply_jacobian_dx_to_network_data, + _get_admittance_matrix_from_network_data, + get_jacobian_data_from_network_data, + calculate_nodal_mismatch_network_data, +) +from dc_plus.interfaces.network_information import BusType from dc_plus.numpy.bsdf_full_rank import compute_bsdf_update +from dc_plus.preprocess.create_network_data import create_network_data_pypowsybl +from dc_plus.preprocess.preprocess_jacobian_bsdf import preprocess_jacobian_bsdf from tests.test_helper.bsdf_helper import ( + derive_bus_order, get_bsdf_cases, prepare_bsdf_test_context, run_reference_one_step, ) +ENABLED_LIMIT_BSDF_CASES = [ + bsdf_test_case + for bsdf_test_case in get_bsdf_cases() + if not bsdf_test_case.shunt_connected_to_bus_b_string and not bsdf_test_case.injections_connected_to_bus_b_string +] + + @pytest.mark.parametrize("bsdf_test_case", get_bsdf_cases()) def test_bsdf_full_rank(bsdf_test_case): setup = prepare_bsdf_test_context(bsdf_test_case=bsdf_test_case) @@ -27,8 +50,8 @@ def test_bsdf_full_rank(bsdf_test_case): bus_to_split=setup.bus_to_split, new_bus_b_index=setup.new_bus_index, new_bus_type=2, # force select PQ node - branches_connected_to_bus_b=setup.branches_to_move, - shunt_connected_to_bus_b=np.array([], dtype=np.int32), + branches_connected_to_bus_b=setup.branches_connected_to_bus_b, + shunt_connected_to_bus_b=setup.shunt_connected_to_bus_b, branch_from=setup.branch_from_original, branch_to=setup.branch_to_original, shunt_to_bus=setup.dynamic_info.shunt_bus_indices, @@ -54,8 +77,9 @@ def test_bsdf_full_rank(bsdf_test_case): ) # test against powsybl - dynamic_info_one_step = run_reference_one_step(setup.net, bsdf_test_case=bsdf_test_case) - dx = -jacobian_inv_bsdf @ setup.mismatch_n1 + dynamic_info_one_step, string_info_one_step = run_reference_one_step(setup.net, bsdf_test_case=bsdf_test_case) + bus_order = derive_bus_order(setup.split_bus_ids, string_info_one_step.bus_ids) + dx = -jacobian_inv_bsdf @ setup.mismatch_bsdf_reference # Map Jacobian increments back to bus ordering using the Jacobian mapping @@ -71,16 +95,200 @@ def test_bsdf_full_rank(bsdf_test_case): vm_actual[setup.pq_indices] + dx[setup.jacobian_data_with_extra_buses.is_magnitude_component] ) - # reorder bus voltages to match new ordering of manual split + dangling_bus_mask = np.char.endswith(np.asarray(string_info_one_step.bus_ids, dtype=str), DANGLING_BUS_STRING_SUFFIX) + np.testing.assert_allclose( - dynamic_info_one_step.bus_voltage_magnitudes[bsdf_test_case.bus_order], - vm_updated_J, + dynamic_info_one_step.bus_voltage_magnitudes[~dangling_bus_mask], + vm_updated_J[bus_order][~dangling_bus_mask], rtol=1e-10, atol=1e-10, ) np.testing.assert_allclose( - dynamic_info_one_step.bus_voltage_angles_rad[bsdf_test_case.bus_order], - theta_updated_J, + dynamic_info_one_step.bus_voltage_angles_rad[~dangling_bus_mask], + theta_updated_J[bus_order][~dangling_bus_mask], rtol=1e-10, atol=1e-10, ) + + # FIXME + # Powsybl one-step results on dangling buses are not bitwise identical to the + # imported Jacobian linearization + np.testing.assert_allclose( + dynamic_info_one_step.bus_voltage_magnitudes[dangling_bus_mask], + vm_updated_J[bus_order][dangling_bus_mask], + rtol=1e-9, + atol=1e-9, + ) + np.testing.assert_allclose( + dynamic_info_one_step.bus_voltage_angles_rad[dangling_bus_mask], + theta_updated_J[bus_order][dangling_bus_mask], + rtol=1e-8, + atol=1e-8, + ) + + +def test_bsdf_full_rank_applies_phase_tap_change_before_split( + micro_grid_be_network_with_replaced_transformers: pypowsybl.network.Network, +): + net = micro_grid_be_network_with_replaced_transformers + network_info_base = create_network_data_pypowsybl(net) + dynamic_info_base = network_info_base.dynamic_network_data + string_info_base = network_info_base.string_network_data + jacobian_data_base = get_jacobian_data_from_network_data(dynamic_info_base) + jacobian_data_with_extra_buses, dynamic_info_with_placeholders = preprocess_jacobian_bsdf( + jacobian_data=jacobian_data_base, + max_bus_splits=1, + dynamic_network_data=dynamic_info_base, + ) + + phase_tap_changer = net.get_phase_tap_changers().reset_index().iloc[0] + branch_index_by_id = {str(branch_id): idx for idx, branch_id in enumerate(string_info_base.branch_ids)} + phase_branch_idx = branch_index_by_id[str(phase_tap_changer["id"])] + + branch_from_bus = int(dynamic_info_base.branch_from_bus[phase_branch_idx]) + branch_to_bus = int(dynamic_info_base.branch_to_bus[phase_branch_idx]) + if dynamic_info_base.bus_type[branch_from_bus] == BusType.PQ: + bus_to_split = branch_from_bus + else: + bus_to_split = branch_to_bus + assert dynamic_info_base.bus_type[bus_to_split] == BusType.PQ + + new_bus_index = dynamic_info_with_placeholders.n_buses - 1 + updated_tap = int(phase_tap_changer["high_tap"]) + if updated_tap == int(phase_tap_changer["tap"]): + updated_tap = int(phase_tap_changer["low_tap"]) + + net.update_phase_tap_changers(df=pd.DataFrame({"id": [phase_tap_changer["id"]], "tap": [updated_tap]}).set_index("id")) + + dynamic_info_tap = create_network_data_pypowsybl(net).dynamic_network_data + jacobian_data_tap = get_jacobian_data_from_network_data(dynamic_info_tap) + _jacobian_data_tap_extended, dynamic_info_tap_with_placeholders = preprocess_jacobian_bsdf( + jacobian_data=jacobian_data_tap, + max_bus_splits=1, + dynamic_network_data=dynamic_info_tap, + ) + + split_voltage_magnitudes = dynamic_info_tap_with_placeholders.bus_voltage_magnitudes.copy() + split_voltage_angles = dynamic_info_tap_with_placeholders.bus_voltage_angles_rad.copy() + split_voltage_magnitudes[new_bus_index] = dynamic_info_tap.bus_voltage_magnitudes[bus_to_split] + split_voltage_angles[new_bus_index] = dynamic_info_tap.bus_voltage_angles_rad[bus_to_split] + + branch_from_split = dynamic_info_tap.branch_from_bus.copy() + branch_to_split = dynamic_info_tap.branch_to_bus.copy() + branch_from_split[phase_branch_idx] = np.where( + branch_from_split[phase_branch_idx] == bus_to_split, + new_bus_index, + branch_from_split[phase_branch_idx], + ) + branch_to_split[phase_branch_idx] = np.where( + branch_to_split[phase_branch_idx] == bus_to_split, + new_bus_index, + branch_to_split[phase_branch_idx], + ) + + dynamic_info_split_expected = replace_network_data( + dynamic_info_tap_with_placeholders, + bus_voltage_magnitudes=split_voltage_magnitudes, + bus_voltage_angles_rad=split_voltage_angles, + branch_from_bus=branch_from_split, + branch_to_bus=branch_to_split, + ) + jacobian_data_split_expected = get_jacobian_data_from_network_data(dynamic_info_split_expected) + + jacobian_inv_bsdf = compute_bsdf_update( + jacobian_inv=jacobian_data_with_extra_buses.inverse_jacobian, + bus_to_split=bus_to_split, + new_bus_b_index=new_bus_index, + new_bus_type=2, + branches_connected_to_bus_b=np.asarray([phase_branch_idx], dtype=np.int32), + shunt_connected_to_bus_b=np.asarray([], dtype=np.int32), + branch_from=dynamic_info_base.branch_from_bus, + branch_to=dynamic_info_base.branch_to_bus, + shunt_to_bus=dynamic_info_base.shunt_bus_indices, + v_mag_hat=dynamic_info_split_expected.bus_voltage_magnitudes, + theta_hat=dynamic_info_split_expected.bus_voltage_angles_rad, + y_ff=dynamic_info_tap.branch_effective_admittance_from_from, + y_ft=dynamic_info_tap.branch_effective_admittance_from_to, + y_tf=dynamic_info_tap.branch_effective_admittance_to_from, + y_tt=dynamic_info_tap.branch_effective_admittance_to_to, + y_shunt=dynamic_info_tap.shunt_effective_bus_admittance, + angle_component_indices=jacobian_data_with_extra_buses.angle_component_indices, + magnitude_component_indices=jacobian_data_with_extra_buses.magnitude_component_indices, + y_ff_base=dynamic_info_base.branch_effective_admittance_from_from, + y_ft_base=dynamic_info_base.branch_effective_admittance_from_to, + y_tf_base=dynamic_info_base.branch_effective_admittance_to_from, + y_tt_base=dynamic_info_base.branch_effective_admittance_to_to, + ) + + np.testing.assert_allclose( + jacobian_inv_bsdf, + jacobian_data_split_expected.inverse_jacobian, + rtol=1e-10, + atol=1e-10, + ) + + +def test_bsdf_full_rank_applies_phase_tap_change_without_split( + micro_grid_be_network_with_replaced_transformers: pypowsybl.network.Network, +): + net = micro_grid_be_network_with_replaced_transformers + network_info_base = create_network_data_pypowsybl(net) + dynamic_info_base = network_info_base.dynamic_network_data + string_info_base = network_info_base.string_network_data + jacobian_data_base = get_jacobian_data_from_network_data(dynamic_info_base) + + phase_tap_changer = net.get_phase_tap_changers().reset_index().iloc[0] + branch_index_by_id = {str(branch_id): idx for idx, branch_id in enumerate(string_info_base.branch_ids)} + phase_branch_idx = branch_index_by_id[str(phase_tap_changer["id"])] + + updated_tap = int(phase_tap_changer["high_tap"]) + if updated_tap == int(phase_tap_changer["tap"]): + updated_tap = int(phase_tap_changer["low_tap"]) + + net.update_phase_tap_changers(df=pd.DataFrame({"id": [phase_tap_changer["id"]], "tap": [updated_tap]}).set_index("id")) + + dynamic_info_tap = create_network_data_pypowsybl(net).dynamic_network_data + jacobian_data_tap = get_jacobian_data_from_network_data(dynamic_info_tap) + + jacobian_inv_bsdf = compute_bsdf_update( + jacobian_inv=jacobian_data_base.inverse_jacobian, + bus_to_split=0, + new_bus_b_index=0, + new_bus_type=2, + branches_connected_to_bus_b=np.asarray([], dtype=np.int32), + shunt_connected_to_bus_b=np.asarray([], dtype=np.int32), + branch_from=dynamic_info_base.branch_from_bus, + branch_to=dynamic_info_base.branch_to_bus, + shunt_to_bus=dynamic_info_base.shunt_bus_indices, + v_mag_hat=dynamic_info_tap.bus_voltage_magnitudes, + theta_hat=dynamic_info_tap.bus_voltage_angles_rad, + y_ff=dynamic_info_tap.branch_effective_admittance_from_from, + y_ft=dynamic_info_tap.branch_effective_admittance_from_to, + y_tf=dynamic_info_tap.branch_effective_admittance_to_from, + y_tt=dynamic_info_tap.branch_effective_admittance_to_to, + y_shunt=dynamic_info_tap.shunt_effective_bus_admittance, + angle_component_indices=jacobian_data_base.angle_component_indices, + magnitude_component_indices=jacobian_data_base.magnitude_component_indices, + y_ff_base=dynamic_info_base.branch_effective_admittance_from_from, + y_ft_base=dynamic_info_base.branch_effective_admittance_from_to, + y_tf_base=dynamic_info_base.branch_effective_admittance_to_from, + y_tt_base=dynamic_info_base.branch_effective_admittance_to_to, + apply_split_bus_adjustment=False, + ) + + np.testing.assert_allclose( + jacobian_inv_bsdf, + jacobian_data_tap.inverse_jacobian, + rtol=1e-10, + atol=1e-10, + ) + + mismatch_tap = calculate_nodal_mismatch_network_data( + dynamic_network_data=dynamic_info_tap, + y_matrix=_get_admittance_matrix_from_network_data(dynamic_info_tap), + jacobian_data=jacobian_data_tap, + ) + dx_bsdf = -jacobian_inv_bsdf @ mismatch_tap + dx_direct = -jacobian_data_tap.inverse_jacobian @ mismatch_tap + + np.testing.assert_allclose(dx_bsdf, dx_direct, rtol=1e-10, atol=1e-10) diff --git a/tests/numpy/test_fixed_jacobian.py b/tests/numpy/test_fixed_jacobian.py new file mode 100644 index 0000000..6e50027 --- /dev/null +++ b/tests/numpy/test_fixed_jacobian.py @@ -0,0 +1,215 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +import numpy as np +import pandas as pd +import pypowsybl +from dc_plus.numpy.fixed_jacobian import run_fixed_jacobian_iterations + +from dc_plus.interfaces.jacobian_network_data import ( + _get_admittance_matrix_from_network_data, + calculate_nodal_mismatch_network_data, + get_jacobian_data_from_network_data, +) +from dc_plus.importing.powsybl.powsybl_loadflow_parameter import get_powsybl_loadflow_parameter +from dc_plus.numpy.bsdf_full_rank import compute_bsdf_update +from dc_plus.preprocess.create_network_data import create_network_data_pypowsybl +import pytest + + +def _build_reactive_limit_one_step_parameter() -> pypowsybl.loadflow.Parameters: + """Build the notebook-style one-step loadflow parameter with reactive limits enabled.""" + provider_param_one_step = { + "newtonRaphsonConvEpsPerEq": "1e300", + "maxNewtonRaphsonIterations": "20", + "svcVoltageMonitoring": "false", + "newtonRaphsonStoppingCriteriaType": "UNIFORM_CRITERIA", + "generatorReactivePowerRemoteControl": "false", + "phaseShifterRegulationOn": "false", + "alwaysUpdateNetwork": "true", + "useActiveLimits": "false", + } + loadflow_parameter = get_powsybl_loadflow_parameter("default") + loadflow_parameter.provider_parameters = provider_param_one_step + loadflow_parameter.distributed_slack = False + loadflow_parameter.use_reactive_limits = True + return loadflow_parameter + + +def test_fixed_jacobian_single_iteration_matches_manual_dx_for_phase_tap_change_without_split( + micro_grid_be_network_with_replaced_transformers: pypowsybl.network.Network, +): + net = micro_grid_be_network_with_replaced_transformers + network_info_base = create_network_data_pypowsybl(net) + dynamic_info_base = network_info_base.dynamic_network_data + string_info_base = network_info_base.string_network_data + jacobian_data_base = get_jacobian_data_from_network_data( + dynamic_info_base, + ) + + phase_tap_changer = net.get_phase_tap_changers().reset_index().iloc[0] + branch_index_by_id = {str(branch_id): idx for idx, branch_id in enumerate(string_info_base.branch_ids)} + phase_branch_idx = branch_index_by_id[str(phase_tap_changer["id"])] + + updated_tap = int(phase_tap_changer["high_tap"]) + if updated_tap == int(phase_tap_changer["tap"]): + updated_tap = int(phase_tap_changer["low_tap"]) + + net.update_phase_tap_changers(df=pd.DataFrame({"id": [phase_tap_changer["id"]], "tap": [updated_tap]}).set_index("id")) + + dynamic_info_tap = create_network_data_pypowsybl(net).dynamic_network_data + jacobian_data_tap = get_jacobian_data_from_network_data( + dynamic_info_tap, + ) + + jacobian_inv_bsdf = compute_bsdf_update( + jacobian_inv=jacobian_data_base.inverse_jacobian, + bus_to_split=0, + new_bus_b_index=0, + new_bus_type=2, + branches_connected_to_bus_b=np.asarray([], dtype=np.int32), + shunt_connected_to_bus_b=np.asarray([], dtype=np.int32), + branch_from=dynamic_info_base.branch_from_bus, + branch_to=dynamic_info_base.branch_to_bus, + shunt_to_bus=dynamic_info_base.shunt_bus_indices, + v_mag_hat=dynamic_info_tap.bus_voltage_magnitudes, + theta_hat=dynamic_info_tap.bus_voltage_angles_rad, + y_ff=dynamic_info_tap.branch_effective_admittance_from_from, + y_ft=dynamic_info_tap.branch_effective_admittance_from_to, + y_tf=dynamic_info_tap.branch_effective_admittance_to_from, + y_tt=dynamic_info_tap.branch_effective_admittance_to_to, + y_shunt=dynamic_info_tap.shunt_effective_bus_admittance, + angle_component_indices=jacobian_data_base.angle_component_indices, + magnitude_component_indices=jacobian_data_base.magnitude_component_indices, + reactive_power_bus_mask=dynamic_info_base.bus_type == 2, + y_ff_base=dynamic_info_base.branch_effective_admittance_from_from, + y_ft_base=dynamic_info_base.branch_effective_admittance_from_to, + y_tf_base=dynamic_info_base.branch_effective_admittance_to_from, + y_tt_base=dynamic_info_base.branch_effective_admittance_to_to, + apply_split_bus_adjustment=False, + ) + + np.testing.assert_allclose( + jacobian_inv_bsdf, + jacobian_data_tap.inverse_jacobian, + rtol=1e-10, + atol=1e-10, + ) + + mismatch_tap = calculate_nodal_mismatch_network_data( + dynamic_network_data=dynamic_info_tap, + y_matrix=_get_admittance_matrix_from_network_data(dynamic_info_tap), + jacobian_data=jacobian_data_tap, + ) + dx_bsdf = -jacobian_inv_bsdf @ mismatch_tap + dx_direct = -jacobian_data_tap.inverse_jacobian @ mismatch_tap + + np.testing.assert_allclose(dx_bsdf, dx_direct, rtol=1e-10, atol=1e-10) + + y_matrix_tap = _get_admittance_matrix_from_network_data(dynamic_info_tap) + + dynamic_info_bsdf_single_step = run_fixed_jacobian_iterations( + jacobian_data=jacobian_data_tap.copy_with_inverse_jacobian(jacobian_inv_bsdf), + dynamic_network_data=dynamic_info_tap, + n_iterations=1, + y_matrix=y_matrix_tap, + ) + non_slack_buses = dynamic_info_tap.pvpq_buses_indices_pvpq_order + pq_buses = dynamic_info_tap.pq_buses_indices + theta_expected = dynamic_info_tap.bus_voltage_angles_rad.copy() + vm_expected = dynamic_info_tap.bus_voltage_magnitudes.copy() + theta_expected[non_slack_buses] += dx_bsdf[: non_slack_buses.size] + vm_expected[pq_buses] += dx_bsdf[non_slack_buses.size :] + + np.testing.assert_allclose(dynamic_info_bsdf_single_step.bus_voltage_angles_rad, theta_expected, rtol=1e-10, atol=1e-10) + np.testing.assert_allclose( + dynamic_info_bsdf_single_step.bus_voltage_magnitudes, + vm_expected, + rtol=1e-10, + atol=1e-10, + ) + + +def test_fixed_jacobian_iterations_improve_far_phase_tap_change( + micro_grid_be_network_with_replaced_transformers: pypowsybl.network.Network, +): + net = micro_grid_be_network_with_replaced_transformers + phase_tap_changer = net.get_phase_tap_changers().reset_index().iloc[0] + base_tap = int(phase_tap_changer["low_tap"]) + updated_tap = min(base_tap + 10, int(phase_tap_changer["high_tap"])) + + net.update_phase_tap_changers(df=pd.DataFrame({"id": [phase_tap_changer["id"]], "tap": [base_tap]}).set_index("id")) + pypowsybl.loadflow.run_ac(net)[0] + dynamic_info_base = create_network_data_pypowsybl(net).dynamic_network_data + jacobian_data_base = get_jacobian_data_from_network_data( + dynamic_info_base, + ) + + net.update_phase_tap_changers(df=pd.DataFrame({"id": [phase_tap_changer["id"]], "tap": [updated_tap]}).set_index("id")) + dynamic_info_tap = create_network_data_pypowsybl(net).dynamic_network_data + y_matrix_tap = _get_admittance_matrix_from_network_data(dynamic_info_tap) + + jacobian_inv_bsdf = compute_bsdf_update( + jacobian_inv=jacobian_data_base.inverse_jacobian, + bus_to_split=0, + new_bus_b_index=0, + new_bus_type=2, + branches_connected_to_bus_b=np.asarray([], dtype=np.int32), + shunt_connected_to_bus_b=np.asarray([], dtype=np.int32), + branch_from=dynamic_info_base.branch_from_bus, + branch_to=dynamic_info_base.branch_to_bus, + shunt_to_bus=dynamic_info_base.shunt_bus_indices, + v_mag_hat=dynamic_info_tap.bus_voltage_magnitudes, + theta_hat=dynamic_info_tap.bus_voltage_angles_rad, + y_ff=dynamic_info_tap.branch_effective_admittance_from_from, + y_ft=dynamic_info_tap.branch_effective_admittance_from_to, + y_tf=dynamic_info_tap.branch_effective_admittance_to_from, + y_tt=dynamic_info_tap.branch_effective_admittance_to_to, + y_shunt=dynamic_info_tap.shunt_effective_bus_admittance, + angle_component_indices=jacobian_data_base.angle_component_indices, + magnitude_component_indices=jacobian_data_base.magnitude_component_indices, + reactive_power_bus_mask=dynamic_info_base.bus_type == 2, + y_ff_base=dynamic_info_base.branch_effective_admittance_from_from, + y_ft_base=dynamic_info_base.branch_effective_admittance_from_to, + y_tf_base=dynamic_info_base.branch_effective_admittance_to_from, + y_tt_base=dynamic_info_base.branch_effective_admittance_to_to, + apply_split_bus_adjustment=False, + ) + + mismatch_before = calculate_nodal_mismatch_network_data( + dynamic_info_tap, + y_matrix_tap, + jacobian_data=jacobian_data_base, + ) + dynamic_info_single_iteration = run_fixed_jacobian_iterations( + jacobian_data=jacobian_data_base.copy_with_inverse_jacobian(jacobian_inv_bsdf), + dynamic_network_data=dynamic_info_tap, + n_iterations=1, + y_matrix=y_matrix_tap, + ) + dynamic_info_two_iterations = run_fixed_jacobian_iterations( + jacobian_data=jacobian_data_base.copy_with_inverse_jacobian(jacobian_inv_bsdf), + dynamic_network_data=dynamic_info_tap, + n_iterations=2, + y_matrix=y_matrix_tap, + ) + + mismatch_after_one = calculate_nodal_mismatch_network_data( + dynamic_info_single_iteration, + y_matrix_tap, + jacobian_data=jacobian_data_base, + ) + mismatch_after_two = calculate_nodal_mismatch_network_data( + dynamic_info_two_iterations, + y_matrix_tap, + jacobian_data=jacobian_data_base, + ) + + assert np.max(np.abs(mismatch_after_one)) < np.max(np.abs(mismatch_before)) + assert np.max(np.abs(mismatch_after_one)) < 0.1 * np.max(np.abs(mismatch_before)) + assert np.max(np.abs(mismatch_after_two)) < np.max(np.abs(mismatch_after_one)) + assert np.max(np.abs(mismatch_after_two)) < 0.2 * np.max(np.abs(mismatch_after_one)) diff --git a/tests/numpy/test_injection_outage.py b/tests/numpy/test_injection_outage.py new file mode 100644 index 0000000..fd450ae --- /dev/null +++ b/tests/numpy/test_injection_outage.py @@ -0,0 +1,221 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +from copy import deepcopy + +import numpy as np +import pypowsybl +import pytest + +from dc_plus.example_grids.pypowsbl.example_grids import POWSYBL_NETWORKS_SHORT_LIST +from dc_plus.importing.powsybl.powsybl_import import DANGLING_BUS_STRING_SUFFIX, _get_injections_powsybl +from dc_plus.importing.powsybl.powsybl_loadflow_parameter import get_powsybl_loadflow_parameter +from dc_plus.importing.powsybl.powsybl_network_helpers import _load_test_grid +from dc_plus.interfaces.jacobian_network_data import _apply_jacobian_dx_to_network_data +from dc_plus.numpy.injection_outage import non_voltage_regulating_injection_outage_dx +from dc_plus.preprocess.create_network_data import create_network_data_pypowsybl + +powsybl_networks = POWSYBL_NETWORKS_SHORT_LIST +TOL = 1e-10 + + +def _connected_injection_indices(injections): + candidates = np.flatnonzero(injections["connected"].to_numpy(dtype=bool)) + if candidates.size == 0: + pytest.skip("No connected injections available for an injection outage test") + return candidates + + +def _expected_bus_delta(dynamic_info, outage_injection_indices, outage_p, outage_q): + delta_p = np.zeros(dynamic_info.n_buses, dtype=float) + delta_q = np.zeros(dynamic_info.n_buses, dtype=float) + outage_indices = np.asarray(outage_injection_indices, dtype=np.int64) + valid_mask = outage_indices >= 0 + if not np.any(valid_mask): + return delta_p, delta_q + + valid_outages = outage_indices[valid_mask] + buses = dynamic_info.injection_to_bus[valid_outages] + np.add.at(delta_p, buses, -outage_p[valid_outages]) + np.add.at(delta_q, buses, -outage_q[valid_outages]) + return delta_p, delta_q + + +def _pad_outage_rows(outage_rows: list[np.ndarray]) -> np.ndarray: + if not outage_rows: + return np.empty((0, 0), dtype=np.int64) + + max_outages = max(outage_row.size for outage_row in outage_rows) + padded = np.full((len(outage_rows), max_outages), -1, dtype=np.int64) + for row_idx, outage_row in enumerate(outage_rows): + padded[row_idx, : outage_row.size] = outage_row + return padded + + +@pytest.mark.parametrize("get_net", powsybl_networks) +def test_non_voltage_regulating_injection_outage_numpy(get_net) -> None: + """Test non voltage regulating injection outages with a one-step update against powsybl load flow results. + + This does not test HVDC converter and dangling line outages. + It does test voltage regulating generator outages, as the powsybl one step does also not switch from PV to PQ + node. This therefore does not account for the voltage drop of the disconnected generator. + + In DC+ dangling lines are modelled as an additional bus with a fixed injection p0 and q0 + a one sided line is added between the original bus and the new bus to account for the line losses + of the dangling line. Here numerical errors occur. + The test still verifies the voltages of the bus where the dangling line is attached to with the + full precision. + + Parameters + ---------- + get_net : Callable[[], pypowsybl.network.Network] + A fixture that returns a powsybl network to test on. + """ + net, network_info, jacobian_data = _load_test_grid(get_net) + dynamic_info = network_info.dynamic_network_data + string_info = network_info.string_network_data + injections = _get_injections_powsybl(net).reset_index(drop=True) + injection_powsybl_type = net.get_injections(attributes=["type"]) + outage_p = injections["setpoint_p"].fillna(injections["p"]).to_numpy(dtype=float) + outage_q = injections["setpoint_q"].fillna(injections["q"]).to_numpy(dtype=float) + dangling_bus_mask = np.char.endswith(np.asarray(string_info.bus_ids, dtype=str), DANGLING_BUS_STRING_SUFFIX) + + assert len(injections) == dynamic_info.n_injections + + accepted_cases: list[tuple[str, np.ndarray, object]] = [] + for injection_idx in _connected_injection_indices(injections): + injection_id = injections.loc[injection_idx, "id_str"] + if (injection_powsybl_type.loc[injection_id, "type"]) in (["HVDC_CONVERTER_STATION", "DANGLING_LINE"]): + # - HVDC would need a multi outage + # - Dangling line removal would change the network for DC+ + continue + + net_n1 = deepcopy(net) + net_n1.remove_elements(injection_id) + + loadflow_parameter = get_powsybl_loadflow_parameter("one_step") + loadflow_res = pypowsybl.loadflow.run_ac(net_n1, parameters=loadflow_parameter)[0] + assert loadflow_res.iteration_count <= 1 + if loadflow_res.status != pypowsybl._pypowsybl.LoadFlowComponentStatus.CONVERGED: + continue + + network_info_n1 = create_network_data_pypowsybl(net_n1) + dynamic_info_n1 = network_info_n1.dynamic_network_data + string_info_n1 = network_info_n1.string_network_data + if not np.array_equal(string_info.bus_ids, string_info_n1.bus_ids): + continue + + same_angle_structure = np.array_equal( + dynamic_info.pvpq_buses_indices_pvpq_order, + dynamic_info_n1.pvpq_buses_indices_pvpq_order, + ) + same_magnitude_structure = np.array_equal( + dynamic_info.pq_buses_indices, + dynamic_info_n1.pq_buses_indices, + ) + if not (same_angle_structure and same_magnitude_structure): + continue + + outage_row = np.array([injection_idx], dtype=np.int64) + expected_delta_p, expected_delta_q = _expected_bus_delta(dynamic_info, outage_row, outage_p, outage_q) + actual_delta_p = dynamic_info_n1.bus_active_power - dynamic_info.bus_active_power + actual_delta_q = dynamic_info_n1.bus_reactive_power - dynamic_info.bus_reactive_power + p_buses = dynamic_info.pvpq_buses_indices_pvpq_order + q_buses = dynamic_info.pq_buses_indices + if not ( + np.allclose(actual_delta_p[p_buses], expected_delta_p[p_buses], rtol=TOL, atol=TOL) + and np.allclose(actual_delta_q[q_buses], expected_delta_q[q_buses], rtol=TOL, atol=TOL) + ): + continue + + accepted_cases.append((injection_id, outage_row, dynamic_info_n1)) + + assert accepted_cases, f"No injection outages matched the fixed-injection assumptions for {get_net.__name__}" + + outage_batch = _pad_outage_rows([outage_row for _, outage_row, _ in accepted_cases]) + dx_batch = non_voltage_regulating_injection_outage_dx( + jacobian_inv=jacobian_data.inverse_jacobian, + outage_injection_indices=outage_batch, + injection_to_bus=dynamic_info.injection_to_bus, + injection_active_power=outage_p, + injection_reactive_power=outage_q, + angle_component_indices=jacobian_data.angle_component_indices, + magnitude_component_indices=jacobian_data.magnitude_component_indices, + ) + + assert dx_batch.shape == (len(accepted_cases), jacobian_data.inverse_jacobian.shape[0]) + + for case_idx, (_, _, dynamic_info_n1) in enumerate(accepted_cases): + dx = dx_batch[case_idx] + theta_updated = dynamic_info.bus_voltage_angles_rad.copy() + vm_updated = dynamic_info.bus_voltage_magnitudes.copy() + theta_updated[dynamic_info.pvpq_buses_indices_pvpq_order] += dx[jacobian_data.is_angle_component] + vm_updated[dynamic_info.pq_buses_indices] += dx[jacobian_data.is_magnitude_component] + np.testing.assert_allclose( + theta_updated[~dangling_bus_mask], + dynamic_info_n1.bus_voltage_angles_rad[~dangling_bus_mask], + rtol=TOL, + atol=TOL, + ) + np.testing.assert_allclose( + vm_updated[~dangling_bus_mask], + dynamic_info_n1.bus_voltage_magnitudes[~dangling_bus_mask], + rtol=TOL, + atol=TOL, + ) + + +@pytest.mark.parametrize("get_net", powsybl_networks) +def test_non_voltage_regulating_injection_outage_numpy_enabled_limit_path(get_net) -> None: + net, network_info, jacobian_data = _load_test_grid( + get_net, + ) + dynamic_info = network_info.dynamic_network_data + + injections = _get_injections_powsybl(net).reset_index(drop=True) + injection_powsybl_type = net.get_injections(attributes=["type"]) + outage_p = injections["setpoint_p"].fillna(injections["p"]).to_numpy(dtype=float) + outage_q = injections["setpoint_q"].fillna(injections["q"]).to_numpy(dtype=float) + outage_rows = [ + np.array([injection_idx], dtype=np.int64) + for injection_idx in _connected_injection_indices(injections) + if (injection_powsybl_type.loc[injections.loc[injection_idx, "id_str"], "type"]) + not in (["HVDC_CONVERTER_STATION", "DANGLING_LINE"]) + ] + if not outage_rows: + pytest.skip(f"No supported injection outages available for {get_net.__name__}") + + outage_batch = _pad_outage_rows(outage_rows[: min(3, len(outage_rows))]) + dx_batch = non_voltage_regulating_injection_outage_dx( + jacobian_inv=jacobian_data.inverse_jacobian, + outage_injection_indices=outage_batch, + injection_to_bus=dynamic_info.injection_to_bus, + injection_active_power=outage_p, + injection_reactive_power=outage_q, + angle_component_indices=jacobian_data.angle_component_indices, + magnitude_component_indices=jacobian_data.magnitude_component_indices, + ) + + for case_idx, outage_row in enumerate(outage_batch): + expected_delta_p, expected_delta_q = _expected_bus_delta(dynamic_info, outage_row, outage_p, outage_q) + expected_mismatch = np.r_[ + expected_delta_p[jacobian_data.bus_angle_indices], + expected_delta_q[jacobian_data.bus_magnitude_indices], + ] + expected_dx = -(jacobian_data.inverse_jacobian @ expected_mismatch) + np.testing.assert_allclose(dx_batch[case_idx], expected_dx, rtol=TOL, atol=TOL) + + updated_dynamic_info = _apply_jacobian_dx_to_network_data( + dynamic_network_data=dynamic_info, + dx=dx_batch[case_idx], + jacobian_data=jacobian_data, + ) + if np.any(expected_delta_q[jacobian_data.bus_magnitude_indices] != 0.0): + assert not np.array_equal( + updated_dynamic_info.bus_voltage_magnitudes[jacobian_data.bus_magnitude_indices], + dynamic_info.bus_voltage_magnitudes[jacobian_data.bus_magnitude_indices], + ) diff --git a/tests/numpy/test_lodf.py b/tests/numpy/test_lodf.py index 2f08638..029888e 100644 --- a/tests/numpy/test_lodf.py +++ b/tests/numpy/test_lodf.py @@ -8,12 +8,14 @@ from copy import deepcopy import numpy as np +import pandas as pd import pypowsybl import pytest from dc_plus.example_grids.pypowsbl.example_grids import ( PANDAPOWER_NETWORKS_FOR_POWSYBL, POWSYBL_NETWORKS, + POWSYBL_NETWORKS_SHORT_LIST, basic_node_breaker_network_powsybl, ) from dc_plus.importing.powsybl.powsybl_import import DANGLING_BUS_STRING_SUFFIX @@ -24,20 +26,106 @@ powsybl_n1_analysis, ) from dc_plus.interfaces.jacobian_network_data import ( + _apply_jacobian_dx_to_network_data, _get_admittance_matrix_from_network_data, - _get_jacobian_data_from_network_data, + get_jacobian_data_from_network_data, calculate_nodal_mismatch_network_data, ) +from dc_plus.numpy.bsdf_full_rank import compute_bsdf_update from dc_plus.numpy.lodf import branch_outage_monitored_bus_dx, branch_outage_update_inverse from dc_plus.preprocess.helper_functions import _find_bridges +from dc_plus.preprocess.create_network_data import create_network_data_pypowsybl powsybl_networks = POWSYBL_NETWORKS +powsybl_networks_short_list = POWSYBL_NETWORKS_SHORT_LIST pandapower_networks = PANDAPOWER_NETWORKS_FOR_POWSYBL +def _assert_branch_outage_one_step_matches_powsybl( + net, + dynamic_info, + string_info, + jacobian_data, + base_inverse, + max_cases: int | None = None, +): + is_bridge = _find_bridges(dynamic_info) + loadflow_parameter_one_step = get_powsybl_loadflow_parameter("one_step") + outage_ids = string_info.branch_ids[~is_bridge] + sa_res = powsybl_n1_analysis(net=net, outage_grid_ids=outage_ids, loadflow_parameter=loadflow_parameter_one_step) + sa_bus_results = get_bus_branch_ids_for_n1_results(net, sa_res) + dangling_bus_mask = np.char.endswith(np.asarray(string_info.bus_ids, dtype=str), DANGLING_BUS_STRING_SUFFIX) + + compared_cases = 0 + for outage_idx in np.flatnonzero(~is_bridge): + if max_cases is not None and compared_cases >= max_cases: + break + + outage_id = string_info.branch_ids[outage_idx] + if outage_id not in sa_bus_results.index: + continue + + is_n1_converged = ( + sa_res.post_contingency_results[outage_id].status + == pypowsybl._pypowsybl.PostContingencyComputationStatus.CONVERGED + ) + if not is_n1_converged: + continue + + dynamic_info_n1 = deepcopy(dynamic_info) + dynamic_info_n1.branch_connected[outage_idx] = False + jacobian_data_n1 = get_jacobian_data_from_network_data( + dynamic_info_n1, + ) + jacobian_inv_n1 = branch_outage_update_inverse( + jacobian_inv=base_inverse, + outage_branches_indices=np.array([outage_idx], dtype=np.int64), + branch_from=dynamic_info.branch_from_bus, + branch_to=dynamic_info.branch_to_bus, + v_mag_hat=dynamic_info.bus_voltage_magnitudes, + theta_hat=dynamic_info.bus_voltage_angles_rad, + y_ft=dynamic_info.branch_effective_admittance_from_to, + y_tf=dynamic_info.branch_effective_admittance_to_from, + y_ff=dynamic_info.branch_effective_admittance_from_from, + y_tt=dynamic_info.branch_effective_admittance_to_to, + angle_component_indices=jacobian_data.angle_component_indices, + magnitude_component_indices=jacobian_data.magnitude_component_indices, + ) + y_matrix_n1 = _get_admittance_matrix_from_network_data(dynamic_info_n1) + mismatch_n1 = calculate_nodal_mismatch_network_data( + dynamic_network_data=dynamic_info_n1, + y_matrix=y_matrix_n1, + jacobian_data=jacobian_data_n1, + ) + updated_dynamic_info_n1 = _apply_jacobian_dx_to_network_data( + dynamic_network_data=dynamic_info_n1, + dx=-(jacobian_inv_n1 @ mismatch_n1), + jacobian_data=jacobian_data_n1, + ) + + sa_bus_results_n1 = sa_bus_results.loc[outage_id] + np.testing.assert_allclose( + updated_dynamic_info_n1.bus_voltage_angles_rad[~dangling_bus_mask], + sa_bus_results_n1["v_angle_rad"].values, + rtol=1e-10, + atol=1e-10, + ) + np.testing.assert_allclose( + updated_dynamic_info_n1.bus_voltage_magnitudes[~dangling_bus_mask], + sa_bus_results_n1["v_mag_pu"].values, + rtol=1e-10, + atol=1e-10, + ) + compared_cases += 1 + + if compared_cases == 0: + pytest.skip("No converged non-bridge outage available for one-step LODF comparison") + + def test_lodf_full_rank_update_multi_outage(): get_net = basic_node_breaker_network_powsybl - net, static_info, dynamic_info, string_info, jacobian_data = _load_test_grid(get_net) + net, network_info, jacobian_data = _load_test_grid(get_net) + dynamic_info = network_info.dynamic_network_data theta_actual = dynamic_info.bus_voltage_angles_rad vm_actual = dynamic_info.bus_voltage_magnitudes @@ -56,7 +144,7 @@ def test_lodf_full_rank_update_multi_outage(): dynamic_info_n2.branch_connected[outage_idx] = False dynamic_info_n2.branch_connected[multi_outage_idx] = False - jacobian_data_n2 = _get_jacobian_data_from_network_data(dynamic_info_n2) + jacobian_data_n2 = get_jacobian_data_from_network_data(dynamic_info_n2) J_inverse_direct_n2 = jacobian_data_n2.inverse_jacobian Yff = dynamic_info.branch_effective_admittance_from_from @@ -90,7 +178,9 @@ def test_lodf_full_rank_update_multi_outage(): # @pytest.mark.parametrize("get_net", [create_complex_grid_battery_hvdc_svc_3w_trafo]) # @pytest.mark.parametrize("get_net", [pypowsybl.network.create_micro_grid_be_network]) def test_lodf_numpy_full_rank_update_compare_powsybl(get_net): - net, static_info, dynamic_info, string_info, jacobian_data = _load_test_grid(get_net) + net, network_info, jacobian_data = _load_test_grid(get_net) + dynamic_info = network_info.dynamic_network_data + string_info = network_info.string_network_data theta_actual = dynamic_info.bus_voltage_angles_rad vm_actual = dynamic_info.bus_voltage_magnitudes v_pu_actual = vm_actual * np.exp(theta_actual * 1j) @@ -105,12 +195,16 @@ def test_lodf_numpy_full_rank_update_compare_powsybl(get_net): sa_bus_results = get_bus_branch_ids_for_n1_results(net, sa_res) y_matrix_n0 = _get_admittance_matrix_from_network_data(dynamic_info) - mismatch_n0 = calculate_nodal_mismatch_network_data(dynamic_network_data=dynamic_info, y_matrix=y_matrix_n0) + mismatch_n0 = calculate_nodal_mismatch_network_data( + dynamic_network_data=dynamic_info, + y_matrix=y_matrix_n0, + jacobian_data=jacobian_data, + ) # get a list of bus ids without dangling buses # string_info.bus_ids is a numpy array bus_ids = string_info.bus_ids - dangling_bus_mask = np.char.endswith(bus_ids.astype(str), DANGLING_BUS_STRING_SUFFIX) + dangling_bus_mask = np.char.endswith(np.asarray(bus_ids, dtype=str), DANGLING_BUS_STRING_SUFFIX) # hotstart precition if isinstance(get_net(), pypowsybl.network.Network): @@ -151,14 +245,18 @@ def test_lodf_numpy_full_rank_update_compare_powsybl(get_net): dynamic_info_n1 = deepcopy(dynamic_info) dynamic_info_n1.branch_connected[outage_idx] = False - jacobian_data_n1_direct = _get_jacobian_data_from_network_data(dynamic_info_n1) + jacobian_data_n1_direct = get_jacobian_data_from_network_data(dynamic_info_n1) J_inverse_direct_n1 = jacobian_data_n1_direct.inverse_jacobian assert abs(jacobian_inv_n1 - J_inverse_direct_n1).max() < 1e-10, ( f"max diff: {abs(jacobian_inv_n1 - J_inverse_direct_n1)}" ) y_matrix_n1 = _get_admittance_matrix_from_network_data(dynamic_info_n1) - mismatch_n1 = calculate_nodal_mismatch_network_data(dynamic_network_data=dynamic_info_n1, y_matrix=y_matrix_n1) + mismatch_n1 = calculate_nodal_mismatch_network_data( + dynamic_network_data=dynamic_info_n1, + y_matrix=y_matrix_n1, + jacobian_data=jacobian_data_n1_direct, + ) dx = -jacobian_inv_n1 @ mismatch_n1 # Map Jacobian increments back to bus ordering using the Jacobian mapping @@ -214,3 +312,99 @@ def test_lodf_numpy_full_rank_update_compare_powsybl(get_net): monitored_bus_voltage = new_mag * np.exp(1.0j * new_theta) # Compare complex voltage (magnitude+angle) against the SA complex voltage assert abs(monitored_bus_voltage - v_pu).max() < 1e-10, f"max diff: {abs(monitored_bus_voltage - v_pu)}" + + +def test_lodf_numpy_enabled_limit_path_compare_powsybl(): + get_net = basic_node_breaker_network_powsybl + net, network_info, jacobian_data = _load_test_grid( + get_net, + ) + dynamic_info = network_info.dynamic_network_data + string_info = network_info.string_network_data + + is_bridge = _find_bridges(dynamic_info) + loadflow_parameter_one_step = get_powsybl_loadflow_parameter("one_step") + outage_ids = string_info.branch_ids[~is_bridge] + sa_res = powsybl_n1_analysis(net=net, outage_grid_ids=outage_ids, loadflow_parameter=loadflow_parameter_one_step) + sa_bus_results = get_bus_branch_ids_for_n1_results(net, sa_res) + + selected_outage: tuple[int, str] | None = None + for outage_idx in np.flatnonzero(~is_bridge): + outage_id = string_info.branch_ids[outage_idx] + if outage_id not in sa_bus_results.index: + continue + is_n1_converged = ( + sa_res.post_contingency_results[outage_id].status + == pypowsybl._pypowsybl.PostContingencyComputationStatus.CONVERGED + ) + if is_n1_converged: + selected_outage = (int(outage_idx), outage_id) + break + + if selected_outage is None: + pytest.skip("No converged non-bridge outage available for enabled-limit LODF coverage") + + outage_idx, outage_id = selected_outage + dynamic_info_n1 = deepcopy(dynamic_info) + dynamic_info_n1.branch_connected[outage_idx] = False + jacobian_data_n1_direct = get_jacobian_data_from_network_data( + dynamic_info_n1, + ) + + jacobian_inv_n1 = branch_outage_update_inverse( + jacobian_inv=jacobian_data.inverse_jacobian, + outage_branches_indices=np.array([outage_idx], dtype=np.int64), + branch_from=dynamic_info.branch_from_bus, + branch_to=dynamic_info.branch_to_bus, + v_mag_hat=dynamic_info.bus_voltage_magnitudes, + theta_hat=dynamic_info.bus_voltage_angles_rad, + y_ft=dynamic_info.branch_effective_admittance_from_to, + y_tf=dynamic_info.branch_effective_admittance_to_from, + y_ff=dynamic_info.branch_effective_admittance_from_from, + y_tt=dynamic_info.branch_effective_admittance_to_to, + angle_component_indices=jacobian_data.angle_component_indices, + magnitude_component_indices=jacobian_data.magnitude_component_indices, + ) + np.testing.assert_allclose(jacobian_inv_n1, jacobian_data_n1_direct.inverse_jacobian, rtol=1e-10, atol=1e-10) + + y_matrix_n1 = _get_admittance_matrix_from_network_data(dynamic_info_n1) + mismatch_n1 = calculate_nodal_mismatch_network_data( + dynamic_network_data=dynamic_info_n1, + y_matrix=y_matrix_n1, + jacobian_data=jacobian_data_n1_direct, + ) + updated_dynamic_info_n1 = _apply_jacobian_dx_to_network_data( + dynamic_network_data=dynamic_info_n1, + dx=-(jacobian_inv_n1 @ mismatch_n1), + jacobian_data=jacobian_data_n1_direct, + ) + + sa_bus_results_n1 = sa_bus_results.loc[outage_id] + dangling_bus_mask = np.char.endswith(np.asarray(string_info.bus_ids, dtype=str), DANGLING_BUS_STRING_SUFFIX) + np.testing.assert_allclose( + updated_dynamic_info_n1.bus_voltage_angles_rad[~dangling_bus_mask], + sa_bus_results_n1["v_angle_rad"].values, + rtol=1e-10, + atol=1e-10, + ) + np.testing.assert_allclose( + updated_dynamic_info_n1.bus_voltage_magnitudes[~dangling_bus_mask], + sa_bus_results_n1["v_mag_pu"].values, + rtol=1e-10, + atol=1e-10, + ) + + +@pytest.mark.parametrize("get_net", powsybl_networks_short_list) +def test_lodf_numpy_short_list_one_step_compare_powsybl(get_net): + net, network_info, jacobian_data = _load_test_grid(get_net) + dynamic_info = network_info.dynamic_network_data + string_info = network_info.string_network_data + + _assert_branch_outage_one_step_matches_powsybl( + net=net, + dynamic_info=dynamic_info, + string_info=string_info, + jacobian_data=jacobian_data, + base_inverse=jacobian_data.inverse_jacobian, + ) diff --git a/tests/numpy/test_quasi_newton.py b/tests/numpy/test_quasi_newton.py new file mode 100644 index 0000000..23f4755 --- /dev/null +++ b/tests/numpy/test_quasi_newton.py @@ -0,0 +1,344 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +from copy import deepcopy + +from dc_plus.example_grids.pypowsbl.example_grids import POWSYBL_NETWORKS_SHORT_LIST +from dc_plus.preprocess.helper_functions import _find_bridges +import numpy as np +import pandas as pd +import pypowsybl +import pytest + +from dc_plus.interfaces.jacobian_network_data import ( + _apply_jacobian_dx_to_network_data, + _get_admittance_matrix_from_network_data, + get_jacobian_data_from_network_data, + calculate_nodal_mismatch_network_data, +) +from dc_plus.interfaces.network_information import BusType +from dc_plus.importing.powsybl.powsybl_loadflow_parameter import get_powsybl_loadflow_parameter +from dc_plus.importing.powsybl.powsybl_import import DANGLING_BUS_STRING_SUFFIX +from dc_plus.importing.powsybl.powsybl_network_helpers import ( + _load_test_grid, + get_bus_branch_ids_for_n1_results, + powsybl_n1_analysis, +) +from dc_plus.numpy.bsdf_full_rank import compute_bsdf_update +from dc_plus.numpy.quasi_newton import run_quasi_newton_updates +from dc_plus.preprocess.create_network_data import create_network_data_pypowsybl as create_network_data_pypowsybl + +from tests.test_helper.injection_state_helper import build_updated_dynamic_injection_state +from tests.test_helper.bsdf_helper import ( + derive_bus_order, + get_bsdf_cases, + prepare_bsdf_test_context, + run_reference_full_ac, +) + +powsybl_networks_short_list = POWSYBL_NETWORKS_SHORT_LIST + + +def test_quasi_newton_zero_iteration_keeps_target_state( + micro_grid_be_network_with_replaced_transformers: pypowsybl.network.Network, +): + net = micro_grid_be_network_with_replaced_transformers + dynamic_info_base = create_network_data_pypowsybl(net).dynamic_network_data + jacobian_data_base = get_jacobian_data_from_network_data( + dynamic_info_base, + ) + + dynamic_info_quasi_newton, mismatch_history, jacobian_inv_updated = run_quasi_newton_updates( + jacobian_data=jacobian_data_base, + dynamic_network_data=dynamic_info_base, + n_iterations=0, + ) + + np.testing.assert_allclose( + dynamic_info_quasi_newton.bus_voltage_magnitudes, + dynamic_info_base.bus_voltage_magnitudes, + rtol=1e-10, + atol=1e-10, + ) + np.testing.assert_allclose( + dynamic_info_quasi_newton.bus_voltage_angles_rad, + dynamic_info_base.bus_voltage_angles_rad, + rtol=1e-10, + atol=1e-10, + ) + np.testing.assert_allclose( + jacobian_inv_updated, + jacobian_data_base.inverse_jacobian, + rtol=1e-10, + atol=1e-10, + ) + assert mismatch_history == [] + + +def test_quasi_newton_reduces_phase_tap_mismatch( + micro_grid_be_network_with_replaced_transformers: pypowsybl.network.Network, +): + net = micro_grid_be_network_with_replaced_transformers + dynamic_info_base = create_network_data_pypowsybl(net).dynamic_network_data + jacobian_data_base = get_jacobian_data_from_network_data( + dynamic_info_base, + ) + + phase_tap_changer = net.get_phase_tap_changers().reset_index().iloc[0] + updated_tap = min(int(phase_tap_changer["tap"]) + 10, int(phase_tap_changer["high_tap"])) + net.update_phase_tap_changers(df=pd.DataFrame({"id": [phase_tap_changer["id"]], "tap": [updated_tap]}).set_index("id")) + + dynamic_info_tap = create_network_data_pypowsybl(net).dynamic_network_data + jacobian_data_tap = get_jacobian_data_from_network_data( + dynamic_info_tap, + ) + y_matrix_tap = _get_admittance_matrix_from_network_data(dynamic_info_tap) + jacobian_inv_bsdf = compute_bsdf_update( + jacobian_inv=jacobian_data_base.inverse_jacobian, + bus_to_split=0, + new_bus_b_index=0, + new_bus_type=2, + branches_connected_to_bus_b=np.asarray([], dtype=np.int32), + shunt_connected_to_bus_b=np.asarray([], dtype=np.int32), + branch_from=dynamic_info_base.branch_from_bus, + branch_to=dynamic_info_base.branch_to_bus, + shunt_to_bus=dynamic_info_base.shunt_bus_indices, + v_mag_hat=dynamic_info_tap.bus_voltage_magnitudes, + theta_hat=dynamic_info_tap.bus_voltage_angles_rad, + y_ff=dynamic_info_tap.branch_effective_admittance_from_from, + y_ft=dynamic_info_tap.branch_effective_admittance_from_to, + y_tf=dynamic_info_tap.branch_effective_admittance_to_from, + y_tt=dynamic_info_tap.branch_effective_admittance_to_to, + y_shunt=dynamic_info_tap.shunt_effective_bus_admittance, + angle_component_indices=jacobian_data_base.angle_component_indices, + magnitude_component_indices=jacobian_data_base.magnitude_component_indices, + reactive_power_bus_mask=dynamic_info_base.bus_type == 2, + y_ff_base=dynamic_info_base.branch_effective_admittance_from_from, + y_ft_base=dynamic_info_base.branch_effective_admittance_from_to, + y_tf_base=dynamic_info_base.branch_effective_admittance_to_from, + y_tt_base=dynamic_info_base.branch_effective_admittance_to_to, + apply_split_bus_adjustment=False, + ) + + dynamic_info_quasi_newton, mismatch_history, _jacobian_inv_updated = run_quasi_newton_updates( + jacobian_data=jacobian_data_tap.copy_with_inverse_jacobian(jacobian_inv_bsdf), + dynamic_network_data=dynamic_info_tap, + n_iterations=10, + y_matrix=y_matrix_tap, + ) + mismatch_before = calculate_nodal_mismatch_network_data( + dynamic_info_tap, + y_matrix_tap, + jacobian_data=jacobian_data_tap, + ) + mismatch_after = calculate_nodal_mismatch_network_data( + dynamic_info_quasi_newton, + y_matrix_tap, + jacobian_data=jacobian_data_tap, + ) + + assert len(mismatch_history) == 10 + assert np.max(np.abs(mismatch_after)) < np.max(np.abs(mismatch_before)) + assert np.all(np.isfinite(mismatch_history)) + assert mismatch_history[-1] < 1e-10 + + +@pytest.mark.parametrize("bsdf_test_case", get_bsdf_cases()) +def test_quasi_newton_matches_full_ac_for_bsdf_cases(bsdf_test_case): + setup = prepare_bsdf_test_context(bsdf_test_case=bsdf_test_case) + + jacobian_inv_bsdf = compute_bsdf_update( + jacobian_inv=setup.jacobian_data_with_extra_buses.inverse_jacobian, + bus_to_split=setup.bus_to_split, + new_bus_b_index=setup.new_bus_index, + new_bus_type=2, + branches_connected_to_bus_b=setup.branches_connected_to_bus_b, + shunt_connected_to_bus_b=setup.shunt_connected_to_bus_b, + branch_from=setup.branch_from_original, + branch_to=setup.branch_to_original, + shunt_to_bus=setup.dynamic_info.shunt_bus_indices, + v_mag_hat=setup.v_mag_hat, + theta_hat=setup.theta_hat, + y_ff=setup.y_ff, + y_ft=setup.y_ft, + y_tf=setup.y_tf, + y_tt=setup.y_tt, + y_shunt=setup.dynamic_info.shunt_effective_bus_admittance, + angle_component_indices=setup.jacobian_data_with_extra_buses.angle_component_indices, + magnitude_component_indices=setup.jacobian_data_with_extra_buses.magnitude_component_indices, + reactive_power_bus_mask=np.asarray(setup.dynamic_info_split_manual.bus_type == 2, dtype=bool), + ) + + y_matrix_split = _get_admittance_matrix_from_network_data(setup.dynamic_info_split_manual) + dynamic_info_quasi_newton, mismatch_history, jacobian_inv_updated = run_quasi_newton_updates( + jacobian_data=setup.jacobian_data_with_extra_buses.copy_with_inverse_jacobian(jacobian_inv_bsdf), + dynamic_network_data=setup.dynamic_info_split_manual, + n_iterations=9, + y_matrix=y_matrix_split, + ) + + dynamic_info_full_ac, string_info_full_ac = run_reference_full_ac(setup.net, bsdf_test_case=bsdf_test_case) + bus_order = derive_bus_order(setup.split_bus_ids, string_info_full_ac.bus_ids) + dangling_bus_mask = np.char.endswith(np.asarray(string_info_full_ac.bus_ids, dtype=str), DANGLING_BUS_STRING_SUFFIX) + vm_original = setup.dynamic_info_split_manual.bus_voltage_magnitudes[bus_order] + theta_original = setup.dynamic_info_split_manual.bus_voltage_angles_rad[bus_order] + + # check that the quasi-Newton result is not accidentally close to the original guess, but matches the full AC result instead + assert not ( + np.allclose( + dynamic_info_full_ac.bus_voltage_magnitudes[~dangling_bus_mask], + vm_original[~dangling_bus_mask], + rtol=1e-4, + atol=1e-4, + ) + and np.allclose( + dynamic_info_full_ac.bus_voltage_angles_rad[~dangling_bus_mask], + theta_original[~dangling_bus_mask], + rtol=1e-4, + atol=1e-4, + ) + ) + assert not ( + np.allclose( + dynamic_info_full_ac.bus_voltage_magnitudes[~dangling_bus_mask], + vm_original[~dangling_bus_mask], + rtol=1e-5, + atol=1e-5, + ) + and np.allclose( + dynamic_info_full_ac.bus_voltage_angles_rad[~dangling_bus_mask], + theta_original[~dangling_bus_mask], + rtol=1e-5, + atol=1e-5, + ) + ) + + # check that the quasi-Newton result matches the full AC result, both for regular and dangling buses + np.testing.assert_allclose( + dynamic_info_full_ac.bus_voltage_magnitudes[~dangling_bus_mask], + dynamic_info_quasi_newton.bus_voltage_magnitudes[bus_order][~dangling_bus_mask], + rtol=1e-10, + atol=1e-10, + ) + np.testing.assert_allclose( + dynamic_info_full_ac.bus_voltage_angles_rad[~dangling_bus_mask], + dynamic_info_quasi_newton.bus_voltage_angles_rad[bus_order][~dangling_bus_mask], + rtol=1e-10, + atol=1e-10, + ) + + np.testing.assert_allclose( + dynamic_info_full_ac.bus_voltage_magnitudes[dangling_bus_mask], + dynamic_info_quasi_newton.bus_voltage_magnitudes[bus_order][dangling_bus_mask], + rtol=1e-10, + atol=1e-10, + ) + np.testing.assert_allclose( + dynamic_info_full_ac.bus_voltage_angles_rad[dangling_bus_mask], + dynamic_info_quasi_newton.bus_voltage_angles_rad[bus_order][dangling_bus_mask], + rtol=1e-10, + atol=1e-10, + ) + + mismatch_after = calculate_nodal_mismatch_network_data( + dynamic_info_quasi_newton, + y_matrix_split, + jacobian_data=setup.jacobian_data_with_extra_buses, + ) + + assert len(mismatch_history) == 9 + assert np.all(np.isfinite(mismatch_history)) + assert np.max(np.abs(mismatch_after)) < 1e-10 + + +# @pytest.mark.parametrize("get_net", [pypowsybl.network.create_ieee30]) +@pytest.mark.parametrize("get_net", powsybl_networks_short_list) +def test_quasi_branch_outage_numpy_full_rank_update_compare_powsybl(get_net): + net, network_info, _jacobian_data = _load_test_grid(get_net) + dynamic_info = network_info.dynamic_network_data + string_info = network_info.string_network_data + iterations = 20 + tol_powsybl = 1e-9 + tol_not_converged = 1e-4 + + is_bridge = _find_bridges(dynamic_info) + outage_candidates = np.flatnonzero(~is_bridge) + + loadflow_parameter = get_powsybl_loadflow_parameter("hotstart_test") + outage_ids = string_info.branch_ids[~is_bridge] + sa_res = powsybl_n1_analysis(net=net, outage_grid_ids=outage_ids, loadflow_parameter=loadflow_parameter) + sa_bus_results = get_bus_branch_ids_for_n1_results(net, sa_res) + loadflow_parameter_limited = get_powsybl_loadflow_parameter("hotstart_test") + loadflow_parameter_limited.use_reactive_limits = True + loadflow_parameter_limited.provider_parameters["useActiveLimits"] = "true" + sa_res_limited = powsybl_n1_analysis( + net=net, + outage_grid_ids=outage_ids, + loadflow_parameter=loadflow_parameter_limited, + ) + + cases_compared = 0 + + for outage_branch in outage_candidates: + outage_id = string_info.branch_ids[outage_branch] + if outage_id not in sa_bus_results.index: + continue + + is_n1_converged = ( + sa_res.post_contingency_results[outage_id].status + == pypowsybl._pypowsybl.PostContingencyComputationStatus.CONVERGED + ) + if not is_n1_converged: + continue + + dynamic_info_n1 = deepcopy(dynamic_info) + dynamic_info_n1.branch_connected[outage_branch] = False + jacobian_data_n1 = get_jacobian_data_from_network_data(dynamic_info_n1) + y_matrix_n1 = _get_admittance_matrix_from_network_data(dynamic_info_n1) + + results_numpy, mismatch_history, _updated_inverse_jacobian = run_quasi_newton_updates( + jacobian_data=jacobian_data_n1, + dynamic_network_data=dynamic_info_n1, + n_iterations=iterations, + y_matrix=y_matrix_n1, + ) + + sa_bus_results_n1 = sa_bus_results.loc[outage_id] + if isinstance(sa_bus_results_n1, pd.Series): + sa_bus_results_n1 = sa_bus_results_n1.to_frame().T + sa_bus_results_n1 = sa_bus_results_n1.set_index("bus_id").reindex(string_info.bus_ids) + dangling_mask = sa_bus_results_n1.index.str.endswith(DANGLING_BUS_STRING_SUFFIX) + sa_bus_results_n1 = sa_bus_results_n1[~dangling_mask] + if sa_bus_results_n1[["v_mag_pu", "v_angle_rad"]].isna().any().any(): + continue + + if ( + sa_res_limited.post_contingency_results[outage_id].status + != pypowsybl._pypowsybl.PostContingencyComputationStatus.CONVERGED + or outage_id == "L1-3-1" + ): + tol_effective = tol_not_converged + else: + tol_effective = tol_powsybl + + np.testing.assert_allclose( + np.asarray(results_numpy.bus_voltage_magnitudes[~dangling_mask]), + sa_bus_results_n1["v_mag_pu"].to_numpy(), + rtol=tol_effective, + atol=tol_effective, + ) + np.testing.assert_allclose( + np.asarray(results_numpy.bus_voltage_angles_rad[~dangling_mask]), + sa_bus_results_n1["v_angle_rad"].to_numpy(), + rtol=tol_effective, + atol=tol_effective, + ) + assert np.all(np.isfinite(np.asarray(mismatch_history))), outage_branch + cases_compared += 1 + + if cases_compared == 0: + pytest.skip("No suitable outage cases found for comparison") diff --git a/tests/preprocess/test_preprocess_jacobian_bsdf.py b/tests/preprocess/test_preprocess_jacobian_bsdf.py index cada92d..2106347 100644 --- a/tests/preprocess/test_preprocess_jacobian_bsdf.py +++ b/tests/preprocess/test_preprocess_jacobian_bsdf.py @@ -21,7 +21,8 @@ def test_preprocess_jacobian_bsdf(): get_net = basic_node_breaker_network_powsybl - net, static_info, dynamic_info, string_info, jacobian_data = _load_test_grid(get_net) + net, network_info, jacobian_data = _load_test_grid(get_net) + dynamic_info = network_info.dynamic_network_data splits = 2 original_jacobian = jacobian_data.jacobian.toarray() original_inverse = jacobian_data.inverse_jacobian.copy() @@ -102,6 +103,7 @@ def test_preprocess_jacobian_bsdf(): np.testing.assert_allclose(extended_dynamic_info.bus_voltage_angles_rad[-splits:], 0.0) np.testing.assert_allclose(extended_dynamic_info.bus_active_power[-splits:], 0.0) np.testing.assert_allclose(extended_dynamic_info.bus_reactive_power[-splits:], 0.0) + np.testing.assert_allclose(extended_dynamic_info.bus_voltage_magnitude_setpoint[-splits:], 1.0) # Ensure original structures remain unchanged assert dynamic_info.n_buses == n_buses_original diff --git a/tests/preprocess/test_preprocessing.py b/tests/preprocess/test_preprocessing.py index 53b62ae..ed3a326 100644 --- a/tests/preprocess/test_preprocessing.py +++ b/tests/preprocess/test_preprocessing.py @@ -10,21 +10,23 @@ import pypowsybl from dc_plus.importing.powsybl.powsybl_network_helpers import _load_test_grid -from dc_plus.interfaces.jacobian_network_data import _get_jacobian_data_from_network_data -from dc_plus.preprocess.create_network_data import create_network_data_pypowsbl +from dc_plus.interfaces.jacobian_network_data import get_jacobian_data_from_network_data +from dc_plus.preprocess.create_network_data import create_network_data_pypowsybl def test_disconnected_branches(): get_net = pypowsybl.network.create_ieee14 - net, static_info, dynamic_info, string_info, jacobian_data = _load_test_grid(get_net) + net, network_info, jacobian_data = _load_test_grid(get_net) + dynamic_info = network_info.dynamic_network_data + string_info = network_info.string_network_data dynamic_info_n1 = deepcopy(dynamic_info) dynamic_info_n1.branch_connected[0] = False # disconnect first branch - jacobian_data_n1 = _get_jacobian_data_from_network_data(dynamic_info_n1) + jacobian_data_n1 = get_jacobian_data_from_network_data(dynamic_info_n1) net_n1 = deepcopy(net) net_n1.remove_elements(string_info.branch_ids[0]) - static_info_n1_direct, dynamic_info_n1_direct, string_info_n1_direct = create_network_data_pypowsbl(net_n1) - jacobian_data_n1_direct = _get_jacobian_data_from_network_data(dynamic_info_n1_direct) + dynamic_info_n1_direct = create_network_data_pypowsybl(net_n1).dynamic_network_data + jacobian_data_n1_direct = get_jacobian_data_from_network_data(dynamic_info_n1_direct) assert abs(jacobian_data_n1.jacobian - jacobian_data_n1_direct.jacobian).max() < 1e-10 diff --git a/tests/test_helper/bsdf_helper.py b/tests/test_helper/bsdf_helper.py index 859f4b4..f92d221 100644 --- a/tests/test_helper/bsdf_helper.py +++ b/tests/test_helper/bsdf_helper.py @@ -5,33 +5,55 @@ # you can obtain one at https://mozilla.org/MPL/2.0/. # Mozilla Public License, version 2.0 -from dataclasses import dataclass, replace +from dataclasses import dataclass +from dc_plus.interfaces.network_information import replace_network_data from typing import Any, Callable, List import numpy as np import pypowsybl -from dc_plus.example_grids.pypowsbl.example_grids import basic_node_breaker_network_powsybl +from dc_plus.example_grids.pypowsbl.example_grids import ( + basic_node_breaker_network_powsybl, + create_complex_grid_battery_hvdc_svc_3w_trafo, +) from dc_plus.importing.powsybl.powsybl_network_helpers import _load_test_grid from dc_plus.importing.powsybl.powsybl_loadflow_parameter import get_powsybl_loadflow_parameter from dc_plus.interfaces.jacobian_network_data import ( _get_admittance_matrix_from_network_data, - _get_jacobian_data_from_network_data, + get_jacobian_data_from_network_data, calculate_nodal_mismatch_network_data, ) -from dc_plus.interfaces.network_information import DynamicNetworkInformation -from dc_plus.preprocess.create_network_data import create_network_data_pypowsbl +from dc_plus.interfaces.network_information import DynamicNetworkInformation, StringNetworkInformation +from dc_plus.numpy.injection_helpers import _aggregate_bus_injections +from dc_plus.preprocess.create_network_data import create_network_data_pypowsybl from dc_plus.preprocess.preprocess_jacobian_bsdf import preprocess_jacobian_bsdf +_NEW_BUS_PLACEHOLDER_ID = "__new_bus_b__" + + @dataclass class BsdfTestCase: get_net: Callable[..., Any] + """Callable that returns a pypowsybl network instance for the test case.""" bus_to_split: int - branches_to_move: np.ndarray + """Index of the bus to split in the BSDF test case. + Note: this index number refers to the DC+ internal ordering of buses, which differs + from the pypowsybl bus numbering. (string index is sorted by name -> internal index) + """ + branches_connected_to_bus_b_string: list[str] + """Powsybl/DC+ branch IDs connected to the new bus B after the split.""" + + shunt_connected_to_bus_b_string: list[str] + """Powsybl/DC+ shunt IDs connected to the new bus B after the split.""" + injections_connected_to_bus_b_string: list[str] + """Powsybl/DC+ injection IDs connected to the new bus B after the split. + Note: you may only reassign injections which have no PV regulation. + If you want to reassign a PV injection, you need to change the bus b from default type PQ to PV.""" open_switches: tuple[str, ...] + """pypowsybl IDs of switches to open in the reference switch pattern for the test case.""" close_switches: tuple[str, ...] - bus_order: np.ndarray + """pypowsybl IDs of switches to close in the reference switch pattern for the test case.""" @dataclass @@ -43,8 +65,11 @@ class BsdfTestContext: jacobian_data_with_extra_buses: Any jacobian_data_split_manual: Any new_bus_index: int + split_bus_ids: np.ndarray bus_to_split: int - branches_to_move: np.ndarray + branches_connected_to_bus_b: np.ndarray + shunt_connected_to_bus_b: np.ndarray + injections_connected_to_bus_b: np.ndarray y_ff: np.ndarray y_ft: np.ndarray y_tf: np.ndarray @@ -53,7 +78,7 @@ class BsdfTestContext: branch_to_original: np.ndarray v_mag_hat: np.ndarray theta_hat: np.ndarray - mismatch_n1: np.ndarray + mismatch_bsdf_reference: np.ndarray theta_base: np.ndarray vm_base: np.ndarray pvpq_indices: np.ndarray @@ -62,9 +87,13 @@ class BsdfTestContext: def get_bsdf_cases() -> List[BsdfTestCase]: """Return test cases for BSDF tests.""" + + # simple_bsdf_test_case + # based on basic_node_breaker_network_powsybl + # simple reasignment and get_net = basic_node_breaker_network_powsybl bus_to_split = 2 - branches_to_move = np.array([2, 5], dtype=np.int32) + branches_connected_to_bus_b_string = ["L3", "L6"] open_switches = ( "VL3_BREAKER", "L32_DISCONNECTOR_3_0", @@ -78,41 +107,219 @@ def get_bsdf_cases() -> List[BsdfTestCase]: "L72_DISCONNECTOR_7_0", "L62_DISCONNECTOR_5_1", ) - bus_order = np.asarray([0, 1, 2, 4, 5, 3], dtype=np.int32) - return [ - BsdfTestCase( - get_net=get_net, - bus_to_split=bus_to_split, - branches_to_move=branches_to_move, - open_switches=open_switches, - close_switches=close_switches, - bus_order=bus_order, + simple_bsdf_test_case = BsdfTestCase( + get_net=get_net, + bus_to_split=bus_to_split, + branches_connected_to_bus_b_string=branches_connected_to_bus_b_string, + open_switches=open_switches, + close_switches=close_switches, + shunt_connected_to_bus_b_string=[], + injections_connected_to_bus_b_string=[], + ) + + get_net = create_complex_grid_battery_hvdc_svc_3w_trafo + bus_to_split = 13 + branches_connected_to_bus_b_string = ["L12", "L6", "L7"] + open_switches = ( + "VL_MV_BREAKER", + "SHUNT_MV_DISCONNECTOR_21_0", + ) + close_switches = ("SHUNT_MV_DISCONNECTOR_21_1",) + shunt_injection_test_case = BsdfTestCase( + get_net=get_net, + bus_to_split=bus_to_split, + branches_connected_to_bus_b_string=branches_connected_to_bus_b_string, + open_switches=open_switches, + close_switches=close_switches, + shunt_connected_to_bus_b_string=["SHUNT_MV"], + injections_connected_to_bus_b_string=["BAT_MV"], + ) + + return [simple_bsdf_test_case, shunt_injection_test_case] + + +def _reassign_selected_bus_indices( + asset_bus_indices: np.ndarray, + selected_asset_indices: np.ndarray, + bus_to_split: int, + new_bus_index: int, +) -> np.ndarray: + """Reassign the selected assets from the split bus to the new bus.""" + reassigned_bus_indices = np.asarray(asset_bus_indices).copy() + for asset_idx in np.asarray(selected_asset_indices, dtype=np.int32): + reassigned_bus_indices[asset_idx] = np.where( + reassigned_bus_indices[asset_idx] == bus_to_split, + new_bus_index, + reassigned_bus_indices[asset_idx], + ) + return reassigned_bus_indices + + +def _build_split_bus_ids(original_bus_ids: np.ndarray) -> np.ndarray: + """Append a placeholder bus ID for the newly created split bus.""" + bus_ids = original_bus_ids + return np.concatenate((bus_ids, [_NEW_BUS_PLACEHOLDER_ID])) + + +def derive_bus_order(candidate_bus_ids: np.ndarray, reference_bus_ids: np.ndarray) -> np.ndarray: + """Map candidate bus ordering to the powsybl reference bus ordering. + + The candidate ordering contains all original bus IDs plus one placeholder for the + newly created split bus. The reference ordering contains the actual new bus ID. + """ + candidate_ids = np.asarray(candidate_bus_ids, dtype=str) + reference_ids = np.asarray(reference_bus_ids, dtype=str) + + if candidate_ids.size != reference_ids.size: + raise ValueError( + "Candidate and reference bus ID arrays must have the same length to derive bus ordering. " + f"Got candidate={candidate_ids.size}, reference={reference_ids.size}." ) + + candidate_lookup = { + bus_id: idx for idx, bus_id in enumerate(candidate_ids.tolist()) if bus_id != _NEW_BUS_PLACEHOLDER_ID + } + bus_order = np.full(reference_ids.size, -1, dtype=np.int32) + used_candidate_indices: set[int] = set() + unresolved_reference_positions: list[int] = [] + + for ref_pos, ref_bus_id in enumerate(reference_ids.tolist()): + candidate_idx = candidate_lookup.get(ref_bus_id) + if candidate_idx is None: + unresolved_reference_positions.append(ref_pos) + continue + + bus_order[ref_pos] = candidate_idx + used_candidate_indices.add(candidate_idx) + + remaining_candidate_indices = [ + idx + for idx in range(candidate_ids.size) + if idx not in used_candidate_indices and candidate_ids[idx] == _NEW_BUS_PLACEHOLDER_ID ] + if len(unresolved_reference_positions) != len(remaining_candidate_indices): + raise ValueError( + "Could not derive a unique bus order from bus IDs. " + f"Unresolved reference positions={unresolved_reference_positions}, " + f"remaining candidate indices={remaining_candidate_indices}, " + f"reference bus ids={reference_ids.tolist()}, candidate bus ids={candidate_ids.tolist()}." + ) + + for ref_pos, candidate_idx in zip(unresolved_reference_positions, remaining_candidate_indices): + bus_order[ref_pos] = candidate_idx + + return bus_order + + +def _resolve_asset_indices_by_id( + available_ids: np.ndarray, + selected_ids: list[str], + asset_kind: str, +) -> np.ndarray: + """Resolve configured asset IDs to their DC+ internal indices.""" + available_ids_arr = np.asarray(available_ids, dtype=str) + selected_ids_list = list(selected_ids) + + id_to_index = {asset_id: idx for idx, asset_id in enumerate(available_ids_arr.tolist())} + missing_ids = [asset_id for asset_id in selected_ids_list if asset_id not in id_to_index] + if missing_ids: + raise ValueError( + f"Unknown {asset_kind} IDs in BSDF test case: {missing_ids}. Available IDs are: {available_ids_arr.tolist()}" + ) + + return np.asarray([id_to_index[asset_id] for asset_id in selected_ids_list], dtype=np.int32) + -def prepare_bsdf_test_context(bsdf_test_case: BsdfTestCase) -> BsdfTestContext: +def _validate_reassignment_targets( + bus_to_split: int, + branches_connected_to_bus_b: np.ndarray, + shunt_connected_to_bus_b: np.ndarray, + injections_connected_to_bus_b: np.ndarray, + branch_from_bus: np.ndarray, + branch_to_bus: np.ndarray, + shunt_bus_indices: np.ndarray, + injection_to_bus: np.ndarray, +) -> None: + """Validate that all selected assets are currently attached to the split bus.""" + invalid_branch_indices = [ + int(branch_idx) + for branch_idx in np.asarray(branches_connected_to_bus_b, dtype=np.int32) + if not np.any(np.asarray(branch_from_bus[branch_idx]) == bus_to_split) + and not np.any(np.asarray(branch_to_bus[branch_idx]) == bus_to_split) + ] + invalid_shunt_indices = [ + int(shunt_idx) + for shunt_idx in np.asarray(shunt_connected_to_bus_b, dtype=np.int32) + if not np.any(np.asarray(shunt_bus_indices[shunt_idx]) == bus_to_split) + ] + invalid_injection_indices = [ + int(injection_idx) + for injection_idx in np.asarray(injections_connected_to_bus_b, dtype=np.int32) + if not np.any(np.asarray(injection_to_bus[injection_idx]) == bus_to_split) + ] + + if invalid_branch_indices or invalid_shunt_indices or invalid_injection_indices: + raise ValueError( + "Invalid BSDF test case selection for bus split " + f"{bus_to_split}: branches={invalid_branch_indices}, " + f"shunts={invalid_shunt_indices}, injections={invalid_injection_indices}" + ) + + +def prepare_bsdf_test_context( + bsdf_test_case: BsdfTestCase, +) -> BsdfTestContext: """Build split-bus context shared by BSDF tests to avoid duplication.""" - branches_to_move = np.asarray(bsdf_test_case.branches_to_move, dtype=np.int32) - net, _, dynamic_info, _, jacobian_data = _load_test_grid(bsdf_test_case.get_net) + net, network_info, jacobian_data = _load_test_grid( + bsdf_test_case.get_net, + ) + dynamic_info = network_info.dynamic_network_data + string_info = network_info.string_network_data + branches_connected_to_bus_b = _resolve_asset_indices_by_id( + available_ids=string_info.branch_ids, + selected_ids=bsdf_test_case.branches_connected_to_bus_b_string, + asset_kind="branch", + ) + shunt_connected_to_bus_b = _resolve_asset_indices_by_id( + available_ids=string_info.shunt_ids, + selected_ids=bsdf_test_case.shunt_connected_to_bus_b_string, + asset_kind="shunt", + ) + injections_connected_to_bus_b = _resolve_asset_indices_by_id( + available_ids=string_info.injection_ids, + selected_ids=bsdf_test_case.injections_connected_to_bus_b_string, + asset_kind="injection", + ) jacobian_data_with_extra_buses, dynamic_info_with_placeholders = preprocess_jacobian_bsdf( jacobian_data=jacobian_data, max_bus_splits=1, dynamic_network_data=dynamic_info, ) + _validate_reassignment_targets( + bus_to_split=bsdf_test_case.bus_to_split, + branches_connected_to_bus_b=branches_connected_to_bus_b, + shunt_connected_to_bus_b=shunt_connected_to_bus_b, + injections_connected_to_bus_b=injections_connected_to_bus_b, + branch_from_bus=dynamic_info.branch_from_bus, + branch_to_bus=dynamic_info.branch_to_bus, + shunt_bus_indices=dynamic_info.shunt_bus_indices, + injection_to_bus=dynamic_info.injection_to_bus, + ) new_bus_index = dynamic_info_with_placeholders.n_buses - 1 + split_bus_ids = _build_split_bus_ids(string_info.bus_ids) v_mag_placeholder = dynamic_info_with_placeholders.bus_voltage_magnitudes.copy() theta_placeholder = dynamic_info_with_placeholders.bus_voltage_angles_rad.copy() v_mag_placeholder[new_bus_index] = dynamic_info.bus_voltage_magnitudes[bsdf_test_case.bus_to_split] theta_placeholder[new_bus_index] = dynamic_info.bus_voltage_angles_rad[bsdf_test_case.bus_to_split] - dynamic_info_with_placeholders = replace( + dynamic_info_with_placeholders = replace_network_data( dynamic_info_with_placeholders, bus_voltage_magnitudes=v_mag_placeholder, bus_voltage_angles_rad=theta_placeholder, ) branch_from_split = dynamic_info.branch_from_bus.copy() branch_to_split = dynamic_info.branch_to_bus.copy() - for branch_idx in branches_to_move: + for branch_idx in branches_connected_to_bus_b: branch_from_split[branch_idx] = np.where( branch_from_split[branch_idx] == bsdf_test_case.bus_to_split, new_bus_index, @@ -123,26 +330,57 @@ def prepare_bsdf_test_context(bsdf_test_case: BsdfTestCase) -> BsdfTestContext: new_bus_index, branch_to_split[branch_idx], ) - dynamic_info_split_manual = replace( + + shunt_bus_split = _reassign_selected_bus_indices( + asset_bus_indices=dynamic_info_with_placeholders.shunt_bus_indices, + selected_asset_indices=shunt_connected_to_bus_b, + bus_to_split=bsdf_test_case.bus_to_split, + new_bus_index=new_bus_index, + ) + injection_to_bus_split = _reassign_selected_bus_indices( + asset_bus_indices=dynamic_info_with_placeholders.injection_to_bus, + selected_asset_indices=injections_connected_to_bus_b, + bus_to_split=bsdf_test_case.bus_to_split, + new_bus_index=new_bus_index, + ) + bus_active_power_split, bus_reactive_power_split = _aggregate_bus_injections( + injection_to_bus=injection_to_bus_split, + injection_active_power=dynamic_info_with_placeholders.injection_active_power, + injection_reactive_power=dynamic_info_with_placeholders.injection_reactive_power, + injection_connected=dynamic_info_with_placeholders.injection_connected, + n_buses=dynamic_info_with_placeholders.n_buses, + ) + + dynamic_info_split_manual = replace_network_data( dynamic_info_with_placeholders, branch_from_bus=branch_from_split, branch_to_bus=branch_to_split, + shunt_bus_indices=shunt_bus_split, + injection_to_bus=injection_to_bus_split, + bus_active_power=bus_active_power_split, + bus_reactive_power=bus_reactive_power_split, ) - y_ff = np.asarray(dynamic_info.branch_effective_admittance_from_from, dtype=np.complex128) - y_ft = np.asarray(dynamic_info.branch_effective_admittance_from_to, dtype=np.complex128) - y_tf = np.asarray(dynamic_info.branch_effective_admittance_to_from, dtype=np.complex128) - y_tt = np.asarray(dynamic_info.branch_effective_admittance_to_to, dtype=np.complex128) - branch_from_original = np.asarray(dynamic_info.branch_from_bus, dtype=np.int32) - branch_to_original = np.asarray(dynamic_info.branch_to_bus, dtype=np.int32) - v_mag_hat = np.asarray(dynamic_info_with_placeholders.bus_voltage_magnitudes, dtype=float).flatten() - theta_hat = np.asarray(dynamic_info_with_placeholders.bus_voltage_angles_rad, dtype=float).flatten() + y_ff = dynamic_info.branch_effective_admittance_from_from + y_ft = dynamic_info.branch_effective_admittance_from_to + y_tf = dynamic_info.branch_effective_admittance_to_from + y_tt = dynamic_info.branch_effective_admittance_to_to + branch_from_original = dynamic_info.branch_from_bus + branch_to_original = dynamic_info.branch_to_bus + v_mag_hat = dynamic_info_with_placeholders.bus_voltage_magnitudes + theta_hat = dynamic_info_with_placeholders.bus_voltage_angles_rad y_matrix_n1 = _get_admittance_matrix_from_network_data(dynamic_info_split_manual) - mismatch_n1 = calculate_nodal_mismatch_network_data(dynamic_network_data=dynamic_info_split_manual, y_matrix=y_matrix_n1) - theta_base = np.asarray(dynamic_info_split_manual.bus_voltage_angles_rad, dtype=float).flatten() - vm_base = np.asarray(dynamic_info_split_manual.bus_voltage_magnitudes, dtype=float).flatten() - jacobian_data_split_manual = _get_jacobian_data_from_network_data(dynamic_info_split_manual) - pvpq_indices = np.asarray(dynamic_info_split_manual.pvpq_buses_indices_pvpq_order, dtype=int) - pq_indices = np.asarray(dynamic_info_split_manual.pq_buses_indices, dtype=int) + mismatch_bsdf_reference = calculate_nodal_mismatch_network_data( + dynamic_network_data=dynamic_info_split_manual, + y_matrix=y_matrix_n1, + jacobian_data=jacobian_data_with_extra_buses, + ) + theta_base = dynamic_info_split_manual.bus_voltage_angles_rad + vm_base = dynamic_info_split_manual.bus_voltage_magnitudes + jacobian_data_split_manual = get_jacobian_data_from_network_data( + dynamic_info_split_manual, + ) + pvpq_indices = dynamic_info_split_manual.pvpq_buses_indices_pvpq_order + pq_indices = dynamic_info_split_manual.pq_buses_indices return BsdfTestContext( net=net, dynamic_info=dynamic_info, @@ -151,8 +389,11 @@ def prepare_bsdf_test_context(bsdf_test_case: BsdfTestCase) -> BsdfTestContext: jacobian_data_with_extra_buses=jacobian_data_with_extra_buses, jacobian_data_split_manual=jacobian_data_split_manual, new_bus_index=new_bus_index, + split_bus_ids=split_bus_ids, bus_to_split=bsdf_test_case.bus_to_split, - branches_to_move=branches_to_move, + branches_connected_to_bus_b=branches_connected_to_bus_b, + shunt_connected_to_bus_b=shunt_connected_to_bus_b, + injections_connected_to_bus_b=injections_connected_to_bus_b, y_ff=y_ff, y_ft=y_ft, y_tf=y_tf, @@ -161,7 +402,7 @@ def prepare_bsdf_test_context(bsdf_test_case: BsdfTestCase) -> BsdfTestContext: branch_to_original=branch_to_original, v_mag_hat=v_mag_hat, theta_hat=theta_hat, - mismatch_n1=mismatch_n1, + mismatch_bsdf_reference=mismatch_bsdf_reference, theta_base=theta_base, vm_base=vm_base, pvpq_indices=pvpq_indices, @@ -169,7 +410,10 @@ def prepare_bsdf_test_context(bsdf_test_case: BsdfTestCase) -> BsdfTestContext: ) -def run_reference_one_step(net: Any, bsdf_test_case: BsdfTestCase) -> DynamicNetworkInformation: +def run_reference_one_step( + net: Any, + bsdf_test_case: BsdfTestCase, +) -> tuple[DynamicNetworkInformation, StringNetworkInformation]: """Apply the reference switch pattern and run one-step AC load-flow for the given case.""" for switch_id in bsdf_test_case.open_switches: net.open_switch(switch_id) @@ -177,6 +421,37 @@ def run_reference_one_step(net: Any, bsdf_test_case: BsdfTestCase) -> DynamicNet net.close_switch(switch_id) loadflow_parameter = get_powsybl_loadflow_parameter("one_step") - pypowsybl.loadflow.run_ac(net, parameters=loadflow_parameter)[0] - _static_info, dynamic_info_one_step, _string_info = create_network_data_pypowsbl(net) - return dynamic_info_one_step + lf_res = pypowsybl.loadflow.run_ac(net, parameters=loadflow_parameter)[0] + assert lf_res.iteration_count == 1, ( + f"Expected the reference load-flow to converge in one step, but got {lf_res.iteration_count} iterations." + ) + network_info_one_step = create_network_data_pypowsybl(net) + dynamic_info_one_step = network_info_one_step.dynamic_network_data + string_info_one_step = network_info_one_step.string_network_data + return dynamic_info_one_step, string_info_one_step + + +def run_reference_full_ac( + net: Any, + bsdf_test_case: BsdfTestCase, + convergence_eps: str = "1e-12", + max_iterations: str = "20", +) -> tuple[DynamicNetworkInformation, StringNetworkInformation]: + """Apply the reference switch pattern and run a tightly converged full AC load-flow.""" + for switch_id in bsdf_test_case.open_switches: + net.open_switch(switch_id) + for switch_id in bsdf_test_case.close_switches: + net.close_switch(switch_id) + + loadflow_parameter = get_powsybl_loadflow_parameter("hotstart_test") + loadflow_parameter.provider_parameters["newtonRaphsonConvEpsPerEq"] = convergence_eps + loadflow_parameter.provider_parameters["maxNewtonRaphsonIterations"] = max_iterations + lf_res = pypowsybl.loadflow.run_ac(net, parameters=loadflow_parameter)[0] + assert lf_res.status == pypowsybl._pypowsybl.LoadFlowComponentStatus.CONVERGED, ( + f"Expected the reference full AC load-flow to converge, but got status={lf_res.status}, " + f"status_text={lf_res.status_text}, reference_bus_id={lf_res.reference_bus_id}." + ) + network_info_full_ac = create_network_data_pypowsybl(net) + dynamic_info_full_ac = network_info_full_ac.dynamic_network_data + string_info_full_ac = network_info_full_ac.string_network_data + return dynamic_info_full_ac, string_info_full_ac diff --git a/tests/test_helper/injection_state_helper.py b/tests/test_helper/injection_state_helper.py new file mode 100644 index 0000000..d04a1e1 --- /dev/null +++ b/tests/test_helper/injection_state_helper.py @@ -0,0 +1,59 @@ +# Copyright 2026 50Hertz Transmission GmbH and Elia Transmission Belgium SA/NV +# +# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, +# you can obtain one at https://mozilla.org/MPL/2.0/. +# Mozilla Public License, version 2.0 + +from dc_plus.interfaces.network_information import replace_network_data + + +import numpy as np +import pandas as pd + + +def build_updated_dynamic_injection_state( + dynamic_info_base, + string_info_base, + load_update_df: pd.DataFrame, + generator_target_p_df: pd.DataFrame, +): + """Return a new dynamic state with only injection powers updated. + + This keeps the original N-0 voltage state and network model intact while + applying new load ``p0/q0`` values and generator ``target_p`` dispatches. + """ + updated_injection_active_power = dynamic_info_base.injection_active_power.copy() + updated_injection_reactive_power = dynamic_info_base.injection_reactive_power.copy() + + injection_ids = string_info_base.injection_ids + injection_types = string_info_base.injection_types + + for load_id, load_row in load_update_df.iterrows(): + load_mask = (injection_ids == load_id) & (injection_types == "LOAD") + if not np.any(load_mask): + continue + updated_injection_active_power[load_mask] = float(load_row["p0"]) + updated_injection_reactive_power[load_mask] = float(load_row["q0"]) + + for generator_id, generator_row in generator_target_p_df.iterrows(): + generator_mask = (injection_ids == generator_id) & (injection_types == "GENERATOR") + if not np.any(generator_mask): + continue + updated_injection_active_power[generator_mask] = -float(generator_row["target_p"]) + + active_power_delta = updated_injection_active_power - dynamic_info_base.injection_active_power + reactive_power_delta = updated_injection_reactive_power - dynamic_info_base.injection_reactive_power + + updated_bus_active_power = dynamic_info_base.bus_active_power.copy() + updated_bus_reactive_power = dynamic_info_base.bus_reactive_power.copy() + np.add.at(updated_bus_active_power, dynamic_info_base.injection_to_bus, active_power_delta) + np.add.at(updated_bus_reactive_power, dynamic_info_base.injection_to_bus, reactive_power_delta) + + return replace_network_data( + dynamic_info_base, + injection_active_power=updated_injection_active_power, + injection_reactive_power=updated_injection_reactive_power, + bus_active_power=updated_bus_active_power, + bus_reactive_power=updated_bus_reactive_power, + ) diff --git a/uv.lock b/uv.lock index 946674c..e82d5b8 100644 --- a/uv.lock +++ b/uv.lock @@ -5,6 +5,15 @@ requires-python = "==3.11.*" [manifest] overrides = [{ name = "nbconvert", marker = "sys_platform == 'never'" }] +[[package]] +name = "absl-py" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/4f/d79676ab82f2e42fc3611618139f13a9c4c31d0cff4b486982047679a802/absl_py-2.5.0.tar.gz", hash = "sha256:0c996f25c0490700fadabe6351630f6111534fa0ae252cc6d2014ea3b141135f", size = 118119, upload-time = "2026-07-03T10:57:48.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/0a/a10b45aab35b175aded078a462dc8d0c698f5b13946e7cb0869097b78bb6/absl_py-2.5.0-py3-none-any.whl", hash = "sha256:0f17b89f2a4eaaedc4f28c622998aa690564b3012a396a4ffad0821007fe03ba", size = 137410, upload-time = "2026-07-03T10:57:46.735Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -16,15 +25,15 @@ wheels = [ [[package]] name = "anyio" -version = "4.13.0" +version = "4.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" }, ] [[package]] @@ -105,6 +114,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] +[[package]] +name = "astunparse" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290, upload-time = "2019-12-22T18:12:13.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732, upload-time = "2019-12-22T18:12:11.297Z" }, +] + [[package]] name = "async-lru" version = "2.3.0" @@ -134,38 +156,37 @@ wheels = [ [[package]] name = "backrefs" -version = "6.2" +version = "7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, - { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, - { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, ] [[package]] name = "beautifulsoup4" -version = "4.14.3" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, ] [[package]] name = "bleach" -version = "6.3.0" +version = "6.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, + { url = "https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109, upload-time = "2026-06-05T13:01:12.504Z" }, ] [package.optional-dependencies] @@ -175,44 +196,79 @@ css = [ [[package]] name = "bracex" -version = "2.6" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/f5/4473ad9b48cd0420a2d762a3750fa0e078e23e060b1af72662e5987e5530/bracex-3.0.tar.gz", hash = "sha256:b73f718d6bd98d8419e45df02426c86e9967c179949f779340d6c3a8c83b9111", size = 43162, upload-time = "2026-06-30T00:43:35.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/2e/68781b78e764e5ccc4af1e3d27e060069c73af90234853fa80000e7ee79d/bracex-3.0-py3-none-any.whl", hash = "sha256:3833e61c2f092d5aa0468fa2e6c6e990a306185abf763b6d122f0158e59c58a5", size = 11738, upload-time = "2026-06-30T00:43:34.196Z" }, +] + +[[package]] +name = "cachebox" +version = "5.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/f6/85f176d2518cf1d1be5f981fc2dadf6b131e33fefd721f36b330e3434d6c/cachebox-5.2.3.tar.gz", hash = "sha256:b1f68246685aa739bbbd2734befb1465363a1e1042407c154feadb065f17a099", size = 63686, upload-time = "2026-04-10T12:21:35.028Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, + { url = "https://files.pythonhosted.org/packages/81/88/154179d492f2c000fe6efab3c3ff6b8eb94fbfaa09efe47999bce6b1e29f/cachebox-5.2.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:996f49d04b234082530afcc650bdd00556afbebc19c6c0daaafb85950340cb3c", size = 374245, upload-time = "2026-04-10T12:20:22.042Z" }, + { url = "https://files.pythonhosted.org/packages/7d/9d/3b03f2e063161bcb1a5e0969d521b5c622c2da02252a5c8bd4ef0e4f9914/cachebox-5.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23a3300ebbb526fa12ce6fa53699002f5fba6da23b4bbbaf8ba8b18a3f03e6b3", size = 356308, upload-time = "2026-04-10T12:20:09.149Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9b/8da38af731e3832e9f987548e4bfb610d7f3054019e12c44a94ba9272b37/cachebox-5.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79c63ee1589364caa04c018405e625d2e44e0bf9994f2715b2f322075d8c45b6", size = 395666, upload-time = "2026-04-10T12:18:51.89Z" }, + { url = "https://files.pythonhosted.org/packages/01/dd/1522aa808f94c904c5eb3640991799fed14dd43c1dd99a9f7b71bd95b1e3/cachebox-5.2.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebd0f8d4ebc3943c1ddcbbdc54f1a8ddf95505c862ed5731319cebd1eb98ae41", size = 353362, upload-time = "2026-04-10T12:19:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/dd/52/95bf883ec9b69a76f3a7d9fb14d015d9a4bdab0143a3eff62ceebc8b1419/cachebox-5.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:569966efcc6309aa7d774443e3513cdbb8671efae0158138ba2ebb7d8cc9d8ed", size = 371007, upload-time = "2026-04-10T12:19:17.484Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3d/cc02066d5ccfcb8b35adbaf867977fdb54572cda56ace56da396f0caa3bf/cachebox-5.2.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5774d06f0da37dd566239a4376d6ca8cf983d3e4c3228712ec22b4130f662f21", size = 390670, upload-time = "2026-04-10T12:19:29.685Z" }, + { url = "https://files.pythonhosted.org/packages/b3/50/8e4d59b3e344405d8393d6cc5cc92754d3cc1d81134041ebffd3f5ab73e6/cachebox-5.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae5bf8755bc66bcf42e7ca5c42d703a041a7aaad58f9a0c3be54d5b1cefd2641", size = 395765, upload-time = "2026-04-10T12:19:56.169Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d4/d731cff1c4cec22404bd3ddda05b233c5efaa5f13d7abf4e2728905b7cdd/cachebox-5.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63f061cc6a5ca70bbce2e6be0588fe2fee00a93a1b0581b1086d54b10288cdb6", size = 425707, upload-time = "2026-04-10T12:19:42.714Z" }, + { url = "https://files.pythonhosted.org/packages/36/01/3ec8aadceb0dcc66dbd0b9b32966cf7b6928ed84471424c24d21b0af62d0/cachebox-5.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577c781f18b559f4dc9eea176c6aed008843ef4b8e045cf61bb519e09dccc9ef", size = 564759, upload-time = "2026-04-10T12:20:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/31cbc8623ecc2e25900f7e8f20f11bfb84786989a59a8046e70b27cbea6d/cachebox-5.2.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7f691e25572a3ddbb018e19d796f774713bd6b0f7ce9be2e71f6e18572de264a", size = 669309, upload-time = "2026-04-10T12:20:51.117Z" }, + { url = "https://files.pythonhosted.org/packages/34/29/5a9e92bdc7b32dc865e73dd776638244f900136daee5bb0591a67e1530fa/cachebox-5.2.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:33368adf86669c29b936fbae5d6219cf90aacd4b1db71dae2e23d584a8219cd6", size = 643705, upload-time = "2026-04-10T12:21:05.882Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/5273a412855fdc11f674e4749aee6d5ec0a91f5c1a9f6e922f7fa0cb7a83/cachebox-5.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:38ce67b7b45713e49459a09411d07f82de04022c04aecde6202cd32f934c2b1f", size = 609751, upload-time = "2026-04-10T12:21:21.331Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a4/0fadb5e6a00f373cc3fe56b4415cdea2fc0147f6ec475611762d16eb4b05/cachebox-5.2.3-cp311-cp311-win32.whl", hash = "sha256:a7cd2c81347063ab6c512d0f569aeb5f75fc2dfe686c8486258ffd08052324f4", size = 275485, upload-time = "2026-04-10T12:21:51.563Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/67c1bf83f815294d2c3acd7631f25b5cbe6067e1d56495f76829dd60057b/cachebox-5.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:7e45798d6b969794840bb302857946d710ecb32af78dfcb3ab40f4e68ee7fdaf", size = 288024, upload-time = "2026-04-10T12:21:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7b/5eead1ca0d437b1993a742c6571079ae58ae4db50d94d42e87b514aed6c3/cachebox-5.2.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c798cddfb780156db09d3d96ed5da4c2d5fc01dad4bc7b54db5b20c34f221926", size = 376199, upload-time = "2026-04-10T12:20:32.674Z" }, + { url = "https://files.pythonhosted.org/packages/77/e3/5e45042f9b552a5087cafc2e0fed834e632531fca17818201d72e78593ce/cachebox-5.2.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c8f3de4afeb3fd721620be3d02f2338bcbc3fdbd464ca14e1c474088c9669db0", size = 357109, upload-time = "2026-04-10T12:20:18.554Z" }, + { url = "https://files.pythonhosted.org/packages/d4/51/3c4743b718b42e4b80166fa61f8722b603eba7bf206768a7892c4699dce7/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b39022c258872185327acffa9ad42d6bdf42f37d006d35c825a684eb5fa98d40", size = 396433, upload-time = "2026-04-10T12:19:01.463Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/678da91187bdb2836db2b8da62519da75359b46bc28697799a7caa314519/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a0599fb85dcb6df9a86502435643fe90c793bbcd50b5d85217c70f2bc2e38fc", size = 354287, upload-time = "2026-04-10T12:19:14.55Z" }, + { url = "https://files.pythonhosted.org/packages/df/06/769446da6c9f2855499aaa19e2d7260aa47934bc2e15a931e5b737f8685a/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3cdbe8f1b7716a44dc82ef3a6830a612260c7379478cfa80804632e2e6252b8e", size = 372507, upload-time = "2026-04-10T12:19:26.763Z" }, + { url = "https://files.pythonhosted.org/packages/79/cf/86c60994a7be734abef0395e440dc11714f84ffcd369cbcd8e61c3d58126/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:783d1b9a0b3c77c43e7ae331b9d6561ad75827e16b2484e2a6cc289ec4d392ee", size = 390831, upload-time = "2026-04-10T12:19:39.591Z" }, + { url = "https://files.pythonhosted.org/packages/9d/db/acfb55f8d5ee4ea1c5f2d32ede25d4d04e944ba09d2832c27c085022490d/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c6476a2a842906fee782d92f8fbcb03ecfd22eecc39adb7fb5b047d7e1cf020", size = 396277, upload-time = "2026-04-10T12:20:05.735Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4f/35e27e85a48e15671c5863addcabde910eb311800a621c3e47c04bd36d17/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:184bbcfa1370415b6d1f09e4fb74ab697dac8df09f522aa217a2fac65f973744", size = 426980, upload-time = "2026-04-10T12:19:52.622Z" }, + { url = "https://files.pythonhosted.org/packages/09/4b/50f2cadf20c02db9e449f2e9fee95f3eb5768ab1804dd0a5eba6c98119ad/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:f89df36b46f8f5e11c0c49701ec3cebddf51191f96afb7bb75c394faf3c1cbc8", size = 565539, upload-time = "2026-04-10T12:20:47.051Z" }, + { url = "https://files.pythonhosted.org/packages/43/53/b8e948cadb48b8bcf1d13c2aa4a788ff0e95b50ddb808c18e998499b4680/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:fb0bdcd9e28686e3b91d5210c843542858f0f10de151181aee27a7978fe4992e", size = 670870, upload-time = "2026-04-10T12:21:02.141Z" }, + { url = "https://files.pythonhosted.org/packages/29/7b/d68ca3f59a9d6963c2f6b19bc4b1926a37db2e4a4f6c9891d12788e49ce2/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:5196f0d2c2f99c92ddf0d2c37803ff90509d14a5df211b7754feb8b61ffd8740", size = 644542, upload-time = "2026-04-10T12:21:17.541Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c8/44ae6d5dff09f044d61a92591e6a8db17f3b2ee51a54d375cce90271527b/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:73671850d8c3634ab217398c83715d3feb52589ec97bd8e2f4d22e472741ea48", size = 610235, upload-time = "2026-04-10T12:21:32.93Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1b/31cf2449da9a296f6c6c0002c7ae91a25c3a4bfef071763bbeb85300b402/cachebox-5.2.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:70c718f6bb77e6ba142b9a055b81ce85412a0c0e5e82a154489b45e6f91d09ec", size = 287614, upload-time = "2026-04-10T12:21:47.909Z" }, ] [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, ] [[package]] @@ -226,39 +282,36 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, - { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, - { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, - { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, ] [[package]] name = "click" -version = "8.3.2" +version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, ] [[package]] @@ -308,26 +361,26 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, - { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, - { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, - { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, - { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, - { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, - { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, - { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, - { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, - { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, - { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +version = "7.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/8b/adeb62ea8951f13c4c7fef2e7a85e1a06b499c8d8237ea589d496029e53f/coverage-7.15.0.tar.gz", hash = "sha256:9ac3fe7a1435986463eaa8ee253ae2f2a268709ba4ae5c7dd1f52a05391ad78f", size = 925362, upload-time = "2026-07-02T13:10:50.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/23/82e910835ef4b8391047025e1d53aa48d66029f444eb8b25373c849bf503/coverage-7.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:003fff99412ea848c0aaebcc78ed2b6ce7d8a1227ed17e68470672770b78a02a", size = 220662, upload-time = "2026-07-02T13:08:39.205Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0d/c7b213dde2f1579de5231062b386d8413f79c11667eb58c39319b25991da/coverage-7.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cbd804bf2784ce7b45114516050f346ecd50f960c4bb630a7ee9e1d78fa2118", size = 221168, upload-time = "2026-07-02T13:08:40.471Z" }, + { url = "https://files.pythonhosted.org/packages/33/77/d000aeedfac085088337b3c7becdad328474b1f8a9e4c9368a0c99605d68/coverage-7.15.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8773e15c23305b58882a4611fb9b2755977eae0dc2e515366a1b6c98866cc4c2", size = 251587, upload-time = "2026-07-02T13:08:42.033Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e0/86787c56b9df17afd370d5e293515dd4d9a107a561d13054873eefad8ecc/coverage-7.15.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f50e40081494c1dc4239ebb202014cbcc3306ea96fb6302a34c8cc0967fc5ae8", size = 253497, upload-time = "2026-07-02T13:08:43.387Z" }, + { url = "https://files.pythonhosted.org/packages/3f/02/181bc917359299c07dead6270f94e411151c8b60cec905c33499da69afe6/coverage-7.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daf96f37f5fc3a7b6c6da862eb4aee61c426bd63da236ed4a73ef0e503b4bca5", size = 255607, upload-time = "2026-07-02T13:08:44.897Z" }, + { url = "https://files.pythonhosted.org/packages/b9/35/ca5e7427699913da6788c4f910e73ab16c5f4b59ec5d3a999dce2a45112f/coverage-7.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:51aa20f6ae2788fd197747766edf4cd8234fd9423309b934257fa6b21a592723", size = 257563, upload-time = "2026-07-02T13:08:46.334Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4d/b8220bacc2fc3c4e9078e27c32e99fb411479a4718a72bdd00036a9891c8/coverage-7.15.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03d1f922757662eb7af586e77834792274cff776bc7b1d1a0b66a49ea9d84735", size = 251726, upload-time = "2026-07-02T13:08:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e4/2e145da1991d72189b9c3cf7eca05c716ee7080d099aaea6757cfc7df008/coverage-7.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a6d6acc9a7666245e6133dd15144ca038a85a9cd5026bb06d6bbae9e77440dc9", size = 253301, upload-time = "2026-07-02T13:08:49.5Z" }, + { url = "https://files.pythonhosted.org/packages/72/28/d2c841d698bf762e481f08bd4839d370246b6d9b61dab085a7b20b201a08/coverage-7.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ac2c4c27c7df851dc9a017c2d7de00b69147e84ba3d96f37a530b0b6fb51035", size = 251361, upload-time = "2026-07-02T13:08:51.304Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ed/55d9ffde994fba3897c0c783f77a7d053b0c18787f6892ed5b0aed73f469/coverage-7.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b761a1d504fd4bd1f20f418753964dca9f5862a511fc854dac58296b3b223671", size = 255129, upload-time = "2026-07-02T13:08:52.661Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c0/ecbf33b8c460ea2718aeb813e2df8140d0370e5f67261c31524ceb0a2a8d/coverage-7.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e43b045e11c16e897895758ae90e4a90cf99e93d58549e2f90c0e2272e155695", size = 251081, upload-time = "2026-07-02T13:08:54.188Z" }, + { url = "https://files.pythonhosted.org/packages/a9/de/fb87b4261f54448dd2b9504ef19a58be42cef0d9520595fbfe1219b15234/coverage-7.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:589b54513e901739f4b4582c705ce96b80c96f57641b1464607e2367a270e540", size = 251988, upload-time = "2026-07-02T13:08:55.726Z" }, + { url = "https://files.pythonhosted.org/packages/df/27/3494d5f291b9a4cb868f73c11221a8bd2d5bd761a8f9acea61ff57128dd1/coverage-7.15.0-cp311-cp311-win32.whl", hash = "sha256:106781b8482749162d0b47056937ba0933508e5d9447f65a5e7d5c422f0d6bb4", size = 222754, upload-time = "2026-07-02T13:08:57.091Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ee/cd4847ebc9be6a9c0123d763645a6f1f3be6b8c58c962706368b79cbac07/coverage-7.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:821e92b3631d762a339695824cadbbc73020354eba2a23a551a99ad34938fbe6", size = 223225, upload-time = "2026-07-02T13:08:58.594Z" }, + { url = "https://files.pythonhosted.org/packages/57/37/5011581aa7f2be498b97dcc7c9902192442a42f4f9a748aeadb3d6506b42/coverage-7.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:309990eb5fb8014b9f67cb211f7fd41876ec8a88a88d3ae76de0ed1d611e3640", size = 222774, upload-time = "2026-07-02T13:09:00.074Z" }, + { url = "https://files.pythonhosted.org/packages/52/30/21b2ad45959cd50e909e02ebac1e30b4ceb7162e91c11d4c570223a458b7/coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19", size = 212632, upload-time = "2026-07-02T13:10:48.641Z" }, ] [package.optional-dependencies] @@ -348,6 +401,7 @@ wheels = [ name = "dc-plus" source = { editable = "." } dependencies = [ + { name = "equinox" }, { name = "jax" }, { name = "jax-dataclasses" }, { name = "matplotlib" }, @@ -383,6 +437,11 @@ dev = [ { name = "pytest-xdist" }, { name = "ruff" }, ] +devgpu = [ + { name = "jax", extra = ["cuda12"] }, + { name = "tensorboard" }, + { name = "tensorflow" }, +] doc = [ { name = "mkdocs" }, { name = "mkdocs-autorefs" }, @@ -395,13 +454,14 @@ doc = [ [package.metadata] requires-dist = [ - { name = "jax", specifier = ">=0.10.0" }, + { name = "equinox", specifier = ">=0.13.8" }, + { name = "jax", specifier = ">=0.4.30" }, { name = "jax-dataclasses", specifier = ">=1.6.2,<2.0.0" }, { name = "matplotlib", specifier = ">=3.10.8" }, { name = "numpydantic", specifier = ">=1.6.4,<2.0.0" }, { name = "pandapower", specifier = ">=3.0.0,<4.0.0" }, { name = "pandapower", marker = "extra == 'pandapower'", specifier = ">=3.0.0" }, - { name = "pydantic", specifier = ">=2.13.4,<3.0.0" }, + { name = "pydantic", specifier = ">=2.8.2,<3.0.0" }, { name = "pypowsybl", specifier = ">=1.14.0" }, { name = "pypowsybl", marker = "extra == 'pypowsybl'", specifier = ">=1.13.0" }, { name = "pypowsybl-jupyter", specifier = ">=1.3.0" }, @@ -425,48 +485,54 @@ dev = [ { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "ruff" }, ] +devgpu = [ + { name = "jax", extras = ["cuda12"], specifier = "==0.6.2" }, + { name = "tensorboard", specifier = ">=2.20.0,<3.0.0" }, + { name = "tensorflow", specifier = ">=2.20.0,<3.0.0" }, +] doc = [ { name = "mkdocs", specifier = ">=1.6.1" }, { name = "mkdocs-autorefs", specifier = ">=1.4.2" }, - { name = "mkdocs-include-markdown-plugin", specifier = ">=7.1.6" }, + { name = "mkdocs-include-markdown-plugin", specifier = ">=7.3.0" }, { name = "mkdocs-material", specifier = ">=9.6.8" }, { name = "mkdocs-material-extensions", specifier = ">=1.3.1" }, { name = "mkdocs-techdocs-core", specifier = ">=1.5.4" }, - { name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.4" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.30.0" }, ] [[package]] name = "debugpy" -version = "1.8.20" +version = "1.8.21" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/aa/12037145b7a56eaa5b29b41872f7a21b538e807e13f32c4d3c46e59be084/debugpy-1.8.21.tar.gz", hash = "sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6", size = 1697577, upload-time = "2026-06-01T19:30:35.156Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, - { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, - { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, - { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/cbf306d6e07a313a91e7171a98669054502840931432c227cfd505ee367f/debugpy-1.8.21-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:da456226c7b4c69e35dbe35dcee6623d912000a77816db7856a41af1c72a0264", size = 2203120, upload-time = "2026-06-01T19:30:43.964Z" }, + { url = "https://files.pythonhosted.org/packages/aa/57/aa739bd4ad2cbf96aeb1b20b56918ddd5ae4c28b68709bfcd327f02123ee/debugpy-1.8.21-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:f68b891688e61bdc08b8d364d919ff0051e0b94657b39dcd027bc3173edb7cdc", size = 3059958, upload-time = "2026-06-01T19:30:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/a8/31/453d2c9a23d133fe2c8ec7ca1d816ded52a913487fe3ffef7c01b4b706af/debugpy-1.8.21-cp311-cp311-win32.whl", hash = "sha256:f843a8b08c2edeaf9b1582eed4f25441af21a297c22ff16bf76a662557aa9c9e", size = 5236515, upload-time = "2026-06-01T19:30:47.461Z" }, + { url = "https://files.pythonhosted.org/packages/60/94/6660de2f2d7bf388f229335ba4637646eebabdbf38564cb439a95a9193c9/debugpy-1.8.21-cp311-cp311-win_amd64.whl", hash = "sha256:84c564d8cc701d41843b29a92814c1f1bef6798724ca9d675c284ad9f6a547d7", size = 5256138, upload-time = "2026-06-01T19:30:49.113Z" }, + { url = "https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92", size = 5352888, upload-time = "2026-06-01T19:31:25.186Z" }, ] [[package]] name = "decorator" -version = "5.2.1" +version = "5.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, ] [[package]] name = "deepdiff" -version = "8.6.2" +version = "9.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "cachebox" }, { name = "orderly-set" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/50/767448e792d41bfb6094ee317a355c1cb221dca24b2e178e2203bbea2a77/deepdiff-8.6.2.tar.gz", hash = "sha256:186dcbd181e4d76cef11ab05f802d0056c5d6083c5a6748c1473e9d7481e183e", size = 634860, upload-time = "2026-03-18T17:16:33.785Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/6b/6a4a5aaf38535eb332c2856aa08e73ed7c549d0851b1215401af0a2db1a7/deepdiff-9.1.0.tar.gz", hash = "sha256:07e9e366fab4297755153c4eab795ad4ef3cbd0d51660e847f5751c6bd727687", size = 382149, upload-time = "2026-05-15T20:18:05.751Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/5f/c52bd1255db763d0cdcb7084d2e90c42119cb229302c56bdf1d0aa78abd2/deepdiff-8.6.2-py3-none-any.whl", hash = "sha256:4d22034a866c3928303a9332c279362f714192d9305bac17c498720d095fd1b4", size = 91979, upload-time = "2026-03-18T17:16:32.171Z" }, + { url = "https://files.pythonhosted.org/packages/c7/26/4a2bad8eb430d8d805a4642c4bff25103a37548d74ab346f8b1e024abcc5/deepdiff-9.1.0-py3-none-any.whl", hash = "sha256:80c0460e1993b04f6f0ca79abf25548b129fd218478c4ebb08f80560f5d10610", size = 184662, upload-time = "2026-05-15T20:18:03.956Z" }, ] [[package]] @@ -480,11 +546,26 @@ wheels = [ [[package]] name = "distlib" -version = "0.4.0" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, +] + +[[package]] +name = "equinox" +version = "0.13.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +dependencies = [ + { name = "jax" }, + { name = "jaxtyping" }, + { name = "typing-extensions" }, + { name = "wadler-lindig" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/ff/522336d2f8264f2ad97119710b76e2cddf66145d03a1e89899175d26b192/equinox-0.13.8.tar.gz", hash = "sha256:dd075050018e2dd02e252e9d29d3060f7e67f085622d8d27a8e89e24bb8523db", size = 145257, upload-time = "2026-05-05T10:03:43.258Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d6/69a76c8ccdef14af687c497040292a46e59fc7a0ab24724b60e50ca61030/equinox-0.13.8-py3-none-any.whl", hash = "sha256:ca004348533cc30a63ebe8823d7dd4bb626dce17743d40bbddb89b402ef2a240", size = 185813, upload-time = "2026-05-05T10:03:41.673Z" }, ] [[package]] @@ -516,11 +597,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.28.0" +version = "3.29.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/17/6e8890271880903e3538660a21d63a6c1fea969ac71d0d6b608b78727fa9/filelock-3.28.0.tar.gz", hash = "sha256:4ed1010aae813c4ee8d9c660e4792475ee60c4a0ba76073ceaf862bd317e3ca6", size = 56474, upload-time = "2026-04-14T22:54:33.625Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/94/00f2059e4835eace3ae8fde680b932c496f8ec7bdc99168dfa53fb2e6b79/filelock-3.29.7.tar.gz", hash = "sha256:5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d", size = 71521, upload-time = "2026-07-08T05:46:58.716Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/21/2f728888c45033d34a417bfcd248ea2564c9e08ab1bfd301377cf05d5586/filelock-3.28.0-py3-none-any.whl", hash = "sha256:de9af6712788e7171df1b28b15eba2446c69721433fa427a9bee07b17820a9db", size = 39189, upload-time = "2026-04-14T22:54:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/60/02/be4a57b60c7149b55b9e3b3c13f609cd8eb5307c751f22bd8fb8d262e75b/filelock-3.29.7-py3-none-any.whl", hash = "sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51", size = 46036, upload-time = "2026-07-08T05:46:57.53Z" }, ] [[package]] @@ -537,21 +618,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, ] +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + [[package]] name = "fonttools" -version = "4.62.1" +version = "4.63.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, - { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, - { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, - { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, - { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, - { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, - { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/a7f1545bdf5da69c4bda0cea2a5781f0ad2a6623e0277267672db43c5fe6/fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f", size = 2881793, upload-time = "2026-05-14T12:02:56.645Z" }, + { url = "https://files.pythonhosted.org/packages/49/50/965308c703f085f225db2886813b27e015b8b3438c350b22dd65b52c2a2c/fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9", size = 2428130, upload-time = "2026-05-14T12:02:58.891Z" }, + { url = "https://files.pythonhosted.org/packages/d8/38/6937fbd7f2dc3a6b48725851bc2c15ec949b9af14d9bbcb5fe83cdf9bdf9/fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b", size = 5111952, upload-time = "2026-05-14T12:03:01.263Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18", size = 5082308, upload-time = "2026-05-14T12:03:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/67/00/cdd9d4944ca6ae280d01e69cc37bde3bf663630b837a6fc6d2cd65d80e0e/fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0", size = 5087932, upload-time = "2026-05-14T12:03:05.147Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f1/0aa0dbea778c75adbef223c42019fd47d22262b905974d62d829545d485f/fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007", size = 5213271, upload-time = "2026-05-14T12:03:07.238Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/253e4056e1f0e67b9390125a154b73b5eb73ad521bece95c004858fdeec2/fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb", size = 2304473, upload-time = "2026-05-14T12:03:09.271Z" }, + { url = "https://files.pythonhosted.org/packages/08/60/defa5e69641db890a63be281f41345f4c33b157824eaf0b9fad3e08b0dcb/fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c", size = 2356389, upload-time = "2026-05-14T12:03:11.53Z" }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, ] [[package]] @@ -563,13 +652,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, ] +[[package]] +name = "gast" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/f6/e73969782a2ecec280f8a176f2476149dd9dba69d5f8779ec6108a7721e6/gast-0.7.0.tar.gz", hash = "sha256:0bb14cd1b806722e91ddbab6fb86bba148c22b40e7ff11e248974e04c8adfdae", size = 33630, upload-time = "2025-11-29T15:30:05.266Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/33/f1c6a276de27b7d7339a34749cc33fa87f077f921969c47185d34a887ae2/gast-0.7.0-py3-none-any.whl", hash = "sha256:99cbf1365633a74099f69c59bd650476b96baa5ef196fec88032b00b31ba36f7", size = 22966, upload-time = "2025-11-29T15:30:03.983Z" }, +] + [[package]] name = "geojson" -version = "3.2.0" +version = "3.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/5a/33e761df75c732fcea94aaf01f993d823138581d10c91133da58bc231e63/geojson-3.2.0.tar.gz", hash = "sha256:b860baba1e8c6f71f8f5f6e3949a694daccf40820fa8f138b3f712bd85804903", size = 24574, upload-time = "2024-12-21T19:35:29.835Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/a9/bd61eee2c7904947094b74866b569f3fd5a8d6ac907ecdfecef74b19d459/geojson-3.3.0.tar.gz", hash = "sha256:92e83b9cb378a450b42f1207bb9b2a031f9fc89185f335153c44369b8b8b71fd", size = 25141, upload-time = "2026-05-28T21:48:08.214Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/a7fa2d650602731c90e0a86279841b4586e14228199e8c09165ba4863e29/geojson-3.2.0-py3-none-any.whl", hash = "sha256:69d14156469e13c79479672eafae7b37e2dcd19bdfd77b53f74fa8fe29910b52", size = 15040, upload-time = "2024-12-21T19:37:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5e/fdd72167b57158d743353f71d453200719744d1e75f18b1c8230508db370/geojson-3.3.0-py3-none-any.whl", hash = "sha256:a2d885187eeaa8b357600b3fcc9d963cb4300d1694196636dbd7eddc82fd0825", size = 15181, upload-time = "2026-05-28T21:48:06.648Z" }, ] [[package]] @@ -584,13 +682,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] +[[package]] +name = "google-pasta" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/4a/0bd53b36ff0323d10d5f24ebd67af2de10a1117f5cf4d7add90df92756f1/google-pasta-0.2.0.tar.gz", hash = "sha256:c9f2c8dfc8f96d0d5808299920721be30c9eec37f2389f28904f454565c8a16e", size = 40430, upload-time = "2020-03-13T18:57:50.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl", hash = "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed", size = 57471, upload-time = "2020-03-13T18:57:48.872Z" }, +] + [[package]] name = "griffelib" -version = "2.0.2" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, +] + +[[package]] +name = "grpcio" +version = "1.82.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/bc/656b89387d6f4ed7e0686c7b64c2ae7e554a759aa58122c8e5fb99392c32/grpcio-1.82.1.tar.gz", hash = "sha256:707b24abd90fcb1e45bcc080577da1dbf9971d107490589b9539af8e1e77b4b5", size = 13187300, upload-time = "2026-07-08T12:36:16.588Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/5b/e5092af97fa671ca279b3e373251af4bf87d5fbda7dc85f6a616899562a7/grpcio-1.82.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:0ddb18a9a9e1f46692b3567ae4abb3f8d117ce6afea48650f8eca06d8ab5d06f", size = 6181472, upload-time = "2026-07-08T12:34:31.009Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/18053a3a2ca03d0c2a1b8cc7271e705007a16aa5dae84bac00935c5b1a7f/grpcio-1.82.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:cf855b1af246720f567b0ce5d0724d45dfa4188eecc3296a2a69257b11b9e94b", size = 11970995, upload-time = "2026-07-08T12:34:33.603Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/21b1acb052876ad00959ec4d1b05fe08607d650bcfa282073bb164c2703c/grpcio-1.82.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb30cb13e25bc13cea70ffc69d6d90c49d36ea6c1d4549e6912f70177834cac", size = 6760127, upload-time = "2026-07-08T12:34:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/3e/12/25eef9c245c54f0061317d13a302357fe8ea03bac240b2b02ececcf54da4/grpcio-1.82.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1e822b2774f719c017cbe700b6e47173b6ae290fb84906f52a5a3c2c60b62e1e", size = 7484377, upload-time = "2026-07-08T12:34:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/1a348767eb9d9bd7765dc4fa8a01723d3bb386d67f981ee5c6f9c02b8b1c/grpcio-1.82.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5dafb1ece8ed45dee7c738f166ec82e19673221ed5ab8967f72858a4685345b2", size = 6924269, upload-time = "2026-07-08T12:34:40.583Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b9/3aae7a03d34c86ea27988db859a6087c186f6c3f53f9b551e07afd989bfa/grpcio-1.82.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06503106e7271e0a49fd5a1ac04747f1e47e87d900476db6fe45bc87ee411f4", size = 7531848, upload-time = "2026-07-08T12:34:43.277Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/3c4afa625d0dac9090707966916284c035fc5b2fb3e2c51e156accee6735/grpcio-1.82.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ff99bc8cafb6a952201c37b995f425e641c93ffa6e072258525feab57290141d", size = 8568217, upload-time = "2026-07-08T12:34:45.502Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/d8489c628e73e20a3d034e7f66912de7b1acb405f01d388f056a88e47924/grpcio-1.82.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:644ae1b94266ac785330f4590a69e52b6a7eb73029043a02209db81c81397d69", size = 7938771, upload-time = "2026-07-08T12:34:48.323Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b7/0a92cfd1658f3a896d4aa12d4efeb7dd4ddfc723725ae22741a5241ea710/grpcio-1.82.1-cp311-cp311-win32.whl", hash = "sha256:e203d2e19d471630084a16c815616f8211dff21c268ab3c5f5bf38417832e074", size = 4256432, upload-time = "2026-07-08T12:34:50.432Z" }, + { url = "https://files.pythonhosted.org/packages/c7/6a/2872c761b025d9ec74386f22a4a7d59c5a5b00ebf718761b33739ffc45de/grpcio-1.82.1-cp311-cp311-win_amd64.whl", hash = "sha256:0d8299c285fe6cc6a1f56badf8d3bc5078c8d20273ee64bafa3783b4bc29a769", size = 5009633, upload-time = "2026-07-08T12:34:52.67Z" }, ] [[package]] @@ -602,6 +733,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "h5py" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/57/dfb3c5c3f1bf5f5ef2e59a22dec4ff1f3d7408b55bfcefcfb0ea69ef21c6/h5py-3.14.0.tar.gz", hash = "sha256:2372116b2e0d5d3e5e705b7f663f7c8d96fa79a4052d250484ef91d24d6a08f4", size = 424323, upload-time = "2025-06-06T14:06:15.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/1b/ad24a8ce846cf0519695c10491e99969d9d203b9632c4fcd5004b1641c2e/h5py-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f30dbc58f2a0efeec6c8836c97f6c94afd769023f44e2bb0ed7b17a16ec46088", size = 3352382, upload-time = "2025-06-06T14:04:37.95Z" }, + { url = "https://files.pythonhosted.org/packages/36/5b/a066e459ca48b47cc73a5c668e9924d9619da9e3c500d9fb9c29c03858ec/h5py-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:543877d7f3d8f8a9828ed5df6a0b78ca3d8846244b9702e99ed0d53610b583a8", size = 2852492, upload-time = "2025-06-06T14:04:42.092Z" }, + { url = "https://files.pythonhosted.org/packages/08/0c/5e6aaf221557314bc15ba0e0da92e40b24af97ab162076c8ae009320a42b/h5py-3.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c497600c0496548810047257e36360ff551df8b59156d3a4181072eed47d8ad", size = 4298002, upload-time = "2025-06-06T14:04:47.106Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/d461649cafd5137088fb7f8e78fdc6621bb0c4ff2c090a389f68e8edc136/h5py-3.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:723a40ee6505bd354bfd26385f2dae7bbfa87655f4e61bab175a49d72ebfc06b", size = 4516618, upload-time = "2025-06-06T14:04:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/db/0c/6c3f879a0f8e891625817637fad902da6e764e36919ed091dc77529004ac/h5py-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d2744b520440a996f2dae97f901caa8a953afc055db4673a993f2d87d7f38713", size = 2874888, upload-time = "2025-06-06T14:04:56.95Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -632,20 +779,20 @@ wheels = [ [[package]] name = "identify" -version = "2.6.18" +version = "2.6.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" }, + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, ] [[package]] name = "idna" -version = "3.15" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] @@ -659,7 +806,7 @@ wheels = [ [[package]] name = "ipykernel" -version = "7.2.0" +version = "7.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "appnope", marker = "sys_platform == 'darwin'" }, @@ -669,21 +816,21 @@ dependencies = [ { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "matplotlib-inline" }, - { name = "nest-asyncio" }, + { name = "nest-asyncio2" }, { name = "packaging" }, { name = "psutil" }, { name = "pyzmq" }, { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/c4/e4a38f579de4225a561305666f7541cdabb30075def2aa1ac17bd73c1fb5/ipykernel-7.3.0.tar.gz", hash = "sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09", size = 184899, upload-time = "2026-06-10T08:41:25.481Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl", hash = "sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057", size = 120583, upload-time = "2026-06-10T08:41:23.648Z" }, ] [[package]] name = "ipython" -version = "9.10.1" +version = "9.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -693,14 +840,15 @@ dependencies = [ { name = "matplotlib-inline" }, { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "prompt-toolkit" }, + { name = "psutil", marker = "sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, { name = "pygments" }, { name = "stack-data" }, { name = "traitlets" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/25/daae0e764047b0a2480c7bbb25d48f4f509b5818636562eeac145d06dfee/ipython-9.10.1.tar.gz", hash = "sha256:e170e9b2a44312484415bdb750492699bf329233b03f2557a9692cce6466ada4", size = 4426663, upload-time = "2026-03-27T09:53:26.244Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/09/ba70f8d662d5671687da55ad2cc0064cf795b15e1eea70907532202e7c97/ipython-9.10.1-py3-none-any.whl", hash = "sha256:82d18ae9fb9164ded080c71ef92a182ee35ee7db2395f67616034bebb020a232", size = 622827, upload-time = "2026-03-27T09:53:24.566Z" }, + { url = "https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl", hash = "sha256:515ad9c3cdf0c932a5a9f6245419e8aba706b7bd03c3e1d3a1c83d9351d6aa6e", size = 630895, upload-time = "2026-06-26T11:03:33.809Z" }, ] [[package]] @@ -745,7 +893,7 @@ wheels = [ [[package]] name = "jax" -version = "0.10.0" +version = "0.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jaxlib" }, @@ -754,9 +902,52 @@ dependencies = [ { name = "opt-einsum" }, { name = "scipy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/f0/bcb81d28267d2054d0daed766c7fa16bcee5e481331b4d1e14f5fbe662be/jax-0.10.0.tar.gz", hash = "sha256:0119c767de1645f407df72345d28a3837dc904f1d698911c121d8f2b396fdece", size = 2663397, upload-time = "2026-04-22T13:22:28.563Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/1e/267f59c8fb7f143c3f778c76cb7ef1389db3fd7e4540f04b9f42ca90764d/jax-0.6.2.tar.gz", hash = "sha256:a437d29038cbc8300334119692744704ca7941490867b9665406b7f90665cd96", size = 2334091, upload-time = "2025-06-17T23:10:27.186Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/aa/dfac6d72cc35bc07e7587115b6946e333ef4ccb2e6cd26ecf639438c5d26/jax-0.10.0-py3-none-any.whl", hash = "sha256:76c42ba163c8db3dc2e449e225b888c0edfb623ded31efdc96d85e0fda1d26e8", size = 3094950, upload-time = "2026-04-16T12:32:11.576Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/97ef0cbb7a17143ace2643d600a7b80d6705b2266fc31078229e406bdef2/jax-0.6.2-py3-none-any.whl", hash = "sha256:bb24a82dc60ccf704dcaf6dbd07d04957f68a6c686db19630dd75260d1fb788c", size = 2722396, upload-time = "2025-06-17T23:10:25.293Z" }, +] + +[package.optional-dependencies] +cuda12 = [ + { name = "jax-cuda12-plugin", extra = ["with-cuda"] }, + { name = "jaxlib" }, +] + +[[package]] +name = "jax-cuda12-pjrt" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/8b/096a12d91b1bc76a13cd8e63f2d8eae5ca9b5693b5ee2687e46be3b5d779/jax_cuda12_pjrt-0.6.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:22faf020d2e8f7ca1e2915633241f7df7678b73c7078f5f0b2f113248337f7de", size = 111228681, upload-time = "2025-06-17T23:11:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8e/21c21b4335fce1c022c339da5e6b6249c246ad062e924d28fb0eda4bcef0/jax_cuda12_pjrt-0.6.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8cd9ead7948ea2c778a508fef5d1159e8b7abf4fccc7037c3fe1dbfcd95012dc", size = 125263999, upload-time = "2025-06-17T23:11:59.986Z" }, +] + +[[package]] +name = "jax-cuda12-plugin" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jax-cuda12-pjrt" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/99/90f81c660bf662698be17e8b959d8302682c5cd5ce0729c0bfc883d8affe/jax_cuda12_plugin-0.6.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:2cd8e279a59a38ba0c978a831e13adeb6ee9e4572fba387c7975ba3ad535dd38", size = 15873970, upload-time = "2025-06-17T23:12:09.492Z" }, + { url = "https://files.pythonhosted.org/packages/ad/00/e733c87a2fb7265c96f48c991a896552c873de949217823d519288724d91/jax_cuda12_plugin-0.6.2-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:0896cbb308d95291e205cd89d254029dee3a1df43d66e9831331a9afd2d27870", size = 15879563, upload-time = "2025-06-17T23:12:11.439Z" }, +] + +[package.optional-dependencies] +with-cuda = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cuda-cupti-cu12" }, + { name = "nvidia-cuda-nvcc-cu12" }, + { name = "nvidia-cuda-nvrtc-cu12" }, + { name = "nvidia-cuda-runtime-cu12" }, + { name = "nvidia-cudnn-cu12" }, + { name = "nvidia-cufft-cu12" }, + { name = "nvidia-cusolver-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nccl-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvshmem-cu12" }, ] [[package]] @@ -775,7 +966,7 @@ wheels = [ [[package]] name = "jaxlib" -version = "0.10.0" +version = "0.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ml-dtypes" }, @@ -783,34 +974,34 @@ dependencies = [ { name = "scipy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/48/5c/64a60f90d48bb6ab68ece63b7fa78855e8f8cefc4045f198a5c8695bfd99/jaxlib-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:277032e9f074c3fd5ffd1e0cb03d4fe66e272de472667cdbc418ad99b21b646a", size = 60115498, upload-time = "2026-04-16T12:33:15.93Z" }, - { url = "https://files.pythonhosted.org/packages/71/bc/b75d9e09bcf46e00fd9cdd6c457219a8fe2033d351c2d133917662e8cbaa/jaxlib-0.10.0-cp311-cp311-manylinux_2_27_aarch64.whl", hash = "sha256:3db94ebc859375d955de3504182add7ce1733ce3d30c15e0ef031602cb51a559", size = 79395106, upload-time = "2026-04-16T12:33:19.648Z" }, - { url = "https://files.pythonhosted.org/packages/64/13/a94b53b0acd3fccce0441e3811e86224e5b21ac122f2dea4be1ccdeb7dc0/jaxlib-0.10.0-cp311-cp311-manylinux_2_27_x86_64.whl", hash = "sha256:9be229993a41e5b2b84f234ecc19a5de02f35eddb1195cf027bd539e1601e15d", size = 85005588, upload-time = "2026-04-16T12:33:23.368Z" }, - { url = "https://files.pythonhosted.org/packages/d2/36/fbc303c0a41ac26daceeba0a9884d9206657e8eb1981f3f76da17f1ecc7f/jaxlib-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:421cdf3a4a5c2ee41471035e586954c8dc599d677ce9b11b063c3926a82a7850", size = 64195649, upload-time = "2026-04-16T12:33:26.972Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/6899b0aed36a4acc51319465ddd83c7c300a062a9e236cceee00984ffe0b/jaxlib-0.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a208ff61c58128d306bb4e5ad0858bd2b0960f2c1c10ad42c548f74a60c0020e", size = 54300346, upload-time = "2025-06-17T23:10:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/e6/03/34bb6b346609079a71942cfbf507892e3c877a06a430a0df8429c455cebc/jaxlib-0.6.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:11eae7e05bc5a79875da36324afb9eddd4baeaef2a0386caf6d4f3720b9aef28", size = 79438425, upload-time = "2025-06-17T23:10:58.356Z" }, + { url = "https://files.pythonhosted.org/packages/80/02/49b05cbab519ffd3cb79586336451fbbf8b6523f67128a794acc9f179000/jaxlib-0.6.2-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:335d7e3515ce78b52a410136f46aa4a7ea14d0e7d640f34e1e137409554ad0ac", size = 89920354, upload-time = "2025-06-17T23:11:03.086Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7a/93b28d9452b46c15fc28dd65405672fc8a158b35d46beabaa0fe9631afb0/jaxlib-0.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6815509997d6b05e5c9daa7994b9ad473ce3e8c8a17bdbbcacc3c744f76f7a0", size = 57895707, upload-time = "2025-06-17T23:11:07.074Z" }, ] [[package]] name = "jaxtyping" -version = "0.3.9" +version = "0.3.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wadler-lindig" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/be/00294e369938937e31b094437d5ea040e4fd1a20b998ebe572c4a1dcfa68/jaxtyping-0.3.9.tar.gz", hash = "sha256:f8c02d1b623d5f1b6665d4f3ddaec675d70004f16a792102c2fc51264190951d", size = 45857, upload-time = "2026-02-16T10:35:13.263Z" } +sdist = { url = "https://files.pythonhosted.org/packages/91/c1/091b8852bd7cbf50bd655543c8506033cf4029300c67f8c176c1286879a9/jaxtyping-0.3.11.tar.gz", hash = "sha256:b09c14acf6686feb9e0df5b0d8c6e7c5b6f8d36bf059ee54cd522a186c2ef050", size = 46489, upload-time = "2026-06-13T18:35:23.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/05/3e39d416fb92b2738a76e8265e6bfc5d10542f90a7c32ad1eb831eea3fa3/jaxtyping-0.3.9-py3-none-any.whl", hash = "sha256:a00557a9d616eff157491f06ed2e21ed94886fad3832399273eb912b345da378", size = 56274, upload-time = "2026-02-16T10:35:11.795Z" }, + { url = "https://files.pythonhosted.org/packages/8b/38/c66bbdc5047f4776c2bd3e47e5295a350e3fa44d5b8942105e71c2a876a0/jaxtyping-0.3.11-py3-none-any.whl", hash = "sha256:8a4bedc4e3f963fa82df41bd13c7ebc2bad925601eb48614c65798f21329d4e3", size = 56593, upload-time = "2026-06-13T18:35:22.01Z" }, ] [[package]] name = "jedi" -version = "0.19.2" +version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, ] [[package]] @@ -827,11 +1018,11 @@ wheels = [ [[package]] name = "json5" -version = "0.14.0" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/4b/6f8906aaf67d501e259b0adab4d312945bb7211e8b8d4dcc77c92320edaa/json5-0.14.0.tar.gz", hash = "sha256:b3f492fad9f6cdbced8b7d40b28b9b1c9701c5f561bef0d33b81c2ff433fefcb", size = 52656, upload-time = "2026-03-27T22:50:48.108Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/7d/05c46a96a78147ae3bf99c2f4169ce144a70220b8d6fcd56f6ec368b8ce9/json5-0.15.0.tar.gz", hash = "sha256:7424d1f1eb1d56da6e3d70643f53619862b4ce81440bdb8ecfd6f875e5ba4a71", size = 53278, upload-time = "2026-06-19T20:08:27.716Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/42/cf027b4ac873b076189d935b135397675dac80cb29acb13e1ab86ad6c631/json5-0.14.0-py3-none-any.whl", hash = "sha256:56cf861bab076b1178eb8c92e1311d273a9b9acea2ccc82c276abf839ebaef3a", size = 36271, upload-time = "2026-03-27T22:50:47.073Z" }, + { url = "https://files.pythonhosted.org/packages/eb/be/59527c99478aade6bb33a68d72e6e18dd4e6ff6eacfc7d01bdb15bc76912/json5-0.15.0-py3-none-any.whl", hash = "sha256:56636a30c0e8a4665fe2179c0212f32eae3796dea89ea6f649b9436ecdb39618", size = 36570, upload-time = "2026-06-19T20:08:26.748Z" }, ] [[package]] @@ -900,9 +1091,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657, upload-time = "2024-08-30T07:15:47.045Z" }, ] +[[package]] +name = "jupyter-builder" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/45/d0df8b43c10a61529c0f4a8af5e19ebe108f0c3af8f57e0fc358969907af/jupyter_builder-1.0.2.tar.gz", hash = "sha256:6155d78a5325010532a6419ffcba89eac643fd1aa56ea83115e661924d6f6aab", size = 968638, upload-time = "2026-06-12T02:33:25.767Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/b6/c418e0b3256f67c04933566b80bfce947350682db92c4b786a8653db32d6/jupyter_builder-1.0.2-py3-none-any.whl", hash = "sha256:b024f65d36e1d530542db597b00dd513261aa59842e0d0fbbb1015a9f1935e9c", size = 910789, upload-time = "2026-06-12T02:33:23.317Z" }, +] + [[package]] name = "jupyter-client" -version = "8.8.0" +version = "8.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-core" }, @@ -910,10 +1114,11 @@ dependencies = [ { name = "pyzmq" }, { name = "tornado" }, { name = "traitlets" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/dc/5512503b088997c2250b8bf18258fba9d9ce5ead641183700960d3c9d342/jupyter_client-8.9.1.tar.gz", hash = "sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa", size = 359256, upload-time = "2026-06-09T13:15:01.033Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl", hash = "sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81", size = 109828, upload-time = "2026-06-09T13:14:58.835Z" }, ] [[package]] @@ -950,7 +1155,7 @@ wheels = [ [[package]] name = "jupyter-events" -version = "0.12.0" +version = "0.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonschema", extra = ["format-nongpl"] }, @@ -962,9 +1167,9 @@ dependencies = [ { name = "rfc3986-validator" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196, upload-time = "2025-02-03T17:23:41.485Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/f8/475c4241b2b75af0deaae453ed003c6c851766dbc44d332d8baf245dc931/jupyter_events-0.12.1.tar.gz", hash = "sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3", size = 62854, upload-time = "2026-04-20T23:17:50.66Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430, upload-time = "2025-02-03T17:23:38.643Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6c/6fcde0c8f616ed360ffd3587f7db9e225a7e62b583a04494d2f069cf64ea/jupyter_events-0.12.1-py3-none-any.whl", hash = "sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf", size = 19512, upload-time = "2026-04-20T23:17:48.927Z" }, ] [[package]] @@ -981,7 +1186,7 @@ wheels = [ [[package]] name = "jupyter-server" -version = "2.18.0" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1004,9 +1209,9 @@ dependencies = [ { name = "traitlets" }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/ec/9302cec1ccacdd33c1b1312ac31681c8975cae56c626d783ab49edf9c681/jupyter_server-2.18.0.tar.gz", hash = "sha256:568b27bce4320a53c3eebf1bdcbee9acf48a8ab7f66ec83d900ca9909d4fb770", size = 751152, upload-time = "2026-05-04T13:39:29.685Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/dc/db3a582633170186f8c8b31298d7eb26ad0eb031a1f53476c258b64eed05/jupyter_server-2.20.0.tar.gz", hash = "sha256:b5778ba337d8015a3dc2b80803ecdd5ac18d3797fddf61a50ea5fb472b4ebe14", size = 756523, upload-time = "2026-06-17T12:09:09.435Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/f9/050312d92072ddb9ce14c11171804c07435790c98d4350935a780d9e10c2/jupyter_server-2.18.0-py3-none-any.whl", hash = "sha256:69a5397a039d689da81a45955f9b23e95ee167f6d8a8d64372fb616f2aac650a", size = 391687, upload-time = "2026-05-04T13:39:27.549Z" }, + { url = "https://files.pythonhosted.org/packages/f3/71/8c002223e873a870f5c41dc69b0a7c922301123e4a31d5d01ecb700aef77/jupyter_server-2.20.0-py3-none-any.whl", hash = "sha256:c3b67c93c471e947c18b5026f04f21614218adb706df8f48227d3ee8e0a7cdcc", size = 393143, upload-time = "2026-06-17T12:09:07.234Z" }, ] [[package]] @@ -1024,26 +1229,26 @@ wheels = [ [[package]] name = "jupyterlab" -version = "4.5.7" +version = "4.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, { name = "httpx" }, { name = "ipykernel" }, { name = "jinja2" }, + { name = "jupyter-builder" }, { name = "jupyter-core" }, { name = "jupyter-lsp" }, { name = "jupyter-server" }, { name = "jupyterlab-server" }, { name = "notebook-shim" }, { name = "packaging" }, - { name = "setuptools" }, { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/22/8440ec827762146e7cdecf04335bd348795899d29dc6ae82238707353a2c/jupyterlab-4.5.7.tar.gz", hash = "sha256:55a9822c4754da305f41e113452c68383e214dcf96de760146af89ce5d5117b0", size = 23992763, upload-time = "2026-04-29T16:43:51.328Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/2a/d6af53bfd45a43a5bfe7e40ba47ee7a8921a807daf4bb708e3a295bbb54d/jupyterlab-4.6.1.tar.gz", hash = "sha256:75315982ed28427edaa62bb85eadb5105e4043a757643c910efd787fe6ed0837", size = 28179125, upload-time = "2026-06-29T12:48:45.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/aa/537b8f7d80e799af19af35fb3ddfc970b951088a13c57dd9387dcfbb7f61/jupyterlab-4.5.7-py3-none-any.whl", hash = "sha256:fba4cb0e2c44a52859669d8c98b45de029d5e515f8407bf8534d2a8fc5f0964d", size = 12450123, upload-time = "2026-04-29T16:43:46.639Z" }, + { url = "https://files.pythonhosted.org/packages/5a/81/90ac6cc31d248e83a0d1eab343a5e6e68bc783d3f74fbe61640f42a61da4/jupyterlab-4.6.1-py3-none-any.whl", hash = "sha256:85a58546c831f3dce6cf919468c26874c9065e99c42279fb4abb8e1b552a98bb", size = 17164660, upload-time = "2026-06-29T12:48:41.21Z" }, ] [[package]] @@ -1082,6 +1287,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, ] +[[package]] +name = "keras" +version = "3.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "h5py" }, + { name = "ml-dtypes" }, + { name = "namex" }, + { name = "numpy" }, + { name = "optree" }, + { name = "packaging" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/0a/c2524cda024e5b24038726de899c4b0ae2603dd0324a30cbc129d6b6314e/keras-3.15.0.tar.gz", hash = "sha256:0123749ac2a704d63f691994b18e432e006cb8ae910cc0ea7035d777aed9c00d", size = 1326815, upload-time = "2026-06-24T23:17:12.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/16/d506c09bbff25927a65d13c542cb4eedc2d573e304f38b6ac3f49ed6be9b/keras-3.15.0-py3-none-any.whl", hash = "sha256:9bbd7df12b7b53500d00dc24d97b7e066c771260482cb7118de0b7687a7a2781", size = 1696954, upload-time = "2026-06-24T23:17:10.808Z" }, +] + [[package]] name = "kiwisolver" version = "1.5.0" @@ -1119,6 +1343,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, ] +[[package]] +name = "libclang" +version = "18.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/5c/ca35e19a4f142adffa27e3d652196b7362fa612243e2b916845d801454fc/libclang-18.1.1.tar.gz", hash = "sha256:a1214966d08d73d971287fc3ead8dfaf82eb07fb197680d8b3859dbbbbf78250", size = 39612, upload-time = "2024-03-17T16:04:37.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/49/f5e3e7e1419872b69f6f5e82ba56e33955a74bd537d8a1f5f1eff2f3668a/libclang-18.1.1-1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:0b2e143f0fac830156feb56f9231ff8338c20aecfe72b4ffe96f19e5a1dbb69a", size = 25836045, upload-time = "2024-06-30T17:40:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e5/fc61bbded91a8830ccce94c5294ecd6e88e496cc85f6704bf350c0634b70/libclang-18.1.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6f14c3f194704e5d09769108f03185fce7acaf1d1ae4bbb2f30a72c2400cb7c5", size = 26502641, upload-time = "2024-03-18T15:52:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/1df62b44db2583375f6a8a5e2ca5432bbdc3edb477942b9b7c848c720055/libclang-18.1.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:83ce5045d101b669ac38e6da8e58765f12da2d3aafb3b9b98d88b286a60964d8", size = 26420207, upload-time = "2024-03-17T15:00:26.63Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/716c1e62e512ef1c160e7984a73a5fc7df45166f2ff3f254e71c58076f7c/libclang-18.1.1-py2.py3-none-manylinux2010_x86_64.whl", hash = "sha256:c533091d8a3bbf7460a00cb6c1a71da93bffe148f172c7d03b1c31fbf8aa2a0b", size = 24515943, upload-time = "2024-03-17T16:03:45.942Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3d/f0ac1150280d8d20d059608cf2d5ff61b7c3b7f7bcf9c0f425ab92df769a/libclang-18.1.1-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:54dda940a4a0491a9d1532bf071ea3ef26e6dbaf03b5000ed94dd7174e8f9592", size = 23784972, upload-time = "2024-03-17T16:12:47.677Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2f/d920822c2b1ce9326a4c78c0c2b4aa3fde610c7ee9f631b600acb5376c26/libclang-18.1.1-py2.py3-none-manylinux2014_armv7l.whl", hash = "sha256:cf4a99b05376513717ab5d82a0db832c56ccea4fd61a69dbb7bccf2dfb207dbe", size = 20259606, upload-time = "2024-03-17T16:17:42.437Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c2/de1db8c6d413597076a4259cea409b83459b2db997c003578affdd32bf66/libclang-18.1.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:69f8eb8f65c279e765ffd28aaa7e9e364c776c17618af8bff22a8df58677ff4f", size = 24921494, upload-time = "2024-03-17T16:14:20.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2d/3f480b1e1d31eb3d6de5e3ef641954e5c67430d5ac93b7fa7e07589576c7/libclang-18.1.1-py2.py3-none-win_amd64.whl", hash = "sha256:4dd2d3b82fab35e2bf9ca717d7b63ac990a3519c7e312f19fa8e86dcc712f7fb", size = 26415083, upload-time = "2024-03-17T16:42:21.703Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/e01dc4cc79779cd82d77888a88ae2fa424d93b445ad4f6c02bfc18335b70/libclang-18.1.1-py2.py3-none-win_arm64.whl", hash = "sha256:3f0e1f49f04d3cd198985fea0511576b0aee16f9ff0e0f0cad7f9c57ec3c20e8", size = 22361112, upload-time = "2024-03-17T16:42:59.565Z" }, +] + [[package]] name = "markdown" version = "3.10.2" @@ -1140,6 +1381,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/d7/382f0835d18633e1c45854eb19c066ce47e5694787256d723816a04d3f16/markdown_graphviz_inline-1.1.3-py3-none-any.whl", hash = "sha256:3571f7da594533b6c2c9ef08a6ce582396b64f778e47ac3678f00e75b2652909", size = 3988, upload-time = "2025-01-27T16:58:50.208Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1161,7 +1414,7 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.10.8" +version = "3.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy" }, @@ -1174,30 +1427,30 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/24/080c99d223d158d3a8902769269ab6da5b50f7a0e6e072513907e02b7a6c/matplotlib-3.11.0.tar.gz", hash = "sha256:68c0c7be01b30dcca3638934f7f591df73401235cbdbf0d1ab1c71e7db7f8b57", size = 33251176, upload-time = "2026-06-12T02:29:15.508Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, - { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, - { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, - { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, - { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, - { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a2/78f662f1b18968531f67d3fcde1b7ea8496920bacd4f16ddb5b79d112e46/matplotlib-3.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f857524b442f0f36e641868ce2171aafa88cb0bc0644f4e1d8a5df9b32649fef", size = 9436261, upload-time = "2026-06-12T02:27:34.161Z" }, + { url = "https://files.pythonhosted.org/packages/5e/92/044f1de43901310202f4c79acf4f141be53b2ca8d8380e2fcefb3d523a75/matplotlib-3.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:57baa92fdc82948ed716eae6d2579d4d6f40965cd8d2f416755b4a72580a3233", size = 9264669, upload-time = "2026-06-12T02:27:37.413Z" }, + { url = "https://files.pythonhosted.org/packages/53/f4/f0b4f9ba7ec14a7af8151f3ad71ecfe3561e6ba38cfab1db3681ba4ca112/matplotlib-3.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630eee0e67d35cce2019a0e670719f4816e3b86aff0fa72729f6c69786fceb45", size = 10021076, upload-time = "2026-06-12T02:27:39.926Z" }, + { url = "https://files.pythonhosted.org/packages/d7/33/4d679c6dcd594a156542080ac907ddccf7b09ca11655c4b28eca8e9ee5da/matplotlib-3.11.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5106c444d0bf966eee2853548c03772af4ab7199118e086c62fbac8ccb07c055", size = 10828999, upload-time = "2026-06-12T02:27:42.433Z" }, + { url = "https://files.pythonhosted.org/packages/07/74/0a3683802037d8cd013144d77c247219b47f2aabace6fdde74faa12bacf7/matplotlib-3.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d7aea652b58e686444079be3376ef546bffa1eee9b9bb9c472b9fcf6cf410d3", size = 10913103, upload-time = "2026-06-12T02:27:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/970fcbf381e82ec66fdf5da8ea76e2e9240f61a24011ce9fd1d42c37ac2d/matplotlib-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:70a5b3e9a5dab708c0f039709ae7c68d5b4d254e291ef76492cdba230c8bb5e4", size = 9310945, upload-time = "2026-06-12T02:27:46.867Z" }, + { url = "https://files.pythonhosted.org/packages/14/4e/6e7cfed23611265ded53806852343b5c59339e506e84c474a9b5afc3b249/matplotlib-3.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:3d68266213e73823ac3be90615bab0cf31f88851e114cdb1dd25dacf3b01e1a7", size = 8999304, upload-time = "2026-06-12T02:27:48.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c2/f5da6cd37ed6871f5c9b3c0507ddb69f14d6c36fac4541e4e0c60cb8cdfc/matplotlib-3.11.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81ae77077a1e16d37a5b61096ccb07c8d90a99b518fa8256b8f21578932f2f62", size = 9434094, upload-time = "2026-06-12T02:29:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/f8/07/56f66906e0f87a0c6d0d0acbd34dbc9432b1931d8f26ef618bd6f92932a9/matplotlib-3.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ddef37840695f5eef65f9f070fe2d2f510f584c2156203f9f622a5b0584efffd", size = 9262183, upload-time = "2026-06-12T02:29:11.283Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d8/c4ecab06b7ea36a570c4f3bd2d48d1799fd5d9174470e45c2194199431e7/matplotlib-3.11.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf662e5ac5707658cb931e19972c4bd99f7b4f8b7bf79d3c821d239fa6b71e64", size = 10015653, upload-time = "2026-06-12T02:29:13.251Z" }, ] [[package]] name = "matplotlib-inline" -version = "0.2.1" +version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, ] [[package]] @@ -1209,6 +1462,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "mdx-truly-sane-lists" version = "1.3" @@ -1232,11 +1494,11 @@ wheels = [ [[package]] name = "mistune" -version = "3.2.1" +version = "3.3.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/84/620cc3f7e3adf6f5067e10f4dbae71295d8f9e16d5d3f9ef97c40f2f592c/mistune-3.2.1.tar.gz", hash = "sha256:7c8e5501d38bac1582e067e46c8343f17d57ea1aaa735823f3aba1fd59c88a28", size = 98003, upload-time = "2026-05-03T14:33:22.312Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/a5/2dab368d6950e6808904dec98f54c7e726ee7be4a0c6afe00e6e011bd52d/mistune-3.3.3.tar.gz", hash = "sha256:c4c6c0c840b8637a2e9b8b6d607eb7c8f00888bf14c754409bcd339e848c2477", size = 115363, upload-time = "2026-07-09T06:18:05.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl", hash = "sha256:78cdb0ba5e938053ccf63651b352508d2efa9411dc8810bfb05f2dc5140c0048", size = 53749, upload-time = "2026-05-03T14:33:20.551Z" }, + { url = "https://files.pythonhosted.org/packages/89/70/b1e4737b84163db5bb1dfde6f216dbfbf32783330a9989c965e121172830/mistune-3.3.3-py3-none-any.whl", hash = "sha256:99de1585e42dcbd826faa9e11a202727a5e202e4e4722a4c69ac1ff615793dd7", size = 63569, upload-time = "2026-07-09T06:18:03.839Z" }, ] [[package]] @@ -1291,17 +1553,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, ] +[[package]] +name = "mkdocs-github-admonitions-plugin" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/62/37f2080af26ec1d569bb21eb2a4d54f5ea54d36a92938abbbc37c6ab671b/mkdocs_github_admonitions_plugin-0.1.1.tar.gz", hash = "sha256:7f81520a0681b9955952d73b21ce99b923921830b6b6d1ace9b3fb95cd1fb61f", size = 5151, upload-time = "2025-06-02T09:45:12.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/a3/8cfd5d9a651612b0d2a15176341b040d43d27b063e17684cb353ec7ce789/mkdocs_github_admonitions_plugin-0.1.1-py3-none-any.whl", hash = "sha256:824dc821764171943c1043c88218d4af0329693870ba9be657f890d484a0aa85", size = 5493, upload-time = "2025-06-02T09:45:11.068Z" }, +] + [[package]] name = "mkdocs-include-markdown-plugin" -version = "7.2.2" +version = "7.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/2d/bdf1aee3f4f7b34148b0f62298b62f03415160cb2707f09503c99a0a7cd5/mkdocs_include_markdown_plugin-7.2.2.tar.gz", hash = "sha256:f052ccb741eccf498116b826c1d78a2d761c56747372594709441cee0963fbc9", size = 25415, upload-time = "2026-03-29T15:15:14.2Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/2b/c788fc5c39ccba342eb9067be637327faebbc0e47da41ea79dc2526e693a/mkdocs_include_markdown_plugin-7.3.0.tar.gz", hash = "sha256:2800126746452e31c2e321bbd43c8190b356e0de353e20cbc16a34a3c3d6796c", size = 25527, upload-time = "2026-05-15T18:16:13.946Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/a5/f6b2f0aa805dbda52f6265e9aff1450c8643195442facf29d475bdeba15d/mkdocs_include_markdown_plugin-7.2.2-py3-none-any.whl", hash = "sha256:f2ec4487cf32d3e33ca528f9366f20fb9280ded9c8d1630eb2bbda244962dcd1", size = 29528, upload-time = "2026-03-29T15:15:13.079Z" }, + { url = "https://files.pythonhosted.org/packages/85/84/c0109c5991b89cbf028b8597f73fa7bd6a6b092407be2369a354b52737ab/mkdocs_include_markdown_plugin-7.3.0-py3-none-any.whl", hash = "sha256:5b5c99b5d3c9b9ce0114a9e60353bbafb6be53a26c2d3b74ec6b767a7a8e55ca", size = 29675, upload-time = "2026-05-15T18:16:12.654Z" }, ] [[package]] @@ -1363,7 +1637,7 @@ wheels = [ [[package]] name = "mkdocs-techdocs-core" -version = "1.6.2" +version = "1.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1371,6 +1645,7 @@ dependencies = [ { name = "markdown-graphviz-inline" }, { name = "mdx-truly-sane-lists" }, { name = "mkdocs" }, + { name = "mkdocs-github-admonitions-plugin" }, { name = "mkdocs-material" }, { name = "mkdocs-monorepo-plugin" }, { name = "mkdocs-redirects" }, @@ -1378,14 +1653,14 @@ dependencies = [ { name = "pygments" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/5a/63c03c78dbc69aaf8cae480bea9653902314b21967587a91a9cc5e1113cf/mkdocs_techdocs_core-1.6.2.tar.gz", hash = "sha256:d6d1517341d108fef20da6e9912ff29cea574915654ef29204c156232d58ccc0", size = 13578, upload-time = "2026-04-15T17:18:32.098Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/03/7f11bcb04862275c5b9832356dbb063ea5d9fceb9fac846dd7af53a4b157/mkdocs_techdocs_core-1.7.0.tar.gz", hash = "sha256:2fc8d0a6d1312b19371c9a40bd7e9ffbfb067a659bbba526fefc5e7abe37ae51", size = 13765, upload-time = "2026-06-10T18:10:29.676Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/43/eab9b0da8c16f411600bf2d841b87640651a2b308429a4d98c0afd7b2bfe/mkdocs_techdocs_core-1.6.2-py3-none-any.whl", hash = "sha256:d4f212fa19bb0e85af7689cc4950b0d0ef00e3528cf2d767a435b75615dd0596", size = 14266, upload-time = "2026-04-15T17:18:31.113Z" }, + { url = "https://files.pythonhosted.org/packages/73/e9/20ce8ceaaee5b9c4a844a57f0544d17c05ad5180c12679b55ac370aa2190/mkdocs_techdocs_core-1.7.0-py3-none-any.whl", hash = "sha256:294c41a69b1c95def9534196fe74f0ef0cfd9625a5aef1fd70c0094ef600f9b4", size = 14458, upload-time = "2026-06-10T18:10:28.434Z" }, ] [[package]] name = "mkdocstrings" -version = "1.0.4" +version = "1.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -1395,9 +1670,9 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/8c/05934c546eb38321e5f24e786b24c7a0dc5f2b35968f6e04dce9cfbb738d/mkdocstrings-1.0.5.tar.gz", hash = "sha256:3a9528223afd6bf459ea85541b600ef3542826bbeb4ab534553f4b3c4e6e5e48", size = 100318, upload-time = "2026-07-10T10:49:10.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, + { url = "https://files.pythonhosted.org/packages/a1/82/2a2d44f2c09563d321ab2690835f066267fe000623c90c0caf44e296cbbb/mkdocstrings-1.0.5-py3-none-any.whl", hash = "sha256:150eff6e8c370cc8d2ba4e452ece1275fe83ad34d8cc5884a89a0ed141618c49", size = 35543, upload-time = "2026-07-10T10:49:08.689Z" }, ] [package.optional-dependencies] @@ -1407,16 +1682,16 @@ python = [ [[package]] name = "mkdocstrings-python" -version = "2.0.3" +version = "2.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffelib" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/b6/e858701499d57eee8b3fd8e78168083956c6683ddbe727b46758b19e1119/mkdocstrings_python-2.0.5.tar.gz", hash = "sha256:3a4d92556ad39637e88af94a5374213af9a8e3040c3824ceaed04b486c017594", size = 199578, upload-time = "2026-06-19T10:41:08.868Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl", hash = "sha256:30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d", size = 104951, upload-time = "2026-06-19T10:41:07.378Z" }, ] [[package]] @@ -1444,9 +1719,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "namex" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/c0/ee95b28f029c73f8d49d8f52edaed02a1d4a9acb8b69355737fdb1faa191/namex-0.1.0.tar.gz", hash = "sha256:117f03ccd302cc48e3f5c58a296838f6b89c83455ab8683a1e85f2a430aa4306", size = 6649, upload-time = "2025-05-26T23:17:38.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/bc/465daf1de06409cdd4532082806770ee0d8d7df434da79c76564d0f69741/namex-0.1.0-py3-none-any.whl", hash = "sha256:e2012a474502f1e2251267062aae3114611f07df4224b6e06334c57b0f2ce87c", size = 5905, upload-time = "2025-05-26T23:17:37.695Z" }, +] + [[package]] name = "nbclient" -version = "0.10.4" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-client" }, @@ -1454,9 +1738,9 @@ dependencies = [ { name = "nbformat" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/a5/b3bae4b590c0cbcada2c63a34f7580024e834a8ba213e949a2f906705787/nbclient-0.11.0.tar.gz", hash = "sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a", size = 62535, upload-time = "2026-06-05T07:52:41.746Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, + { url = "https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl", hash = "sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895", size = 25288, upload-time = "2026-06-05T07:52:40.115Z" }, ] [[package]] @@ -1500,12 +1784,12 @@ wheels = [ ] [[package]] -name = "nest-asyncio" -version = "1.6.0" +name = "nest-asyncio2" +version = "1.7.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/73/731debf26e27e0a0323d7bda270dc2f634b398e38f040a09da1f4351d0aa/nest_asyncio2-1.7.2.tar.gz", hash = "sha256:1921d70b92cc4612c374928d081552efb59b83d91b2b789d935c665fa01729a8", size = 14743, upload-time = "2026-02-13T00:34:04.386Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl", hash = "sha256:f5dfa702f3f81f6a03857e9a19e2ba578c0946a4ad417b4c50a24d7ba641fe01", size = 7843, upload-time = "2026-02-13T00:34:02.691Z" }, ] [[package]] @@ -1528,18 +1812,19 @@ wheels = [ [[package]] name = "notebook" -version = "7.5.6" +version = "7.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "jupyter-builder" }, { name = "jupyter-server" }, { name = "jupyterlab" }, { name = "jupyterlab-server" }, { name = "notebook-shim" }, { name = "tornado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/c2/cf59bd2e6f2c8b976b52477e3e53bf6f97bc714ed046a51821afb428eaee/notebook-7.5.6.tar.gz", hash = "sha256:621174aade80108f0020b0f00738000b215f75fa3cd90771ad7aa0f24536a4e1", size = 14170814, upload-time = "2026-04-30T11:46:26.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/44/d5c65783f490298473bb1c05722e05ee2256231389559c2c5ae0a3e5d975/notebook-7.6.0.tar.gz", hash = "sha256:ea13e79e601bf273074895fdfb17dd3f2da916d3c045e0b9c47d18b16ab62481", size = 5497344, upload-time = "2026-06-18T16:18:55.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/d6/1fd0646b9bbd9efbb0b8ae21b2325fbef515769a5621c03e31d8eb8da587/notebook-7.5.6-py3-none-any.whl", hash = "sha256:4dde3f8fb55fa8fb7946d58c6e869ce9baf46d00fc070664f62604569d0faca0", size = 14581730, upload-time = "2026-04-30T11:46:22.342Z" }, + { url = "https://files.pythonhosted.org/packages/93/d1/e617c40db57ff40e75f43a7d4d1c305e3a54c053ab5cb0534a6c314664f9/notebook-7.6.0-py3-none-any.whl", hash = "sha256:98aa2811b54ac191321d5dfce12ca700f8a511a33a26e4de2fa106a357c43d6a", size = 5544575, upload-time = "2026-06-18T16:18:52.551Z" }, ] [[package]] @@ -1556,41 +1841,189 @@ wheels = [ [[package]] name = "numpy" -version = "2.3.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, - { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, - { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, - { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, - { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, - { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, - { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, ] [[package]] name = "numpydantic" -version = "1.8.1" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/28/fb2fa636e777d3a0636697b59e0c3310dbfa5a4126ba3515bb0848e20032/numpydantic-1.8.1.tar.gz", hash = "sha256:e5192efee5784df5cb70f99937e3538fdcbe58ed445c53935f39e5ab2b3f8ea6", size = 82121, upload-time = "2026-04-14T00:01:18.931Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/5c/0cfed22c6483338c73d2faf182d863c47836e5f8f34e65b70aea345e7b72/numpydantic-1.10.0.tar.gz", hash = "sha256:a17d5ccc3b893c4a2e539c81c2f18960d70884ad7fcaa09c9eb56a95e8daf4a3", size = 98771, upload-time = "2026-06-22T22:04:46.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/15/0dd8e1ff306f97bee18a3ef024bb694f9043ee57474c57f7330f90804cae/numpydantic-1.10.0-py3-none-any.whl", hash = "sha256:1c62a445d305da69a5fa4510f7e7e1c25218f58fe462e5d59548042f1f769e66", size = 101858, upload-time = "2026-06-22T22:04:44.918Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.9.2.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/a2/c96163a0fff1839c0c9548bbdeae7b853b867009e33b9b9264adc238b1cf/nvidia_cublas_cu12-12.9.2.10-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:5572131a59c3eebeeb1c4c8144f772d49372c20124916e072a0e3fc30df421d5", size = 575012079, upload-time = "2026-04-08T18:51:47.303Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c0/0a517bfe63ccd3b92eb254d264e28fca3c7cab75d07daea315250fb1bf73/nvidia_cublas_cu12-12.9.2.10-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:e4f53a8ca8c5d6e8c492d0d0a3d565ecb59a751b19cfdaa4f6da0ab2104c1702", size = 581240110, upload-time = "2026-04-08T18:52:31.532Z" }, + { url = "https://files.pythonhosted.org/packages/20/e2/fc9a0e985249d873150276d5afb02e39a66817fedbf1a385724393e505ed/nvidia_cublas_cu12-12.9.2.10-py3-none-win_amd64.whl", hash = "sha256:623f43027d40d44ceadf0043f002bd25cf353e8f13ce90b9a87057019f560661", size = 553162896, upload-time = "2026-04-08T18:53:10.035Z" }, +] + +[[package]] +name = "nvidia-cuda-cccl-cu12" +version = "12.9.27" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/7e/82e49956b046bdc506c789235c587d9b3ef58b8bc1782258c1e247229647/nvidia_cuda_cccl_cu12-12.9.27-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7898b38aa68beaa234d48f0868273702342a196d6e2e9d0ef058dca2390ebea", size = 3152245, upload-time = "2025-05-01T19:32:04.802Z" }, + { url = "https://files.pythonhosted.org/packages/18/2a/d4cd8506d2044e082f8cd921be57392e6a9b5ccd3ffdf050362430a3d5d5/nvidia_cuda_cccl_cu12-12.9.27-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37869e17ce2e1ecec6eddf1927cca0f8c34e64fd848d40453df559091e2d7117", size = 3152243, upload-time = "2025-05-01T19:32:13.955Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9b/1daf405620c7ac371b76b823c6336dd742673d41a150d9a04eec2c690379/nvidia_cuda_cccl_cu12-12.9.27-py3-none-win_amd64.whl", hash = "sha256:72106f95a9bb3be18472806b4f663ebf0f9248a86d14b4ae3305725b855d9d92", size = 3152175, upload-time = "2025-05-01T19:45:11.372Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.9.79" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/78/351b5c8cdbd9a6b4fb0d6ee73fb176dcdc1b6b6ad47c2ffff5ae8ca4a1f7/nvidia_cuda_cupti_cu12-12.9.79-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:791853b030602c6a11d08b5578edfb957cadea06e9d3b26adbf8d036135a4afe", size = 10077166, upload-time = "2025-06-05T20:01:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2e/b84e32197e33f39907b455b83395a017e697c07a449a2b15fd07fc1c9981/nvidia_cuda_cupti_cu12-12.9.79-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:096bcf334f13e1984ba36685ad4c1d6347db214de03dbb6eebb237b41d9d934f", size = 10814997, upload-time = "2025-06-05T20:01:10.168Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b4/298983ab1a83de500f77d0add86d16d63b19d1a82c59f8eaf04f90445703/nvidia_cuda_cupti_cu12-12.9.79-py3-none-win_amd64.whl", hash = "sha256:1848a9380067560d5bee10ed240eecc22991713e672c0515f9c3d9396adf93c8", size = 7730496, upload-time = "2025-06-05T20:11:26.444Z" }, +] + +[[package]] +name = "nvidia-cuda-nvcc-cu12" +version = "12.9.86" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/48/b54a06168a2190572a312bfe4ce443687773eb61367ced31e064953dd2f7/nvidia_cuda_nvcc_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:5d6a0d32fdc7ea39917c20065614ae93add6f577d840233237ff08e9a38f58f0", size = 40546229, upload-time = "2025-06-05T20:01:53.357Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/8cc072436787104bbbcbde1f76ab4a0d89e68f7cebc758dd2ad7913a43d0/nvidia_cuda_nvcc_cu12-12.9.86-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44e1eca4d08926193a558d2434b1bf83d57b4d5743e0c431c0c83d51da1df62b", size = 39411138, upload-time = "2025-06-05T20:01:43.182Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9e/c71c53655a65d7531c89421c282359e2f626838762f1ce6180ea0bbebd29/nvidia_cuda_nvcc_cu12-12.9.86-py3-none-win_amd64.whl", hash = "sha256:8ed7f0b17dea662755395be029376db3b94fed5cbb17c2d35cc866c5b1b84099", size = 34669845, upload-time = "2025-06-05T20:11:56.308Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.9.86" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/85/e4af82cc9202023862090bfca4ea827d533329e925c758f0cde964cb54b7/nvidia_cuda_nvrtc_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:210cf05005a447e29214e9ce50851e83fc5f4358df8b453155d5e1918094dcb4", size = 89568129, upload-time = "2025-06-05T20:02:41.973Z" }, + { url = "https://files.pythonhosted.org/packages/64/eb/c2295044b8f3b3b08860e2f6a912b702fc92568a167259df5dddb78f325e/nvidia_cuda_nvrtc_cu12-12.9.86-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:096d4de6bda726415dfaf3198d4f5c522b8e70139c97feef5cd2ca6d4cd9cead", size = 44528905, upload-time = "2025-06-05T20:02:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/52/de/823919be3b9d0ccbf1f784035423c5f18f4267fb0123558d58b813c6ec86/nvidia_cuda_nvrtc_cu12-12.9.86-py3-none-win_amd64.whl", hash = "sha256:72972ebdcf504d69462d3bcd67e7b81edd25d0fb85a2c46d3ea3517666636349", size = 76408187, upload-time = "2025-06-05T20:12:27.819Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.9.79" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/e0/0279bd94539fda525e0c8538db29b72a5a8495b0c12173113471d28bce78/nvidia_cuda_runtime_cu12-12.9.79-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83469a846206f2a733db0c42e223589ab62fd2fabac4432d2f8802de4bded0a4", size = 3515012, upload-time = "2025-06-05T20:00:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/bc/46/a92db19b8309581092a3add7e6fceb4c301a3fd233969856a8cbf042cd3c/nvidia_cuda_runtime_cu12-12.9.79-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25bba2dfb01d48a9b59ca474a1ac43c6ebf7011f1b0b8cc44f54eb6ac48a96c3", size = 3493179, upload-time = "2025-06-05T20:00:53.735Z" }, + { url = "https://files.pythonhosted.org/packages/59/df/e7c3a360be4f7b93cee39271b792669baeb3846c58a4df6dfcf187a7ffab/nvidia_cuda_runtime_cu12-12.9.79-py3-none-win_amd64.whl", hash = "sha256:8e018af8fa02363876860388bd10ccb89eb9ab8fb0aa749aaf58430a9f7c4891", size = 3591604, upload-time = "2025-06-05T20:11:17.036Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.24.0.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/f1/cd42563325fa827f54ff30da05686c747652bdbd4cb5654cea54d7d0ad4f/nvidia_cudnn_cu12-9.24.0.43-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:a42996943f0cd78ddfd61c8bf59361672a19b63e0491aa22a53d6fe63a3f854a", size = 856490582, upload-time = "2026-07-02T16:21:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/10/13/b8887c869cf2471339a24b60d3c28e761facbb534935f572b61423371abb/nvidia_cudnn_cu12-9.24.0.43-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:f424192dd85e7d29f44be18df2dae4c80d32c67a29c0d42f5c283c40cfdf871c", size = 799083985, upload-time = "2026-07-02T16:25:37.467Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/2c9a2a97a8b3fedcf74a14f38fd5edfae12274380a829fdc6b16ce29be4c/nvidia_cudnn_cu12-9.24.0.43-py3-none-win_amd64.whl", hash = "sha256:cbd41a0ab084422c936dc9fb2fc89be5ea9a85bc421c6f23d0243bdfc945fbef", size = 737103728, upload-time = "2026-07-02T16:30:10.901Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.4.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/2b/76445b0af890da61b501fde30650a1a4bd910607261b209cccb5235d3daa/nvidia_cufft_cu12-11.4.1.4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1a28c9b12260a1aa7a8fd12f5ebd82d027963d635ba82ff39a1acfa7c4c0fbcf", size = 200822453, upload-time = "2025-06-05T20:05:27.889Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/61e6996dd20481ee834f57a8e9dca28b1869366a135e0d42e2aa8493bdd4/nvidia_cufft_cu12-11.4.1.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c67884f2a7d276b4b80eb56a79322a95df592ae5e765cf1243693365ccab4e28", size = 200877592, upload-time = "2025-06-05T20:05:45.862Z" }, + { url = "https://files.pythonhosted.org/packages/20/ee/29955203338515b940bd4f60ffdbc073428f25ef9bfbce44c9a066aedc5c/nvidia_cufft_cu12-11.4.1.4-py3-none-win_amd64.whl", hash = "sha256:8e5bfaac795e93f80611f807d42844e8e27e340e0cde270dcb6c65386d795b80", size = 200067309, upload-time = "2025-06-05T20:13:59.762Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.5.82" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] wheels = [ - { url = "https://files.pythonhosted.org/packages/99/e1/9af63cd9a101ac1c1362200705fb3fa5aabe80a5d63674d0253b18a1072b/numpydantic-1.8.1-py3-none-any.whl", hash = "sha256:f3170b44cdcb168d3799eccbbbe8993f146e90211db9d9903f62b56006e8baca", size = 87926, upload-time = "2026-04-14T00:01:17.388Z" }, + { url = "https://files.pythonhosted.org/packages/03/99/686ff9bf3a82a531c62b1a5c614476e8dfa24a9d89067aeedf3592ee4538/nvidia_cusolver_cu12-11.7.5.82-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:62efa83e4ace59a4c734d052bb72158e888aa7b770e1a5f601682f16fe5b4fd2", size = 337869834, upload-time = "2025-06-05T20:06:53.125Z" }, + { url = "https://files.pythonhosted.org/packages/33/40/79b0c64d44d6c166c0964ec1d803d067f4a145cca23e23925fd351d0e642/nvidia_cusolver_cu12-11.7.5.82-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:15da72d1340d29b5b3cf3fd100e3cd53421dde36002eda6ed93811af63c40d88", size = 338117415, upload-time = "2025-06-05T20:07:16.809Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/feb7f86b809f89b14193beffebe24cf2e4bf7af08372ab8cdd34d19a65a0/nvidia_cusolver_cu12-11.7.5.82-py3-none-win_amd64.whl", hash = "sha256:77666337237716783c6269a658dea310195cddbd80a5b2919b1ba8735cec8efd", size = 326215953, upload-time = "2025-06-05T20:14:41.76Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.10.65" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/6f/8710fbd17cdd1d0fc3fea7d36d5b65ce1933611c31e1861da330206b253a/nvidia_cusparse_cu12-12.5.10.65-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:221c73e7482dd93eda44e65ce567c031c07e2f93f6fa0ecd3ba876a195023e83", size = 366359408, upload-time = "2025-06-05T20:07:42.501Z" }, + { url = "https://files.pythonhosted.org/packages/12/46/b0fd4b04f86577921feb97d8e2cf028afe04f614d17fb5013de9282c9216/nvidia_cusparse_cu12-12.5.10.65-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:73060ce019ac064a057267c585bf1fd5a353734151f87472ff02b2c5c9984e78", size = 366465088, upload-time = "2025-06-05T20:08:20.413Z" }, + { url = "https://files.pythonhosted.org/packages/73/ef/063500c25670fbd1cbb0cd3eb7c8a061585b53adb4dd8bf3492bb49b0df3/nvidia_cusparse_cu12-12.5.10.65-py3-none-win_amd64.whl", hash = "sha256:9e487468a22a1eaf1fbd1d2035936a905feb79c4ce5c2f67626764ee4f90227c", size = 362504719, upload-time = "2025-06-05T20:15:17.947Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.30.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8c/554bb020501d6c04ad8127d83f728137f8f9123f991666efbdcf9095a221/nvidia_nccl_cu12-2.30.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:03ecd776fd1d58fd2c9a0a687dcf8db9ecd0057382dba646fa3d65786d4a9ea1", size = 303277471, upload-time = "2026-06-09T03:24:16.327Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/e7ffa9c324ae260e5dbb4af2cd557bf7a8d155c8ac7b79a785fe1796fb92/nvidia_nccl_cu12-2.30.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:8ce1b8213f61f2bfac132e6df890af6450b77cbd140c6ce4e98cb0c2d8e678c9", size = 303361239, upload-time = "2026-06-09T03:24:53.816Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.9.86" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/0c/c75bbfb967457a0b7670b8ad267bfc4fffdf341c074e0a80db06c24ccfd4/nvidia_nvjitlink_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:e3f1171dbdc83c5932a45f0f4c99180a70de9bd2718c1ab77d14104f6d7147f9", size = 39748338, upload-time = "2025-06-05T20:10:25.613Z" }, + { url = "https://files.pythonhosted.org/packages/97/bc/2dcba8e70cf3115b400fef54f213bcd6715a3195eba000f8330f11e40c45/nvidia_nvjitlink_cu12-12.9.86-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:994a05ef08ef4b0b299829cde613a424382aff7efb08a7172c1fa616cc3af2ca", size = 39514880, upload-time = "2025-06-05T20:10:04.89Z" }, + { url = "https://files.pythonhosted.org/packages/dd/7e/2eecb277d8a98184d881fb98a738363fd4f14577a4d2d7f8264266e82623/nvidia_nvjitlink_cu12-12.9.86-py3-none-win_amd64.whl", hash = "sha256:cc6fcec260ca843c10e34c936921a1c426b351753587fdd638e8cff7b16bb9db", size = 35584936, upload-time = "2025-06-05T20:16:08.525Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-cccl-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/9a/a60b98b629e3b7c4ef6385b08c0e169f2048382c75476f377da54b83a8ca/nvidia_nvshmem_cu12-3.7.1-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7de83f2aa2b58570b4058543fd3c3e992c19f96571380d46ec9b00e0062c6015", size = 229879072, upload-time = "2026-06-30T19:25:39.671Z" }, + { url = "https://files.pythonhosted.org/packages/61/a2/d1d065914f1860782171c6bd2e5eabcbe4488c66352b468569a6b69d4bf6/nvidia_nvshmem_cu12-3.7.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:faca8717c321f088d3c17490fbfc682bac214986e80b97e232c5ae94f900c45c", size = 230087166, upload-time = "2026-06-30T19:26:24.652Z" }, ] [[package]] @@ -1602,6 +2035,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl", hash = "sha256:69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd", size = 71932, upload-time = "2024-09-26T14:33:23.039Z" }, ] +[[package]] +name = "optree" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/63/92328a17ab7836562fe0129e605f685a88db35ce98427c34ff48ee4ec157/optree-0.19.1.tar.gz", hash = "sha256:4497d1c9197b8c6842e511368163d318ce536521ebdcff8bebb7551dcdfac532", size = 177531, upload-time = "2026-05-06T02:32:39.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/f2/4671a78193f96e86c1343fe04324091e163973d0058b292c10bc3387bb70/optree-0.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a496b864fe1fe0b5ea23d1ee3d1ef958d910704661808db2b2d2e16a0cfac96c", size = 414314, upload-time = "2026-05-06T02:30:43.812Z" }, + { url = "https://files.pythonhosted.org/packages/d6/93/7decea24656f416d61fa57b7113b1fbdbc042b7ab421399a84e1755676a1/optree-0.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1667e502e0eda9477925fb17c2ad879b199a2283ac98f18e6453692819b7811", size = 385006, upload-time = "2026-05-06T02:30:44.895Z" }, + { url = "https://files.pythonhosted.org/packages/af/2e/9d1bd2527481681c4399beeeabba11dca36b16ec814579f2e8cc6bc2af96/optree-0.19.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:42e367a9d81e57c31a23247094727987a2f64b708901233a42a24d44d24e93f6", size = 406124, upload-time = "2026-05-06T02:30:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/df/29/cdb40de6307809fa8e9452e4f9a65881a3140d01d9d589a07e9d054d8e1c/optree-0.19.1-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:96fad6c7b3a6fde3a0c8655fd003359cd247f8400749217502591a5ffc328699", size = 466772, upload-time = "2026-05-06T02:30:47.766Z" }, + { url = "https://files.pythonhosted.org/packages/cb/15/4645e1816e815a1306bbb7e3e2e6ba124f6dc325f8088a2db69301219a0c/optree-0.19.1-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f3a900df0ffb9b8259961b337289754531a7e0a5de2f681e9c80866b6a7cb74e", size = 466203, upload-time = "2026-05-06T02:30:49.04Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d2/5758c76bdd7034b721d84c7f0fd911f3b39dcb489eeb27f674aaae8a5f5c/optree-0.19.1-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:27c8dc0f89ade9233aa7ed25ce15991da188e6950eb17cc0c313fc1f327c5b0b", size = 465030, upload-time = "2026-05-06T02:30:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/09/b9/f668bc51129c0fec7728ae8b43180417fe1c1fe99f71d302739f6cc50944/optree-0.19.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:38f2e503fad50aff58cade85db448002d4adc72f4b3b50dcc7f3ef4bcd3b0173", size = 447141, upload-time = "2026-05-06T02:30:51.42Z" }, + { url = "https://files.pythonhosted.org/packages/a4/08/a7b8862e4465bf250c3ccc78db4d10b9a2cf90ce4db3681cbdf7eb076fb7/optree-0.19.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:5dc35cb31540ab6ed9850b0f8865ccd400994ebd51fcf0c156cc772073f43c04", size = 410016, upload-time = "2026-05-06T02:30:52.695Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/04b71a34cf5e663a1df029acceb5efc8a96c8dc4b0b6af6e98486638e913/optree-0.19.1-cp311-cp311-win32.whl", hash = "sha256:d32b1261be71211f77837e839e43a3e3e8fc57707091d2454d0a88590fb6abe8", size = 311810, upload-time = "2026-05-06T02:30:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/22/64/3cc7b08cb1c0f1949895f9490217ca8db6ced7f3bf75c65a5bf31c07bf1e/optree-0.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:cd28a527bb363a1d7d28e8b2fb62816ace6743418bb86e9c5f27ea6877dcdf6c", size = 337620, upload-time = "2026-05-06T02:30:55.262Z" }, + { url = "https://files.pythonhosted.org/packages/6c/14/85f4b05765287658529f09ede10461224161dcf0e29e6fce1ae488451cfe/optree-0.19.1-cp311-cp311-win_arm64.whl", hash = "sha256:7853b58aa084e882ea078f390936bd92e46972eb8f9b5e654360b6480ca7283b", size = 349337, upload-time = "2026-05-06T02:30:56.647Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d4/ffeedc86f8b91e5c17994f38bd1f7aa2e20f9b70a6d3ae906af16414626c/optree-0.19.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3f4f1c276fa06227cdaf58349d22a3231b3dd3d47de1f90a86222ebf831fc397", size = 417543, upload-time = "2026-05-06T02:32:32.592Z" }, + { url = "https://files.pythonhosted.org/packages/52/0b/80fb1b289940e34858cb89f05bc7ce23d6d1272886c2f78bc7e3ab1a306b/optree-0.19.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:77d93eafbd0046c7350bc592ab8e3814abbd39a6d716b5b1e5d652cc486f445c", size = 390184, upload-time = "2026-05-06T02:32:34.273Z" }, + { url = "https://files.pythonhosted.org/packages/fc/67/f31784a7a2dcc0c1f84b691afc552ea5b26db5f56657692a12954a828db4/optree-0.19.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3507ae5db5827eef3da42d04c5a41df649cedc2e42d5d39dc0f869d36915a00b", size = 409025, upload-time = "2026-05-06T02:32:35.817Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a5/647b93eb16244cc7f6dfccc025ac495245e306ff4cb8f9ad15718219141a/optree-0.19.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:732c4581fb666869b8b391ec4ca13d2729795f9abe72b5aec2e582bcbea1975d", size = 449514, upload-time = "2026-05-06T02:32:37.014Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8e/d251c9338771ef0f9db8e538bd77810100c495734b57494464c7e223f0d0/optree-0.19.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e12ee3776a16f6feaa8263b92469ad546b870af71d50602745855d8449219221", size = 341586, upload-time = "2026-05-06T02:32:38.308Z" }, +] + [[package]] name = "orderly-set" version = "5.5.0" @@ -1622,11 +2082,11 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] @@ -1640,9 +2100,10 @@ wheels = [ [[package]] name = "pandapower" -version = "3.4.0" +version = "3.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "colorama" }, { name = "deepdiff" }, { name = "geojson" }, { name = "networkx" }, @@ -1654,9 +2115,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/cd/e7ba8e484505b1af0595b3ce88e12ea32ceea092e483dddc273b0ecd73aa/pandapower-3.4.0.tar.gz", hash = "sha256:bad4e267603fe3ea23e01d9d318d86572ff037cf4968693a851c91b2bb45b8da", size = 5223544, upload-time = "2026-02-09T15:00:09.96Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/ed/ca0d92fc6576fdf53710b99906c6156a26fae5c8d320470aa26be354ddaf/pandapower-3.5.4.tar.gz", hash = "sha256:b5248608038ed13245367d3c4c2cf1cc3a924ad7b93e51946f379335dfd0f314", size = 5319688, upload-time = "2026-07-08T18:05:51.983Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/d0/048318e7fca4a67fdd89c57b2ac02bc8450ad6ca7a7f738968bec0844ae9/pandapower-3.4.0-py3-none-any.whl", hash = "sha256:8ee08fe37039dac0be6c3469cb01e9ea3e90d900f44ee7f65ae45e4beabd3676", size = 5531008, upload-time = "2026-02-09T15:00:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/94f6dc386e4f7d0340a2df93f7c4fa520ce29beea2b9256867d574c6c138/pandapower-3.5.4-py3-none-any.whl", hash = "sha256:d4996221743d990bdb4e98466c5274997d9e1cf39db7a24fbea5849fe17f9536", size = 5635342, upload-time = "2026-07-08T18:05:49.636Z" }, ] [[package]] @@ -1682,7 +2143,7 @@ wheels = [ [[package]] name = "pandera" -version = "0.26.1" +version = "0.32.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, @@ -1691,9 +2152,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspect" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/0b/bb312b98a92b00ff48e869e2769ce5ca6c7bc4ec793a429d450dc3c9bba2/pandera-0.26.1.tar.gz", hash = "sha256:81a55a6429770d31b3bf4c3e8e1096a38296bd3009f9eca5780fad3c3c17fd82", size = 560263, upload-time = "2025-08-26T17:06:30.907Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/74/7ef34611e49990b7e630dcf02c2225d76c2e328ec2804186c2b9c52e916b/pandera-0.32.1.tar.gz", hash = "sha256:72ecd74226847abf0f0437c05f7f10cc8368e306d88a2acc78fa93762c5a0a02", size = 875349, upload-time = "2026-06-29T16:01:48.72Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/3b/91622e08086a6be44d2c0f34947d94c5282b53d217003d3ba390ee2d174b/pandera-0.26.1-py3-none-any.whl", hash = "sha256:1ff5b70556ce2f85c6b27e8fbe835a1761972f4d05f6548b4686b0db26ecb73b", size = 292907, upload-time = "2025-08-26T17:06:29.193Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d0/411c82285a7586e97326020f6b5ecbc2f2ffcbef72aa108c897de1b0a540/pandera-0.32.1-py3-none-any.whl", hash = "sha256:1a17a3ffa906174d19207715f4f082ec3db3709647927ad8c095c147d74d8454", size = 447840, upload-time = "2026-06-29T16:01:46.877Z" }, ] [[package]] @@ -1707,20 +2168,20 @@ wheels = [ [[package]] name = "parso" -version = "0.8.6" +version = "0.8.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, ] [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] @@ -1749,51 +2210,42 @@ wheels = [ [[package]] name = "pillow" -version = "12.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, - { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, - { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, - { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, - { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, - { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, - { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, ] [[package]] name = "plantuml-markdown" -version = "3.11.1" +version = "3.11.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "requests" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f7/c5/1749bae9c838a78d9f6bb0c628fd47251572d5ad1502e33b51fb72582838/plantuml_markdown-3.11.1.tar.gz", hash = "sha256:0ac39c5d13d3ea8b84ec88f57369a85d058ff15c0c225591dd649b327586142c", size = 32120, upload-time = "2025-02-07T17:30:57.131Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/41/807c38a6c703ee9b94be4120102247691aeeb4aefea034eaf687ef515594/plantuml_markdown-3.11.2.tar.gz", hash = "sha256:700a6ebf5c265da488aafa3c376f27e23d604dc4a151ff811fc3aec156ed51a3", size = 32319, upload-time = "2026-04-18T06:09:53.132Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/06/fed3b67b5488854eb7934bfbbb580d0651d8af243ffb39937cb00838bc3d/plantuml_markdown-3.11.1-py3-none-any.whl", hash = "sha256:52c8507be5b4e5b57e3c4c1cfbe8894b6204f4b6bac2279896efb03d3b79bacf", size = 20116, upload-time = "2025-02-07T17:30:54.88Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ca/e0afead2f0466b5af237cf01beaa04b6c7d9479b24ead4ad10cc3c7a4209/plantuml_markdown-3.11.2-py3-none-any.whl", hash = "sha256:45eb97fed669cc23c90593ae1d42122809bfc3693970a6edb0004c0dc20cde27", size = 20158, upload-time = "2026-04-18T06:09:51.581Z" }, ] [[package]] name = "platformdirs" -version = "4.9.6" +version = "4.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, ] [[package]] @@ -1835,14 +2287,14 @@ wheels = [ [[package]] name = "prettytable" -version = "3.17.0" +version = "3.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/45/b0847d88d6cfeb4413566738c8bbf1e1995fad3d42515327ff32cc1eb578/prettytable-3.17.0.tar.gz", hash = "sha256:59f2590776527f3c9e8cf9fe7b66dd215837cca96a9c39567414cbc632e8ddb0", size = 67892, upload-time = "2025-11-14T17:33:20.212Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/74/ba08d81e668ccfe8658d7520a307e63c19862c08eb4ccb26f356c5239a7a/prettytable-3.18.0.tar.gz", hash = "sha256:439217116152244369caf3d9f1caf2f9fe29b03bd79e88d2928c8e718c95d680", size = 76373, upload-time = "2026-06-22T16:07:50.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl", hash = "sha256:aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287", size = 34433, upload-time = "2025-11-14T17:33:19.093Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/2e6798ace5cc036f5d05d36b7b2fd85346f1a708c87060890b070d0ec607/prettytable-3.18.0-py3-none-any.whl", hash = "sha256:b3346e0e6f79180833aebaac088ae926340586cf6d7d991b9eb125b65f72313a", size = 37357, upload-time = "2026-06-22T16:07:48.595Z" }, ] [[package]] @@ -1889,6 +2341,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/4d/fc923f5c85318ee8cc903566dc4e0ebe41b2dfc1d2ecf5546db232397ed6/properdocs-1.6.7-py3-none-any.whl", hash = "sha256:6fa0cfa2e01bf338f684892c8a506cf70ea88ae7f3479c933b6fa20168101cbd", size = 225406, upload-time = "2026-03-20T20:07:46.875Z" }, ] +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + [[package]] name = "psutil" version = "7.2.2" @@ -2028,15 +2495,15 @@ wheels = [ [[package]] name = "pymdown-extensions" -version = "10.21.2" +version = "10.21.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, ] [[package]] @@ -2050,7 +2517,7 @@ wheels = [ [[package]] name = "pypowsybl" -version = "1.14.0" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "networkx" }, @@ -2058,10 +2525,10 @@ dependencies = [ { name = "prettytable" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/95/7c/578f5807067d973ca280cbc2e0a8915968f948435f1f61b7843987816d76/pypowsybl-1.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d99274b3011ec126e9a8829d97d7061b275bc83a8ac705e7a45f9fdcf8d8287b", size = 72030547, upload-time = "2026-01-15T12:34:32.611Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ce/575c3c92553c6cc71d13671ac65f11d3f055d8fbcef657686bfec6bfbf57/pypowsybl-1.14.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:9d40f1da138402c0b6c440a0f81fbfd3946cbc0ae8be9464e9e2989cf9d82f31", size = 73169745, upload-time = "2026-01-15T12:34:53.491Z" }, - { url = "https://files.pythonhosted.org/packages/14/97/7ec85b2a9c92adff0c019c826ceee5e0e24c9c446b0e7d6f0bfb3408a921/pypowsybl-1.14.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:edd2d194f4889550a163d286c03497a57f479babdddc142bc6fc04cd7666f723", size = 87300518, upload-time = "2026-01-15T12:35:16.161Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c8/0d6bba79bf7683331757e9df006edd0bf0ad6719c7460de0c02308fe5146/pypowsybl-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e0d48e886bc7f466de438f9e4b8df9a3233006728de854255769ce9a308ecdbf", size = 68940777, upload-time = "2026-01-15T12:35:38.147Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/1a57e7274c35ad1445f9b6950adaafba328112cfbbaefaf53130879e0661/pypowsybl-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b5878246c4ae8ea020841b2423c957dbe331d9a021f53ed9187f3c7b1f8a4dd2", size = 70071991, upload-time = "2026-04-16T15:29:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/bb/98/aef7c8d4c6ff9bd21d5cfae801cb80b12788613acef6c1f2c27e6e2d9465/pypowsybl-1.15.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:25feaf84790e0d3cc05c1828b2a8c7e3776e842352159ca14654f508141e1de5", size = 71256864, upload-time = "2026-04-16T15:29:24.36Z" }, + { url = "https://files.pythonhosted.org/packages/26/bf/36c0c02c47aa96f983f99a2af6e20145e297d14ca79a5dbfcd08ba91190b/pypowsybl-1.15.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:904660e7cf54ab83a7eee5a4c7400107cc559b08da52920321d27b728adb7b94", size = 84735526, upload-time = "2026-04-16T15:29:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/20/16/cee08cbd7f80ce988ca803cfbbfbd0a042e50b962466bc1549a793747955/pypowsybl-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:979af9fc896ff8cd0e7beff77e58cf517a0e9b03ab897a49299367eba8188ae2", size = 66921392, upload-time = "2026-04-16T15:30:12.46Z" }, ] [[package]] @@ -2078,7 +2545,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2087,9 +2554,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -2170,15 +2637,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.2.2" +version = "1.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/81/58c70036dffeccb7fe7d79d6260c69f7a28272bbd3909c29a01ea9422744/python_discovery-1.4.4.tar.gz", hash = "sha256:5cad33982d412c1f3ffb8f9ca4ea292c9680bca3942451d30b69c37fce53a4a3", size = 72212, upload-time = "2026-07-08T23:06:50.691Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl", hash = "sha256:abebe9120b43453b68c908acfb1e72a19d1a959ed2cb620ad38fc57d08056dbe", size = 34181, upload-time = "2026-07-08T23:06:49.402Z" }, ] [[package]] @@ -2213,21 +2680,21 @@ wheels = [ [[package]] name = "pytz" -version = "2026.1.post1" +version = "2026.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, + { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, ] [[package]] name = "pywinpty" -version = "3.0.3" +version = "3.0.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f7/54/37c7370ba91f579235049dc26cd2c5e657d2a943e01820844ffc81f32176/pywinpty-3.0.3.tar.gz", hash = "sha256:523441dc34d231fb361b4b00f8c99d3f16de02f5005fd544a0183112bcc22412", size = 31309, upload-time = "2026-02-04T21:51:09.524Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/ef/2d27f30c59a67be7025b2d7858c8c2d282b74d66544b2384730b82de74fd/pywinpty-3.0.5.tar.gz", hash = "sha256:61db0db063de9865adbea66db294628f8577f608d9764a4c7d3384eeacc4e81b", size = 16223484, upload-time = "2026-06-11T00:11:58.93Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/c3/3e75075c7f71735f22b66fab0481f2c98e3a4d58cba55cb50ba29114bcf6/pywinpty-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:dff25a9a6435f527d7c65608a7e62783fc12076e7d44487a4911ee91be5a8ac8", size = 2114430, upload-time = "2026-02-04T21:54:19.485Z" }, - { url = "https://files.pythonhosted.org/packages/8d/1e/8a54166a8c5e4f5cb516514bdf4090be4d51a71e8d9f6d98c0aa00fe45d4/pywinpty-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:fbc1e230e5b193eef4431cba3f39996a288f9958f9c9f092c8a961d930ee8f68", size = 236191, upload-time = "2026-02-04T21:50:36.239Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5c/31feb3dd82d1b33ae0bd09ca601edb993d9da1b7f0226b3336d4b4c39e1e/pywinpty-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:af7a8720c78776ddd6259b71dd567944f766a6cd67f8d2887fbc4973967bacda", size = 2092466, upload-time = "2026-06-10T23:44:24.453Z" }, + { url = "https://files.pythonhosted.org/packages/ee/fe/fe23e2229ffec0c10190cef5964f5c9b2dba179d23b69ae537b7ea90bcab/pywinpty-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:c2406f54f699eab75953fb75ce805f2ae55a33a957cd070890abd454fb4b7680", size = 818395, upload-time = "2026-06-10T23:41:56.93Z" }, ] [[package]] @@ -2311,7 +2778,7 @@ wheels = [ [[package]] name = "requests" -version = "2.33.1" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2319,9 +2786,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -2357,39 +2824,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + [[package]] name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, - { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, - { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, - { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, - { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, - { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, - { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, - { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, - { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, - { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, - { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, - { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, - { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, - { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, - { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, - { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, - { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, - { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, - { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, - { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, - { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, ] [[package]] @@ -2403,27 +2883,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, - { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, - { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, - { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, - { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, - { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, - { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, - { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, - { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, - { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, - { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, - { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, - { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, - { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, +version = "0.15.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" }, + { url = "https://files.pythonhosted.org/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" }, + { url = "https://files.pythonhosted.org/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" }, + { url = "https://files.pythonhosted.org/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" }, + { url = "https://files.pythonhosted.org/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" }, + { url = "https://files.pythonhosted.org/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" }, + { url = "https://files.pythonhosted.org/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" }, ] [[package]] @@ -2458,11 +2938,11 @@ wheels = [ [[package]] name = "setuptools" -version = "82.0.1" +version = "83.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, ] [[package]] @@ -2476,11 +2956,11 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8.3" +version = "2.8.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, ] [[package]] @@ -2497,6 +2977,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "tensorboard" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "grpcio" }, + { name = "markdown" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "protobuf" }, + { name = "setuptools" }, + { name = "tensorboard-data-server" }, + { name = "werkzeug" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/4b/cd2eec9642781a8f5b2fb9994e3933a7b259ab18e9d49aeede9b5acf6311/tensorboard-2.21.0-py3-none-any.whl", hash = "sha256:7279316dcb6bd5bc391d623dea841531299cde1887310e8133bc34a996d32255", size = 5516204, upload-time = "2026-06-29T20:48:04.472Z" }, +] + +[[package]] +name = "tensorboard-data-server" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb", size = 2356, upload-time = "2023-10-23T21:23:32.16Z" }, + { url = "https://files.pythonhosted.org/packages/b7/85/dabeaf902892922777492e1d253bb7e1264cadce3cea932f7ff599e53fea/tensorboard_data_server-0.7.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:9fe5d24221b29625dbc7328b0436ca7fc1c23de4acf4d272f1180856e32f9f60", size = 4823598, upload-time = "2023-10-23T21:23:33.714Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363, upload-time = "2023-10-23T21:23:35.583Z" }, +] + +[[package]] +name = "tensorflow" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "astunparse" }, + { name = "flatbuffers" }, + { name = "gast" }, + { name = "google-pasta" }, + { name = "grpcio" }, + { name = "h5py" }, + { name = "keras" }, + { name = "libclang" }, + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "opt-einsum" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "six" }, + { name = "termcolor" }, + { name = "typing-extensions" }, + { name = "wrapt" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/76/b649b02a243c09f0348199dbd81408c1558cfbec2b6d77d820c428a95254/tensorflow-2.21.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:51f46f4806139d01fc7920cc72a4c75876f80649bf40ff083ab6212ceb43ca88", size = 223405849, upload-time = "2026-03-06T17:23:12.976Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7c/57b7ee953b40d5d837136d20af6851b20ea6ca63753fc05699e8bcfab6d5/tensorflow-2.21.0-cp311-cp311-manylinux_2_27_aarch64.whl", hash = "sha256:4b24aca16d527a3cf1c37f720c04c484c3d20f4d0fdfa98632ac8b9f3be6eb5c", size = 281828020, upload-time = "2026-03-06T17:23:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/26/a1/23b9b39c046c723db63a1c2c93397703efa3400a36f98d9c717b473aafb8/tensorflow-2.21.0-cp311-cp311-manylinux_2_27_x86_64.whl", hash = "sha256:9056fbc9ba04235810b71ae6cbd958a196e8804fb53bbcffbf3e23b56155f124", size = 572440367, upload-time = "2026-03-06T17:23:35.279Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a2/6d7e6a738e302530586d484895de2cf3fc158ad9c73b4504a670b2956dd9/tensorflow-2.21.0-cp311-cp311-win_amd64.whl", hash = "sha256:0064a19bdc054a4b7c5e7e21cdf50ce38a114c2bada578b4f5267a37410f6784", size = 350834005, upload-time = "2026-03-06T17:23:53.021Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + [[package]] name = "terminado" version = "0.18.1" @@ -2522,14 +3074,14 @@ wheels = [ [[package]] name = "tinycss2" -version = "1.4.0" +version = "1.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, ] [[package]] @@ -2552,61 +3104,61 @@ wheels = [ [[package]] name = "tornado" -version = "6.5.5" +version = "6.5.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, - { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, - { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, - { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, - { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, - { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, - { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, - { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, + { url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" }, + { url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" }, + { url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" }, + { url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" }, + { url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" }, ] [[package]] name = "tqdm" -version = "4.67.3" +version = "4.68.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/5f/57ff8b434839e70dab45601284ea413e947a63799891b7553e5960a793a8/tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520", size = 792418, upload-time = "2026-07-07T09:58:18.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/5e5e750890ada51017d18d0d4c30da696e5b5bd3180947729927628fc3cb/tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2", size = 676612, upload-time = "2026-07-07T09:58:16.256Z" }, ] [[package]] name = "traitlets" -version = "5.14.3" +version = "5.15.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, + { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, ] [[package]] name = "typeguard" -version = "4.5.1" +version = "4.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/e8/66e25efcc18542d58706ce4e50415710593721aae26e794ab1dec34fb66f/typeguard-4.5.1.tar.gz", hash = "sha256:f6f8ecbbc819c9bc749983cc67c02391e16a9b43b8b27f15dc70ed7c4a007274", size = 80121, upload-time = "2026-02-19T16:09:03.392Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/1c/dfba5c4633cafc4c701f237d2ba63b416805047fd6d96aab4cfc40969f98/typeguard-4.5.2.tar.gz", hash = "sha256:5a16dcac23502039299c97c8941651bc33d7ea8cc4b2f7d6bbb1b528f6eea423", size = 80240, upload-time = "2026-05-14T12:59:40.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl", hash = "sha256:44d2bf329d49a244110a090b55f5f91aa82d9a9834ebfd30bcc73651e4a8cc40", size = 36745, upload-time = "2026-02-19T16:09:01.6Z" }, + { url = "https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl", hash = "sha256:fcf9de18bd945cdb4c7b996e12b4c51ce83f92f191314a6d7cf1739586ec98cf", size = 36748, upload-time = "2026-05-14T12:59:39.473Z" }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] @@ -2636,11 +3188,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2026.1" +version = "2026.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, ] [[package]] @@ -2663,7 +3215,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.2.4" +version = "21.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -2671,9 +3223,9 @@ dependencies = [ { name = "platformdirs" }, { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/98/3a7e644e19cb26133488caff231be390579860bbbb3da35913c49a1d0a46/virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada", size = 5850742, upload-time = "2026-04-14T22:15:31.438Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/65/ec1d92091671e6407d3e7c1f5801413bb7b2b57630a50cae7750456ba0ed/virtualenv-21.6.0.tar.gz", hash = "sha256:e18a4d750f2b64dea73e72ffde3922f3c52365fabdc8157ebd3da20d031c4734", size = 5526111, upload-time = "2026-07-06T22:49:56.972Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e7/2fbd0cc1653c53eed8f10670538bb547de2b3e37aacad283faa82a71094b/virtualenv-21.6.0-py3-none-any.whl", hash = "sha256:bce9d097950fef9d81129b333babfb7767072850c2f1acce0ec536708401bfd1", size = 5506216, upload-time = "2026-07-06T22:49:54.941Z" }, ] [[package]] @@ -2708,23 +3260,23 @@ wheels = [ [[package]] name = "wcmatch" -version = "10.1" +version = "11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bracex" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/25/1da725838132221e33568973da484ff43813662ccc06ebf7f6e3abddfcd5/wcmatch-11.0.tar.gz", hash = "sha256:55d95c2447789712774b198ceec72939e88b5618f1f8f0a9b605bf7740b63b96", size = 141360, upload-time = "2026-07-10T05:50:24.183Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, + { url = "https://files.pythonhosted.org/packages/28/12/f38b6fee116274d7221743caab07d765032e1370bb54cad8714f87aeb0e8/wcmatch-11.0-py3-none-any.whl", hash = "sha256:3a5977ace27e075eef67eb03d539563f1a19018b62881949a42932cf66926934", size = 42914, upload-time = "2026-07-10T05:50:22.995Z" }, ] [[package]] name = "wcwidth" -version = "0.6.0" +version = "0.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, ] [[package]] @@ -2754,6 +3306,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, ] +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +] + +[[package]] +name = "wheel" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854, upload-time = "2026-04-22T15:51:27.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl", hash = "sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced", size = 32218, upload-time = "2026-04-22T15:51:26.296Z" }, +] + [[package]] name = "widgetsnbextension" version = "4.0.15" @@ -2762,3 +3338,23 @@ sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b wheels = [ { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" }, ] + +[[package]] +name = "wrapt" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/a4/282c8e64300a59fc834518a54bf0afabb4ff9218b5fa76958b450459a844/wrapt-2.2.2.tar.gz", hash = "sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302", size = 129068, upload-time = "2026-06-20T23:49:44.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/15/0c2d55168707465abfc41f33c0b23d792a5fa9b65c26983606940900a120/wrapt-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f1a2ff355ece6a111ca7a20dc86df6659c9205d3fcee674ca34f2a2854fd4e73", size = 80782, upload-time = "2026-06-20T23:47:44.367Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b5/5c0b093eb48f8a062ef6267d3cb36e9bb1b88440181f6545a383c60efdf8/wrapt-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55b9a899e6fff5444f229d30aa6e9ac92d2216d9d60f33c771b5d76a760d5f8e", size = 81678, upload-time = "2026-06-20T23:47:45.857Z" }, + { url = "https://files.pythonhosted.org/packages/34/f3/de70937472dd3e8a4e6811192f9c6075efdffd4a2cd9b4596bf160f89668/wrapt-2.2.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a2d78c363f97d8bd718ee40432c66395685e9e98528ccaa423c3355d1715a26d", size = 159671, upload-time = "2026-06-20T23:47:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/40aed2330e7f02ecf74386ffcfef9ccb7108c6a430f15b6a252b663b1bed/wrapt-2.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d619e1eed9bd4f6ed9f24cd61971aa086fa86505289628d464bcf8a2c2e3f328", size = 160785, upload-time = "2026-06-20T23:47:48.759Z" }, + { url = "https://files.pythonhosted.org/packages/45/04/aa5309beed5344b00220ae6b3b24055852192656194c27947bee1736306a/wrapt-2.2.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:518b0c5e323511ec56a38894802ddd5e1222626484e68efe63f201854ad788e5", size = 153699, upload-time = "2026-06-20T23:47:50.177Z" }, + { url = "https://files.pythonhosted.org/packages/01/df/2def7e99d1fe87eea413f95f671924cdddcb08823b1ffd212748dfa6d062/wrapt-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4bccea5cdecffa9dd70e343741f0e41e0a16619313d04b72f78bb525162ebcd0", size = 159695, upload-time = "2026-06-20T23:47:51.602Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f6/a906d01a2ce12157bad2404957b3e2140da354b8a70b2fa48bbf282871c0/wrapt-2.2.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:209112cafd963710a05d199aae431d79a28bc76eb8e6d1bbbb8ad24340722cae", size = 152813, upload-time = "2026-06-20T23:47:53.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/49/bc0086292d239575b4c08f4cf8a4079fa58abbad58ec23abf84833a283ed/wrapt-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5a5290e4bf2f332fc29ce72ffb9a2fff678aaac047e2e9f5f7165cd7792e099", size = 158809, upload-time = "2026-06-20T23:47:54.391Z" }, + { url = "https://files.pythonhosted.org/packages/55/83/8fbd034de1f3e907edaa18786d5dd8f6932874edee0826c7cecb5cab03a1/wrapt-2.2.2-cp311-cp311-win32.whl", hash = "sha256:5499236ad1dc116012e2a5dd943f3f31af12fce452128e2bbcbd55a7d3d4d14c", size = 77414, upload-time = "2026-06-20T23:47:55.882Z" }, + { url = "https://files.pythonhosted.org/packages/7e/9c/23695baa331c6de4e874c3d78b8e0bed92e1d2a274e665b29858f6841672/wrapt-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:8636809939152be6ae20a6cef0fed9fe60f411b47847d0426a826884b469e971", size = 80368, upload-time = "2026-06-20T23:47:57.237Z" }, + { url = "https://files.pythonhosted.org/packages/08/49/40cefc342bf89b234a4490d741290fce781774b831aefb39c25471da96c9/wrapt-2.2.2-cp311-cp311-win_arm64.whl", hash = "sha256:5d0a142f7af07caeb5e5da87493162a7b8efa19ba919e550a746f7446e13fb30", size = 79489, upload-time = "2026-06-20T23:47:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d2/6317eb6d4554855bbf12d61857774af34747bf88a42c19bf306de67e2fa3/wrapt-2.2.2-py3-none-any.whl", hash = "sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048", size = 61460, upload-time = "2026-06-20T23:49:42.966Z" }, +]