powerio

Parse, transform, and emit power system data.

parse returns a module whose value is the typed power system object and whose diagnostics record what the parser found::

import powerio as pio

module = pio.parse("case9.m")
net = module.value
print(net.n_buses, net.base_mva)         # 9 100.0
matpower = pio.emit(module, "matpower")
emitted = pio.emit(module, "psse", "case9.raw")

B = net.calc_bprime_matrix()             # scipy.sparse, MATPOWER Bp
Y = net.calc_admittance_matrix()         # complex csr, G + jB
G = net.to_networkx()                    # networkx.Graph keyed by bus id

PyPSA CSV folders carry static network topology. NetCDF and HDF5 time series are tracked in https://github.com/eigenergy/powerio/issues/107.

A source that defines a calculation parses to that calculation's typed value. Use isinstance(module.value, ...) to branch on the result type.

import powerio and the base parse and emit paths require no third party Python package. Matrix methods require SciPy and NumPy. Graph methods require NetworkX. Install them with powerio[matrix], powerio[graph], or powerio[all]. Missing extras raise ImportError.

   1"""Parse, transform, and emit power system data.
   2
   3``parse`` returns a module whose ``value`` is the typed power system object
   4and whose ``diagnostics`` record what the parser found::
   5
   6    import powerio as pio
   7
   8    module = pio.parse("case9.m")
   9    net = module.value
  10    print(net.n_buses, net.base_mva)         # 9 100.0
  11    matpower = pio.emit(module, "matpower")
  12    emitted = pio.emit(module, "psse", "case9.raw")
  13
  14    B = net.calc_bprime_matrix()             # scipy.sparse, MATPOWER Bp
  15    Y = net.calc_admittance_matrix()         # complex csr, G + jB
  16    G = net.to_networkx()                    # networkx.Graph keyed by bus id
  17
  18PyPSA CSV folders carry static network topology. NetCDF and HDF5 time series
  19are tracked in https://github.com/eigenergy/powerio/issues/107.
  20
  21A source that defines a calculation parses to that calculation's typed value.
  22Use ``isinstance(module.value, ...)`` to branch on the result type.
  23
  24``import powerio`` and the base parse and emit paths require no
  25third party Python package. Matrix methods require SciPy and NumPy. Graph
  26methods require NetworkX. Install them with ``powerio[matrix]``,
  27``powerio[graph]``, or ``powerio[all]``. Missing extras raise ``ImportError``.
  28"""
  29
  30from __future__ import annotations
  31
  32import importlib
  33import io as _io
  34import json as _json
  35import operator as _operator
  36import os as _os
  37from collections import namedtuple
  38from collections.abc import Mapping, Sequence
  39from dataclasses import dataclass
  40from typing import Any, Iterable, Optional, Union
  41
  42from . import _powerio
  43from ._guard import guard as _guard
  44from ._guard import guard_class as _guard_class
  45from ._powerio import (
  46    ActivePower,
  47    ApparentPower,
  48    CalculationUpdate,
  49    ComponentId,
  50    Diagnostic,
  51    NetworkUpdate,
  52    OperatingPointUpdate,
  53    PowerIODataError,
  54    PowerIOError,
  55    PowerIOParseError,
  56    ReactivePower,
  57    Residuals,
  58    ScucActiveReserveZone,
  59    ScucBranchSwitchingCost,
  60    ScucContingency,
  61    ScucDevice,
  62    ScucDeviceOutputs,
  63    ScucDevicePeriod,
  64    ScucEnergyCostBlock,
  65    ScucEnergyRequirement,
  66    ScucInitialCommitment,
  67    ScucInputs,
  68    ScucNetworkOutputs,
  69    ScucRampLimits,
  70    ScucReactiveCapability,
  71    ScucReactiveReserveZone,
  72    ScucReserveCosts,
  73    ScucReserveLimits,
  74    ScucShunt,
  75    ScucStartupCostAdjustment,
  76    ScucStartupLimit,
  77    ScucTransformerControl,
  78    ScucViolationCosts,
  79    SourceSpan,
  80    UpdateChange,
  81    UpdateReport,
  82    __version__,
  83)
  84
  85__all__ = [
  86    "AcOpfInstance",
  87    "AcOpfSolution",
  88    "AcPfInstance",
  89    "AcPfSolution",
  90    "AcScucInstance",
  91    "AcScucSolution",
  92    "ActivePower",
  93    "ApparentPower",
  94    "Artifact",
  95    "BalancedNetwork",
  96    "CalculationUpdate",
  97    "ComponentId",
  98    "DcOpfInstance",
  99    "DcOpfSolution",
 100    "DcPfInstance",
 101    "DcPfSolution",
 102    "Diagnostic",
 103    "DisplayData",
 104    "EmitResult",
 105    "FormatInfo",
 106    "GeoLayer",
 107    "McAcOpfInstance",
 108    "McAcOpfSolution",
 109    "McAcPfInstance",
 110    "McAcPfSolution",
 111    "MulticonductorNetwork",
 112    "NetworkUpdate",
 113    "OperatingPoint",
 114    "OperatingPointUpdate",
 115    "PioModule",
 116    "PowerIODataError",
 117    "PowerIOError",
 118    "PowerIOParseError",
 119    "PwdDisplay",
 120    "PwdSubstation",
 121    "ReactivePower",
 122    "Residuals",
 123    "Scenario",
 124    "ScenarioSet",
 125    "ScucActiveReserveZone",
 126    "ScucBranchSwitchingCost",
 127    "ScucContingency",
 128    "ScucDevice",
 129    "ScucDeviceOutputs",
 130    "ScucDevicePeriod",
 131    "ScucEnergyCostBlock",
 132    "ScucEnergyRequirement",
 133    "ScucInitialCommitment",
 134    "ScucInputs",
 135    "ScucNetworkOutputs",
 136    "ScucRampLimits",
 137    "ScucReactiveCapability",
 138    "ScucReactiveReserveZone",
 139    "ScucReserveCosts",
 140    "ScucReserveLimits",
 141    "ScucShunt",
 142    "ScucStartupCostAdjustment",
 143    "ScucStartupLimit",
 144    "ScucTransformerControl",
 145    "ScucViolationCosts",
 146    "SocwrOpfSolution",
 147    "SourceSpan",
 148    "TimePoint",
 149    "TimeSeries",
 150    "UpdateChange",
 151    "UpdateReport",
 152    "__version__",
 153    "apply_bus_load_active_power",
 154    "apply_updates",
 155    "deserialize",
 156    "dist",
 157    "emit",
 158    "features",
 159    "from_ppc",
 160    "parse",
 161    "parse_display",
 162    "parse_geo",
 163    "resolve_format",
 164    "serialize",
 165    "versions",
 166]
 167
 168@dataclass(frozen=True)
 169class Artifact:
 170    """One artifact produced by :func:`emit` or :func:`serialize`.
 171
 172    ``data`` is set for an in-memory result. ``path`` is set after committing
 173    to a filesystem destination.
 174    """
 175
 176    name: str
 177    data: Optional[bytes]
 178    path: Optional[str]
 179
 180    @property
 181    def text(self) -> str:
 182        """Decode an in-memory UTF-8 artifact."""
 183        if self.data is None:
 184            raise ValueError("this artifact was committed to a destination")
 185        return self.data.decode("utf-8")
 186
 187
 188@dataclass(frozen=True)
 189class EmitResult:
 190    """Artifact inventory and diagnostics from an emission or serialization."""
 191
 192    artifacts: tuple[Artifact, ...]
 193    layout: str
 194    fidelity: str
 195    diagnostics: tuple[Diagnostic, ...]
 196
 197    @property
 198    def text(self) -> Optional[str]:
 199        """The sole UTF-8 memory artifact, or ``None`` for other inventories."""
 200        if len(self.artifacts) != 1 or self.artifacts[0].data is None:
 201            return None
 202        return self.artifacts[0].text
 203
 204FormatInfo = namedtuple(
 205    "FormatInfo", ["token", "extension", "is_directory", "can_emit"]
 206)
 207FormatInfo.__doc__ = """Canonical metadata returned by :func:`resolve_format`.
 208
 209``extension`` is the conventional filename suffix without a leading dot; it
 210can be compound and is ``None`` when a directory format has no primary case
 211file. ``can_emit`` reports whether a fresh universal emitter exists for the
 212format. It is not a promise for every concrete module value or a feature probe. A
 213false value neither promises nor forbids a same format retained source echo.
 214"""
 215
 216DisplayData = namedtuple("DisplayData", ["kind", "data"])
 217DisplayData.__doc__ = """Output of :func:`parse_display`.
 218
 219``kind`` names the display format. For PowerWorld PWD data,
 220``kind == "powerworld"`` and
 221``data`` is a :class:`PwdDisplay`.
 222"""
 223
 224PwdDisplay = namedtuple(
 225    "PwdDisplay", ["canvas_width", "canvas_height", "stamp", "substations"]
 226)
 227PwdDisplay.__doc__ = """Decoded PowerWorld ``.pwd`` display metadata."""
 228
 229PwdSubstation = namedtuple("PwdSubstation", ["number", "name", "x", "y"])
 230PwdSubstation.__doc__ = """One decoded PowerWorld display substation."""
 231
 232def _require(module: str, extra: str):
 233    """Import ``module`` or raise a clear ImportError naming the extra to install."""
 234    try:
 235        return importlib.import_module(module)
 236    except ImportError as exc:
 237        # Only rewrite "module is absent". A present-but-broken install (e.g. a
 238        # failed C-extension load) raises ImportError from a sub-import; let its
 239        # own traceback through instead of misdirecting the user to reinstall.
 240        if getattr(exc, "name", None) not in (module, module.split(".")[0]):
 241            raise
 242        raise ImportError(
 243            f"powerio needs {module!r} for this call; install it with "
 244            f"`pip install 'powerio[{extra}]'`"
 245        ) from exc
 246
 247
 248def _to_csr(coo):
 249    """Assemble a ``(data, row, col, shape)`` COO tuple into a csr_matrix."""
 250    sparse = _require("scipy.sparse", "matrix")
 251    data, row, col, shape = coo
 252    return sparse.coo_matrix((data, (row, col)), shape=shape).tocsr()
 253
 254
 255def _dc_angles(n_buses: int, voltage_angles):
 256    np = _require("numpy", "matrix")
 257    angles = np.asarray(voltage_angles, dtype=float)
 258    if angles.ndim != 1 or angles.shape[0] != n_buses:
 259        raise ValueError(
 260            f"voltage_angles must be a one dimensional array of length {n_buses}"
 261        )
 262    return np, angles
 263
 264
 265def _wrap_display(raw) -> DisplayData:
 266    kind, payload = raw
 267    if kind == "powerworld":
 268        substations = [
 269            PwdSubstation(
 270                row["number"],
 271                row["name"],
 272                row["x"],
 273                row["y"],
 274            )
 275            for row in payload["substations"]
 276        ]
 277        payload = PwdDisplay(
 278            payload["canvas_width"],
 279            payload["canvas_height"],
 280            payload["stamp"],
 281            substations,
 282        )
 283    return DisplayData(kind, payload)
 284
 285
 286_BALANCED_DELEGATED_NAMES = frozenset(
 287    {
 288        "areas",
 289        "base_frequency",
 290        "base_mva",
 291        "branches",
 292        "buses",
 293        "detailed_connectivity",
 294        "generators",
 295        "hvdc",
 296        "is_radial",
 297        "loads",
 298        "n_areas",
 299        "n_branches",
 300        "n_buses",
 301        "n_generators",
 302        "n_hvdc",
 303        "n_islands",
 304        "n_loads",
 305        "n_shunts",
 306        "n_static_var_compensators",
 307        "n_storage",
 308        "n_switches",
 309        "n_transformers_3w",
 310        "name",
 311        "reference_bus_index",
 312        "reference_bus_indices",
 313        "shunts",
 314        "static_var_compensators",
 315        "source_format",
 316        "storage",
 317        "switches",
 318        "transformers_3w",
 319    }
 320)
 321
 322
 323@_guard_class
 324class BalancedNetwork:
 325    """A parsed balanced power network.
 326
 327    The data attributes (``buses``, ``branches``, ``generators``, ``loads``,
 328    ``shunts``) and reference bus queries delegate to the compiled handle; the
 329    matrix methods below return ``scipy.sparse`` objects. Parse and transform
 330    diagnostics belong to the owning :class:`PioModule`.
 331
 332    Errors: a bad file path raises the standard ``OSError`` subclass
 333    (``FileNotFoundError``); a malformed case raises :class:`PowerIOParseError`
 334    and an unmet calculation precondition (no generators, no reference bus) raises
 335    :class:`PowerIODataError`; both subclass :class:`PowerIOError`, so
 336    ``except PowerIOError`` catches either; an unknown
 337    ``scheme``/``formula``/``units`` string raises ``ValueError``.
 338    """
 339
 340    def __init__(self, inner: "_powerio._BalancedNetwork"):
 341        self._inner = inner
 342
 343    def __dir__(self):
 344        # The data attributes arrive through __getattr__, so name them here or
 345        # they stay invisible to tab completion.
 346        return sorted(set(super().__dir__()) | _BALANCED_DELEGATED_NAMES)
 347
 348    def __getattr__(self, name: str):
 349        # Reached only when normal lookup misses, so the matrix methods below
 350        # win. Guard underscore names so a lookup before _inner exists raises
 351        # AttributeError instead of recursing forever.
 352        if name not in _BALANCED_DELEGATED_NAMES:
 353            raise AttributeError(
 354                f"{type(self).__name__!r} object has no attribute {name!r}"
 355            )
 356        return getattr(self._inner, name)
 357
 358    def __repr__(self) -> str:
 359        # The inner handle's __repr__ already renders the public ``BalancedNetwork(...)``
 360        # form, so this is a straight delegate.
 361        return repr(self._inner)
 362
 363    def calc_connectivity_report(self) -> dict[str, Any]:
 364        """Calculate the in-service topology summary."""
 365        return self._inner.calc_connectivity_report()
 366
 367    def to_geo_layer(self) -> dict[str, Any]:
 368        """Transform coordinates to a canonical GeoJSON FeatureCollection.
 369
 370        A case without coordinates produces an empty feature collection.
 371        """
 372        return _json.loads(self._inner.to_geo_layer_json())
 373
 374    def apply_geo_layer(
 375        self, text: str, name_hint: Optional[str] = None
 376    ) -> tuple["BalancedNetwork", dict[str, Any]]:
 377        """Apply a geographic sidecar and return ``(placed, report)``.
 378
 379        ``text`` is any form :func:`parse_geo` accepts; this case is
 380        unchanged. The report carries ``matched_buses``, ``matched_branches``,
 381        ``unmatched_features``, ``unlocated_buses``, ``unlocated_branches``,
 382        and ``notes``. The two unlocated counts cover the whole case when the
 383        pass ends, so a layer that matched nothing reads apart from a case
 384        that needed nothing. The placed copy drops the retained source text,
 385        so a same-format emission re-serializes.
 386        """
 387        inner, report = self._inner.apply_geo_layer(text, name_hint)
 388        return BalancedNetwork(inner), report
 389
 390    # --- matrix calculations (scipy.sparse) -----------------------------
 391
 392    def calc_bprime_matrix(
 393        self, scheme: str = "bx", *, skip_zero_impedance: bool = False
 394    ):
 395        """MATPOWER FDPF Bp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.
 396
 397        ``skip_zero_impedance=False`` refuses a zero impedance branch
 398        (``r`` and ``x`` both zero); pass ``True`` to drop it instead.
 399        """
 400        return _to_csr(
 401            self._inner.bprime(scheme, skip_zero_impedance=skip_zero_impedance)
 402        )
 403
 404    def calc_incidence_matrix(self, formula: str = "series_susceptance"):
 405        """Return PowerModels incidence ``A`` (branches by buses)."""
 406        return _to_csr(self._inner.calc_incidence_matrix(formula))
 407
 408    def calc_branch_susceptances(self, formula: str = "series_susceptance"):
 409        """Return per branch susceptances in active branch order."""
 410        np = _require("numpy", "matrix")
 411        return np.asarray(self._inner.calc_branch_susceptances(formula), dtype=float)
 412
 413    def calc_branch_flow_matrix(self, formula: str = "series_susceptance"):
 414        """Return ``Bf = diag(b) A`` as a CSR matrix."""
 415        return _to_csr(self._inner.calc_branch_flow_matrix(formula))
 416
 417    def calc_bus_susceptance_matrix(self, formula: str = "series_susceptance"):
 418        """Return ``B = A.T diag(b) A`` as a CSR matrix."""
 419        return _to_csr(self._inner.calc_bus_susceptance_matrix(formula))
 420
 421    def calc_branch_phase_shift_injection(
 422        self, formula: str = "series_susceptance"
 423    ):
 424        """Return ``b * shift`` in active branch order."""
 425        np = _require("numpy", "matrix")
 426        return np.asarray(
 427            self._inner.calc_branch_phase_shift_injection(formula), dtype=float
 428        )
 429
 430    def calc_bus_phase_shift_injection(self, formula: str = "series_susceptance"):
 431        """Return ``A.T @ (b * shift)`` in bus order."""
 432        np = _require("numpy", "matrix")
 433        return np.asarray(
 434            self._inner.calc_bus_phase_shift_injection(formula), dtype=float
 435        )
 436
 437    def calc_branch_flow_dc(self, voltage_angles, formula: str = "series_susceptance"):
 438        """Compute ``-Bf @ va + b * shift`` in active branch order."""
 439        np, angles = _dc_angles(self.n_buses, voltage_angles)
 440        return np.asarray(
 441            self._inner.calc_branch_flow_dc(angles.tolist(), formula), dtype=float
 442        )
 443
 444    def calc_bus_injection_dc(
 445        self, voltage_angles, formula: str = "series_susceptance"
 446    ):
 447        """Compute ``-B @ va + p_shift`` in bus order."""
 448        np, angles = _dc_angles(self.n_buses, voltage_angles)
 449        return np.asarray(
 450            self._inner.calc_bus_injection_dc(angles.tolist(), formula), dtype=float
 451        )
 452
 453    def calc_bdoubleprime_matrix(
 454        self, scheme: str = "bx", *, skip_zero_impedance: bool = False
 455    ):
 456        """MATPOWER FDPF Bpp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.
 457        ``skip_zero_impedance`` as in :meth:`calc_bprime_matrix`.
 458        """
 459        return _to_csr(
 460            self._inner.bdoubleprime(scheme, skip_zero_impedance=skip_zero_impedance)
 461        )
 462
 463    def calc_lacpf_matrix(
 464        self,
 465        *,
 466        include_taps: bool = True,
 467        include_shifts: bool = True,
 468        skip_zero_impedance: bool = False,
 469    ):
 470        """LACPF 2n×2n block ``[[G, -B], [-B, -G]]``. ``skip_zero_impedance``
 471        as in :meth:`calc_bprime_matrix`."""
 472        return _to_csr(
 473            self._inner.lacpf(
 474                include_taps=include_taps,
 475                include_shifts=include_shifts,
 476                skip_zero_impedance=skip_zero_impedance,
 477            )
 478        )
 479
 480    def calc_adjacency_matrix(self):
 481        """0/1 bus adjacency matrix."""
 482        return _to_csr(self._inner.adjacency())
 483
 484    def calc_admittance_matrix(
 485        self,
 486        *,
 487        include_taps: bool = True,
 488        include_shifts: bool = True,
 489        skip_zero_impedance: bool = False,
 490    ):
 491        """``Y_bus = G + jB`` as a complex csr_matrix. ``skip_zero_impedance``
 492        as in :meth:`calc_bprime_matrix`."""
 493        g, b = self._inner.ybus_parts(
 494            include_taps=include_taps,
 495            include_shifts=include_shifts,
 496            skip_zero_impedance=skip_zero_impedance,
 497        )
 498        g, b = _to_csr(g), _to_csr(b)
 499        return (g + 1j * b).tocsr()
 500
 501    def calc_ptdf(self, formula: str = "series_susceptance", solver: str = "auto"):
 502        """DC PTDF (m×n). ``formula`` is ``"series_susceptance"``,
 503        ``"tap_adjusted_reactance"``, or ``"reactance_only"``.
 504
 505        ``solver`` is ``"auto"``, ``"dense"``, or ``"sparse"``. ``"auto"``
 506        uses the dense factorization on small cases and the sparse Cholesky
 507        path on large ones, the same policy as the CLI.
 508        """
 509        return _to_csr(self._inner.ptdf(formula, solver))
 510
 511    def calc_lodf(self, formula: str = "series_susceptance", solver: str = "auto"):
 512        """DC LODF (m×m). ``formula`` and ``solver`` as in :meth:`calc_ptdf`."""
 513        return _to_csr(self._inner.lodf(formula, solver))
 514
 515    def calc_weighted_laplacian(
 516        self,
 517        formula: str = "series_susceptance",
 518    ):
 519        """Weighted Laplacian ``L = -B``. ``formula`` as in :meth:`calc_ptdf`."""
 520        return _to_csr(self._inner.weighted_laplacian(formula))
 521
 522    def to_normalized(
 523        self,
 524        *,
 525        clamp_angle_bounds: bool = False,
 526        angle_bound_pad: Optional[float] = None,
 527    ) -> "BalancedNetwork":
 528        """Return a normalized copy with per unit power and radian angles.
 529
 530        The result removes out of service elements, preserves source bus IDs,
 531        and normalizes bus types. It carries no retained source, so
 532        :func:`powerio.emit` produces a grid exchange representation from the
 533        derived module. Raises
 534        :class:`PowerIODataError` if the network cannot be
 535        normalized (no reference bus can be chosen, or a non-positive base MVA).
 536
 537        ``clamp_angle_bounds=True`` applies the PowerModels angle difference
 538        bound repair: limits at or beyond ``+/-pi/2`` and zero/zero windows
 539        become ``[-angle_bound_pad, angle_bound_pad]``. A repair that would
 540        invert the interval widens to that same window. The default pad is
 541        1.0472 radians.
 542        """
 543        if not clamp_angle_bounds and angle_bound_pad is None:
 544            return BalancedNetwork(self._inner.to_normalized())
 545        return BalancedNetwork(
 546            self._inner.to_normalized_with_options(
 547                clamp_angle_bounds=clamp_angle_bounds, angle_bound_pad=angle_bound_pad
 548            )
 549        )
 550
 551    def to_ppc(self):
 552        """PYPOWER case dict (``ppc``) with MATPOWER-style numpy tables.
 553
 554        Values are emitted as the model holds them, so a case read from a
 555        file carries MW, MVAr, and degrees. A network from
 556        :meth:`to_normalized` holds per unit and radians, and those are what
 557        its tables carry — PYPOWER reads a ppc dict as MW and degrees, so
 558        build this from the raw network unless the consumer expects per unit.
 559
 560        Loads and shunts are summed onto their bus in the
 561        ``PD``/``QD``/``GS``/``BS`` columns, the same aggregation as the
 562        MATPOWER emitter. The bus table has no per element status
 563        column, so an element the model marks out of service still
 564        contributes its value, and a de-energized bus is carried as type 4.
 565        ``gencost`` is present only when every generator carries cost data,
 566        because MATPOWER requires cost rows for all generators or none.
 567        :func:`from_ppc` reads the tables back.
 568        """
 569        np = _require("numpy", "matrix")
 570        buses = self._inner.buses
 571        bus = np.array(
 572            [
 573                (
 574                    b["id"],
 575                    _PPC_BUS_TYPE.get(b["kind"], 1.0),
 576                    0.0,
 577                    0.0,
 578                    0.0,
 579                    0.0,
 580                    b["area"],
 581                    b["vm"],
 582                    b["va"],
 583                    b["base_kv"],
 584                    b["zone"],
 585                    b["vmax"],
 586                    b["vmin"],
 587                )
 588                for b in buses
 589            ],
 590            dtype=float,
 591        ).reshape(len(buses), 13)
 592        bus[:, 2], bus[:, 3], bus[:, 4], bus[:, 5] = _bus_sums(
 593            np, buses, self._inner.loads, self._inner.shunts
 594        )
 595
 596        # The capability and ramp columns past PMIN are an OPF extension that a
 597        # source need not carry. Widen to the full 21 only when a generator
 598        # actually states one: a table of zeros there reads back as eleven
 599        # explicit zero limits, which a ramp aware solver takes as a generator
 600        # that cannot move.
 601        gens = self._inner.generators
 602        caps = [g["caps"] for g in gens]
 603        width = 21 if any(c is not None for row in caps for c in row) else 10
 604        gen = np.array(
 605            [
 606                [
 607                    g["bus"],
 608                    g["pg"],
 609                    g["qg"],
 610                    g["qmax"],
 611                    g["qmin"],
 612                    g["vg"],
 613                    g["mbase"],
 614                    float(g["in_service"]),
 615                    g["pmax"],
 616                    g["pmin"],
 617                ]
 618                + ([0.0 if c is None else c for c in row] if width == 21 else [])
 619                for g, row in zip(gens, caps)
 620            ],
 621            dtype=float,
 622        ).reshape(len(gens), width)
 623
 624        branches = self._inner.branches
 625        branch = np.array(
 626            [
 627                (
 628                    br["from_id"],
 629                    br["to_id"],
 630                    br["r"],
 631                    br["x"],
 632                    br["b"],
 633                    br["rate_a"],
 634                    br["rate_b"],
 635                    br["rate_c"],
 636                    br["tap"],
 637                    br["shift"],
 638                    float(br["in_service"]),
 639                    br["angmin"],
 640                    br["angmax"],
 641                )
 642                for br in branches
 643            ],
 644            dtype=float,
 645        ).reshape(len(branches), 13)
 646
 647        ppc = {
 648            "version": "2",
 649            "baseMVA": float(self._inner.base_mva),
 650            "bus": bus,
 651            "gen": gen,
 652            "branch": branch,
 653        }
 654
 655        # Coefficients sit left-aligned after ncost, padded to the widest
 656        # row, which is the layout PYPOWER's own loadcase produces.
 657        costs = [g["cost"] for g in gens]
 658        if costs and all(c is not None for c in costs):
 659            gencost = np.zeros((len(costs), 4 + max(len(c["coeffs"]) for c in costs)))
 660            for i, c in enumerate(costs):
 661                gencost[i, :4] = (
 662                    c["model"],
 663                    c["startup"],
 664                    c["shutdown"],
 665                    c["ncost"],
 666                )
 667                gencost[i, 4 : 4 + len(c["coeffs"])] = c["coeffs"]
 668            ppc["gencost"] = gencost
 669        return ppc
 670
 671    def to_networkx(self):
 672        """Undirected networkx graph keyed by bus id.
 673
 674        In-service branches become edges carrying ``branch`` (index), ``r``,
 675        ``x``, and ``b``.
 676        """
 677        nx = _require("networkx", "graph")
 678        g = nx.Graph()
 679        g.add_nodes_from(bus["id"] for bus in self._inner.buses)
 680        for k, br in enumerate(self._inner.branches):
 681            if br["in_service"]:
 682                g.add_edge(
 683                    br["from_id"],
 684                    br["to_id"],
 685                    branch=k,
 686                    r=br["r"],
 687                    x=br["x"],
 688                    b=br["b"],
 689                )
 690        return g
 691
 692
 693@_guard
 694def parse_display(path: Any, format: Optional[str] = None) -> DisplayData:
 695    """Parse a display artifact such as a PowerWorld ``.pwd`` file."""
 696    return _wrap_display(_powerio.parse_display(str(path), format))
 697
 698
 699@_guard
 700def resolve_format(name: str) -> Optional[FormatInfo]:
 701    """Resolve a format token or common alias to its canonical metadata."""
 702    resolved = _powerio.resolve_format(name)
 703    return None if resolved is None else FormatInfo(*resolved)
 704
 705
 706@_guard
 707def parse_geo(text: str, name_hint: Optional[str] = None) -> dict[str, Any]:
 708    """Tolerantly read a geographic sidecar and return its canonical form.
 709
 710    Accepts headerless buscoords CSV, aliased CSV/JSON records, and GeoJSON
 711    Point/LineString features. Returns ``{"geojson": <FeatureCollection dict>,
 712    "diagnostics": [...]}``; ``name_hint`` (a file name) picks CSV against JSON
 713    when the content alone is ambiguous. Input with no usable coordinates
 714    raises :class:`PowerIOParseError`.
 715    """
 716    parsed = _powerio.parse_geo(text, name_hint)
 717    parsed["geojson"] = _json.loads(parsed["geojson"])
 718    return parsed
 719
 720
 721# powerio bus kind -> MATPOWER/PYPOWER BUS_TYPE code.
 722def _bus_sums(np, buses, loads, shunts):
 723    """Per bus `(pd, qd, gs, bs)` in bus order.
 724
 725    :meth:`BalancedNetwork.to_ppc` folds the element
 726    tables onto their bus the way the Rust indexed analysis view does. This is
 727    that fold, once.
 728    """
 729    row_of = {b["id"]: i for i, b in enumerate(buses)}
 730    pd, qd, gs, bs = (np.zeros(len(buses), dtype=float) for _ in range(4))
 731    for load in loads:
 732        i = row_of.get(load["bus"])
 733        if i is not None:
 734            pd[i] += load["p"]
 735            qd[i] += load["q"]
 736    for shunt in shunts:
 737        i = row_of.get(shunt["bus"])
 738        if i is not None:
 739            gs[i] += shunt["g"]
 740            bs[i] += shunt["b"]
 741    return pd, qd, gs, bs
 742
 743
 744_PPC_BUS_TYPE = {"PQ": 1.0, "PV": 2.0, "REF": 3.0, "ISOLATED": 4.0}
 745
 746# MATPOWER case-input table widths. PYPOWER result tables append columns
 747# (LAM_P, MU_*) past these; from_ppc drops them.
 748_PPC_INPUT_WIDTH = {"bus": 13, "gen": 21, "branch": 13}
 749
 750# Columns a table must carry, which is what the MATPOWER reader requires. The
 751# gen table's capability and ramp columns are an OPF extension, so a 10 column
 752# gen table is a complete case and passes through at its own width; padding it
 753# would hand the reader eleven explicit zero limits the source never stated. A
 754# bus or branch row below 13 is truncated data, and zero padding it would
 755# invent a bus at 0 p.u. and 0 kV, so it is refused here as the reader refuses
 756# it in a `.m` file.
 757_PPC_MIN_WIDTH = {"bus": 13, "gen": 10, "branch": 13}
 758
 759
 760def _ppc_rows(name, table):
 761    """The table's rows as float lists, trimmed to the MATPOWER input width."""
 762    width = _PPC_INPUT_WIDTH.get(name)
 763    minimum = _PPC_MIN_WIDTH.get(name)
 764    out = []
 765    for i, row in enumerate(table):
 766        try:
 767            vals = [float(v) for v in row]
 768        except TypeError as e:
 769            raise ValueError(
 770                f"ppc table {name!r} row {i} is not a sequence of numbers: "
 771                f"pass a 2-D array, one row per element"
 772            ) from e
 773        except ValueError as e:
 774            raise ValueError(
 775                f"ppc table {name!r} row {i} has a non-numeric value: {e}"
 776            ) from e
 777        if minimum is not None and len(vals) < minimum:
 778            raise ValueError(
 779                f"ppc table {name!r} row {i} has {len(vals)} columns; "
 780                f"MATPOWER requires at least {minimum}"
 781            )
 782        out.append(vals[:width] if width is not None else vals)
 783    return out
 784
 785
 786def _ppc_to_matpower_text(ppc) -> str:
 787    missing = [k for k in ("baseMVA", "bus", "gen", "branch") if k not in ppc]
 788    if missing:
 789        raise ValueError(f"ppc dict is missing required keys: {missing}")
 790    lines = [
 791        "function mpc = from_ppc",
 792        f"mpc.version = '{ppc.get('version', '2')}';",
 793        f"mpc.baseMVA = {float(ppc['baseMVA'])!r};",
 794    ]
 795    names = ["bus", "gen", "branch"] + (["gencost"] if "gencost" in ppc else [])
 796    for name in names:
 797        rows = _ppc_rows(name, ppc[name])
 798        lines.append(f"mpc.{name} = [")
 799        for vals in rows:
 800            lines.append("  " + "  ".join(repr(v) for v in vals) + ";")
 801        lines.append("];")
 802    return "\n".join(lines) + "\n"
 803
 804
 805@_guard
 806def from_ppc(ppc) -> BalancedNetwork:
 807    """Case from a PYPOWER dict (``ppc``); the inverse of :meth:`BalancedNetwork.to_ppc`.
 808
 809    The tables route through the MATPOWER reader, so the semantics match a
 810    ``.m`` case exactly: bus ``PD``/``QD`` become loads, ``GS``/``BS`` become
 811    shunts, and ``gencost`` is read when present. Result columns past the
 812    MATPOWER input widths are dropped. A 10 column ``gen`` table (the layout
 813    without the OPF capability columns) passes through at its own width, so
 814    the generators come back with no capability limits rather than eleven
 815    zero ones. Raises :class:`ValueError` when a required table is absent,
 816    when a ``bus`` or ``branch`` row is below its 13 column width, when a row
 817    is not a sequence of numbers, or when a cell is not numeric; the message
 818    names the table and the row.
 819    """
 820    value = parse(
 821        _io.StringIO(_ppc_to_matpower_text(ppc)),
 822        format="matpower",
 823        name="from_ppc.m",
 824    ).value
 825    assert isinstance(value, BalancedNetwork)
 826    return value
 827
 828
 829from . import dist  # noqa: E402  (needs EmitResult defined above)
 830
 831MulticonductorNetwork = dist.MulticonductorNetwork
 832
 833
 834@_guard
 835def versions() -> Any:
 836    """Return the PowerIO release, sole IR identity, and BMOPF schema."""
 837    return _json.loads(_powerio.versions_json())
 838
 839
 840class _TypedValue:
 841    """Typed view rooted in its owning :class:`PioModule`."""
 842
 843    __slots__ = ("module", "_collection_entry")
 844
 845    def __init__(self, module: "PioModule") -> None:
 846        self.module = module
 847        self._collection_entry = None
 848
 849    def __repr__(self) -> str:
 850        return f"{type(self).__name__}()"
 851
 852
 853@dataclass(frozen=True)
 854class TimePoint:
 855    label: str
 856    duration_seconds: Optional[float] = None
 857
 858
 859@dataclass(frozen=True)
 860class Scenario:
 861    id: str
 862    probability: Optional[float] = None
 863
 864
 865@dataclass(frozen=True)
 866class _CollectionEntry:
 867    root: "PioModule"
 868    time_index: Optional[int] = None
 869    scenario_id: Optional[str] = None
 870
 871
 872def _bind_collection_entry(
 873    value: Any,
 874    location: _CollectionEntry,
 875) -> Any:
 876    value._collection_entry = location
 877    return value
 878
 879
 880@_guard_class
 881class TimeSeries(_TypedValue, Sequence):
 882    """Values of one type ordered in time."""
 883
 884    def __init__(
 885        self,
 886        values: Sequence[Any],
 887        *,
 888        time_points: Sequence[TimePoint],
 889    ) -> None:
 890        if isinstance(values, (str, bytes, bytearray)) or not isinstance(
 891            values, Sequence
 892        ):
 893            raise TypeError("TimeSeries values must be a sequence of PowerIO values")
 894        if not isinstance(time_points, Sequence):
 895            raise TypeError("time_points must be a sequence of TimePoint values")
 896        points = tuple(time_points)
 897        if not all(isinstance(point, TimePoint) for point in points):
 898            raise TypeError("time_points must contain only TimePoint values")
 899        modules = [PioModule.from_value(value)._inner for value in values]
 900        inner = _powerio._PioModule._from_time_series(
 901            modules,
 902            [(point.label, point.duration_seconds) for point in points],
 903        )
 904        super().__init__(PioModule(inner))
 905
 906    @classmethod
 907    def _from_module(cls, module: "PioModule") -> "TimeSeries":
 908        value = object.__new__(cls)
 909        _TypedValue.__init__(value, module)
 910        return value
 911
 912    @property
 913    def time_points(self) -> tuple[TimePoint, ...]:
 914        return tuple(TimePoint(*point) for point in self.module._inner._time_series_points())
 915
 916    def __len__(self) -> int:
 917        return self.module._inner._time_series_len()
 918
 919    def __getitem__(self, index):
 920        if isinstance(index, slice):
 921            return [self[position] for position in range(*index.indices(len(self)))]
 922        try:
 923            position = _operator.index(index)
 924        except TypeError:
 925            raise TypeError("time series indices must be integers") from None
 926        if position < 0:
 927            position += len(self)
 928        if position < 0 or position >= len(self):
 929            raise IndexError("time series index out of range")
 930        current = self._collection_entry or _CollectionEntry(self.module)
 931        if current.time_index is not None:
 932            raise TypeError("nested TimeSeries values are not supported")
 933        value = PioModule(self.module._inner._time_series_get(position)).value
 934        return _bind_collection_entry(
 935            value,
 936            _CollectionEntry(
 937                root=current.root,
 938                time_index=position,
 939                scenario_id=current.scenario_id,
 940            ),
 941        )
 942
 943    def __iter__(self):
 944        return (self[position] for position in range(len(self)))
 945
 946
 947@_guard_class
 948class ScenarioSet(_TypedValue, Mapping):
 949    """Named alternatives of one type, with optional probabilities."""
 950
 951    def __init__(
 952        self,
 953        values: Mapping[str, Any],
 954        *,
 955        probabilities: Optional[Mapping[str, float]] = None,
 956    ) -> None:
 957        if not isinstance(values, Mapping):
 958            raise TypeError("ScenarioSet values must be a mapping from IDs to values")
 959        if probabilities is not None and not isinstance(probabilities, Mapping):
 960            raise TypeError("probabilities must be a mapping from scenario IDs to numbers")
 961        ids = list(values)
 962        modules = [PioModule.from_value(values[id])._inner for id in ids]
 963        inner = _powerio._PioModule._from_scenario_set(
 964            modules,
 965            ids,
 966            None if probabilities is None else dict(probabilities),
 967        )
 968        super().__init__(PioModule(inner))
 969
 970    @classmethod
 971    def _from_module(cls, module: "PioModule") -> "ScenarioSet":
 972        value = object.__new__(cls)
 973        _TypedValue.__init__(value, module)
 974        return value
 975
 976    @property
 977    def scenarios(self) -> tuple[Scenario, ...]:
 978        return tuple(Scenario(*entry) for entry in self.module._inner._scenario_entries())
 979
 980    def __len__(self) -> int:
 981        return len(self.scenarios)
 982
 983    def __iter__(self):
 984        return (scenario.id for scenario in self.scenarios)
 985
 986    def __contains__(self, scenario: object) -> bool:
 987        return isinstance(scenario, str) and any(
 988            entry.id == scenario for entry in self.scenarios
 989        )
 990
 991    def __getitem__(self, scenario: str) -> Any:
 992        if not isinstance(scenario, str):
 993            raise TypeError("scenario keys must be strings")
 994        if scenario not in self:
 995            raise KeyError(scenario)
 996        current = self._collection_entry or _CollectionEntry(self.module)
 997        if current.scenario_id is not None:
 998            raise TypeError("nested ScenarioSet values are not supported")
 999        value = PioModule(self.module._inner._scenario_get(scenario)).value
1000        return _bind_collection_entry(
1001            value,
1002            _CollectionEntry(
1003                root=current.root,
1004                time_index=current.time_index,
1005                scenario_id=scenario,
1006            ),
1007        )
1008
1009
1010@_guard_class
1011class OperatingPoint(_TypedValue):
1012    """A possibly partial assignment over fixed equipment identities."""
1013
1014
1015@_guard_class
1016class _BalancedCalculation(_TypedValue):
1017    """A calculation over one shared balanced network."""
1018
1019    @property
1020    def network(self) -> BalancedNetwork:
1021        """The balanced network used by this calculation."""
1022        return BalancedNetwork(self.module._inner._balanced_calculation_network())
1023
1024
1025@_guard_class
1026class _MulticonductorCalculation(_TypedValue):
1027    """A calculation over one shared multiconductor network."""
1028
1029    @property
1030    def network(self) -> MulticonductorNetwork:
1031        """The multiconductor network used by this calculation."""
1032        return MulticonductorNetwork(
1033            self.module._inner._multiconductor_calculation_network()
1034        )
1035
1036
1037@_guard_class
1038class _CalculationSolution(_TypedValue):
1039    """A solution that retains the exact typed instance it solves."""
1040
1041    @property
1042    def instance(self) -> _TypedValue:
1043        """The calculation instance solved by this result."""
1044        return PioModule(self.module._inner._calculation_solution_instance()).value
1045
1046
1047class DcPfInstance(_BalancedCalculation):
1048    """A DC power flow calculation instance."""
1049
1050
1051class AcPfInstance(_BalancedCalculation):
1052    """An AC power flow calculation instance."""
1053
1054
1055class DcOpfInstance(_BalancedCalculation):
1056    """A DC optimal power flow calculation instance."""
1057
1058
1059class AcOpfInstance(_BalancedCalculation):
1060    """An AC optimal power flow calculation instance."""
1061
1062
1063class McAcPfInstance(_MulticonductorCalculation):
1064    """A multiconductor AC power flow calculation instance."""
1065
1066
1067class McAcOpfInstance(_MulticonductorCalculation):
1068    """A multiconductor AC optimal power flow calculation instance."""
1069
1070
1071@_guard_class
1072class AcScucInstance(_BalancedCalculation):
1073    """An AC security constrained unit commitment calculation instance."""
1074
1075    @property
1076    def inputs(self) -> ScucInputs:
1077        """Scheduling, reserve, and contingency inputs."""
1078        return self.module._inner._ac_scuc_inputs()
1079
1080
1081class DcPfSolution(_BalancedCalculation, _CalculationSolution):
1082    """A DC power flow solution."""
1083
1084
1085class AcPfSolution(_BalancedCalculation, _CalculationSolution):
1086    """An AC power flow solution."""
1087
1088
1089class DcOpfSolution(_BalancedCalculation, _CalculationSolution):
1090    """A DC optimal power flow solution."""
1091
1092
1093class AcOpfSolution(_BalancedCalculation, _CalculationSolution):
1094    """An AC optimal power flow solution."""
1095
1096
1097class SocwrOpfSolution(_BalancedCalculation, _CalculationSolution):
1098    """A PowerModels SOCWR relaxation solution and objective lower bound."""
1099
1100
1101class McAcPfSolution(_MulticonductorCalculation, _CalculationSolution):
1102    """A multiconductor AC power flow solution."""
1103
1104
1105class McAcOpfSolution(_MulticonductorCalculation, _CalculationSolution):
1106    """A multiconductor AC optimal power flow solution."""
1107
1108
1109@_guard_class
1110class AcScucSolution(_BalancedCalculation, _CalculationSolution):
1111    """An AC security constrained unit commitment solution."""
1112
1113    @property
1114    def termination(self) -> str:
1115        """How the calculation ended."""
1116        return self.module._inner._ac_scuc_solution_termination()
1117
1118    @property
1119    def residuals(self) -> Residuals:
1120        """Reported active and reactive power balance residuals."""
1121        return self.module._inner._ac_scuc_solution_residuals()
1122
1123    @property
1124    def producer(self) -> Optional[str]:
1125        """Producer or solver identity, when recorded."""
1126        return self.module._inner._ac_scuc_solution_producer()
1127
1128    @property
1129    def network_outputs(self) -> ScucNetworkOutputs:
1130        """Per interval network outputs."""
1131        return self.module._inner._ac_scuc_solution_network_outputs()
1132
1133    @property
1134    def device_outputs(self) -> ScucDeviceOutputs:
1135        """Per interval dispatchable device outputs."""
1136        return self.module._inner._ac_scuc_solution_device_outputs()
1137
1138    @property
1139    def objective(self) -> Optional[float]:
1140        """Reported objective value, when present."""
1141        return self.module._inner._ac_scuc_solution_objective()
1142
1143
1144@_guard_class
1145class GeoLayer(_TypedValue):
1146    """A standalone geographic document: element points and routes keyed by
1147    element identity, in one coordinate space.
1148
1149    :func:`parse` returns it for the canonical ``.geo.json``, GeoJSON, aliased
1150    CSV or JSON records, headerless buscoords CSV, and a PowerWorld ``.pwd``
1151    display. :meth:`PioModule.emit` writes the canonical document as
1152    ``geo-json``, and :func:`serialize` carries the layer through PowerIO IR.
1153    Place a layer onto a case with
1154    ``network.apply_geo_layer(layer.geojson)``.
1155    """
1156
1157    @property
1158    def geojson(self) -> str:
1159        """The canonical ``.geo.json`` document for this layer."""
1160        result = emit(self.module, "geo-json")
1161        data = result.artifacts[0].data
1162        if data is None:
1163            raise ValueError("the layer emission returned no artifact bytes")
1164        return data.decode("utf-8")
1165
1166
1167
1168_VALUE_CLASSES: dict[str, type[_TypedValue]] = {
1169    "powerio.GeoLayer": GeoLayer,
1170    "powerio.OperatingPoint<powerio.BalancedNetwork>": OperatingPoint,
1171    "powerio.OperatingPoint<powerio.MulticonductorNetwork>": OperatingPoint,
1172    "powerio.DcPfInstance": DcPfInstance,
1173    "powerio.AcPfInstance": AcPfInstance,
1174    "powerio.DcOpfInstance": DcOpfInstance,
1175    "powerio.AcOpfInstance": AcOpfInstance,
1176    "powerio.McAcPfInstance": McAcPfInstance,
1177    "powerio.McAcOpfInstance": McAcOpfInstance,
1178    "powerio.AcScucInstance": AcScucInstance,
1179    "powerio.DcPfSolution": DcPfSolution,
1180    "powerio.AcPfSolution": AcPfSolution,
1181    "powerio.DcOpfSolution": DcOpfSolution,
1182    "powerio.AcOpfSolution": AcOpfSolution,
1183    "powerio.SocwrOpfSolution": SocwrOpfSolution,
1184    "powerio.McAcPfSolution": McAcPfSolution,
1185    "powerio.McAcOpfSolution": McAcOpfSolution,
1186    "powerio.AcScucSolution": AcScucSolution,
1187}
1188
1189
1190@_guard_class
1191class PioModule:
1192    """One typed value with diagnostics, producer, sources, source mappings,
1193    history, and extensions.
1194    """
1195
1196    def __init__(self, inner: "_powerio._PioModule"):
1197        self._inner = inner
1198
1199    @classmethod
1200    def from_value(cls, value: Any) -> "PioModule":
1201        """Wrap an existing typed value without serializing it."""
1202        if isinstance(value, BalancedNetwork):
1203            return cls(_powerio._PioModule.from_balanced_network(value._inner))
1204        if isinstance(value, dist.MulticonductorNetwork):
1205            return cls(_powerio._PioModule.from_multiconductor_network(value._inner))
1206        if isinstance(value, _TypedValue):
1207            location = value._collection_entry
1208            inner = value.module._inner
1209            if location is not None:
1210                inner = location.root._inner
1211                if location.scenario_id is not None:
1212                    inner = inner._scenario_get(location.scenario_id)
1213                if location.time_index is not None:
1214                    inner = inner._time_series_get(location.time_index)
1215            return cls(inner._copy())
1216        raise TypeError("PioModule.from_value expects a typed PowerIO value")
1217
1218    @property
1219    def value(self) -> Any:
1220        """The contained typed value."""
1221        type_name = self._inner._type_name
1222        if type_name == "powerio.BalancedNetwork":
1223            return BalancedNetwork(self._inner.as_balanced_network())
1224        if type_name == "powerio.MulticonductorNetwork":
1225            return dist.MulticonductorNetwork(self._inner.as_multiconductor_network())
1226        if type_name.startswith("powerio.TimeSeries<"):
1227            return TimeSeries._from_module(self)
1228        if type_name.startswith("powerio.ScenarioSet<"):
1229            return ScenarioSet._from_module(self)
1230        value_class = _VALUE_CLASSES.get(type_name)
1231        if value_class is None:
1232            raise RuntimeError(f"this binding has no Python class for {type_name}")
1233        return value_class(self)
1234
1235    @property
1236    def diagnostics(self) -> list[Diagnostic]:
1237        """The diagnostics stored on this module, in encounter order."""
1238        return list(self._inner.diagnostics)
1239
1240    def to_balanced_report(self, base_mva: float = 100.0) -> Any:
1241        """Report whether a multiconductor network can become balanced."""
1242        return _json.loads(self._inner.lowering_readiness_json(base_mva))
1243
1244    def to_balanced(self, base_mva: float = 100.0) -> "PioModule":
1245        """Transform a multiconductor network to a balanced module."""
1246        return PioModule(self._inner.lower_to_balanced(base_mva))
1247
1248    def to_dc_pf_instance(self) -> "PioModule":
1249        """Build a DC power flow instance from a balanced network module."""
1250        return PioModule(self._inner._to_dc_pf_instance())
1251
1252    def to_ac_pf_instance(self) -> "PioModule":
1253        """Build an AC power flow instance from a balanced network module."""
1254        return PioModule(self._inner._to_ac_pf_instance())
1255
1256    def to_dc_opf_instance(self) -> "PioModule":
1257        """Build a DC optimal power flow instance from a balanced network module."""
1258        return PioModule(self._inner._to_dc_opf_instance())
1259
1260    def to_ac_opf_instance(self) -> "PioModule":
1261        """Build an AC optimal power flow instance from a balanced network module."""
1262        return PioModule(self._inner._to_ac_opf_instance())
1263
1264    def to_mc_ac_pf_instance(self) -> "PioModule":
1265        """Build a multiconductor AC power flow instance from a network module."""
1266        return PioModule(self._inner._to_mc_ac_pf_instance())
1267
1268    def to_mc_ac_opf_instance(self) -> "PioModule":
1269        """Build a multiconductor AC optimal power flow instance from a network module."""
1270        return PioModule(self._inner._to_mc_ac_opf_instance())
1271
1272    def __repr__(self) -> str:
1273        return repr(self._inner)
1274
1275
1276def _selected_collection_value(location: _CollectionEntry) -> Any:
1277    value = location.root.value
1278    time_index = location.time_index
1279    scenario_id = location.scenario_id
1280    while time_index is not None or scenario_id is not None:
1281        if isinstance(value, TimeSeries) and time_index is not None:
1282            value = value[time_index]
1283            time_index = None
1284        elif isinstance(value, ScenarioSet) and scenario_id is not None:
1285            value = value[scenario_id]
1286            scenario_id = None
1287        elif time_index is not None:
1288            raise TypeError("the selected value is not a TimeSeries")
1289        else:
1290            raise TypeError("the selected value is not a ScenarioSet")
1291    return value
1292
1293
1294def _refresh_collection_entry(target: Any, location: _CollectionEntry) -> None:
1295    refreshed = _selected_collection_value(location)
1296    if type(refreshed) is not type(target):
1297        raise RuntimeError("a collection update changed the entry type")
1298    if isinstance(target, BalancedNetwork):
1299        target._inner = refreshed._inner
1300    elif isinstance(target, dist.MulticonductorNetwork):
1301        target._inner = refreshed._inner
1302    elif isinstance(target, _TypedValue):
1303        target.module = refreshed.module
1304        target._collection_entry = refreshed._collection_entry
1305    else:
1306        raise TypeError("the selected value does not support typed updates")
1307
1308
1309@_guard
1310def apply_updates(
1311    target: Any,
1312    updates: Union[
1313        Iterable[OperatingPointUpdate],
1314        Iterable[NetworkUpdate],
1315        Iterable[CalculationUpdate],
1316    ],
1317) -> UpdateReport:
1318    """Validate and apply one batch of typed updates atomically.
1319
1320    ``updates`` contains one update class: :class:`OperatingPointUpdate`,
1321    :class:`NetworkUpdate`, or :class:`CalculationUpdate`. Values are absolute
1322    replacements and power quantities carry their units in the typed value.
1323    ``target`` is a module or a value obtained by indexing a :class:`TimeSeries`
1324    or :class:`ScenarioSet`. If validation fails, the module is unchanged.
1325    """
1326    batch = list(updates)
1327    if isinstance(target, PioModule):
1328        return target._inner._apply_updates(batch)
1329    location = getattr(target, "_collection_entry", None)
1330    if not isinstance(location, _CollectionEntry):
1331        raise TypeError(
1332            "target must be a PioModule or a TimeSeries/ScenarioSet entry"
1333        )
1334    report = location.root._inner._apply_collection_updates(
1335        batch,
1336        time_index=location.time_index,
1337        scenario_id=location.scenario_id,
1338    )
1339    _refresh_collection_entry(target, location)
1340    return report
1341
1342
1343@_guard
1344def apply_bus_load_active_power(
1345    module: PioModule,
1346    bus_id: int,
1347    total: ActivePower,
1348    *,
1349    allocation: str = "proportional_to_current_active_power",
1350) -> UpdateReport:
1351    """Replace aggregate bus demand through an explicit PowerIO allocation rule.
1352
1353    ``"proportional_to_current_active_power"`` preserves each participating
1354    load's current share. ``"equal"`` gives every participating load the same
1355    share, including when their current aggregate demand is zero. PowerIO
1356    requires stable load IDs and reports each load changed.
1357    """
1358    if not isinstance(module, PioModule):
1359        raise TypeError("module must be a PioModule")
1360    if not isinstance(total, ActivePower):
1361        raise TypeError("total must be an ActivePower")
1362    return module._inner._apply_bus_load_active_power(
1363        bus_id,
1364        total,
1365        allocation=allocation,
1366    )
1367
1368
1369def _path_from_source(source: Any) -> Optional[str]:
1370    if isinstance(source, str):
1371        return source
1372    if isinstance(source, (bytes, bytearray, memoryview)):
1373        return None
1374    try:
1375        path = _os.fspath(source)
1376    except TypeError:
1377        return None
1378    if isinstance(path, bytes):
1379        raise TypeError("a path-like source must return str, not bytes")
1380    return path
1381
1382
1383def _memory_from_source(source: Any, name: Optional[str]) -> tuple[bytes, str]:
1384    if isinstance(source, (bytes, bytearray, memoryview)):
1385        data = bytes(source)
1386    else:
1387        read = getattr(source, "read", None)
1388        if read is None:
1389            raise TypeError(
1390                "source must be a path, file object, or bytes-like object"
1391            )
1392        data = read()
1393        if isinstance(data, str):
1394            data = data.encode("utf-8")
1395        elif isinstance(data, (bytes, bytearray, memoryview)):
1396            data = bytes(data)
1397        else:
1398            raise TypeError("source.read() must return str or bytes-like data")
1399    if name is None:
1400        candidate = getattr(source, "name", None)
1401        try:
1402            candidate = _os.fspath(candidate) if candidate is not None else None
1403        except TypeError:
1404            candidate = None
1405        name = candidate if isinstance(candidate, str) else "<memory>"
1406    if not isinstance(name, str):
1407        raise TypeError("name must be a string")
1408    return data, name
1409
1410
1411@_guard
1412def parse(
1413    source: Any,
1414    *,
1415    format: Optional[str] = None,
1416    name: Optional[str] = None,
1417) -> PioModule:
1418    """Parse a path, file object, or bytes-like source.
1419
1420    A string is always a path. Pass raw text through ``io.StringIO`` or
1421    another file object.
1422    """
1423    path = _path_from_source(source)
1424    if path is not None:
1425        if name is not None:
1426            raise ValueError("name is only valid for memory and file object sources")
1427        return PioModule(_powerio._PioModule._parse_path(path, format))
1428    data, source_name = _memory_from_source(source, name)
1429    return PioModule(_powerio._PioModule._parse_memory(data, source_name, format))
1430
1431
1432def _result_from_native(result: dict[str, Any]) -> EmitResult:
1433    return EmitResult(
1434        artifacts=tuple(Artifact(**artifact) for artifact in result["artifacts"]),
1435        layout=result["layout"],
1436        fidelity=result["fidelity"],
1437        diagnostics=tuple(result["diagnostics"]),
1438    )
1439
1440
1441def _emit_to_destination(
1442    module: PioModule,
1443    destination: Optional[Any],
1444    memory_call: Any,
1445    path_call: Any,
1446) -> EmitResult:
1447    if not isinstance(module, PioModule):
1448        raise TypeError("module must be a PioModule")
1449    if destination is None:
1450        return _result_from_native(memory_call())
1451    path = _path_from_source(destination)
1452    if path is not None:
1453        return _result_from_native(path_call(path))
1454    write = getattr(destination, "write", None)
1455    if write is None:
1456        raise TypeError("destination must be a path or writable file object")
1457    result = _result_from_native(memory_call())
1458    if result.layout != "file" or len(result.artifacts) != 1:
1459        raise ValueError("a directory emission requires a path destination")
1460    data = result.artifacts[0].data
1461    if data is None:
1462        raise ValueError("the emission returned no artifact bytes to write")
1463    # A text mode stream takes str and a binary one takes bytes. Ask a real
1464    # stream which it is rather than writing bytes and retrying on TypeError:
1465    # a TypeError raised inside the stream's own write would otherwise trigger
1466    # a second, partially duplicated write. A duck typed sink states neither,
1467    # so it keeps the retry.
1468    if isinstance(destination, _io.TextIOBase) or isinstance(
1469        getattr(destination, "encoding", None), str
1470    ):
1471        write(data.decode("utf-8"))
1472    elif isinstance(destination, (_io.RawIOBase, _io.BufferedIOBase)):
1473        write(data)
1474    else:
1475        try:
1476            write(data)
1477        except TypeError:
1478            write(data.decode("utf-8"))
1479    return result
1480
1481
1482@_guard
1483def emit(module: PioModule, format: str, destination: Optional[Any] = None) -> EmitResult:
1484    """Emit a module as one grid exchange format."""
1485    return _emit_to_destination(
1486        module,
1487        destination,
1488        lambda: module._inner._emit_memory(format),
1489        lambda path: module._inner._emit_path(format, path),
1490    )
1491
1492
1493@_guard
1494def serialize(module: PioModule, destination: Optional[Any] = None) -> EmitResult:
1495    """Serialize a module as PowerIO IR."""
1496    return _emit_to_destination(
1497        module,
1498        destination,
1499        module._inner._serialize_memory,
1500        module._inner._serialize_path,
1501    )
1502
1503
1504@_guard
1505def deserialize(source: Any) -> PioModule:
1506    """Deserialize PowerIO IR from a path, file object, or bytes-like source."""
1507    path = _path_from_source(source)
1508    if path is not None:
1509        return PioModule(_powerio._PioModule._deserialize_path(path))
1510    data, _ = _memory_from_source(source, None)
1511    return PioModule(_powerio._PioModule._deserialize_memory(data))
1512
1513
1514@_guard
1515def features() -> dict[str, bool]:
1516    """The build-time features compiled into this powerio installation.
1517
1518    ``matrix``, ``dist``, and ``prob`` are unconditional dependencies of the
1519    extension and are always ``True``. ``gridfm`` reports whether GridFM
1520    Parquet parsing and emission were compiled in; the published wheel
1521    includes them, while a custom source build can omit them.
1522    """
1523    return {
1524        "matrix": True,
1525        "gridfm": bool(getattr(_powerio, "_has_gridfm", False)),
1526        "dist": True,
1527        "prob": True,
1528    }
class AcOpfInstance(_BalancedCalculation):
1060class AcOpfInstance(_BalancedCalculation):
1061    """An AC optimal power flow calculation instance."""

An AC optimal power flow calculation instance.

class AcOpfSolution(_BalancedCalculation, _CalculationSolution):
1094class AcOpfSolution(_BalancedCalculation, _CalculationSolution):
1095    """An AC optimal power flow solution."""

An AC optimal power flow solution.

class AcPfInstance(_BalancedCalculation):
1052class AcPfInstance(_BalancedCalculation):
1053    """An AC power flow calculation instance."""

An AC power flow calculation instance.

class AcPfSolution(_BalancedCalculation, _CalculationSolution):
1086class AcPfSolution(_BalancedCalculation, _CalculationSolution):
1087    """An AC power flow solution."""

An AC power flow solution.

class AcScucInstance(_BalancedCalculation):
1072@_guard_class
1073class AcScucInstance(_BalancedCalculation):
1074    """An AC security constrained unit commitment calculation instance."""
1075
1076    @property
1077    def inputs(self) -> ScucInputs:
1078        """Scheduling, reserve, and contingency inputs."""
1079        return self.module._inner._ac_scuc_inputs()

An AC security constrained unit commitment calculation instance.

inputs: ScucInputs
1076    @property
1077    def inputs(self) -> ScucInputs:
1078        """Scheduling, reserve, and contingency inputs."""
1079        return self.module._inner._ac_scuc_inputs()

Scheduling, reserve, and contingency inputs.

class AcScucSolution(_BalancedCalculation, _CalculationSolution):
1110@_guard_class
1111class AcScucSolution(_BalancedCalculation, _CalculationSolution):
1112    """An AC security constrained unit commitment solution."""
1113
1114    @property
1115    def termination(self) -> str:
1116        """How the calculation ended."""
1117        return self.module._inner._ac_scuc_solution_termination()
1118
1119    @property
1120    def residuals(self) -> Residuals:
1121        """Reported active and reactive power balance residuals."""
1122        return self.module._inner._ac_scuc_solution_residuals()
1123
1124    @property
1125    def producer(self) -> Optional[str]:
1126        """Producer or solver identity, when recorded."""
1127        return self.module._inner._ac_scuc_solution_producer()
1128
1129    @property
1130    def network_outputs(self) -> ScucNetworkOutputs:
1131        """Per interval network outputs."""
1132        return self.module._inner._ac_scuc_solution_network_outputs()
1133
1134    @property
1135    def device_outputs(self) -> ScucDeviceOutputs:
1136        """Per interval dispatchable device outputs."""
1137        return self.module._inner._ac_scuc_solution_device_outputs()
1138
1139    @property
1140    def objective(self) -> Optional[float]:
1141        """Reported objective value, when present."""
1142        return self.module._inner._ac_scuc_solution_objective()

An AC security constrained unit commitment solution.

termination: Literal['converged', 'iteration_limit', 'infeasible', 'unbounded', 'failed', 'not_reported']
1114    @property
1115    def termination(self) -> str:
1116        """How the calculation ended."""
1117        return self.module._inner._ac_scuc_solution_termination()

How the calculation ended.

residuals: Residuals
1119    @property
1120    def residuals(self) -> Residuals:
1121        """Reported active and reactive power balance residuals."""
1122        return self.module._inner._ac_scuc_solution_residuals()

Reported active and reactive power balance residuals.

producer: Optional[str]
1124    @property
1125    def producer(self) -> Optional[str]:
1126        """Producer or solver identity, when recorded."""
1127        return self.module._inner._ac_scuc_solution_producer()

Producer or solver identity, when recorded.

network_outputs: ScucNetworkOutputs
1129    @property
1130    def network_outputs(self) -> ScucNetworkOutputs:
1131        """Per interval network outputs."""
1132        return self.module._inner._ac_scuc_solution_network_outputs()

Per interval network outputs.

device_outputs: ScucDeviceOutputs
1134    @property
1135    def device_outputs(self) -> ScucDeviceOutputs:
1136        """Per interval dispatchable device outputs."""
1137        return self.module._inner._ac_scuc_solution_device_outputs()

Per interval dispatchable device outputs.

objective: Optional[float]
1139    @property
1140    def objective(self) -> Optional[float]:
1141        """Reported objective value, when present."""
1142        return self.module._inner._ac_scuc_solution_objective()

Reported objective value, when present.

class ActivePower:
def watts(value):
def megawatts(value):
value
unit
class ApparentPower:
def volt_amperes(value):
def megavolt_amperes(value):
unit
value
@dataclass(frozen=True)
class Artifact:
169@dataclass(frozen=True)
170class Artifact:
171    """One artifact produced by :func:`emit` or :func:`serialize`.
172
173    ``data`` is set for an in-memory result. ``path`` is set after committing
174    to a filesystem destination.
175    """
176
177    name: str
178    data: Optional[bytes]
179    path: Optional[str]
180
181    @property
182    def text(self) -> str:
183        """Decode an in-memory UTF-8 artifact."""
184        if self.data is None:
185            raise ValueError("this artifact was committed to a destination")
186        return self.data.decode("utf-8")

One artifact produced by emit() or serialize().

data is set for an in-memory result. path is set after committing to a filesystem destination.

Artifact(name: str, data: Optional[bytes], path: Optional[str])
name: str
data: Optional[bytes]
path: Optional[str]
text: str
181    @property
182    def text(self) -> str:
183        """Decode an in-memory UTF-8 artifact."""
184        if self.data is None:
185            raise ValueError("this artifact was committed to a destination")
186        return self.data.decode("utf-8")

Decode an in-memory UTF-8 artifact.

class BalancedNetwork:
324@_guard_class
325class BalancedNetwork:
326    """A parsed balanced power network.
327
328    The data attributes (``buses``, ``branches``, ``generators``, ``loads``,
329    ``shunts``) and reference bus queries delegate to the compiled handle; the
330    matrix methods below return ``scipy.sparse`` objects. Parse and transform
331    diagnostics belong to the owning :class:`PioModule`.
332
333    Errors: a bad file path raises the standard ``OSError`` subclass
334    (``FileNotFoundError``); a malformed case raises :class:`PowerIOParseError`
335    and an unmet calculation precondition (no generators, no reference bus) raises
336    :class:`PowerIODataError`; both subclass :class:`PowerIOError`, so
337    ``except PowerIOError`` catches either; an unknown
338    ``scheme``/``formula``/``units`` string raises ``ValueError``.
339    """
340
341    def __init__(self, inner: "_powerio._BalancedNetwork"):
342        self._inner = inner
343
344    def __dir__(self):
345        # The data attributes arrive through __getattr__, so name them here or
346        # they stay invisible to tab completion.
347        return sorted(set(super().__dir__()) | _BALANCED_DELEGATED_NAMES)
348
349    def __getattr__(self, name: str):
350        # Reached only when normal lookup misses, so the matrix methods below
351        # win. Guard underscore names so a lookup before _inner exists raises
352        # AttributeError instead of recursing forever.
353        if name not in _BALANCED_DELEGATED_NAMES:
354            raise AttributeError(
355                f"{type(self).__name__!r} object has no attribute {name!r}"
356            )
357        return getattr(self._inner, name)
358
359    def __repr__(self) -> str:
360        # The inner handle's __repr__ already renders the public ``BalancedNetwork(...)``
361        # form, so this is a straight delegate.
362        return repr(self._inner)
363
364    def calc_connectivity_report(self) -> dict[str, Any]:
365        """Calculate the in-service topology summary."""
366        return self._inner.calc_connectivity_report()
367
368    def to_geo_layer(self) -> dict[str, Any]:
369        """Transform coordinates to a canonical GeoJSON FeatureCollection.
370
371        A case without coordinates produces an empty feature collection.
372        """
373        return _json.loads(self._inner.to_geo_layer_json())
374
375    def apply_geo_layer(
376        self, text: str, name_hint: Optional[str] = None
377    ) -> tuple["BalancedNetwork", dict[str, Any]]:
378        """Apply a geographic sidecar and return ``(placed, report)``.
379
380        ``text`` is any form :func:`parse_geo` accepts; this case is
381        unchanged. The report carries ``matched_buses``, ``matched_branches``,
382        ``unmatched_features``, ``unlocated_buses``, ``unlocated_branches``,
383        and ``notes``. The two unlocated counts cover the whole case when the
384        pass ends, so a layer that matched nothing reads apart from a case
385        that needed nothing. The placed copy drops the retained source text,
386        so a same-format emission re-serializes.
387        """
388        inner, report = self._inner.apply_geo_layer(text, name_hint)
389        return BalancedNetwork(inner), report
390
391    # --- matrix calculations (scipy.sparse) -----------------------------
392
393    def calc_bprime_matrix(
394        self, scheme: str = "bx", *, skip_zero_impedance: bool = False
395    ):
396        """MATPOWER FDPF Bp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.
397
398        ``skip_zero_impedance=False`` refuses a zero impedance branch
399        (``r`` and ``x`` both zero); pass ``True`` to drop it instead.
400        """
401        return _to_csr(
402            self._inner.bprime(scheme, skip_zero_impedance=skip_zero_impedance)
403        )
404
405    def calc_incidence_matrix(self, formula: str = "series_susceptance"):
406        """Return PowerModels incidence ``A`` (branches by buses)."""
407        return _to_csr(self._inner.calc_incidence_matrix(formula))
408
409    def calc_branch_susceptances(self, formula: str = "series_susceptance"):
410        """Return per branch susceptances in active branch order."""
411        np = _require("numpy", "matrix")
412        return np.asarray(self._inner.calc_branch_susceptances(formula), dtype=float)
413
414    def calc_branch_flow_matrix(self, formula: str = "series_susceptance"):
415        """Return ``Bf = diag(b) A`` as a CSR matrix."""
416        return _to_csr(self._inner.calc_branch_flow_matrix(formula))
417
418    def calc_bus_susceptance_matrix(self, formula: str = "series_susceptance"):
419        """Return ``B = A.T diag(b) A`` as a CSR matrix."""
420        return _to_csr(self._inner.calc_bus_susceptance_matrix(formula))
421
422    def calc_branch_phase_shift_injection(
423        self, formula: str = "series_susceptance"
424    ):
425        """Return ``b * shift`` in active branch order."""
426        np = _require("numpy", "matrix")
427        return np.asarray(
428            self._inner.calc_branch_phase_shift_injection(formula), dtype=float
429        )
430
431    def calc_bus_phase_shift_injection(self, formula: str = "series_susceptance"):
432        """Return ``A.T @ (b * shift)`` in bus order."""
433        np = _require("numpy", "matrix")
434        return np.asarray(
435            self._inner.calc_bus_phase_shift_injection(formula), dtype=float
436        )
437
438    def calc_branch_flow_dc(self, voltage_angles, formula: str = "series_susceptance"):
439        """Compute ``-Bf @ va + b * shift`` in active branch order."""
440        np, angles = _dc_angles(self.n_buses, voltage_angles)
441        return np.asarray(
442            self._inner.calc_branch_flow_dc(angles.tolist(), formula), dtype=float
443        )
444
445    def calc_bus_injection_dc(
446        self, voltage_angles, formula: str = "series_susceptance"
447    ):
448        """Compute ``-B @ va + p_shift`` in bus order."""
449        np, angles = _dc_angles(self.n_buses, voltage_angles)
450        return np.asarray(
451            self._inner.calc_bus_injection_dc(angles.tolist(), formula), dtype=float
452        )
453
454    def calc_bdoubleprime_matrix(
455        self, scheme: str = "bx", *, skip_zero_impedance: bool = False
456    ):
457        """MATPOWER FDPF Bpp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.
458        ``skip_zero_impedance`` as in :meth:`calc_bprime_matrix`.
459        """
460        return _to_csr(
461            self._inner.bdoubleprime(scheme, skip_zero_impedance=skip_zero_impedance)
462        )
463
464    def calc_lacpf_matrix(
465        self,
466        *,
467        include_taps: bool = True,
468        include_shifts: bool = True,
469        skip_zero_impedance: bool = False,
470    ):
471        """LACPF 2n×2n block ``[[G, -B], [-B, -G]]``. ``skip_zero_impedance``
472        as in :meth:`calc_bprime_matrix`."""
473        return _to_csr(
474            self._inner.lacpf(
475                include_taps=include_taps,
476                include_shifts=include_shifts,
477                skip_zero_impedance=skip_zero_impedance,
478            )
479        )
480
481    def calc_adjacency_matrix(self):
482        """0/1 bus adjacency matrix."""
483        return _to_csr(self._inner.adjacency())
484
485    def calc_admittance_matrix(
486        self,
487        *,
488        include_taps: bool = True,
489        include_shifts: bool = True,
490        skip_zero_impedance: bool = False,
491    ):
492        """``Y_bus = G + jB`` as a complex csr_matrix. ``skip_zero_impedance``
493        as in :meth:`calc_bprime_matrix`."""
494        g, b = self._inner.ybus_parts(
495            include_taps=include_taps,
496            include_shifts=include_shifts,
497            skip_zero_impedance=skip_zero_impedance,
498        )
499        g, b = _to_csr(g), _to_csr(b)
500        return (g + 1j * b).tocsr()
501
502    def calc_ptdf(self, formula: str = "series_susceptance", solver: str = "auto"):
503        """DC PTDF (m×n). ``formula`` is ``"series_susceptance"``,
504        ``"tap_adjusted_reactance"``, or ``"reactance_only"``.
505
506        ``solver`` is ``"auto"``, ``"dense"``, or ``"sparse"``. ``"auto"``
507        uses the dense factorization on small cases and the sparse Cholesky
508        path on large ones, the same policy as the CLI.
509        """
510        return _to_csr(self._inner.ptdf(formula, solver))
511
512    def calc_lodf(self, formula: str = "series_susceptance", solver: str = "auto"):
513        """DC LODF (m×m). ``formula`` and ``solver`` as in :meth:`calc_ptdf`."""
514        return _to_csr(self._inner.lodf(formula, solver))
515
516    def calc_weighted_laplacian(
517        self,
518        formula: str = "series_susceptance",
519    ):
520        """Weighted Laplacian ``L = -B``. ``formula`` as in :meth:`calc_ptdf`."""
521        return _to_csr(self._inner.weighted_laplacian(formula))
522
523    def to_normalized(
524        self,
525        *,
526        clamp_angle_bounds: bool = False,
527        angle_bound_pad: Optional[float] = None,
528    ) -> "BalancedNetwork":
529        """Return a normalized copy with per unit power and radian angles.
530
531        The result removes out of service elements, preserves source bus IDs,
532        and normalizes bus types. It carries no retained source, so
533        :func:`powerio.emit` produces a grid exchange representation from the
534        derived module. Raises
535        :class:`PowerIODataError` if the network cannot be
536        normalized (no reference bus can be chosen, or a non-positive base MVA).
537
538        ``clamp_angle_bounds=True`` applies the PowerModels angle difference
539        bound repair: limits at or beyond ``+/-pi/2`` and zero/zero windows
540        become ``[-angle_bound_pad, angle_bound_pad]``. A repair that would
541        invert the interval widens to that same window. The default pad is
542        1.0472 radians.
543        """
544        if not clamp_angle_bounds and angle_bound_pad is None:
545            return BalancedNetwork(self._inner.to_normalized())
546        return BalancedNetwork(
547            self._inner.to_normalized_with_options(
548                clamp_angle_bounds=clamp_angle_bounds, angle_bound_pad=angle_bound_pad
549            )
550        )
551
552    def to_ppc(self):
553        """PYPOWER case dict (``ppc``) with MATPOWER-style numpy tables.
554
555        Values are emitted as the model holds them, so a case read from a
556        file carries MW, MVAr, and degrees. A network from
557        :meth:`to_normalized` holds per unit and radians, and those are what
558        its tables carry — PYPOWER reads a ppc dict as MW and degrees, so
559        build this from the raw network unless the consumer expects per unit.
560
561        Loads and shunts are summed onto their bus in the
562        ``PD``/``QD``/``GS``/``BS`` columns, the same aggregation as the
563        MATPOWER emitter. The bus table has no per element status
564        column, so an element the model marks out of service still
565        contributes its value, and a de-energized bus is carried as type 4.
566        ``gencost`` is present only when every generator carries cost data,
567        because MATPOWER requires cost rows for all generators or none.
568        :func:`from_ppc` reads the tables back.
569        """
570        np = _require("numpy", "matrix")
571        buses = self._inner.buses
572        bus = np.array(
573            [
574                (
575                    b["id"],
576                    _PPC_BUS_TYPE.get(b["kind"], 1.0),
577                    0.0,
578                    0.0,
579                    0.0,
580                    0.0,
581                    b["area"],
582                    b["vm"],
583                    b["va"],
584                    b["base_kv"],
585                    b["zone"],
586                    b["vmax"],
587                    b["vmin"],
588                )
589                for b in buses
590            ],
591            dtype=float,
592        ).reshape(len(buses), 13)
593        bus[:, 2], bus[:, 3], bus[:, 4], bus[:, 5] = _bus_sums(
594            np, buses, self._inner.loads, self._inner.shunts
595        )
596
597        # The capability and ramp columns past PMIN are an OPF extension that a
598        # source need not carry. Widen to the full 21 only when a generator
599        # actually states one: a table of zeros there reads back as eleven
600        # explicit zero limits, which a ramp aware solver takes as a generator
601        # that cannot move.
602        gens = self._inner.generators
603        caps = [g["caps"] for g in gens]
604        width = 21 if any(c is not None for row in caps for c in row) else 10
605        gen = np.array(
606            [
607                [
608                    g["bus"],
609                    g["pg"],
610                    g["qg"],
611                    g["qmax"],
612                    g["qmin"],
613                    g["vg"],
614                    g["mbase"],
615                    float(g["in_service"]),
616                    g["pmax"],
617                    g["pmin"],
618                ]
619                + ([0.0 if c is None else c for c in row] if width == 21 else [])
620                for g, row in zip(gens, caps)
621            ],
622            dtype=float,
623        ).reshape(len(gens), width)
624
625        branches = self._inner.branches
626        branch = np.array(
627            [
628                (
629                    br["from_id"],
630                    br["to_id"],
631                    br["r"],
632                    br["x"],
633                    br["b"],
634                    br["rate_a"],
635                    br["rate_b"],
636                    br["rate_c"],
637                    br["tap"],
638                    br["shift"],
639                    float(br["in_service"]),
640                    br["angmin"],
641                    br["angmax"],
642                )
643                for br in branches
644            ],
645            dtype=float,
646        ).reshape(len(branches), 13)
647
648        ppc = {
649            "version": "2",
650            "baseMVA": float(self._inner.base_mva),
651            "bus": bus,
652            "gen": gen,
653            "branch": branch,
654        }
655
656        # Coefficients sit left-aligned after ncost, padded to the widest
657        # row, which is the layout PYPOWER's own loadcase produces.
658        costs = [g["cost"] for g in gens]
659        if costs and all(c is not None for c in costs):
660            gencost = np.zeros((len(costs), 4 + max(len(c["coeffs"]) for c in costs)))
661            for i, c in enumerate(costs):
662                gencost[i, :4] = (
663                    c["model"],
664                    c["startup"],
665                    c["shutdown"],
666                    c["ncost"],
667                )
668                gencost[i, 4 : 4 + len(c["coeffs"])] = c["coeffs"]
669            ppc["gencost"] = gencost
670        return ppc
671
672    def to_networkx(self):
673        """Undirected networkx graph keyed by bus id.
674
675        In-service branches become edges carrying ``branch`` (index), ``r``,
676        ``x``, and ``b``.
677        """
678        nx = _require("networkx", "graph")
679        g = nx.Graph()
680        g.add_nodes_from(bus["id"] for bus in self._inner.buses)
681        for k, br in enumerate(self._inner.branches):
682            if br["in_service"]:
683                g.add_edge(
684                    br["from_id"],
685                    br["to_id"],
686                    branch=k,
687                    r=br["r"],
688                    x=br["x"],
689                    b=br["b"],
690                )
691        return g

A parsed balanced power network.

The data attributes (buses, branches, generators, loads, shunts) and reference bus queries delegate to the compiled handle; the matrix methods below return scipy.sparse objects. Parse and transform diagnostics belong to the owning PioModule.

Errors: a bad file path raises the standard OSError subclass (FileNotFoundError); a malformed case raises PowerIOParseError and an unmet calculation precondition (no generators, no reference bus) raises PowerIODataError; both subclass PowerIOError, so except PowerIOError catches either; an unknown scheme/formula/units string raises ValueError.

BalancedNetwork(inner: Any)
341    def __init__(self, inner: "_powerio._BalancedNetwork"):
342        self._inner = inner
def calc_connectivity_report(self) -> Dict[str, Any]:
364    def calc_connectivity_report(self) -> dict[str, Any]:
365        """Calculate the in-service topology summary."""
366        return self._inner.calc_connectivity_report()

Calculate the in-service topology summary.

def to_geo_layer(self) -> Dict[str, Any]:
368    def to_geo_layer(self) -> dict[str, Any]:
369        """Transform coordinates to a canonical GeoJSON FeatureCollection.
370
371        A case without coordinates produces an empty feature collection.
372        """
373        return _json.loads(self._inner.to_geo_layer_json())

Transform coordinates to a canonical GeoJSON FeatureCollection.

A case without coordinates produces an empty feature collection.

def apply_geo_layer( self, text: str, name_hint: Optional[str] = Ellipsis) -> Tuple[BalancedNetwork, Dict[str, Any]]:
375    def apply_geo_layer(
376        self, text: str, name_hint: Optional[str] = None
377    ) -> tuple["BalancedNetwork", dict[str, Any]]:
378        """Apply a geographic sidecar and return ``(placed, report)``.
379
380        ``text`` is any form :func:`parse_geo` accepts; this case is
381        unchanged. The report carries ``matched_buses``, ``matched_branches``,
382        ``unmatched_features``, ``unlocated_buses``, ``unlocated_branches``,
383        and ``notes``. The two unlocated counts cover the whole case when the
384        pass ends, so a layer that matched nothing reads apart from a case
385        that needed nothing. The placed copy drops the retained source text,
386        so a same-format emission re-serializes.
387        """
388        inner, report = self._inner.apply_geo_layer(text, name_hint)
389        return BalancedNetwork(inner), report

Apply a geographic sidecar and return (placed, report).

text is any form parse_geo() accepts; this case is unchanged. The report carries matched_buses, matched_branches, unmatched_features, unlocated_buses, unlocated_branches, and notes. The two unlocated counts cover the whole case when the pass ends, so a layer that matched nothing reads apart from a case that needed nothing. The placed copy drops the retained source text, so a same-format emission re-serializes.

def calc_bprime_matrix( self, scheme: Literal['bx', 'xb'] = Ellipsis, *, skip_zero_impedance: bool = Ellipsis) -> Any:
393    def calc_bprime_matrix(
394        self, scheme: str = "bx", *, skip_zero_impedance: bool = False
395    ):
396        """MATPOWER FDPF Bp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.
397
398        ``skip_zero_impedance=False`` refuses a zero impedance branch
399        (``r`` and ``x`` both zero); pass ``True`` to drop it instead.
400        """
401        return _to_csr(
402            self._inner.bprime(scheme, skip_zero_impedance=skip_zero_impedance)
403        )

MATPOWER FDPF Bp matrix. scheme is "bx" or "xb".

skip_zero_impedance=False refuses a zero impedance branch (r and x both zero); pass True to drop it instead.

def calc_incidence_matrix(self, formula: str = Ellipsis) -> Any:
405    def calc_incidence_matrix(self, formula: str = "series_susceptance"):
406        """Return PowerModels incidence ``A`` (branches by buses)."""
407        return _to_csr(self._inner.calc_incidence_matrix(formula))

Return PowerModels incidence A (branches by buses).

def calc_branch_susceptances(self, formula: str = Ellipsis) -> Any:
409    def calc_branch_susceptances(self, formula: str = "series_susceptance"):
410        """Return per branch susceptances in active branch order."""
411        np = _require("numpy", "matrix")
412        return np.asarray(self._inner.calc_branch_susceptances(formula), dtype=float)

Return per branch susceptances in active branch order.

def calc_branch_flow_matrix(self, formula: str = Ellipsis) -> Any:
414    def calc_branch_flow_matrix(self, formula: str = "series_susceptance"):
415        """Return ``Bf = diag(b) A`` as a CSR matrix."""
416        return _to_csr(self._inner.calc_branch_flow_matrix(formula))

Return Bf = diag(b) A as a CSR matrix.

def calc_bus_susceptance_matrix(self, formula: str = Ellipsis) -> Any:
418    def calc_bus_susceptance_matrix(self, formula: str = "series_susceptance"):
419        """Return ``B = A.T diag(b) A`` as a CSR matrix."""
420        return _to_csr(self._inner.calc_bus_susceptance_matrix(formula))

Return B = A.T diag(b) A as a CSR matrix.

def calc_branch_phase_shift_injection(self, formula: str = Ellipsis) -> Any:
422    def calc_branch_phase_shift_injection(
423        self, formula: str = "series_susceptance"
424    ):
425        """Return ``b * shift`` in active branch order."""
426        np = _require("numpy", "matrix")
427        return np.asarray(
428            self._inner.calc_branch_phase_shift_injection(formula), dtype=float
429        )

Return b * shift in active branch order.

def calc_bus_phase_shift_injection(self, formula: str = Ellipsis) -> Any:
431    def calc_bus_phase_shift_injection(self, formula: str = "series_susceptance"):
432        """Return ``A.T @ (b * shift)`` in bus order."""
433        np = _require("numpy", "matrix")
434        return np.asarray(
435            self._inner.calc_bus_phase_shift_injection(formula), dtype=float
436        )

Return A.T @ (b * shift) in bus order.

def calc_branch_flow_dc(self, voltage_angles: Any, formula: str = Ellipsis) -> Any:
438    def calc_branch_flow_dc(self, voltage_angles, formula: str = "series_susceptance"):
439        """Compute ``-Bf @ va + b * shift`` in active branch order."""
440        np, angles = _dc_angles(self.n_buses, voltage_angles)
441        return np.asarray(
442            self._inner.calc_branch_flow_dc(angles.tolist(), formula), dtype=float
443        )

Compute -Bf @ va + b * shift in active branch order.

def calc_bus_injection_dc(self, voltage_angles: Any, formula: str = Ellipsis) -> Any:
445    def calc_bus_injection_dc(
446        self, voltage_angles, formula: str = "series_susceptance"
447    ):
448        """Compute ``-B @ va + p_shift`` in bus order."""
449        np, angles = _dc_angles(self.n_buses, voltage_angles)
450        return np.asarray(
451            self._inner.calc_bus_injection_dc(angles.tolist(), formula), dtype=float
452        )

Compute -B @ va + p_shift in bus order.

def calc_bdoubleprime_matrix( self, scheme: Literal['bx', 'xb'] = Ellipsis, *, skip_zero_impedance: bool = Ellipsis) -> Any:
454    def calc_bdoubleprime_matrix(
455        self, scheme: str = "bx", *, skip_zero_impedance: bool = False
456    ):
457        """MATPOWER FDPF Bpp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.
458        ``skip_zero_impedance`` as in :meth:`calc_bprime_matrix`.
459        """
460        return _to_csr(
461            self._inner.bdoubleprime(scheme, skip_zero_impedance=skip_zero_impedance)
462        )

MATPOWER FDPF Bpp matrix. scheme is "bx" or "xb". skip_zero_impedance as in calc_bprime_matrix().

def calc_lacpf_matrix( self, *, include_taps: bool = Ellipsis, include_shifts: bool = Ellipsis, skip_zero_impedance: bool = Ellipsis) -> Any:
464    def calc_lacpf_matrix(
465        self,
466        *,
467        include_taps: bool = True,
468        include_shifts: bool = True,
469        skip_zero_impedance: bool = False,
470    ):
471        """LACPF 2n×2n block ``[[G, -B], [-B, -G]]``. ``skip_zero_impedance``
472        as in :meth:`calc_bprime_matrix`."""
473        return _to_csr(
474            self._inner.lacpf(
475                include_taps=include_taps,
476                include_shifts=include_shifts,
477                skip_zero_impedance=skip_zero_impedance,
478            )
479        )

LACPF 2n×2n block [[G, -B], [-B, -G]]. skip_zero_impedance as in calc_bprime_matrix().

def calc_adjacency_matrix(self) -> Any:
481    def calc_adjacency_matrix(self):
482        """0/1 bus adjacency matrix."""
483        return _to_csr(self._inner.adjacency())

0/1 bus adjacency matrix.

def calc_admittance_matrix( self, *, include_taps: bool = Ellipsis, include_shifts: bool = Ellipsis, skip_zero_impedance: bool = Ellipsis) -> Any:
485    def calc_admittance_matrix(
486        self,
487        *,
488        include_taps: bool = True,
489        include_shifts: bool = True,
490        skip_zero_impedance: bool = False,
491    ):
492        """``Y_bus = G + jB`` as a complex csr_matrix. ``skip_zero_impedance``
493        as in :meth:`calc_bprime_matrix`."""
494        g, b = self._inner.ybus_parts(
495            include_taps=include_taps,
496            include_shifts=include_shifts,
497            skip_zero_impedance=skip_zero_impedance,
498        )
499        g, b = _to_csr(g), _to_csr(b)
500        return (g + 1j * b).tocsr()

Y_bus = G + jB as a complex csr_matrix. skip_zero_impedance as in calc_bprime_matrix().

def calc_ptdf( self, formula: Literal['series_susceptance', 'tap_adjusted_reactance', 'reactance_only'] = Ellipsis, solver: Literal['auto', 'dense', 'sparse'] = Ellipsis) -> Any:
502    def calc_ptdf(self, formula: str = "series_susceptance", solver: str = "auto"):
503        """DC PTDF (m×n). ``formula`` is ``"series_susceptance"``,
504        ``"tap_adjusted_reactance"``, or ``"reactance_only"``.
505
506        ``solver`` is ``"auto"``, ``"dense"``, or ``"sparse"``. ``"auto"``
507        uses the dense factorization on small cases and the sparse Cholesky
508        path on large ones, the same policy as the CLI.
509        """
510        return _to_csr(self._inner.ptdf(formula, solver))

DC PTDF (m×n). formula is "series_susceptance", "tap_adjusted_reactance", or "reactance_only".

solver is "auto", "dense", or "sparse". "auto" uses the dense factorization on small cases and the sparse Cholesky path on large ones, the same policy as the CLI.

def calc_lodf( self, formula: Literal['series_susceptance', 'tap_adjusted_reactance', 'reactance_only'] = Ellipsis, solver: Literal['auto', 'dense', 'sparse'] = Ellipsis) -> Any:
512    def calc_lodf(self, formula: str = "series_susceptance", solver: str = "auto"):
513        """DC LODF (m×m). ``formula`` and ``solver`` as in :meth:`calc_ptdf`."""
514        return _to_csr(self._inner.lodf(formula, solver))

DC LODF (m×m). formula and solver as in calc_ptdf().

def calc_weighted_laplacian( self, formula: Literal['series_susceptance', 'tap_adjusted_reactance', 'reactance_only'] = Ellipsis) -> Any:
516    def calc_weighted_laplacian(
517        self,
518        formula: str = "series_susceptance",
519    ):
520        """Weighted Laplacian ``L = -B``. ``formula`` as in :meth:`calc_ptdf`."""
521        return _to_csr(self._inner.weighted_laplacian(formula))

Weighted Laplacian L = -B. formula as in calc_ptdf().

def to_normalized( self, *, clamp_angle_bounds: bool = Ellipsis, angle_bound_pad: Optional[float] = Ellipsis) -> BalancedNetwork:
523    def to_normalized(
524        self,
525        *,
526        clamp_angle_bounds: bool = False,
527        angle_bound_pad: Optional[float] = None,
528    ) -> "BalancedNetwork":
529        """Return a normalized copy with per unit power and radian angles.
530
531        The result removes out of service elements, preserves source bus IDs,
532        and normalizes bus types. It carries no retained source, so
533        :func:`powerio.emit` produces a grid exchange representation from the
534        derived module. Raises
535        :class:`PowerIODataError` if the network cannot be
536        normalized (no reference bus can be chosen, or a non-positive base MVA).
537
538        ``clamp_angle_bounds=True`` applies the PowerModels angle difference
539        bound repair: limits at or beyond ``+/-pi/2`` and zero/zero windows
540        become ``[-angle_bound_pad, angle_bound_pad]``. A repair that would
541        invert the interval widens to that same window. The default pad is
542        1.0472 radians.
543        """
544        if not clamp_angle_bounds and angle_bound_pad is None:
545            return BalancedNetwork(self._inner.to_normalized())
546        return BalancedNetwork(
547            self._inner.to_normalized_with_options(
548                clamp_angle_bounds=clamp_angle_bounds, angle_bound_pad=angle_bound_pad
549            )
550        )

Return a normalized copy with per unit power and radian angles.

The result removes out of service elements, preserves source bus IDs, and normalizes bus types. It carries no retained source, so powerio.emit() produces a grid exchange representation from the derived module. Raises PowerIODataError if the network cannot be normalized (no reference bus can be chosen, or a non-positive base MVA).

clamp_angle_bounds=True applies the PowerModels angle difference bound repair: limits at or beyond +/-pi/2 and zero/zero windows become [-angle_bound_pad, angle_bound_pad]. A repair that would invert the interval widens to that same window. The default pad is 1.0472 radians.

def to_ppc(self) -> Dict[str, Any]:
552    def to_ppc(self):
553        """PYPOWER case dict (``ppc``) with MATPOWER-style numpy tables.
554
555        Values are emitted as the model holds them, so a case read from a
556        file carries MW, MVAr, and degrees. A network from
557        :meth:`to_normalized` holds per unit and radians, and those are what
558        its tables carry — PYPOWER reads a ppc dict as MW and degrees, so
559        build this from the raw network unless the consumer expects per unit.
560
561        Loads and shunts are summed onto their bus in the
562        ``PD``/``QD``/``GS``/``BS`` columns, the same aggregation as the
563        MATPOWER emitter. The bus table has no per element status
564        column, so an element the model marks out of service still
565        contributes its value, and a de-energized bus is carried as type 4.
566        ``gencost`` is present only when every generator carries cost data,
567        because MATPOWER requires cost rows for all generators or none.
568        :func:`from_ppc` reads the tables back.
569        """
570        np = _require("numpy", "matrix")
571        buses = self._inner.buses
572        bus = np.array(
573            [
574                (
575                    b["id"],
576                    _PPC_BUS_TYPE.get(b["kind"], 1.0),
577                    0.0,
578                    0.0,
579                    0.0,
580                    0.0,
581                    b["area"],
582                    b["vm"],
583                    b["va"],
584                    b["base_kv"],
585                    b["zone"],
586                    b["vmax"],
587                    b["vmin"],
588                )
589                for b in buses
590            ],
591            dtype=float,
592        ).reshape(len(buses), 13)
593        bus[:, 2], bus[:, 3], bus[:, 4], bus[:, 5] = _bus_sums(
594            np, buses, self._inner.loads, self._inner.shunts
595        )
596
597        # The capability and ramp columns past PMIN are an OPF extension that a
598        # source need not carry. Widen to the full 21 only when a generator
599        # actually states one: a table of zeros there reads back as eleven
600        # explicit zero limits, which a ramp aware solver takes as a generator
601        # that cannot move.
602        gens = self._inner.generators
603        caps = [g["caps"] for g in gens]
604        width = 21 if any(c is not None for row in caps for c in row) else 10
605        gen = np.array(
606            [
607                [
608                    g["bus"],
609                    g["pg"],
610                    g["qg"],
611                    g["qmax"],
612                    g["qmin"],
613                    g["vg"],
614                    g["mbase"],
615                    float(g["in_service"]),
616                    g["pmax"],
617                    g["pmin"],
618                ]
619                + ([0.0 if c is None else c for c in row] if width == 21 else [])
620                for g, row in zip(gens, caps)
621            ],
622            dtype=float,
623        ).reshape(len(gens), width)
624
625        branches = self._inner.branches
626        branch = np.array(
627            [
628                (
629                    br["from_id"],
630                    br["to_id"],
631                    br["r"],
632                    br["x"],
633                    br["b"],
634                    br["rate_a"],
635                    br["rate_b"],
636                    br["rate_c"],
637                    br["tap"],
638                    br["shift"],
639                    float(br["in_service"]),
640                    br["angmin"],
641                    br["angmax"],
642                )
643                for br in branches
644            ],
645            dtype=float,
646        ).reshape(len(branches), 13)
647
648        ppc = {
649            "version": "2",
650            "baseMVA": float(self._inner.base_mva),
651            "bus": bus,
652            "gen": gen,
653            "branch": branch,
654        }
655
656        # Coefficients sit left-aligned after ncost, padded to the widest
657        # row, which is the layout PYPOWER's own loadcase produces.
658        costs = [g["cost"] for g in gens]
659        if costs and all(c is not None for c in costs):
660            gencost = np.zeros((len(costs), 4 + max(len(c["coeffs"]) for c in costs)))
661            for i, c in enumerate(costs):
662                gencost[i, :4] = (
663                    c["model"],
664                    c["startup"],
665                    c["shutdown"],
666                    c["ncost"],
667                )
668                gencost[i, 4 : 4 + len(c["coeffs"])] = c["coeffs"]
669            ppc["gencost"] = gencost
670        return ppc

PYPOWER case dict (ppc) with MATPOWER-style numpy tables.

Values are emitted as the model holds them, so a case read from a file carries MW, MVAr, and degrees. A network from to_normalized() holds per unit and radians, and those are what its tables carry — PYPOWER reads a ppc dict as MW and degrees, so build this from the raw network unless the consumer expects per unit.

Loads and shunts are summed onto their bus in the PD/QD/GS/BS columns, the same aggregation as the MATPOWER emitter. The bus table has no per element status column, so an element the model marks out of service still contributes its value, and a de-energized bus is carried as type 4. gencost is present only when every generator carries cost data, because MATPOWER requires cost rows for all generators or none. from_ppc() reads the tables back.

def to_networkx(self) -> Any:
672    def to_networkx(self):
673        """Undirected networkx graph keyed by bus id.
674
675        In-service branches become edges carrying ``branch`` (index), ``r``,
676        ``x``, and ``b``.
677        """
678        nx = _require("networkx", "graph")
679        g = nx.Graph()
680        g.add_nodes_from(bus["id"] for bus in self._inner.buses)
681        for k, br in enumerate(self._inner.branches):
682            if br["in_service"]:
683                g.add_edge(
684                    br["from_id"],
685                    br["to_id"],
686                    branch=k,
687                    r=br["r"],
688                    x=br["x"],
689                    b=br["b"],
690                )
691        return g

Undirected networkx graph keyed by bus id.

In-service branches become edges carrying branch (index), r, x, and b.

class CalculationUpdate:
data_role
class ComponentId:
local_id
component_type
class DcOpfInstance(_BalancedCalculation):
1056class DcOpfInstance(_BalancedCalculation):
1057    """A DC optimal power flow calculation instance."""

A DC optimal power flow calculation instance.

class DcOpfSolution(_BalancedCalculation, _CalculationSolution):
1090class DcOpfSolution(_BalancedCalculation, _CalculationSolution):
1091    """A DC optimal power flow solution."""

A DC optimal power flow solution.

class DcPfInstance(_BalancedCalculation):
1048class DcPfInstance(_BalancedCalculation):
1049    """A DC power flow calculation instance."""

A DC power flow calculation instance.

class DcPfSolution(_BalancedCalculation, _CalculationSolution):
1082class DcPfSolution(_BalancedCalculation, _CalculationSolution):
1083    """A DC power flow solution."""

A DC power flow solution.

class Diagnostic:

One coded, user facing finding from a parse, read, transform, or write pass: the Python mirror of powerio_core::Diagnostic. Every module carries a list of these; PioModule.diagnostics returns them natively instead of the diagnostics_json string form.

details

Free form structured detail, or None when the finding carries none.

spans
suggested_action
related
target
code
message
severity

"error", "warning", "remark", or "note".

id
class DisplayData(builtins.tuple):

DisplayData(kind, data)

DisplayData( kind: ForwardRef("Literal['powerworld']"), data: ForwardRef('PwdDisplay'))

Create new instance of DisplayData(kind, data)

kind: Literal['powerworld']

Alias for field number 0

data: PwdDisplay

Alias for field number 1

@dataclass(frozen=True)
class EmitResult:
189@dataclass(frozen=True)
190class EmitResult:
191    """Artifact inventory and diagnostics from an emission or serialization."""
192
193    artifacts: tuple[Artifact, ...]
194    layout: str
195    fidelity: str
196    diagnostics: tuple[Diagnostic, ...]
197
198    @property
199    def text(self) -> Optional[str]:
200        """The sole UTF-8 memory artifact, or ``None`` for other inventories."""
201        if len(self.artifacts) != 1 or self.artifacts[0].data is None:
202            return None
203        return self.artifacts[0].text

Artifact inventory and diagnostics from an emission or serialization.

EmitResult( artifacts: Tuple[Artifact, ...], layout: Literal['file', 'directory'], fidelity: Literal['exact_same_format', 'canonical'], diagnostics: Tuple[Diagnostic, ...])
artifacts: Tuple[Artifact, ...]
layout: Literal['file', 'directory']
fidelity: Literal['exact_same_format', 'canonical']
diagnostics: Tuple[Diagnostic, ...]
text: Optional[str]
198    @property
199    def text(self) -> Optional[str]:
200        """The sole UTF-8 memory artifact, or ``None`` for other inventories."""
201        if len(self.artifacts) != 1 or self.artifacts[0].data is None:
202            return None
203        return self.artifacts[0].text

The sole UTF-8 memory artifact, or None for other inventories.

class FormatInfo(builtins.tuple):

FormatInfo(token, extension, is_directory, can_emit)

FormatInfo( token: str, extension: ForwardRef('Optional[str]'), is_directory: bool, can_emit: bool)

Create new instance of FormatInfo(token, extension, is_directory, can_emit)

token: str

Alias for field number 0

extension: Optional[str]

Alias for field number 1

is_directory: bool

Alias for field number 2

can_emit: bool

Alias for field number 3

class GeoLayer(_TypedValue):
1145@_guard_class
1146class GeoLayer(_TypedValue):
1147    """A standalone geographic document: element points and routes keyed by
1148    element identity, in one coordinate space.
1149
1150    :func:`parse` returns it for the canonical ``.geo.json``, GeoJSON, aliased
1151    CSV or JSON records, headerless buscoords CSV, and a PowerWorld ``.pwd``
1152    display. :meth:`PioModule.emit` writes the canonical document as
1153    ``geo-json``, and :func:`serialize` carries the layer through PowerIO IR.
1154    Place a layer onto a case with
1155    ``network.apply_geo_layer(layer.geojson)``.
1156    """
1157
1158    @property
1159    def geojson(self) -> str:
1160        """The canonical ``.geo.json`` document for this layer."""
1161        result = emit(self.module, "geo-json")
1162        data = result.artifacts[0].data
1163        if data is None:
1164            raise ValueError("the layer emission returned no artifact bytes")
1165        return data.decode("utf-8")

A standalone geographic document: element points and routes keyed by element identity, in one coordinate space.

parse() returns it for the canonical .geo.json, GeoJSON, aliased CSV or JSON records, headerless buscoords CSV, and a PowerWorld .pwd display. PioModule.emit() writes the canonical document as geo-json, and serialize() carries the layer through PowerIO IR. Place a layer onto a case with network.apply_geo_layer(layer.geojson).

geojson: str
1158    @property
1159    def geojson(self) -> str:
1160        """The canonical ``.geo.json`` document for this layer."""
1161        result = emit(self.module, "geo-json")
1162        data = result.artifacts[0].data
1163        if data is None:
1164            raise ValueError("the layer emission returned no artifact bytes")
1165        return data.decode("utf-8")

The canonical .geo.json document for this layer.

Inherited Members
_TypedValue
_TypedValue
module
class McAcOpfInstance(_MulticonductorCalculation):
1068class McAcOpfInstance(_MulticonductorCalculation):
1069    """A multiconductor AC optimal power flow calculation instance."""

A multiconductor AC optimal power flow calculation instance.

class McAcOpfSolution(_MulticonductorCalculation, _CalculationSolution):
1106class McAcOpfSolution(_MulticonductorCalculation, _CalculationSolution):
1107    """A multiconductor AC optimal power flow solution."""

A multiconductor AC optimal power flow solution.

class McAcPfInstance(_MulticonductorCalculation):
1064class McAcPfInstance(_MulticonductorCalculation):
1065    """A multiconductor AC power flow calculation instance."""

A multiconductor AC power flow calculation instance.

class McAcPfSolution(_MulticonductorCalculation, _CalculationSolution):
1102class McAcPfSolution(_MulticonductorCalculation, _CalculationSolution):
1103    """A multiconductor AC power flow solution."""

A multiconductor AC power flow solution.

class MulticonductorNetwork:
 28@_guard_class
 29class MulticonductorNetwork:
 30    """A parsed multiconductor distribution network in wire coordinates.
 31
 32    Buses carry named terminals, lines carry conductor impedance matrices, and
 33    transformers carry per winding connections. This type is distinct from the
 34    positive sequence :class:`powerio.BalancedNetwork`; balanced matrix calculations do not
 35    accept it.
 36    """
 37
 38    def __init__(self, inner) -> None:
 39        self._inner = inner
 40
 41    @property
 42    def name(self) -> Optional[str]:
 43        """Distribution network name when the source format carries one."""
 44        return self._inner.name()
 45
 46    @property
 47    def source_format(self) -> Optional[str]:
 48        """Format parsed from: ``dss``, ``pmd-json``, or ``bmopf-json``."""
 49        return self._inner.source_format()
 50
 51    @property
 52    def base_frequency(self) -> float:
 53        """System base frequency in hertz."""
 54        return self._inner.base_frequency()
 55
 56    @property
 57    def n_buses(self) -> int:
 58        return self._inner.n_buses()
 59
 60    @property
 61    def n_lines(self) -> int:
 62        return self._inner.n_lines()
 63
 64    @property
 65    def n_line_codes(self) -> int:
 66        return self._inner.n_line_codes()
 67
 68    @property
 69    def n_switches(self) -> int:
 70        return self._inner.n_switches()
 71
 72    @property
 73    def n_transformers(self) -> int:
 74        return self._inner.n_transformers()
 75
 76    @property
 77    def n_loads(self) -> int:
 78        return self._inner.n_loads()
 79
 80    @property
 81    def n_generators(self) -> int:
 82        return self._inner.n_generators()
 83
 84    @property
 85    def n_ibrs(self) -> int:
 86        return self._inner.n_ibrs()
 87
 88    @property
 89    def n_control_profiles(self) -> int:
 90        return self._inner.n_control_profiles()
 91
 92    @property
 93    def n_shunts(self) -> int:
 94        return self._inner.n_shunts()
 95
 96    @property
 97    def n_capacitors(self) -> int:
 98        return self._inner.n_capacitors()
 99
100    @property
101    def n_voltage_sources(self) -> int:
102        """Number of grid forming voltage sources."""
103        return self._inner.n_voltage_sources()
104
105    @property
106    def n_untyped_objects(self) -> int:
107        return self._inner.n_untyped_objects()
108
109    # These properties are copies of the native model tables. Nested field
110    # names come from the Rust model's serialization, so this wrapper does not
111    # maintain a second distribution schema.
112
113    @property
114    def buses(self) -> "list[dict[str, Any]]":
115        return self._inner.buses()
116
117    @property
118    def line_codes(self) -> "list[dict[str, Any]]":
119        return self._inner.line_codes()
120
121    @property
122    def lines(self) -> "list[dict[str, Any]]":
123        return self._inner.lines()
124
125    @property
126    def switches(self) -> "list[dict[str, Any]]":
127        return self._inner.switches()
128
129    @property
130    def transformers(self) -> "list[dict[str, Any]]":
131        return self._inner.transformers()
132
133    @property
134    def loads(self) -> "list[dict[str, Any]]":
135        return self._inner.loads()
136
137    @property
138    def generators(self) -> "list[dict[str, Any]]":
139        return self._inner.generators()
140
141    @property
142    def ibrs(self) -> "list[dict[str, Any]]":
143        return self._inner.ibrs()
144
145    @property
146    def control_profiles(self) -> "list[dict[str, Any]]":
147        return self._inner.control_profiles()
148
149    @property
150    def shunts(self) -> "list[dict[str, Any]]":
151        return self._inner.shunts()
152
153    @property
154    def capacitors(self) -> "list[dict[str, Any]]":
155        return self._inner.capacitors()
156
157    @property
158    def voltage_sources(self) -> "list[dict[str, Any]]":
159        return self._inner.voltage_sources()
160
161    @property
162    def untyped_objects(self) -> "list[dict[str, Any]]":
163        return self._inner.untyped_objects()
164
165    def to_graph(self) -> Any:
166        """Transform the network to collapsed bus and terminal graph data."""
167        return _json.loads(self._inner.graph_json())
168
169    def to_geo_layer(self) -> Any:
170        """Transform coordinates to a canonical GeoJSON FeatureCollection.
171
172        A network without coordinates produces an empty feature collection.
173        """
174        return _json.loads(self._inner.to_geo_layer_json())
175
176    def apply_geo_layer(
177        self, text: str, name_hint: Optional[str] = None
178    ) -> tuple["MulticonductorNetwork", Any]:
179        """Apply a geographic sidecar and return ``(placed, report)``.
180
181        ``text`` is any form :func:`powerio.parse_geo` accepts. This network
182        is unchanged; the placed copy drops the retained source text, so a
183        same-format emission re-serializes.
184        """
185        inner, report = self._inner.apply_geo_layer(text, name_hint)
186        return MulticonductorNetwork(inner), report
187
188    def __repr__(self) -> str:
189        return self._inner.__repr__()

A parsed multiconductor distribution network in wire coordinates.

Buses carry named terminals, lines carry conductor impedance matrices, and transformers carry per winding connections. This type is distinct from the positive sequence powerio.BalancedNetwork; balanced matrix calculations do not accept it.

MulticonductorNetwork(inner)
38    def __init__(self, inner) -> None:
39        self._inner = inner
name: Optional[str]
41    @property
42    def name(self) -> Optional[str]:
43        """Distribution network name when the source format carries one."""
44        return self._inner.name()

Distribution network name when the source format carries one.

source_format: Optional[str]
46    @property
47    def source_format(self) -> Optional[str]:
48        """Format parsed from: ``dss``, ``pmd-json``, or ``bmopf-json``."""
49        return self._inner.source_format()

Format parsed from: dss, pmd-json, or bmopf-json.

base_frequency: float
51    @property
52    def base_frequency(self) -> float:
53        """System base frequency in hertz."""
54        return self._inner.base_frequency()

System base frequency in hertz.

n_buses: int
56    @property
57    def n_buses(self) -> int:
58        return self._inner.n_buses()
n_lines: int
60    @property
61    def n_lines(self) -> int:
62        return self._inner.n_lines()
n_line_codes: int
64    @property
65    def n_line_codes(self) -> int:
66        return self._inner.n_line_codes()
n_switches: int
68    @property
69    def n_switches(self) -> int:
70        return self._inner.n_switches()
n_transformers: int
72    @property
73    def n_transformers(self) -> int:
74        return self._inner.n_transformers()
n_loads: int
76    @property
77    def n_loads(self) -> int:
78        return self._inner.n_loads()
n_generators: int
80    @property
81    def n_generators(self) -> int:
82        return self._inner.n_generators()
n_ibrs: int
84    @property
85    def n_ibrs(self) -> int:
86        return self._inner.n_ibrs()
n_control_profiles: int
88    @property
89    def n_control_profiles(self) -> int:
90        return self._inner.n_control_profiles()
n_shunts: int
92    @property
93    def n_shunts(self) -> int:
94        return self._inner.n_shunts()
n_capacitors: int
96    @property
97    def n_capacitors(self) -> int:
98        return self._inner.n_capacitors()
n_voltage_sources: int
100    @property
101    def n_voltage_sources(self) -> int:
102        """Number of grid forming voltage sources."""
103        return self._inner.n_voltage_sources()

Number of grid forming voltage sources.

n_untyped_objects: int
105    @property
106    def n_untyped_objects(self) -> int:
107        return self._inner.n_untyped_objects()
buses: list[dict[str, typing.Any]]
113    @property
114    def buses(self) -> "list[dict[str, Any]]":
115        return self._inner.buses()
line_codes: list[dict[str, typing.Any]]
117    @property
118    def line_codes(self) -> "list[dict[str, Any]]":
119        return self._inner.line_codes()
lines: list[dict[str, typing.Any]]
121    @property
122    def lines(self) -> "list[dict[str, Any]]":
123        return self._inner.lines()
switches: list[dict[str, typing.Any]]
125    @property
126    def switches(self) -> "list[dict[str, Any]]":
127        return self._inner.switches()
transformers: list[dict[str, typing.Any]]
129    @property
130    def transformers(self) -> "list[dict[str, Any]]":
131        return self._inner.transformers()
loads: list[dict[str, typing.Any]]
133    @property
134    def loads(self) -> "list[dict[str, Any]]":
135        return self._inner.loads()
generators: list[dict[str, typing.Any]]
137    @property
138    def generators(self) -> "list[dict[str, Any]]":
139        return self._inner.generators()
ibrs: list[dict[str, typing.Any]]
141    @property
142    def ibrs(self) -> "list[dict[str, Any]]":
143        return self._inner.ibrs()
control_profiles: list[dict[str, typing.Any]]
145    @property
146    def control_profiles(self) -> "list[dict[str, Any]]":
147        return self._inner.control_profiles()
shunts: list[dict[str, typing.Any]]
149    @property
150    def shunts(self) -> "list[dict[str, Any]]":
151        return self._inner.shunts()
capacitors: list[dict[str, typing.Any]]
153    @property
154    def capacitors(self) -> "list[dict[str, Any]]":
155        return self._inner.capacitors()
voltage_sources: list[dict[str, typing.Any]]
157    @property
158    def voltage_sources(self) -> "list[dict[str, Any]]":
159        return self._inner.voltage_sources()
untyped_objects: list[dict[str, typing.Any]]
161    @property
162    def untyped_objects(self) -> "list[dict[str, Any]]":
163        return self._inner.untyped_objects()
def to_graph(self) -> Any:
165    def to_graph(self) -> Any:
166        """Transform the network to collapsed bus and terminal graph data."""
167        return _json.loads(self._inner.graph_json())

Transform the network to collapsed bus and terminal graph data.

def to_geo_layer(self) -> Any:
169    def to_geo_layer(self) -> Any:
170        """Transform coordinates to a canonical GeoJSON FeatureCollection.
171
172        A network without coordinates produces an empty feature collection.
173        """
174        return _json.loads(self._inner.to_geo_layer_json())

Transform coordinates to a canonical GeoJSON FeatureCollection.

A network without coordinates produces an empty feature collection.

def apply_geo_layer( self, text: str, name_hint: Optional[str] = None) -> tuple[MulticonductorNetwork, typing.Any]:
176    def apply_geo_layer(
177        self, text: str, name_hint: Optional[str] = None
178    ) -> tuple["MulticonductorNetwork", Any]:
179        """Apply a geographic sidecar and return ``(placed, report)``.
180
181        ``text`` is any form :func:`powerio.parse_geo` accepts. This network
182        is unchanged; the placed copy drops the retained source text, so a
183        same-format emission re-serializes.
184        """
185        inner, report = self._inner.apply_geo_layer(text, name_hint)
186        return MulticonductorNetwork(inner), report

Apply a geographic sidecar and return (placed, report).

text is any form powerio.parse_geo() accepts. This network is unchanged; the placed copy drops the retained source text, so a same-format emission re-serializes.

class NetworkUpdate:
def set_branch_thermal_rating(branch, rating, *, terminal=None):
field
class OperatingPoint(_TypedValue):
1011@_guard_class
1012class OperatingPoint(_TypedValue):
1013    """A possibly partial assignment over fixed equipment identities."""

A possibly partial assignment over fixed equipment identities.

Inherited Members
_TypedValue
_TypedValue
module
class OperatingPointUpdate:
def set_load_active_power(load, p, *, terminal=None):
def set_load_reactive_power(load, q, *, terminal=None):
def set_generator_active_power(generator, p, *, terminal=None):
def set_generator_reactive_power(generator, q, *, terminal=None):
def set_generator_voltage_magnitude(generator, vm_pu):
def set_generator_in_service(generator, in_service):
def set_branch_in_service(branch, in_service):
def set_transformer_tap_ratio(transformer, tap_ratio):
def set_transformer_phase_shift(transformer, shift_degrees):
def set_switch_closed(switch, closed):
field
class PioModule:
1191@_guard_class
1192class PioModule:
1193    """One typed value with diagnostics, producer, sources, source mappings,
1194    history, and extensions.
1195    """
1196
1197    def __init__(self, inner: "_powerio._PioModule"):
1198        self._inner = inner
1199
1200    @classmethod
1201    def from_value(cls, value: Any) -> "PioModule":
1202        """Wrap an existing typed value without serializing it."""
1203        if isinstance(value, BalancedNetwork):
1204            return cls(_powerio._PioModule.from_balanced_network(value._inner))
1205        if isinstance(value, dist.MulticonductorNetwork):
1206            return cls(_powerio._PioModule.from_multiconductor_network(value._inner))
1207        if isinstance(value, _TypedValue):
1208            location = value._collection_entry
1209            inner = value.module._inner
1210            if location is not None:
1211                inner = location.root._inner
1212                if location.scenario_id is not None:
1213                    inner = inner._scenario_get(location.scenario_id)
1214                if location.time_index is not None:
1215                    inner = inner._time_series_get(location.time_index)
1216            return cls(inner._copy())
1217        raise TypeError("PioModule.from_value expects a typed PowerIO value")
1218
1219    @property
1220    def value(self) -> Any:
1221        """The contained typed value."""
1222        type_name = self._inner._type_name
1223        if type_name == "powerio.BalancedNetwork":
1224            return BalancedNetwork(self._inner.as_balanced_network())
1225        if type_name == "powerio.MulticonductorNetwork":
1226            return dist.MulticonductorNetwork(self._inner.as_multiconductor_network())
1227        if type_name.startswith("powerio.TimeSeries<"):
1228            return TimeSeries._from_module(self)
1229        if type_name.startswith("powerio.ScenarioSet<"):
1230            return ScenarioSet._from_module(self)
1231        value_class = _VALUE_CLASSES.get(type_name)
1232        if value_class is None:
1233            raise RuntimeError(f"this binding has no Python class for {type_name}")
1234        return value_class(self)
1235
1236    @property
1237    def diagnostics(self) -> list[Diagnostic]:
1238        """The diagnostics stored on this module, in encounter order."""
1239        return list(self._inner.diagnostics)
1240
1241    def to_balanced_report(self, base_mva: float = 100.0) -> Any:
1242        """Report whether a multiconductor network can become balanced."""
1243        return _json.loads(self._inner.lowering_readiness_json(base_mva))
1244
1245    def to_balanced(self, base_mva: float = 100.0) -> "PioModule":
1246        """Transform a multiconductor network to a balanced module."""
1247        return PioModule(self._inner.lower_to_balanced(base_mva))
1248
1249    def to_dc_pf_instance(self) -> "PioModule":
1250        """Build a DC power flow instance from a balanced network module."""
1251        return PioModule(self._inner._to_dc_pf_instance())
1252
1253    def to_ac_pf_instance(self) -> "PioModule":
1254        """Build an AC power flow instance from a balanced network module."""
1255        return PioModule(self._inner._to_ac_pf_instance())
1256
1257    def to_dc_opf_instance(self) -> "PioModule":
1258        """Build a DC optimal power flow instance from a balanced network module."""
1259        return PioModule(self._inner._to_dc_opf_instance())
1260
1261    def to_ac_opf_instance(self) -> "PioModule":
1262        """Build an AC optimal power flow instance from a balanced network module."""
1263        return PioModule(self._inner._to_ac_opf_instance())
1264
1265    def to_mc_ac_pf_instance(self) -> "PioModule":
1266        """Build a multiconductor AC power flow instance from a network module."""
1267        return PioModule(self._inner._to_mc_ac_pf_instance())
1268
1269    def to_mc_ac_opf_instance(self) -> "PioModule":
1270        """Build a multiconductor AC optimal power flow instance from a network module."""
1271        return PioModule(self._inner._to_mc_ac_opf_instance())
1272
1273    def __repr__(self) -> str:
1274        return repr(self._inner)

Abstract base class for generic types.

On Python 3.12 and newer, generic classes implicitly inherit from Generic when they declare a parameter list after the class's name::

class Mapping[KT, VT]:
    def __getitem__(self, key: KT) -> VT:
        ...
    # Etc.

On older versions of Python, however, generic classes have to explicitly inherit from Generic.

After a class has been declared to be generic, it can then be used as follows::

def lookup_name[KT, VT](mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:
    try:
        return mapping[key]
    except KeyError:
        return default
PioModule(inner: Any)
1197    def __init__(self, inner: "_powerio._PioModule"):
1198        self._inner = inner
@classmethod
def from_value(*args, **kwds):
1200    @classmethod
1201    def from_value(cls, value: Any) -> "PioModule":
1202        """Wrap an existing typed value without serializing it."""
1203        if isinstance(value, BalancedNetwork):
1204            return cls(_powerio._PioModule.from_balanced_network(value._inner))
1205        if isinstance(value, dist.MulticonductorNetwork):
1206            return cls(_powerio._PioModule.from_multiconductor_network(value._inner))
1207        if isinstance(value, _TypedValue):
1208            location = value._collection_entry
1209            inner = value.module._inner
1210            if location is not None:
1211                inner = location.root._inner
1212                if location.scenario_id is not None:
1213                    inner = inner._scenario_get(location.scenario_id)
1214                if location.time_index is not None:
1215                    inner = inner._time_series_get(location.time_index)
1216            return cls(inner._copy())
1217        raise TypeError("PioModule.from_value expects a typed PowerIO value")

Wrap an existing typed value without serializing it.

value: ~_T
1219    @property
1220    def value(self) -> Any:
1221        """The contained typed value."""
1222        type_name = self._inner._type_name
1223        if type_name == "powerio.BalancedNetwork":
1224            return BalancedNetwork(self._inner.as_balanced_network())
1225        if type_name == "powerio.MulticonductorNetwork":
1226            return dist.MulticonductorNetwork(self._inner.as_multiconductor_network())
1227        if type_name.startswith("powerio.TimeSeries<"):
1228            return TimeSeries._from_module(self)
1229        if type_name.startswith("powerio.ScenarioSet<"):
1230            return ScenarioSet._from_module(self)
1231        value_class = _VALUE_CLASSES.get(type_name)
1232        if value_class is None:
1233            raise RuntimeError(f"this binding has no Python class for {type_name}")
1234        return value_class(self)

The contained typed value.

diagnostics: List[Diagnostic]
1236    @property
1237    def diagnostics(self) -> list[Diagnostic]:
1238        """The diagnostics stored on this module, in encounter order."""
1239        return list(self._inner.diagnostics)

The diagnostics stored on this module, in encounter order.

def to_balanced_report(self, base_mva: float = Ellipsis) -> Any:
1241    def to_balanced_report(self, base_mva: float = 100.0) -> Any:
1242        """Report whether a multiconductor network can become balanced."""
1243        return _json.loads(self._inner.lowering_readiness_json(base_mva))

Report whether a multiconductor network can become balanced.

def to_balanced( self, base_mva: float = Ellipsis) -> PioModule[BalancedNetwork]:
1245    def to_balanced(self, base_mva: float = 100.0) -> "PioModule":
1246        """Transform a multiconductor network to a balanced module."""
1247        return PioModule(self._inner.lower_to_balanced(base_mva))

Transform a multiconductor network to a balanced module.

def to_dc_pf_instance(self) -> PioModule[DcPfInstance]:
1249    def to_dc_pf_instance(self) -> "PioModule":
1250        """Build a DC power flow instance from a balanced network module."""
1251        return PioModule(self._inner._to_dc_pf_instance())

Build a DC power flow instance from a balanced network module.

def to_ac_pf_instance(self) -> PioModule[AcPfInstance]:
1253    def to_ac_pf_instance(self) -> "PioModule":
1254        """Build an AC power flow instance from a balanced network module."""
1255        return PioModule(self._inner._to_ac_pf_instance())

Build an AC power flow instance from a balanced network module.

def to_dc_opf_instance(self) -> PioModule[DcOpfInstance]:
1257    def to_dc_opf_instance(self) -> "PioModule":
1258        """Build a DC optimal power flow instance from a balanced network module."""
1259        return PioModule(self._inner._to_dc_opf_instance())

Build a DC optimal power flow instance from a balanced network module.

def to_ac_opf_instance(self) -> PioModule[AcOpfInstance]:
1261    def to_ac_opf_instance(self) -> "PioModule":
1262        """Build an AC optimal power flow instance from a balanced network module."""
1263        return PioModule(self._inner._to_ac_opf_instance())

Build an AC optimal power flow instance from a balanced network module.

def to_mc_ac_pf_instance(self) -> PioModule[McAcPfInstance]:
1265    def to_mc_ac_pf_instance(self) -> "PioModule":
1266        """Build a multiconductor AC power flow instance from a network module."""
1267        return PioModule(self._inner._to_mc_ac_pf_instance())

Build a multiconductor AC power flow instance from a network module.

def to_mc_ac_opf_instance(self) -> PioModule[McAcOpfInstance]:
1269    def to_mc_ac_opf_instance(self) -> "PioModule":
1270        """Build a multiconductor AC optimal power flow instance from a network module."""
1271        return PioModule(self._inner._to_mc_ac_opf_instance())

Build a multiconductor AC optimal power flow instance from a network module.

class PowerIODataError(PowerIOError):

A well-formed case cannot satisfy a requested operation.

A refused pass (e.g. PioModule.to_balanced()) additionally sets diagnostics: the pass's structured findings, each a dict with code, severity, message, and target. Absent on a PowerIODataError raised elsewhere.

class PowerIOError(builtins.ValueError):

Base error from the powerio parser, emitter, or matrix calculations.

Failures mapped from the Rust core carry the diagnostic code string as code; it is set at raise time, so it is instance-only.

class PowerIOParseError(PowerIOError):

A case file is malformed or unparseable.

class PwdDisplay(builtins.tuple):

PwdDisplay(canvas_width, canvas_height, stamp, substations)

PwdDisplay( canvas_width: int, canvas_height: int, stamp: int, substations: ForwardRef('List[PwdSubstation]'))

Create new instance of PwdDisplay(canvas_width, canvas_height, stamp, substations)

canvas_width: int

Alias for field number 0

canvas_height: int

Alias for field number 1

stamp: int

Alias for field number 2

substations: List[PwdSubstation]

Alias for field number 3

class PwdSubstation(builtins.tuple):

PwdSubstation(number, name, x, y)

PwdSubstation(number: int, name: str, x: float, y: float)

Create new instance of PwdSubstation(number, name, x, y)

number: int

Alias for field number 0

name: str

Alias for field number 1

x: float

Alias for field number 2

y: float

Alias for field number 3

class ReactivePower:
def vars(value):
def megavars(value):
value
unit
class Residuals:
max_reactive_power_mismatch
max_active_power_mismatch
@dataclass(frozen=True)
class Scenario:
860@dataclass(frozen=True)
861class Scenario:
862    id: str
863    probability: Optional[float] = None
Scenario(id: str, probability: Optional[float] = Ellipsis)
id: str
probability: Optional[float] = None
class ScenarioSet(_TypedValue, collections.abc.Mapping):
 948@_guard_class
 949class ScenarioSet(_TypedValue, Mapping):
 950    """Named alternatives of one type, with optional probabilities."""
 951
 952    def __init__(
 953        self,
 954        values: Mapping[str, Any],
 955        *,
 956        probabilities: Optional[Mapping[str, float]] = None,
 957    ) -> None:
 958        if not isinstance(values, Mapping):
 959            raise TypeError("ScenarioSet values must be a mapping from IDs to values")
 960        if probabilities is not None and not isinstance(probabilities, Mapping):
 961            raise TypeError("probabilities must be a mapping from scenario IDs to numbers")
 962        ids = list(values)
 963        modules = [PioModule.from_value(values[id])._inner for id in ids]
 964        inner = _powerio._PioModule._from_scenario_set(
 965            modules,
 966            ids,
 967            None if probabilities is None else dict(probabilities),
 968        )
 969        super().__init__(PioModule(inner))
 970
 971    @classmethod
 972    def _from_module(cls, module: "PioModule") -> "ScenarioSet":
 973        value = object.__new__(cls)
 974        _TypedValue.__init__(value, module)
 975        return value
 976
 977    @property
 978    def scenarios(self) -> tuple[Scenario, ...]:
 979        return tuple(Scenario(*entry) for entry in self.module._inner._scenario_entries())
 980
 981    def __len__(self) -> int:
 982        return len(self.scenarios)
 983
 984    def __iter__(self):
 985        return (scenario.id for scenario in self.scenarios)
 986
 987    def __contains__(self, scenario: object) -> bool:
 988        return isinstance(scenario, str) and any(
 989            entry.id == scenario for entry in self.scenarios
 990        )
 991
 992    def __getitem__(self, scenario: str) -> Any:
 993        if not isinstance(scenario, str):
 994            raise TypeError("scenario keys must be strings")
 995        if scenario not in self:
 996            raise KeyError(scenario)
 997        current = self._collection_entry or _CollectionEntry(self.module)
 998        if current.scenario_id is not None:
 999            raise TypeError("nested ScenarioSet values are not supported")
1000        value = PioModule(self.module._inner._scenario_get(scenario)).value
1001        return _bind_collection_entry(
1002            value,
1003            _CollectionEntry(
1004                root=current.root,
1005                time_index=current.time_index,
1006                scenario_id=scenario,
1007            ),
1008        )

A Mapping is a generic container for associating key/value pairs.

This class provides concrete generic implementations of all methods except for __getitem__, __iter__, and __len__.

ScenarioSet(module: PioModule[typing.Any])
952    def __init__(
953        self,
954        values: Mapping[str, Any],
955        *,
956        probabilities: Optional[Mapping[str, float]] = None,
957    ) -> None:
958        if not isinstance(values, Mapping):
959            raise TypeError("ScenarioSet values must be a mapping from IDs to values")
960        if probabilities is not None and not isinstance(probabilities, Mapping):
961            raise TypeError("probabilities must be a mapping from scenario IDs to numbers")
962        ids = list(values)
963        modules = [PioModule.from_value(values[id])._inner for id in ids]
964        inner = _powerio._PioModule._from_scenario_set(
965            modules,
966            ids,
967            None if probabilities is None else dict(probabilities),
968        )
969        super().__init__(PioModule(inner))
scenarios: Tuple[Scenario, ...]
977    @property
978    def scenarios(self) -> tuple[Scenario, ...]:
979        return tuple(Scenario(*entry) for entry in self.module._inner._scenario_entries())
Inherited Members
_TypedValue
module
class ScucActiveReserveZone:
regulation_down_violation_cost
ramping_down_violation_cost
ramping_up_violation_cost
nonsynchronized_violation_cost
regulation_up_requirement_fraction
nonsynchronized_requirement_fraction
ramping_down_requirement
regulation_up_violation_cost
synchronized_violation_cost
buses
id
ramping_up_requirement
regulation_down_requirement_fraction
synchronized_requirement_fraction
class ScucBranchSwitchingCost:
disconnection_cost
id
connection_cost
class ScucContingency:
components
id
class ScucDevice:
on_cost
startup_cost
startup_limits
initial_commitment
shutdown_cost
reactive_capability
energy_lower_bounds
kind
initial_on_status
startup_cost_adjustments
periods
id
minimum_up_time
minimum_down_time
energy_upper_bounds
ramp_limits
reserve_limits
class ScucDeviceOutputs:
shutdown_status
p_reg_res_down
on_status
p_ramp_res_up_offline
q_res_down
startup_status
p_ramp_res_up_online
p_on
p_ramp_res_down_online
q_res_up
p_reg_res_up
p_nsyn_res
p_ramp_res_down_offline
q
p_syn_res
class ScucDevicePeriod:
reactive_power_max
reactive_power_min
on_status_min
on_status_max
energy_cost_blocks
active_power_min
reserve_costs
active_power_max
class ScucEnergyCostBlock:
marginal_cost
block_size
class ScucEnergyRequirement:
start_time
end_time
energy
class ScucInitialCommitment:
accumulated_up_time
accumulated_down_time
class ScucInputs:
branch_switching_costs
shunts
active_reserve_zones
reactive_reserve_zones
contingencies
interval_durations
violation_costs
transformer_controls
devices
class ScucNetworkOutputs:
dc_line_qdc_to
transformer_ta
ac_line_on_status
bus_vm
shunt_step
transformer_on_status
transformer_tm
dc_line_qdc_fr
bus_va
dc_line_pdc_fr
class ScucRampLimits:
shutdown
down
startup
up
class ScucReactiveCapability:
slope_min
reactive_power_at_zero_active_power_min
slope_max
kind
slope
reactive_power_at_zero_active_power
reactive_power_at_zero_active_power_max
class ScucReactiveReserveZone:
reactive_down_requirement
id
reactive_up_requirement
reactive_up_violation_cost
buses
reactive_down_violation_cost
class ScucReserveCosts:
regulation_up
ramping_up_online
reactive_up
ramping_down_online
regulation_down
nonsynchronized
ramping_down_offline
reactive_down
ramping_up_offline
synchronized
class ScucReserveLimits:
ramping_up_offline
synchronized
ramping_down_online
ramping_up_online
ramping_down_offline
nonsynchronized
regulation_up
regulation_down
class ScucShunt:
susceptance_per_step
initial_step
conductance_per_step
step_max
id
step_min
class ScucStartupCostAdjustment:
cost
maximum_down_time
class ScucStartupLimit:
end_time
start_time
maximum_startups
class ScucTransformerControl:
tap_ratio_max
phase_shift_min
id
tap_ratio_min
phase_shift_max
class ScucViolationCosts:
energy_requirement
active_power_balance
branch_thermal_limit
reactive_power_balance
class SocwrOpfSolution(_BalancedCalculation, _CalculationSolution):
1098class SocwrOpfSolution(_BalancedCalculation, _CalculationSolution):
1099    """A PowerModels SOCWR relaxation solution and objective lower bound."""

A PowerModels SOCWR relaxation solution and objective lower bound.

class SourceSpan:

One source byte range a diagnostic points at: the Python mirror of powerio_core::SourceSpan. source is the source ID string, not the bytes themselves; a caller resolves it against the owning module's sources.

source
byte_start
byte_end
@dataclass(frozen=True)
class TimePoint:
854@dataclass(frozen=True)
855class TimePoint:
856    label: str
857    duration_seconds: Optional[float] = None
TimePoint(label: str, duration_seconds: Optional[float] = Ellipsis)
label: str
duration_seconds: Optional[float] = None
class TimeSeries(_TypedValue, collections.abc.Sequence):
881@_guard_class
882class TimeSeries(_TypedValue, Sequence):
883    """Values of one type ordered in time."""
884
885    def __init__(
886        self,
887        values: Sequence[Any],
888        *,
889        time_points: Sequence[TimePoint],
890    ) -> None:
891        if isinstance(values, (str, bytes, bytearray)) or not isinstance(
892            values, Sequence
893        ):
894            raise TypeError("TimeSeries values must be a sequence of PowerIO values")
895        if not isinstance(time_points, Sequence):
896            raise TypeError("time_points must be a sequence of TimePoint values")
897        points = tuple(time_points)
898        if not all(isinstance(point, TimePoint) for point in points):
899            raise TypeError("time_points must contain only TimePoint values")
900        modules = [PioModule.from_value(value)._inner for value in values]
901        inner = _powerio._PioModule._from_time_series(
902            modules,
903            [(point.label, point.duration_seconds) for point in points],
904        )
905        super().__init__(PioModule(inner))
906
907    @classmethod
908    def _from_module(cls, module: "PioModule") -> "TimeSeries":
909        value = object.__new__(cls)
910        _TypedValue.__init__(value, module)
911        return value
912
913    @property
914    def time_points(self) -> tuple[TimePoint, ...]:
915        return tuple(TimePoint(*point) for point in self.module._inner._time_series_points())
916
917    def __len__(self) -> int:
918        return self.module._inner._time_series_len()
919
920    def __getitem__(self, index):
921        if isinstance(index, slice):
922            return [self[position] for position in range(*index.indices(len(self)))]
923        try:
924            position = _operator.index(index)
925        except TypeError:
926            raise TypeError("time series indices must be integers") from None
927        if position < 0:
928            position += len(self)
929        if position < 0 or position >= len(self):
930            raise IndexError("time series index out of range")
931        current = self._collection_entry or _CollectionEntry(self.module)
932        if current.time_index is not None:
933            raise TypeError("nested TimeSeries values are not supported")
934        value = PioModule(self.module._inner._time_series_get(position)).value
935        return _bind_collection_entry(
936            value,
937            _CollectionEntry(
938                root=current.root,
939                time_index=position,
940                scenario_id=current.scenario_id,
941            ),
942        )
943
944    def __iter__(self):
945        return (self[position] for position in range(len(self)))

All the operations on a read-only sequence.

Concrete subclasses must override __new__ or __init__, __getitem__, and __len__.

TimeSeries(module: PioModule[typing.Any])
885    def __init__(
886        self,
887        values: Sequence[Any],
888        *,
889        time_points: Sequence[TimePoint],
890    ) -> None:
891        if isinstance(values, (str, bytes, bytearray)) or not isinstance(
892            values, Sequence
893        ):
894            raise TypeError("TimeSeries values must be a sequence of PowerIO values")
895        if not isinstance(time_points, Sequence):
896            raise TypeError("time_points must be a sequence of TimePoint values")
897        points = tuple(time_points)
898        if not all(isinstance(point, TimePoint) for point in points):
899            raise TypeError("time_points must contain only TimePoint values")
900        modules = [PioModule.from_value(value)._inner for value in values]
901        inner = _powerio._PioModule._from_time_series(
902            modules,
903            [(point.label, point.duration_seconds) for point in points],
904        )
905        super().__init__(PioModule(inner))
time_points: Tuple[TimePoint, ...]
913    @property
914    def time_points(self) -> tuple[TimePoint, ...]:
915        return tuple(TimePoint(*point) for point in self.module._inner._time_series_points())
Inherited Members
_TypedValue
module
class UpdateChange:
field
terminal
component_id
class UpdateReport:
changes
connectivity_changed
__version__: str = '0.11.0'
def apply_bus_load_active_power( module: PioModule[typing.Any], bus_id: int, total: ActivePower, *, allocation: Literal['equal', 'proportional_to_current_active_power'] = Ellipsis) -> UpdateReport:
1344@_guard
1345def apply_bus_load_active_power(
1346    module: PioModule,
1347    bus_id: int,
1348    total: ActivePower,
1349    *,
1350    allocation: str = "proportional_to_current_active_power",
1351) -> UpdateReport:
1352    """Replace aggregate bus demand through an explicit PowerIO allocation rule.
1353
1354    ``"proportional_to_current_active_power"`` preserves each participating
1355    load's current share. ``"equal"`` gives every participating load the same
1356    share, including when their current aggregate demand is zero. PowerIO
1357    requires stable load IDs and reports each load changed.
1358    """
1359    if not isinstance(module, PioModule):
1360        raise TypeError("module must be a PioModule")
1361    if not isinstance(total, ActivePower):
1362        raise TypeError("total must be an ActivePower")
1363    return module._inner._apply_bus_load_active_power(
1364        bus_id,
1365        total,
1366        allocation=allocation,
1367    )

Replace aggregate bus demand through an explicit PowerIO allocation rule.

"proportional_to_current_active_power" preserves each participating load's current share. "equal" gives every participating load the same share, including when their current aggregate demand is zero. PowerIO requires stable load IDs and reports each load changed.

def apply_updates( target: Union[PioModule[Any], BalancedNetwork, MulticonductorNetwork, powerio._TypedValue], updates: Union[Iterable[OperatingPointUpdate], Iterable[NetworkUpdate], Iterable[CalculationUpdate]]) -> UpdateReport:
1310@_guard
1311def apply_updates(
1312    target: Any,
1313    updates: Union[
1314        Iterable[OperatingPointUpdate],
1315        Iterable[NetworkUpdate],
1316        Iterable[CalculationUpdate],
1317    ],
1318) -> UpdateReport:
1319    """Validate and apply one batch of typed updates atomically.
1320
1321    ``updates`` contains one update class: :class:`OperatingPointUpdate`,
1322    :class:`NetworkUpdate`, or :class:`CalculationUpdate`. Values are absolute
1323    replacements and power quantities carry their units in the typed value.
1324    ``target`` is a module or a value obtained by indexing a :class:`TimeSeries`
1325    or :class:`ScenarioSet`. If validation fails, the module is unchanged.
1326    """
1327    batch = list(updates)
1328    if isinstance(target, PioModule):
1329        return target._inner._apply_updates(batch)
1330    location = getattr(target, "_collection_entry", None)
1331    if not isinstance(location, _CollectionEntry):
1332        raise TypeError(
1333            "target must be a PioModule or a TimeSeries/ScenarioSet entry"
1334        )
1335    report = location.root._inner._apply_collection_updates(
1336        batch,
1337        time_index=location.time_index,
1338        scenario_id=location.scenario_id,
1339    )
1340    _refresh_collection_entry(target, location)
1341    return report

Validate and apply one batch of typed updates atomically.

updates contains one update class: OperatingPointUpdate, NetworkUpdate, or CalculationUpdate. Values are absolute replacements and power quantities carry their units in the typed value. target is a module or a value obtained by indexing a TimeSeries or ScenarioSet. If validation fails, the module is unchanged.

def deserialize(source: Any) -> PioModule[typing.Any]:
1505@_guard
1506def deserialize(source: Any) -> PioModule:
1507    """Deserialize PowerIO IR from a path, file object, or bytes-like source."""
1508    path = _path_from_source(source)
1509    if path is not None:
1510        return PioModule(_powerio._PioModule._deserialize_path(path))
1511    data, _ = _memory_from_source(source, None)
1512    return PioModule(_powerio._PioModule._deserialize_memory(data))

Deserialize PowerIO IR from a path, file object, or bytes-like source.

def emit( module: PioModule[typing.Any], format: str, destination: Optional[Any] = Ellipsis) -> EmitResult:
1483@_guard
1484def emit(module: PioModule, format: str, destination: Optional[Any] = None) -> EmitResult:
1485    """Emit a module as one grid exchange format."""
1486    return _emit_to_destination(
1487        module,
1488        destination,
1489        lambda: module._inner._emit_memory(format),
1490        lambda path: module._inner._emit_path(format, path),
1491    )

Emit a module as one grid exchange format.

def features() -> Dict[str, bool]:
1515@_guard
1516def features() -> dict[str, bool]:
1517    """The build-time features compiled into this powerio installation.
1518
1519    ``matrix``, ``dist``, and ``prob`` are unconditional dependencies of the
1520    extension and are always ``True``. ``gridfm`` reports whether GridFM
1521    Parquet parsing and emission were compiled in; the published wheel
1522    includes them, while a custom source build can omit them.
1523    """
1524    return {
1525        "matrix": True,
1526        "gridfm": bool(getattr(_powerio, "_has_gridfm", False)),
1527        "dist": True,
1528        "prob": True,
1529    }

The build-time features compiled into this powerio installation.

matrix, dist, and prob are unconditional dependencies of the extension and are always True. gridfm reports whether GridFM Parquet parsing and emission were compiled in; the published wheel includes them, while a custom source build can omit them.

def from_ppc(ppc: Dict[str, Any]) -> BalancedNetwork:
806@_guard
807def from_ppc(ppc) -> BalancedNetwork:
808    """Case from a PYPOWER dict (``ppc``); the inverse of :meth:`BalancedNetwork.to_ppc`.
809
810    The tables route through the MATPOWER reader, so the semantics match a
811    ``.m`` case exactly: bus ``PD``/``QD`` become loads, ``GS``/``BS`` become
812    shunts, and ``gencost`` is read when present. Result columns past the
813    MATPOWER input widths are dropped. A 10 column ``gen`` table (the layout
814    without the OPF capability columns) passes through at its own width, so
815    the generators come back with no capability limits rather than eleven
816    zero ones. Raises :class:`ValueError` when a required table is absent,
817    when a ``bus`` or ``branch`` row is below its 13 column width, when a row
818    is not a sequence of numbers, or when a cell is not numeric; the message
819    names the table and the row.
820    """
821    value = parse(
822        _io.StringIO(_ppc_to_matpower_text(ppc)),
823        format="matpower",
824        name="from_ppc.m",
825    ).value
826    assert isinstance(value, BalancedNetwork)
827    return value

Case from a PYPOWER dict (ppc); the inverse of BalancedNetwork.to_ppc().

The tables route through the MATPOWER reader, so the semantics match a .m case exactly: bus PD/QD become loads, GS/BS become shunts, and gencost is read when present. Result columns past the MATPOWER input widths are dropped. A 10 column gen table (the layout without the OPF capability columns) passes through at its own width, so the generators come back with no capability limits rather than eleven zero ones. Raises ValueError when a required table is absent, when a bus or branch row is below its 13 column width, when a row is not a sequence of numbers, or when a cell is not numeric; the message names the table and the row.

def parse( source: Any, *, format: Optional[str] = Ellipsis, name: Optional[str] = Ellipsis) -> PioModule[typing.Any]:
1412@_guard
1413def parse(
1414    source: Any,
1415    *,
1416    format: Optional[str] = None,
1417    name: Optional[str] = None,
1418) -> PioModule:
1419    """Parse a path, file object, or bytes-like source.
1420
1421    A string is always a path. Pass raw text through ``io.StringIO`` or
1422    another file object.
1423    """
1424    path = _path_from_source(source)
1425    if path is not None:
1426        if name is not None:
1427            raise ValueError("name is only valid for memory and file object sources")
1428        return PioModule(_powerio._PioModule._parse_path(path, format))
1429    data, source_name = _memory_from_source(source, name)
1430    return PioModule(_powerio._PioModule._parse_memory(data, source_name, format))

Parse a path, file object, or bytes-like source.

A string is always a path. Pass raw text through io.StringIO or another file object.

def parse_display(path: Any, format: Optional[str] = Ellipsis) -> DisplayData:
694@_guard
695def parse_display(path: Any, format: Optional[str] = None) -> DisplayData:
696    """Parse a display artifact such as a PowerWorld ``.pwd`` file."""
697    return _wrap_display(_powerio.parse_display(str(path), format))

Parse a display artifact such as a PowerWorld .pwd file.

def parse_geo(text: str, name_hint: Optional[str] = Ellipsis) -> Dict[str, Any]:
707@_guard
708def parse_geo(text: str, name_hint: Optional[str] = None) -> dict[str, Any]:
709    """Tolerantly read a geographic sidecar and return its canonical form.
710
711    Accepts headerless buscoords CSV, aliased CSV/JSON records, and GeoJSON
712    Point/LineString features. Returns ``{"geojson": <FeatureCollection dict>,
713    "diagnostics": [...]}``; ``name_hint`` (a file name) picks CSV against JSON
714    when the content alone is ambiguous. Input with no usable coordinates
715    raises :class:`PowerIOParseError`.
716    """
717    parsed = _powerio.parse_geo(text, name_hint)
718    parsed["geojson"] = _json.loads(parsed["geojson"])
719    return parsed

Tolerantly read a geographic sidecar and return its canonical form.

Accepts headerless buscoords CSV, aliased CSV/JSON records, and GeoJSON Point/LineString features. Returns {"geojson": <FeatureCollection dict>, "diagnostics": [...]}; name_hint (a file name) picks CSV against JSON when the content alone is ambiguous. Input with no usable coordinates raises PowerIOParseError.

def resolve_format(name: str) -> Optional[FormatInfo]:
700@_guard
701def resolve_format(name: str) -> Optional[FormatInfo]:
702    """Resolve a format token or common alias to its canonical metadata."""
703    resolved = _powerio.resolve_format(name)
704    return None if resolved is None else FormatInfo(*resolved)

Resolve a format token or common alias to its canonical metadata.

def serialize( module: PioModule[typing.Any], destination: Optional[Any] = Ellipsis) -> EmitResult:
1494@_guard
1495def serialize(module: PioModule, destination: Optional[Any] = None) -> EmitResult:
1496    """Serialize a module as PowerIO IR."""
1497    return _emit_to_destination(
1498        module,
1499        destination,
1500        module._inner._serialize_memory,
1501        module._inner._serialize_path,
1502    )

Serialize a module as PowerIO IR.

def versions() -> Any:
835@_guard
836def versions() -> Any:
837    """Return the PowerIO release, sole IR identity, and BMOPF schema."""
838    return _json.loads(_powerio.versions_json())

Return the PowerIO release, sole IR identity, and BMOPF schema.