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

An AC optimal power flow calculation instance.

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

An AC optimal power flow solution.

class AcPfInstance(_BalancedCalculation):
1197class AcPfInstance(_BalancedCalculation):
1198    """An AC power flow calculation instance."""

An AC power flow calculation instance.

class AcPfSolution(_BalancedCalculation, _CalculationSolution):
1241class AcPfSolution(_BalancedCalculation, _CalculationSolution):
1242    """An AC power flow solution."""

An AC power flow solution.

class AcScucInstance(_BalancedCalculation):
1227@_guard_class
1228class AcScucInstance(_BalancedCalculation):
1229    """An AC security constrained unit commitment calculation instance."""
1230
1231    @property
1232    def inputs(self) -> ScucInputs:
1233        """Scheduling, reserve, and contingency inputs."""
1234        return self.module._inner._ac_scuc_inputs()

An AC security constrained unit commitment calculation instance.

inputs: ScucInputs
1231    @property
1232    def inputs(self) -> ScucInputs:
1233        """Scheduling, reserve, and contingency inputs."""
1234        return self.module._inner._ac_scuc_inputs()

Scheduling, reserve, and contingency inputs.

class AcScucSolution(_BalancedCalculation, _CalculationSolution):
1284@_guard_class
1285class AcScucSolution(_BalancedCalculation, _CalculationSolution):
1286    """An AC security constrained unit commitment solution."""
1287
1288    @property
1289    def termination(self) -> str:
1290        """How the calculation ended."""
1291        return self.module._inner._ac_scuc_solution_termination()
1292
1293    @property
1294    def residuals(self) -> Residuals:
1295        """Reported active and reactive power balance residuals."""
1296        return self.module._inner._ac_scuc_solution_residuals()
1297
1298    @property
1299    def producer(self) -> Optional[str]:
1300        """Producer or solver identity, when recorded."""
1301        return self.module._inner._ac_scuc_solution_producer()
1302
1303    @property
1304    def network_outputs(self) -> ScucNetworkOutputs:
1305        """Per interval network outputs."""
1306        return self.module._inner._ac_scuc_solution_network_outputs()
1307
1308    @property
1309    def device_outputs(self) -> ScucDeviceOutputs:
1310        """Per interval dispatchable device outputs."""
1311        return self.module._inner._ac_scuc_solution_device_outputs()
1312
1313    @property
1314    def objective(self) -> Optional[float]:
1315        """Reported objective value, when present."""
1316        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']
1288    @property
1289    def termination(self) -> str:
1290        """How the calculation ended."""
1291        return self.module._inner._ac_scuc_solution_termination()

How the calculation ended.

residuals: Residuals
1293    @property
1294    def residuals(self) -> Residuals:
1295        """Reported active and reactive power balance residuals."""
1296        return self.module._inner._ac_scuc_solution_residuals()

Reported active and reactive power balance residuals.

producer: Optional[str]
1298    @property
1299    def producer(self) -> Optional[str]:
1300        """Producer or solver identity, when recorded."""
1301        return self.module._inner._ac_scuc_solution_producer()

Producer or solver identity, when recorded.

network_outputs: ScucNetworkOutputs
1303    @property
1304    def network_outputs(self) -> ScucNetworkOutputs:
1305        """Per interval network outputs."""
1306        return self.module._inner._ac_scuc_solution_network_outputs()

Per interval network outputs.

device_outputs: ScucDeviceOutputs
1308    @property
1309    def device_outputs(self) -> ScucDeviceOutputs:
1310        """Per interval dispatchable device outputs."""
1311        return self.module._inner._ac_scuc_solution_device_outputs()

Per interval dispatchable device outputs.

objective: Optional[float]
1313    @property
1314    def objective(self) -> Optional[float]:
1315        """Reported objective value, when present."""
1316        return self.module._inner._ac_scuc_solution_objective()

Reported objective value, when present.

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

Decode an in-memory UTF-8 artifact.

class BalancedNetwork:
331@_guard_class
332class BalancedNetwork:
333    """A parsed balanced power network.
334
335    The data attributes (``buses``, ``branches``, ``generators``, ``loads``,
336    ``shunts``) and reference bus queries delegate to the compiled handle; the
337    matrix methods below return ``scipy.sparse`` objects. Parse and transform
338    diagnostics belong to the owning :class:`PioModule`.
339
340    Errors: a bad file path raises the standard ``OSError`` subclass
341    (``FileNotFoundError``); a malformed case raises :class:`PowerIOParseError`
342    and an unmet calculation precondition (no generators, no reference bus) raises
343    :class:`PowerIODataError`; both subclass :class:`PowerIOError`, so
344    ``except PowerIOError`` catches either; an unknown
345    ``scheme``/``formula``/``units`` string raises ``ValueError``.
346    """
347
348    def __init__(self, inner: "_powerio._BalancedNetwork"):
349        self._inner = inner
350
351    def __dir__(self):
352        # The data attributes arrive through __getattr__, so name them here or
353        # they stay invisible to tab completion.
354        return sorted(set(super().__dir__()) | _BALANCED_DELEGATED_NAMES)
355
356    def __getattr__(self, name: str):
357        # Reached only when normal lookup misses, so the matrix methods below
358        # win. Guard underscore names so a lookup before _inner exists raises
359        # AttributeError instead of recursing forever.
360        if name not in _BALANCED_DELEGATED_NAMES:
361            raise AttributeError(
362                f"{type(self).__name__!r} object has no attribute {name!r}"
363            )
364        return getattr(self._inner, name)
365
366    def __repr__(self) -> str:
367        # The inner handle's __repr__ already renders the public ``BalancedNetwork(...)``
368        # form, so this is a straight delegate.
369        return repr(self._inner)
370
371    def calc_connectivity_report(self) -> dict[str, Any]:
372        """Calculate the in-service topology summary."""
373        return self._inner.calc_connectivity_report()
374
375    def to_geo_layer(self) -> dict[str, Any]:
376        """Transform coordinates to a canonical GeoJSON FeatureCollection.
377
378        A case without coordinates produces an empty feature collection.
379        """
380        return _json.loads(self._inner.to_geo_layer_json())
381
382    def apply_geo_layer(
383        self, text: str, name_hint: Optional[str] = None
384    ) -> tuple["BalancedNetwork", dict[str, Any]]:
385        """Apply a geographic sidecar and return ``(placed, report)``.
386
387        ``text`` is any form :func:`parse_geo` accepts; this case is
388        unchanged. The report carries ``matched_buses``, ``matched_branches``,
389        ``unmatched_features``, ``unlocated_buses``, ``unlocated_branches``,
390        and ``notes``. The two unlocated counts cover the whole case when the
391        pass ends, so a layer that matched nothing reads apart from a case
392        that needed nothing. The placed copy drops the retained source text,
393        so a same-format emission re-serializes.
394        """
395        inner, report = self._inner.apply_geo_layer(text, name_hint)
396        return BalancedNetwork(inner), report
397
398    def resolve_contingencies(self, text: str) -> dict[str, Any]:
399        """Read PSS/E contingency text and bind every case to this network.
400
401        ``text`` is the content of a ``.con`` file, such as
402        ``parse("cases.con").value.text``. The result carries ``cases``,
403        ``resolved``, ``unresolved``, and ``unrecognized_statements`` as
404        counts; ``case_results``, one entry per case in the file's order with
405        its ``name``, whether it ``resolved``, the ``components`` it bound to,
406        and the actions that bound to nothing as ``{"action", "reason"}``; and
407        ``diagnostics``, the reader's notes on statements it kept as text.
408
409        Each component states the ``type`` naming the table, the ``row`` it
410        occupies there, the element's own ``in_service`` flag, and the ``id``
411        the network states for it. ``id`` is ``None`` when the network states
412        no identity for that row; ``type`` and ``row`` name the element either
413        way.
414
415        Binding reports rather than refuses, so a case naming an element this
416        network does not hold is counted unresolved and listed.
417        """
418        return self._inner.resolve_contingencies(text)
419
420    def expand_contingencies(
421        self, con_text: str, sub_text: str
422    ) -> tuple[str, list[Diagnostic]]:
423        """Turn automatic contingency specifications into explicit cases.
424
425        ``con_text`` and ``sub_text`` are the contents of a ``.con`` and a
426        ``.sub`` file. A specification such as ``SINGLE BRANCH IN SUBSYSTEM
427        'A1'`` states a rule, so expanding it needs both the network and the
428        subsystem the ``.sub`` file names. Returns the expanded ``.con`` text,
429        which states every outage explicitly, and the notes from the two
430        readers followed by the expansion's own notes. Only elements this
431        network states in service expand into cases.
432        """
433        return self._inner.expand_contingencies(con_text, sub_text)
434
435    def select_subsystem_buses(self, sub_text: str, name: str) -> list[int]:
436        """The bus numbers one named subsystem of ``sub_text`` selects.
437
438        ``sub_text`` is the content of a ``.sub`` file. The numbers come back
439        in ascending order. A name the file does not state raises
440        ``ValueError``.
441        """
442        return self._inner.select_subsystem_buses(sub_text, name)
443
444    # --- matrix calculations (scipy.sparse) -----------------------------
445
446    def calc_bprime_matrix(
447        self, scheme: str = "bx", *, skip_zero_impedance: bool = False
448    ):
449        """MATPOWER FDPF Bp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.
450
451        ``skip_zero_impedance=False`` refuses a zero impedance branch
452        (``r`` and ``x`` both zero); pass ``True`` to drop it instead.
453        """
454        return _to_csr(
455            self._inner.bprime(scheme, skip_zero_impedance=skip_zero_impedance)
456        )
457
458    def calc_incidence_matrix(
459        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
460    ):
461        """Return PowerModels incidence ``A`` (branches by buses).
462
463        Rows are the in service, non self loop branches in table order
464        (three winding transformer windings follow the branches); columns
465        are every bus in table order. :meth:`calc_dc_index_map` names both
466        axes. ``skip_zero_impedance=False`` refuses a zero impedance branch;
467        ``True`` drops it from the branch axis.
468        """
469        return _to_csr(
470            self._inner.calc_incidence_matrix(
471                formula, skip_zero_impedance=skip_zero_impedance
472            )
473        )
474
475    def calc_branch_susceptances(
476        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
477    ):
478        """Return per branch susceptances over the branch axis of
479        :meth:`calc_dc_index_map`."""
480        np = _require("numpy", "matrix")
481        return np.asarray(
482            self._inner.calc_branch_susceptances(
483                formula, skip_zero_impedance=skip_zero_impedance
484            ),
485            dtype=float,
486        )
487
488    def calc_branch_flow_matrix(
489        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
490    ):
491        """Return ``Bf = diag(b) A`` as a CSR matrix (branches by buses)."""
492        return _to_csr(
493            self._inner.calc_branch_flow_matrix(
494                formula, skip_zero_impedance=skip_zero_impedance
495            )
496        )
497
498    def calc_bus_susceptance_matrix(
499        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
500    ):
501        """Return ``B = A.T diag(b) A`` as a CSR matrix (buses by buses)."""
502        return _to_csr(
503            self._inner.calc_bus_susceptance_matrix(
504                formula, skip_zero_impedance=skip_zero_impedance
505            )
506        )
507
508    def calc_branch_phase_shift_injection(
509        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
510    ):
511        """Return ``b * shift`` over the branch axis."""
512        np = _require("numpy", "matrix")
513        return np.asarray(
514            self._inner.calc_branch_phase_shift_injection(
515                formula, skip_zero_impedance=skip_zero_impedance
516            ),
517            dtype=float,
518        )
519
520    def calc_bus_phase_shift_injection(
521        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
522    ):
523        """Return ``A.T @ (b * shift)`` over the bus axis."""
524        np = _require("numpy", "matrix")
525        return np.asarray(
526            self._inner.calc_bus_phase_shift_injection(
527                formula, skip_zero_impedance=skip_zero_impedance
528            ),
529            dtype=float,
530        )
531
532    def calc_branch_flow_dc(
533        self,
534        voltage_angles,
535        formula: str = "series_susceptance",
536        *,
537        skip_zero_impedance: bool = False,
538    ):
539        """Compute ``-Bf @ va + b * shift`` over the branch axis."""
540        np, angles = _dc_angles(self.n_buses, voltage_angles)
541        return np.asarray(
542            self._inner.calc_branch_flow_dc(
543                angles.tolist(), formula, skip_zero_impedance=skip_zero_impedance
544            ),
545            dtype=float,
546        )
547
548    def calc_bus_injection_dc(
549        self,
550        voltage_angles,
551        formula: str = "series_susceptance",
552        *,
553        skip_zero_impedance: bool = False,
554    ):
555        """Compute ``-B @ va + p_shift`` over the bus axis."""
556        np, angles = _dc_angles(self.n_buses, voltage_angles)
557        return np.asarray(
558            self._inner.calc_bus_injection_dc(
559                angles.tolist(), formula, skip_zero_impedance=skip_zero_impedance
560            ),
561            dtype=float,
562        )
563
564    def calc_dc_index_map(
565        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
566    ) -> dict[str, Any]:
567        """Return the axes every DC calculation shares.
568
569        ``bus_ids`` maps a bus axis row to the source bus id (every bus, in
570        table order). ``branch_rows`` maps a branch axis row to the position
571        in the branch table (three winding transformer windings follow the
572        branches) and ``branch_ids`` to the stable identity, the branch uid
573        when the source states one and ``branches:<row>`` otherwise; out of
574        service branches and self loops have no row. ``skipped_branch_rows``
575        lists the zero impedance branches dropped under
576        ``skip_zero_impedance=True`` and is empty otherwise. The same
577        selection applies to :meth:`calc_ptdf` rows and :meth:`calc_lodf`.
578        """
579        return self._inner.calc_dc_index_map(
580            formula, skip_zero_impedance=skip_zero_impedance
581        )
582
583    def calc_bdoubleprime_matrix(
584        self, scheme: str = "bx", *, skip_zero_impedance: bool = False
585    ):
586        """MATPOWER FDPF Bpp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.
587        ``skip_zero_impedance`` as in :meth:`calc_bprime_matrix`.
588        """
589        return _to_csr(
590            self._inner.bdoubleprime(scheme, skip_zero_impedance=skip_zero_impedance)
591        )
592
593    def calc_lacpf_matrix(
594        self,
595        *,
596        include_taps: bool = True,
597        include_shifts: bool = True,
598        skip_zero_impedance: bool = False,
599    ):
600        """LACPF 2n×2n block ``[[G, -B], [-B, -G]]``. ``skip_zero_impedance``
601        as in :meth:`calc_bprime_matrix`."""
602        return _to_csr(
603            self._inner.lacpf(
604                include_taps=include_taps,
605                include_shifts=include_shifts,
606                skip_zero_impedance=skip_zero_impedance,
607            )
608        )
609
610    def calc_adjacency_matrix(self):
611        """0/1 bus adjacency matrix."""
612        return _to_csr(self._inner.adjacency())
613
614    def calc_admittance_matrix(
615        self,
616        *,
617        include_taps: bool = True,
618        include_shifts: bool = True,
619        skip_zero_impedance: bool = False,
620    ):
621        """``Y_bus = G + jB`` as a complex csr_matrix. ``skip_zero_impedance``
622        as in :meth:`calc_bprime_matrix`."""
623        g, b = self._inner.ybus_parts(
624            include_taps=include_taps,
625            include_shifts=include_shifts,
626            skip_zero_impedance=skip_zero_impedance,
627        )
628        g, b = _to_csr(g), _to_csr(b)
629        return (g + 1j * b).tocsr()
630
631    def calc_ptdf(self, formula: str = "series_susceptance", solver: str = "auto"):
632        """DC PTDF (m×n). ``formula`` is ``"series_susceptance"``,
633        ``"tap_adjusted_reactance"``, or ``"reactance_only"``.
634
635        ``solver`` is ``"auto"``, ``"dense"``, or ``"sparse"``. ``"auto"``
636        uses the dense factorization on small cases and the sparse Cholesky
637        path on large ones, the same policy as the CLI.
638        """
639        return _to_csr(self._inner.ptdf(formula, solver))
640
641    def calc_lodf(self, formula: str = "series_susceptance", solver: str = "auto"):
642        """DC LODF (m×m). ``formula`` and ``solver`` as in :meth:`calc_ptdf`."""
643        return _to_csr(self._inner.lodf(formula, solver))
644
645    def calc_weighted_laplacian(
646        self,
647        formula: str = "series_susceptance",
648    ):
649        """Weighted Laplacian ``L = -B``. ``formula`` as in :meth:`calc_ptdf`."""
650        return _to_csr(self._inner.weighted_laplacian(formula))
651
652    def to_normalized(
653        self,
654        *,
655        clamp_angle_bounds: bool = False,
656        angle_bound_pad: Optional[float] = None,
657    ) -> "BalancedNetwork":
658        """Return a normalized copy with per unit power and radian angles.
659
660        The result removes out of service elements, preserves source bus IDs,
661        and normalizes bus types. It carries no retained source, so
662        :func:`powerio.emit` produces a grid exchange representation from the
663        derived module. Raises
664        :class:`PowerIODataError` if the network cannot be
665        normalized (no reference bus can be chosen, or a non-positive base MVA).
666
667        ``clamp_angle_bounds=True`` applies the PowerModels angle difference
668        bound repair: limits at or beyond ``+/-pi/2`` and zero/zero windows
669        become ``[-angle_bound_pad, angle_bound_pad]``. A repair that would
670        invert the interval widens to that same window. The default pad is
671        1.0472 radians.
672        """
673        if not clamp_angle_bounds and angle_bound_pad is None:
674            return BalancedNetwork(self._inner.to_normalized())
675        return BalancedNetwork(
676            self._inner.to_normalized_with_options(
677                clamp_angle_bounds=clamp_angle_bounds, angle_bound_pad=angle_bound_pad
678            )
679        )
680
681    def to_ppc(self):
682        """PYPOWER case dict (``ppc``) with MATPOWER-style numpy tables.
683
684        Values are emitted as the model holds them, so a case read from a
685        file carries MW, MVAr, and degrees. A network from
686        :meth:`to_normalized` holds per unit and radians, and those are what
687        its tables carry — PYPOWER reads a ppc dict as MW and degrees, so
688        build this from the raw network unless the consumer expects per unit.
689
690        Loads and shunts are summed onto their bus in the
691        ``PD``/``QD``/``GS``/``BS`` columns, the same aggregation as the
692        MATPOWER emitter. The bus table has no per element status
693        column, so an element the model marks out of service still
694        contributes its value, and a de-energized bus is carried as type 4.
695        ``gencost`` is present only when every generator carries cost data,
696        because MATPOWER requires cost rows for all generators or none.
697        :func:`from_ppc` reads the tables back.
698        """
699        np = _require("numpy", "matrix")
700        buses = self._inner.buses
701        bus = np.array(
702            [
703                (
704                    b["id"],
705                    _PPC_BUS_TYPE.get(b["kind"], 1.0),
706                    0.0,
707                    0.0,
708                    0.0,
709                    0.0,
710                    b["area"],
711                    b["vm"],
712                    b["va"],
713                    b["base_kv"],
714                    b["zone"],
715                    b["vmax"],
716                    b["vmin"],
717                )
718                for b in buses
719            ],
720            dtype=float,
721        ).reshape(len(buses), 13)
722        bus[:, 2], bus[:, 3], bus[:, 4], bus[:, 5] = _bus_sums(
723            np, buses, self._inner.loads, self._inner.shunts
724        )
725
726        # The capability and ramp columns past PMIN are an OPF extension that a
727        # source need not carry. Widen to the full 21 only when a generator
728        # actually states one: a table of zeros there reads back as eleven
729        # explicit zero limits, which a ramp aware solver takes as a generator
730        # that cannot move.
731        gens = self._inner.generators
732        caps = [g["caps"] for g in gens]
733        width = 21 if any(c is not None for row in caps for c in row) else 10
734        gen = np.array(
735            [
736                [
737                    g["bus"],
738                    g["pg"],
739                    g["qg"],
740                    g["qmax"],
741                    g["qmin"],
742                    g["vg"],
743                    g["mbase"],
744                    float(g["in_service"]),
745                    g["pmax"],
746                    g["pmin"],
747                ]
748                + ([0.0 if c is None else c for c in row] if width == 21 else [])
749                for g, row in zip(gens, caps)
750            ],
751            dtype=float,
752        ).reshape(len(gens), width)
753
754        branches = self._inner.branches
755        branch = np.array(
756            [
757                (
758                    br["from_id"],
759                    br["to_id"],
760                    br["r"],
761                    br["x"],
762                    br["b"],
763                    br["rate_a"],
764                    br["rate_b"],
765                    br["rate_c"],
766                    br["tap"],
767                    br["shift"],
768                    float(br["in_service"]),
769                    br["angmin"],
770                    br["angmax"],
771                )
772                for br in branches
773            ],
774            dtype=float,
775        ).reshape(len(branches), 13)
776
777        ppc = {
778            "version": "2",
779            "baseMVA": float(self._inner.base_mva),
780            "bus": bus,
781            "gen": gen,
782            "branch": branch,
783        }
784
785        # Coefficients sit left-aligned after ncost, padded to the widest
786        # row, which is the layout PYPOWER's own loadcase produces.
787        costs = [g["cost"] for g in gens]
788        if costs and all(c is not None for c in costs):
789            gencost = np.zeros((len(costs), 4 + max(len(c["coeffs"]) for c in costs)))
790            for i, c in enumerate(costs):
791                gencost[i, :4] = (
792                    c["model"],
793                    c["startup"],
794                    c["shutdown"],
795                    c["ncost"],
796                )
797                gencost[i, 4 : 4 + len(c["coeffs"])] = c["coeffs"]
798            ppc["gencost"] = gencost
799        return ppc
800
801    def to_networkx(self):
802        """Undirected networkx graph keyed by bus id.
803
804        In-service branches become edges carrying ``branch`` (index), ``r``,
805        ``x``, and ``b``.
806        """
807        nx = _require("networkx", "graph")
808        g = nx.Graph()
809        g.add_nodes_from(bus["id"] for bus in self._inner.buses)
810        for k, br in enumerate(self._inner.branches):
811            if br["in_service"]:
812                g.add_edge(
813                    br["from_id"],
814                    br["to_id"],
815                    branch=k,
816                    r=br["r"],
817                    x=br["x"],
818                    b=br["b"],
819                )
820        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)
348    def __init__(self, inner: "_powerio._BalancedNetwork"):
349        self._inner = inner
def calc_connectivity_report(self) -> Dict[str, Any]:
371    def calc_connectivity_report(self) -> dict[str, Any]:
372        """Calculate the in-service topology summary."""
373        return self._inner.calc_connectivity_report()

Calculate the in-service topology summary.

def to_geo_layer(self) -> Dict[str, Any]:
375    def to_geo_layer(self) -> dict[str, Any]:
376        """Transform coordinates to a canonical GeoJSON FeatureCollection.
377
378        A case without coordinates produces an empty feature collection.
379        """
380        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]]:
382    def apply_geo_layer(
383        self, text: str, name_hint: Optional[str] = None
384    ) -> tuple["BalancedNetwork", dict[str, Any]]:
385        """Apply a geographic sidecar and return ``(placed, report)``.
386
387        ``text`` is any form :func:`parse_geo` accepts; this case is
388        unchanged. The report carries ``matched_buses``, ``matched_branches``,
389        ``unmatched_features``, ``unlocated_buses``, ``unlocated_branches``,
390        and ``notes``. The two unlocated counts cover the whole case when the
391        pass ends, so a layer that matched nothing reads apart from a case
392        that needed nothing. The placed copy drops the retained source text,
393        so a same-format emission re-serializes.
394        """
395        inner, report = self._inner.apply_geo_layer(text, name_hint)
396        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 resolve_contingencies(self, text: str) -> Dict[str, Any]:
398    def resolve_contingencies(self, text: str) -> dict[str, Any]:
399        """Read PSS/E contingency text and bind every case to this network.
400
401        ``text`` is the content of a ``.con`` file, such as
402        ``parse("cases.con").value.text``. The result carries ``cases``,
403        ``resolved``, ``unresolved``, and ``unrecognized_statements`` as
404        counts; ``case_results``, one entry per case in the file's order with
405        its ``name``, whether it ``resolved``, the ``components`` it bound to,
406        and the actions that bound to nothing as ``{"action", "reason"}``; and
407        ``diagnostics``, the reader's notes on statements it kept as text.
408
409        Each component states the ``type`` naming the table, the ``row`` it
410        occupies there, the element's own ``in_service`` flag, and the ``id``
411        the network states for it. ``id`` is ``None`` when the network states
412        no identity for that row; ``type`` and ``row`` name the element either
413        way.
414
415        Binding reports rather than refuses, so a case naming an element this
416        network does not hold is counted unresolved and listed.
417        """
418        return self._inner.resolve_contingencies(text)

Read PSS/E contingency text and bind every case to this network.

text is the content of a .con file, such as parse("cases.con").value.text. The result carries cases, resolved, unresolved, and unrecognized_statements as counts; case_results, one entry per case in the file's order with its name, whether it resolved, the components it bound to, and the actions that bound to nothing as {"action", "reason"}; and diagnostics, the reader's notes on statements it kept as text.

Each component states the type naming the table, the row it occupies there, the element's own in_service flag, and the id the network states for it. id is None when the network states no identity for that row; type and row name the element either way.

Binding reports rather than refuses, so a case naming an element this network does not hold is counted unresolved and listed.

def expand_contingencies( self, con_text: str, sub_text: str) -> Tuple[str, List[Diagnostic]]:
420    def expand_contingencies(
421        self, con_text: str, sub_text: str
422    ) -> tuple[str, list[Diagnostic]]:
423        """Turn automatic contingency specifications into explicit cases.
424
425        ``con_text`` and ``sub_text`` are the contents of a ``.con`` and a
426        ``.sub`` file. A specification such as ``SINGLE BRANCH IN SUBSYSTEM
427        'A1'`` states a rule, so expanding it needs both the network and the
428        subsystem the ``.sub`` file names. Returns the expanded ``.con`` text,
429        which states every outage explicitly, and the notes from the two
430        readers followed by the expansion's own notes. Only elements this
431        network states in service expand into cases.
432        """
433        return self._inner.expand_contingencies(con_text, sub_text)

Turn automatic contingency specifications into explicit cases.

con_text and sub_text are the contents of a .con and a .sub file. A specification such as SINGLE BRANCH IN SUBSYSTEM 'A1' states a rule, so expanding it needs both the network and the subsystem the .sub file names. Returns the expanded .con text, which states every outage explicitly, and the notes from the two readers followed by the expansion's own notes. Only elements this network states in service expand into cases.

def select_subsystem_buses(self, sub_text: str, name: str) -> List[int]:
435    def select_subsystem_buses(self, sub_text: str, name: str) -> list[int]:
436        """The bus numbers one named subsystem of ``sub_text`` selects.
437
438        ``sub_text`` is the content of a ``.sub`` file. The numbers come back
439        in ascending order. A name the file does not state raises
440        ``ValueError``.
441        """
442        return self._inner.select_subsystem_buses(sub_text, name)

The bus numbers one named subsystem of sub_text selects.

sub_text is the content of a .sub file. The numbers come back in ascending order. A name the file does not state raises ValueError.

def calc_bprime_matrix( self, scheme: Literal['bx', 'xb'] = Ellipsis, *, skip_zero_impedance: bool = Ellipsis) -> Any:
446    def calc_bprime_matrix(
447        self, scheme: str = "bx", *, skip_zero_impedance: bool = False
448    ):
449        """MATPOWER FDPF Bp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.
450
451        ``skip_zero_impedance=False`` refuses a zero impedance branch
452        (``r`` and ``x`` both zero); pass ``True`` to drop it instead.
453        """
454        return _to_csr(
455            self._inner.bprime(scheme, skip_zero_impedance=skip_zero_impedance)
456        )

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, *, skip_zero_impedance: bool = Ellipsis) -> Any:
458    def calc_incidence_matrix(
459        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
460    ):
461        """Return PowerModels incidence ``A`` (branches by buses).
462
463        Rows are the in service, non self loop branches in table order
464        (three winding transformer windings follow the branches); columns
465        are every bus in table order. :meth:`calc_dc_index_map` names both
466        axes. ``skip_zero_impedance=False`` refuses a zero impedance branch;
467        ``True`` drops it from the branch axis.
468        """
469        return _to_csr(
470            self._inner.calc_incidence_matrix(
471                formula, skip_zero_impedance=skip_zero_impedance
472            )
473        )

Return PowerModels incidence A (branches by buses).

Rows are the in service, non self loop branches in table order (three winding transformer windings follow the branches); columns are every bus in table order. calc_dc_index_map() names both axes. skip_zero_impedance=False refuses a zero impedance branch; True drops it from the branch axis.

def calc_branch_susceptances( self, formula: str = Ellipsis, *, skip_zero_impedance: bool = Ellipsis) -> Any:
475    def calc_branch_susceptances(
476        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
477    ):
478        """Return per branch susceptances over the branch axis of
479        :meth:`calc_dc_index_map`."""
480        np = _require("numpy", "matrix")
481        return np.asarray(
482            self._inner.calc_branch_susceptances(
483                formula, skip_zero_impedance=skip_zero_impedance
484            ),
485            dtype=float,
486        )

Return per branch susceptances over the branch axis of calc_dc_index_map().

def calc_branch_flow_matrix( self, formula: str = Ellipsis, *, skip_zero_impedance: bool = Ellipsis) -> Any:
488    def calc_branch_flow_matrix(
489        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
490    ):
491        """Return ``Bf = diag(b) A`` as a CSR matrix (branches by buses)."""
492        return _to_csr(
493            self._inner.calc_branch_flow_matrix(
494                formula, skip_zero_impedance=skip_zero_impedance
495            )
496        )

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

def calc_bus_susceptance_matrix( self, formula: str = Ellipsis, *, skip_zero_impedance: bool = Ellipsis) -> Any:
498    def calc_bus_susceptance_matrix(
499        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
500    ):
501        """Return ``B = A.T diag(b) A`` as a CSR matrix (buses by buses)."""
502        return _to_csr(
503            self._inner.calc_bus_susceptance_matrix(
504                formula, skip_zero_impedance=skip_zero_impedance
505            )
506        )

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

def calc_branch_phase_shift_injection( self, formula: str = Ellipsis, *, skip_zero_impedance: bool = Ellipsis) -> Any:
508    def calc_branch_phase_shift_injection(
509        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
510    ):
511        """Return ``b * shift`` over the branch axis."""
512        np = _require("numpy", "matrix")
513        return np.asarray(
514            self._inner.calc_branch_phase_shift_injection(
515                formula, skip_zero_impedance=skip_zero_impedance
516            ),
517            dtype=float,
518        )

Return b * shift over the branch axis.

def calc_bus_phase_shift_injection( self, formula: str = Ellipsis, *, skip_zero_impedance: bool = Ellipsis) -> Any:
520    def calc_bus_phase_shift_injection(
521        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
522    ):
523        """Return ``A.T @ (b * shift)`` over the bus axis."""
524        np = _require("numpy", "matrix")
525        return np.asarray(
526            self._inner.calc_bus_phase_shift_injection(
527                formula, skip_zero_impedance=skip_zero_impedance
528            ),
529            dtype=float,
530        )

Return A.T @ (b * shift) over the bus axis.

def calc_branch_flow_dc( self, voltage_angles: Any, formula: str = Ellipsis, *, skip_zero_impedance: bool = Ellipsis) -> Any:
532    def calc_branch_flow_dc(
533        self,
534        voltage_angles,
535        formula: str = "series_susceptance",
536        *,
537        skip_zero_impedance: bool = False,
538    ):
539        """Compute ``-Bf @ va + b * shift`` over the branch axis."""
540        np, angles = _dc_angles(self.n_buses, voltage_angles)
541        return np.asarray(
542            self._inner.calc_branch_flow_dc(
543                angles.tolist(), formula, skip_zero_impedance=skip_zero_impedance
544            ),
545            dtype=float,
546        )

Compute -Bf @ va + b * shift over the branch axis.

def calc_bus_injection_dc( self, voltage_angles: Any, formula: str = Ellipsis, *, skip_zero_impedance: bool = Ellipsis) -> Any:
548    def calc_bus_injection_dc(
549        self,
550        voltage_angles,
551        formula: str = "series_susceptance",
552        *,
553        skip_zero_impedance: bool = False,
554    ):
555        """Compute ``-B @ va + p_shift`` over the bus axis."""
556        np, angles = _dc_angles(self.n_buses, voltage_angles)
557        return np.asarray(
558            self._inner.calc_bus_injection_dc(
559                angles.tolist(), formula, skip_zero_impedance=skip_zero_impedance
560            ),
561            dtype=float,
562        )

Compute -B @ va + p_shift over the bus axis.

def calc_dc_index_map( self, formula: str = Ellipsis, *, skip_zero_impedance: bool = Ellipsis) -> powerio.DcIndexMap:
564    def calc_dc_index_map(
565        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
566    ) -> dict[str, Any]:
567        """Return the axes every DC calculation shares.
568
569        ``bus_ids`` maps a bus axis row to the source bus id (every bus, in
570        table order). ``branch_rows`` maps a branch axis row to the position
571        in the branch table (three winding transformer windings follow the
572        branches) and ``branch_ids`` to the stable identity, the branch uid
573        when the source states one and ``branches:<row>`` otherwise; out of
574        service branches and self loops have no row. ``skipped_branch_rows``
575        lists the zero impedance branches dropped under
576        ``skip_zero_impedance=True`` and is empty otherwise. The same
577        selection applies to :meth:`calc_ptdf` rows and :meth:`calc_lodf`.
578        """
579        return self._inner.calc_dc_index_map(
580            formula, skip_zero_impedance=skip_zero_impedance
581        )

Return the axes every DC calculation shares.

bus_ids maps a bus axis row to the source bus id (every bus, in table order). branch_rows maps a branch axis row to the position in the branch table (three winding transformer windings follow the branches) and branch_ids to the stable identity, the branch uid when the source states one and branches:<row> otherwise; out of service branches and self loops have no row. skipped_branch_rows lists the zero impedance branches dropped under skip_zero_impedance=True and is empty otherwise. The same selection applies to calc_ptdf() rows and calc_lodf().

def calc_bdoubleprime_matrix( self, scheme: Literal['bx', 'xb'] = Ellipsis, *, skip_zero_impedance: bool = Ellipsis) -> Any:
583    def calc_bdoubleprime_matrix(
584        self, scheme: str = "bx", *, skip_zero_impedance: bool = False
585    ):
586        """MATPOWER FDPF Bpp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.
587        ``skip_zero_impedance`` as in :meth:`calc_bprime_matrix`.
588        """
589        return _to_csr(
590            self._inner.bdoubleprime(scheme, skip_zero_impedance=skip_zero_impedance)
591        )

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:
593    def calc_lacpf_matrix(
594        self,
595        *,
596        include_taps: bool = True,
597        include_shifts: bool = True,
598        skip_zero_impedance: bool = False,
599    ):
600        """LACPF 2n×2n block ``[[G, -B], [-B, -G]]``. ``skip_zero_impedance``
601        as in :meth:`calc_bprime_matrix`."""
602        return _to_csr(
603            self._inner.lacpf(
604                include_taps=include_taps,
605                include_shifts=include_shifts,
606                skip_zero_impedance=skip_zero_impedance,
607            )
608        )

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

def calc_adjacency_matrix(self) -> Any:
610    def calc_adjacency_matrix(self):
611        """0/1 bus adjacency matrix."""
612        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:
614    def calc_admittance_matrix(
615        self,
616        *,
617        include_taps: bool = True,
618        include_shifts: bool = True,
619        skip_zero_impedance: bool = False,
620    ):
621        """``Y_bus = G + jB`` as a complex csr_matrix. ``skip_zero_impedance``
622        as in :meth:`calc_bprime_matrix`."""
623        g, b = self._inner.ybus_parts(
624            include_taps=include_taps,
625            include_shifts=include_shifts,
626            skip_zero_impedance=skip_zero_impedance,
627        )
628        g, b = _to_csr(g), _to_csr(b)
629        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:
631    def calc_ptdf(self, formula: str = "series_susceptance", solver: str = "auto"):
632        """DC PTDF (m×n). ``formula`` is ``"series_susceptance"``,
633        ``"tap_adjusted_reactance"``, or ``"reactance_only"``.
634
635        ``solver`` is ``"auto"``, ``"dense"``, or ``"sparse"``. ``"auto"``
636        uses the dense factorization on small cases and the sparse Cholesky
637        path on large ones, the same policy as the CLI.
638        """
639        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:
641    def calc_lodf(self, formula: str = "series_susceptance", solver: str = "auto"):
642        """DC LODF (m×m). ``formula`` and ``solver`` as in :meth:`calc_ptdf`."""
643        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:
645    def calc_weighted_laplacian(
646        self,
647        formula: str = "series_susceptance",
648    ):
649        """Weighted Laplacian ``L = -B``. ``formula`` as in :meth:`calc_ptdf`."""
650        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:
652    def to_normalized(
653        self,
654        *,
655        clamp_angle_bounds: bool = False,
656        angle_bound_pad: Optional[float] = None,
657    ) -> "BalancedNetwork":
658        """Return a normalized copy with per unit power and radian angles.
659
660        The result removes out of service elements, preserves source bus IDs,
661        and normalizes bus types. It carries no retained source, so
662        :func:`powerio.emit` produces a grid exchange representation from the
663        derived module. Raises
664        :class:`PowerIODataError` if the network cannot be
665        normalized (no reference bus can be chosen, or a non-positive base MVA).
666
667        ``clamp_angle_bounds=True`` applies the PowerModels angle difference
668        bound repair: limits at or beyond ``+/-pi/2`` and zero/zero windows
669        become ``[-angle_bound_pad, angle_bound_pad]``. A repair that would
670        invert the interval widens to that same window. The default pad is
671        1.0472 radians.
672        """
673        if not clamp_angle_bounds and angle_bound_pad is None:
674            return BalancedNetwork(self._inner.to_normalized())
675        return BalancedNetwork(
676            self._inner.to_normalized_with_options(
677                clamp_angle_bounds=clamp_angle_bounds, angle_bound_pad=angle_bound_pad
678            )
679        )

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]:
681    def to_ppc(self):
682        """PYPOWER case dict (``ppc``) with MATPOWER-style numpy tables.
683
684        Values are emitted as the model holds them, so a case read from a
685        file carries MW, MVAr, and degrees. A network from
686        :meth:`to_normalized` holds per unit and radians, and those are what
687        its tables carry — PYPOWER reads a ppc dict as MW and degrees, so
688        build this from the raw network unless the consumer expects per unit.
689
690        Loads and shunts are summed onto their bus in the
691        ``PD``/``QD``/``GS``/``BS`` columns, the same aggregation as the
692        MATPOWER emitter. The bus table has no per element status
693        column, so an element the model marks out of service still
694        contributes its value, and a de-energized bus is carried as type 4.
695        ``gencost`` is present only when every generator carries cost data,
696        because MATPOWER requires cost rows for all generators or none.
697        :func:`from_ppc` reads the tables back.
698        """
699        np = _require("numpy", "matrix")
700        buses = self._inner.buses
701        bus = np.array(
702            [
703                (
704                    b["id"],
705                    _PPC_BUS_TYPE.get(b["kind"], 1.0),
706                    0.0,
707                    0.0,
708                    0.0,
709                    0.0,
710                    b["area"],
711                    b["vm"],
712                    b["va"],
713                    b["base_kv"],
714                    b["zone"],
715                    b["vmax"],
716                    b["vmin"],
717                )
718                for b in buses
719            ],
720            dtype=float,
721        ).reshape(len(buses), 13)
722        bus[:, 2], bus[:, 3], bus[:, 4], bus[:, 5] = _bus_sums(
723            np, buses, self._inner.loads, self._inner.shunts
724        )
725
726        # The capability and ramp columns past PMIN are an OPF extension that a
727        # source need not carry. Widen to the full 21 only when a generator
728        # actually states one: a table of zeros there reads back as eleven
729        # explicit zero limits, which a ramp aware solver takes as a generator
730        # that cannot move.
731        gens = self._inner.generators
732        caps = [g["caps"] for g in gens]
733        width = 21 if any(c is not None for row in caps for c in row) else 10
734        gen = np.array(
735            [
736                [
737                    g["bus"],
738                    g["pg"],
739                    g["qg"],
740                    g["qmax"],
741                    g["qmin"],
742                    g["vg"],
743                    g["mbase"],
744                    float(g["in_service"]),
745                    g["pmax"],
746                    g["pmin"],
747                ]
748                + ([0.0 if c is None else c for c in row] if width == 21 else [])
749                for g, row in zip(gens, caps)
750            ],
751            dtype=float,
752        ).reshape(len(gens), width)
753
754        branches = self._inner.branches
755        branch = np.array(
756            [
757                (
758                    br["from_id"],
759                    br["to_id"],
760                    br["r"],
761                    br["x"],
762                    br["b"],
763                    br["rate_a"],
764                    br["rate_b"],
765                    br["rate_c"],
766                    br["tap"],
767                    br["shift"],
768                    float(br["in_service"]),
769                    br["angmin"],
770                    br["angmax"],
771                )
772                for br in branches
773            ],
774            dtype=float,
775        ).reshape(len(branches), 13)
776
777        ppc = {
778            "version": "2",
779            "baseMVA": float(self._inner.base_mva),
780            "bus": bus,
781            "gen": gen,
782            "branch": branch,
783        }
784
785        # Coefficients sit left-aligned after ncost, padded to the widest
786        # row, which is the layout PYPOWER's own loadcase produces.
787        costs = [g["cost"] for g in gens]
788        if costs and all(c is not None for c in costs):
789            gencost = np.zeros((len(costs), 4 + max(len(c["coeffs"]) for c in costs)))
790            for i, c in enumerate(costs):
791                gencost[i, :4] = (
792                    c["model"],
793                    c["startup"],
794                    c["shutdown"],
795                    c["ncost"],
796                )
797                gencost[i, 4 : 4 + len(c["coeffs"])] = c["coeffs"]
798            ppc["gencost"] = gencost
799        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:
801    def to_networkx(self):
802        """Undirected networkx graph keyed by bus id.
803
804        In-service branches become edges carrying ``branch`` (index), ``r``,
805        ``x``, and ``b``.
806        """
807        nx = _require("networkx", "graph")
808        g = nx.Graph()
809        g.add_nodes_from(bus["id"] for bus in self._inner.buses)
810        for k, br in enumerate(self._inner.branches):
811            if br["in_service"]:
812                g.add_edge(
813                    br["from_id"],
814                    br["to_id"],
815                    branch=k,
816                    r=br["r"],
817                    x=br["x"],
818                    b=br["b"],
819                )
820        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:
component_type
local_id
class ContingencySet(_TypedValue):
1351@_guard_class
1352class ContingencySet(_TypedValue):
1353    """A PSS/E contingency description file: the cases to run, the automatic
1354    specifications that state cases by rule, and the ``SKIP`` rules.
1355
1356    :func:`parse` returns it for a ``.con`` file or for text declared as
1357    ``psse-con``, :func:`powerio.emit` writes it back under that token, and
1358    :func:`serialize` carries it through PowerIO IR. Bind a set to a case with
1359    ``network.resolve_contingencies(cases.text)`` and turn its automatic
1360    specifications into explicit cases with
1361    ``network.expand_contingencies(cases.text, groups.text)``.
1362    """
1363
1364    @property
1365    def text(self) -> str:
1366        """The ``.con`` text for this set."""
1367        return _emitted_text(self.module, "psse-con")

A PSS/E contingency description file: the cases to run, the automatic specifications that state cases by rule, and the SKIP rules.

parse() returns it for a .con file or for text declared as psse-con, powerio.emit() writes it back under that token, and serialize() carries it through PowerIO IR. Bind a set to a case with network.resolve_contingencies(cases.text) and turn its automatic specifications into explicit cases with network.expand_contingencies(cases.text, groups.text).

text: str
1364    @property
1365    def text(self) -> str:
1366        """The ``.con`` text for this set."""
1367        return _emitted_text(self.module, "psse-con")

The .con text for this set.

Inherited Members
_TypedValue
_TypedValue
module
class DcOpfInstance(_BalancedCalculation):
1201class DcOpfInstance(_BalancedCalculation):
1202    """A DC optimal power flow calculation instance."""

A DC optimal power flow calculation instance.

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

A DC optimal power flow solution.

class DcPfInstance(_BalancedCalculation):
1193class DcPfInstance(_BalancedCalculation):
1194    """A DC power flow calculation instance."""

A DC power flow calculation instance.

class DcPfSolution(_BalancedCalculation, _CalculationSolution):
1237class DcPfSolution(_BalancedCalculation, _CalculationSolution):
1238    """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.

suggested_action
spans
severity

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

message
details

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

target
code
related
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:
196@dataclass(frozen=True)
197class EmitResult:
198    """Artifact inventory and diagnostics from an emission or serialization."""
199
200    artifacts: tuple[Artifact, ...]
201    layout: str
202    fidelity: str
203    diagnostics: tuple[Diagnostic, ...]
204
205    @property
206    def text(self) -> Optional[str]:
207        """The sole UTF-8 memory artifact, or ``None`` for other inventories."""
208        if len(self.artifacts) != 1 or self.artifacts[0].data is None:
209            return None
210        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]
205    @property
206    def text(self) -> Optional[str]:
207        """The sole UTF-8 memory artifact, or ``None`` for other inventories."""
208        if len(self.artifacts) != 1 or self.artifacts[0].data is None:
209            return None
210        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):
1319@_guard_class
1320class GeoLayer(_TypedValue):
1321    """A standalone geographic document: element points and routes keyed by
1322    element identity, in one coordinate space.
1323
1324    :func:`parse` returns it for the canonical ``.geo.json``, GeoJSON, aliased
1325    CSV or JSON records, headerless buscoords CSV, and a PowerWorld ``.pwd``
1326    display. :func:`powerio.emit` writes the canonical document as
1327    ``geo-json``, and :func:`serialize` carries the layer through PowerIO IR.
1328    Place a layer onto a case with
1329    ``network.apply_geo_layer(layer.geojson)``.
1330    """
1331
1332    @property
1333    def geojson(self) -> str:
1334        """The canonical ``.geo.json`` document for this layer."""
1335        result = emit(self.module, "geo-json")
1336        data = result.artifacts[0].data
1337        if data is None:
1338            raise ValueError("the layer emission returned no artifact bytes")
1339        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. powerio.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
1332    @property
1333    def geojson(self) -> str:
1334        """The canonical ``.geo.json`` document for this layer."""
1335        result = emit(self.module, "geo-json")
1336        data = result.artifacts[0].data
1337        if data is None:
1338            raise ValueError("the layer emission returned no artifact bytes")
1339        return data.decode("utf-8")

The canonical .geo.json document for this layer.

Inherited Members
_TypedValue
_TypedValue
module
class LinDist3FlowOpfInstance(_MulticonductorCalculation):
1213@_guard_class
1214class LinDist3FlowOpfInstance(_MulticonductorCalculation):
1215    """A radial fixed-reference multiconductor linear OPF instance."""
1216
1217    @property
1218    def metadata(self) -> dict[str, Any]:
1219        """Node and conductor axes, roots, and reference phasors in volts/radians."""
1220        return self.module._inner._lindist3flow_metadata()

A radial fixed-reference multiconductor linear OPF instance.

metadata: dict[str, typing.Any]
1217    @property
1218    def metadata(self) -> dict[str, Any]:
1219        """Node and conductor axes, roots, and reference phasors in volts/radians."""
1220        return self.module._inner._lindist3flow_metadata()

Node and conductor axes, roots, and reference phasors in volts/radians.

class LinDist3FlowOpfSolution(_MulticonductorCalculation, _CalculationSolution):
1261@_guard_class
1262class LinDist3FlowOpfSolution(_MulticonductorCalculation, _CalculationSolution):
1263    """LinDist3Flow primal values in squared volts, watts, and vars."""
1264
1265    @property
1266    def termination(self) -> str:
1267        """How the calculation ended."""
1268        return self.module._inner._lindist3flow_solution_termination()
1269
1270    @property
1271    def objective(self) -> float:
1272        """The reported objective value."""
1273        return self.module._inner._lindist3flow_solution_objective()
1274
1275    def __getitem__(self, quantity: str) -> list[float]:
1276        """Copy one named primal column in its physical instance order."""
1277        return self.module._inner._lindist3flow_solution_values(quantity)

LinDist3Flow primal values in squared volts, watts, and vars.

termination: str
1265    @property
1266    def termination(self) -> str:
1267        """How the calculation ended."""
1268        return self.module._inner._lindist3flow_solution_termination()

How the calculation ended.

objective: float
1270    @property
1271    def objective(self) -> float:
1272        """The reported objective value."""
1273        return self.module._inner._lindist3flow_solution_objective()

The reported objective value.

class McAcOpfInstance(_MulticonductorCalculation):
1223class McAcOpfInstance(_MulticonductorCalculation):
1224    """A multiconductor AC optimal power flow calculation instance."""

A multiconductor AC optimal power flow calculation instance.

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

A multiconductor AC optimal power flow solution.

class McAcPfInstance(_MulticonductorCalculation):
1209class McAcPfInstance(_MulticonductorCalculation):
1210    """A multiconductor AC power flow calculation instance."""

A multiconductor AC power flow calculation instance.

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

A multiconductor AC power flow solution.

class MonitoredSet(_TypedValue):
1386@_guard_class
1387class MonitoredSet(_TypedValue):
1388    """A PSS/E monitored element file: the branches, interfaces, and voltage
1389    scopes a contingency run reports on.
1390
1391    :func:`parse` returns it for a ``.mon`` file or for text declared as
1392    ``psse-mon``.
1393    """
1394
1395    @property
1396    def text(self) -> str:
1397        """The ``.mon`` text for this set."""
1398        return _emitted_text(self.module, "psse-mon")

A PSS/E monitored element file: the branches, interfaces, and voltage scopes a contingency run reports on.

parse() returns it for a .mon file or for text declared as psse-mon.

text: str
1395    @property
1396    def text(self) -> str:
1397        """The ``.mon`` text for this set."""
1398        return _emitted_text(self.module, "psse-mon")

The .mon text for this set.

Inherited Members
_TypedValue
_TypedValue
module
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):
1140@_guard_class
1141class OperatingPoint(_TypedValue):
1142    """A possibly partial assignment over fixed equipment identities."""
1143
1144    @property
1145    def network(self) -> "BalancedNetwork":
1146        """The balanced network this operating point is stated over.
1147
1148        The point's own values are applied to the returned copy, so it is the
1149        network a solver receives for this entry; the collection's shared
1150        base network is not changed. A multiconductor operating point raises
1151        :class:`PowerIOError`.
1152
1153        Net bus injection quantities have no balanced network field, so they
1154        are dropped here and the property reports nothing. Emit the
1155        collection when you need that omission reported: `emit` warns
1156        `EMIT.OPERATING_POINT.DATA_OMITTED` for the same point.
1157        """
1158        return BalancedNetwork(self.module._inner._operating_point_network())

A possibly partial assignment over fixed equipment identities.

network: BalancedNetwork
1144    @property
1145    def network(self) -> "BalancedNetwork":
1146        """The balanced network this operating point is stated over.
1147
1148        The point's own values are applied to the returned copy, so it is the
1149        network a solver receives for this entry; the collection's shared
1150        base network is not changed. A multiconductor operating point raises
1151        :class:`PowerIOError`.
1152
1153        Net bus injection quantities have no balanced network field, so they
1154        are dropped here and the property reports nothing. Emit the
1155        collection when you need that omission reported: `emit` warns
1156        `EMIT.OPERATING_POINT.DATA_OMITTED` for the same point.
1157        """
1158        return BalancedNetwork(self.module._inner._operating_point_network())

The balanced network this operating point is stated over.

The point's own values are applied to the returned copy, so it is the network a solver receives for this entry; the collection's shared base network is not changed. A multiconductor operating point raises PowerIOError.

Net bus injection quantities have no balanced network field, so they are dropped here and the property reports nothing. Emit the collection when you need that omission reported: emit warns EMIT.OPERATING_POINT.DATA_OMITTED for the same point.

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:
1428@_guard_class
1429class PioModule:
1430    """One typed value with diagnostics, producer, sources, source mappings,
1431    history, and extensions.
1432    """
1433
1434    def __init__(self, inner: "_powerio._PioModule"):
1435        self._inner = inner
1436
1437    @classmethod
1438    def from_value(cls, value: Any) -> "PioModule":
1439        """Wrap an existing typed value without serializing it."""
1440        if isinstance(value, BalancedNetwork):
1441            return cls(_powerio._PioModule.from_balanced_network(value._inner))
1442        if isinstance(value, dist.MulticonductorNetwork):
1443            return cls(_powerio._PioModule.from_multiconductor_network(value._inner))
1444        if isinstance(value, _TypedValue):
1445            location = value._collection_entry
1446            inner = value.module._inner
1447            if location is not None:
1448                inner = location.root._inner
1449                if location.scenario_id is not None:
1450                    inner = inner._scenario_get(location.scenario_id)
1451                if location.time_index is not None:
1452                    inner = inner._time_series_get(location.time_index)
1453            return cls(inner._copy())
1454        raise TypeError("PioModule.from_value expects a typed PowerIO value")
1455
1456    @property
1457    def value(self) -> Any:
1458        """The contained typed value."""
1459        type_name = self._inner._type_name
1460        if type_name == "powerio.BalancedNetwork":
1461            return BalancedNetwork(self._inner.as_balanced_network())
1462        if type_name == "powerio.MulticonductorNetwork":
1463            return dist.MulticonductorNetwork(self._inner.as_multiconductor_network())
1464        if type_name.startswith("powerio.TimeSeries<"):
1465            return TimeSeries._from_module(self)
1466        if type_name.startswith("powerio.ScenarioSet<"):
1467            return ScenarioSet._from_module(self)
1468        value_class = _VALUE_CLASSES.get(type_name)
1469        if value_class is None:
1470            raise RuntimeError(f"this binding has no Python class for {type_name}")
1471        return value_class(self)
1472
1473    @property
1474    def type_name(self) -> str:
1475        """The canonical structural name of the contained value, such as
1476        ``powerio.OperatingPoint<powerio.BalancedNetwork>``. The C ABI and
1477        PowerIO IR name the same type with the same string.
1478        """
1479        return str(self._inner._type_name)
1480
1481    @property
1482    def diagnostics(self) -> list[Diagnostic]:
1483        """The diagnostics stored on this module, in encounter order."""
1484        return list(self._inner.diagnostics)
1485
1486    def to_balanced_report(self, base_mva: float = 100.0) -> Any:
1487        """Report whether a multiconductor network can become balanced."""
1488        return _json.loads(self._inner.lowering_readiness_json(base_mva))
1489
1490    def to_balanced(self, base_mva: float = 100.0) -> "PioModule":
1491        """Transform a multiconductor network to a balanced module."""
1492        return PioModule(self._inner.lower_to_balanced(base_mva))
1493
1494    def to_dc_pf_instance(self) -> "PioModule":
1495        """Build a DC power flow instance from a balanced network module."""
1496        return PioModule(self._inner._to_dc_pf_instance())
1497
1498    def to_ac_pf_instance(self) -> "PioModule":
1499        """Build an AC power flow instance from a balanced network module."""
1500        return PioModule(self._inner._to_ac_pf_instance())
1501
1502    def to_dc_opf_instance(self) -> "PioModule":
1503        """Build a DC optimal power flow instance from a balanced network module."""
1504        return PioModule(self._inner._to_dc_opf_instance())
1505
1506    def to_ac_opf_instance(self) -> "PioModule":
1507        """Build an AC optimal power flow instance from a balanced network module."""
1508        return PioModule(self._inner._to_ac_opf_instance())
1509
1510    def to_mc_ac_pf_instance(self) -> "PioModule":
1511        """Build a multiconductor AC power flow instance from a network module."""
1512        return PioModule(self._inner._to_mc_ac_pf_instance())
1513
1514    def to_mc_ac_opf_instance(self) -> "PioModule":
1515        """Build a multiconductor AC optimal power flow instance from a network module."""
1516        return PioModule(self._inner._to_mc_ac_opf_instance())
1517
1518    def to_lindist3flow_opf_instance(self) -> "PioModule":
1519        """Build a LinDist3Flow optimal power flow instance from a network module."""
1520        return PioModule(self._inner._to_lindist3flow_opf_instance())
1521
1522    def __repr__(self) -> str:
1523        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)
1434    def __init__(self, inner: "_powerio._PioModule"):
1435        self._inner = inner
@classmethod
def from_value(*args, **kwds):
1437    @classmethod
1438    def from_value(cls, value: Any) -> "PioModule":
1439        """Wrap an existing typed value without serializing it."""
1440        if isinstance(value, BalancedNetwork):
1441            return cls(_powerio._PioModule.from_balanced_network(value._inner))
1442        if isinstance(value, dist.MulticonductorNetwork):
1443            return cls(_powerio._PioModule.from_multiconductor_network(value._inner))
1444        if isinstance(value, _TypedValue):
1445            location = value._collection_entry
1446            inner = value.module._inner
1447            if location is not None:
1448                inner = location.root._inner
1449                if location.scenario_id is not None:
1450                    inner = inner._scenario_get(location.scenario_id)
1451                if location.time_index is not None:
1452                    inner = inner._time_series_get(location.time_index)
1453            return cls(inner._copy())
1454        raise TypeError("PioModule.from_value expects a typed PowerIO value")

Wrap an existing typed value without serializing it.

value: ~_T
1456    @property
1457    def value(self) -> Any:
1458        """The contained typed value."""
1459        type_name = self._inner._type_name
1460        if type_name == "powerio.BalancedNetwork":
1461            return BalancedNetwork(self._inner.as_balanced_network())
1462        if type_name == "powerio.MulticonductorNetwork":
1463            return dist.MulticonductorNetwork(self._inner.as_multiconductor_network())
1464        if type_name.startswith("powerio.TimeSeries<"):
1465            return TimeSeries._from_module(self)
1466        if type_name.startswith("powerio.ScenarioSet<"):
1467            return ScenarioSet._from_module(self)
1468        value_class = _VALUE_CLASSES.get(type_name)
1469        if value_class is None:
1470            raise RuntimeError(f"this binding has no Python class for {type_name}")
1471        return value_class(self)

The contained typed value.

type_name: str
1473    @property
1474    def type_name(self) -> str:
1475        """The canonical structural name of the contained value, such as
1476        ``powerio.OperatingPoint<powerio.BalancedNetwork>``. The C ABI and
1477        PowerIO IR name the same type with the same string.
1478        """
1479        return str(self._inner._type_name)

The canonical structural name of the contained value, such as powerio.OperatingPoint<powerio.BalancedNetwork>. The C ABI and PowerIO IR name the same type with the same string.

diagnostics: List[Diagnostic]
1481    @property
1482    def diagnostics(self) -> list[Diagnostic]:
1483        """The diagnostics stored on this module, in encounter order."""
1484        return list(self._inner.diagnostics)

The diagnostics stored on this module, in encounter order.

def to_balanced_report(self, base_mva: float = Ellipsis) -> Any:
1486    def to_balanced_report(self, base_mva: float = 100.0) -> Any:
1487        """Report whether a multiconductor network can become balanced."""
1488        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]:
1490    def to_balanced(self, base_mva: float = 100.0) -> "PioModule":
1491        """Transform a multiconductor network to a balanced module."""
1492        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]:
1494    def to_dc_pf_instance(self) -> "PioModule":
1495        """Build a DC power flow instance from a balanced network module."""
1496        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]:
1498    def to_ac_pf_instance(self) -> "PioModule":
1499        """Build an AC power flow instance from a balanced network module."""
1500        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]:
1502    def to_dc_opf_instance(self) -> "PioModule":
1503        """Build a DC optimal power flow instance from a balanced network module."""
1504        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]:
1506    def to_ac_opf_instance(self) -> "PioModule":
1507        """Build an AC optimal power flow instance from a balanced network module."""
1508        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]:
1510    def to_mc_ac_pf_instance(self) -> "PioModule":
1511        """Build a multiconductor AC power flow instance from a network module."""
1512        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]:
1514    def to_mc_ac_opf_instance(self) -> "PioModule":
1515        """Build a multiconductor AC optimal power flow instance from a network module."""
1516        return PioModule(self._inner._to_mc_ac_opf_instance())

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

def to_lindist3flow_opf_instance(self) -> PioModule[LinDist3FlowOpfInstance]:
1518    def to_lindist3flow_opf_instance(self) -> "PioModule":
1519        """Build a LinDist3Flow optimal power flow instance from a network module."""
1520        return PioModule(self._inner._to_lindist3flow_opf_instance())

Build a LinDist3Flow 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:
989@dataclass(frozen=True)
990class Scenario:
991    id: str
992    probability: Optional[float] = None
Scenario(id: str, probability: Optional[float] = Ellipsis)
id: str
probability: Optional[float] = None
class ScenarioSet(_TypedValue, collections.abc.Mapping):
1077@_guard_class
1078class ScenarioSet(_TypedValue, Mapping):
1079    """Named alternatives of one type, with optional probabilities."""
1080
1081    def __init__(
1082        self,
1083        values: Mapping[str, Any],
1084        *,
1085        probabilities: Optional[Mapping[str, float]] = None,
1086    ) -> None:
1087        if not isinstance(values, Mapping):
1088            raise TypeError("ScenarioSet values must be a mapping from IDs to values")
1089        if probabilities is not None and not isinstance(probabilities, Mapping):
1090            raise TypeError("probabilities must be a mapping from scenario IDs to numbers")
1091        ids = list(values)
1092        modules = [PioModule.from_value(values[id])._inner for id in ids]
1093        inner = _powerio._PioModule._from_scenario_set(
1094            modules,
1095            ids,
1096            None if probabilities is None else dict(probabilities),
1097        )
1098        super().__init__(PioModule(inner))
1099
1100    @classmethod
1101    def _from_module(cls, module: "PioModule") -> "ScenarioSet":
1102        value = object.__new__(cls)
1103        _TypedValue.__init__(value, module)
1104        return value
1105
1106    @property
1107    def scenarios(self) -> tuple[Scenario, ...]:
1108        return tuple(Scenario(*entry) for entry in self.module._inner._scenario_entries())
1109
1110    def __len__(self) -> int:
1111        return len(self.scenarios)
1112
1113    def __iter__(self):
1114        return (scenario.id for scenario in self.scenarios)
1115
1116    def __contains__(self, scenario: object) -> bool:
1117        return isinstance(scenario, str) and any(
1118            entry.id == scenario for entry in self.scenarios
1119        )
1120
1121    def __getitem__(self, scenario: str) -> Any:
1122        if not isinstance(scenario, str):
1123            raise TypeError("scenario keys must be strings")
1124        if scenario not in self:
1125            raise KeyError(scenario)
1126        current = self._collection_entry or _CollectionEntry(self.module)
1127        if current.scenario_id is not None:
1128            raise TypeError("nested ScenarioSet values are not supported")
1129        value = PioModule(self.module._inner._scenario_get(scenario)).value
1130        return _bind_collection_entry(
1131            value,
1132            _CollectionEntry(
1133                root=current.root,
1134                time_index=current.time_index,
1135                scenario_id=scenario,
1136            ),
1137        )

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])
1081    def __init__(
1082        self,
1083        values: Mapping[str, Any],
1084        *,
1085        probabilities: Optional[Mapping[str, float]] = None,
1086    ) -> None:
1087        if not isinstance(values, Mapping):
1088            raise TypeError("ScenarioSet values must be a mapping from IDs to values")
1089        if probabilities is not None and not isinstance(probabilities, Mapping):
1090            raise TypeError("probabilities must be a mapping from scenario IDs to numbers")
1091        ids = list(values)
1092        modules = [PioModule.from_value(values[id])._inner for id in ids]
1093        inner = _powerio._PioModule._from_scenario_set(
1094            modules,
1095            ids,
1096            None if probabilities is None else dict(probabilities),
1097        )
1098        super().__init__(PioModule(inner))
scenarios: Tuple[Scenario, ...]
1106    @property
1107    def scenarios(self) -> tuple[Scenario, ...]:
1108        return tuple(Scenario(*entry) for entry in self.module._inner._scenario_entries())
Inherited Members
_TypedValue
module
class ScucActiveReserveZone:
synchronized_requirement_fraction
regulation_down_requirement_fraction
nonsynchronized_requirement_fraction
ramping_down_requirement
nonsynchronized_violation_cost
ramping_up_violation_cost
ramping_down_violation_cost
ramping_up_requirement
buses
regulation_down_violation_cost
regulation_up_requirement_fraction
synchronized_violation_cost
id
regulation_up_violation_cost
class ScucBranchSwitchingCost:
connection_cost
disconnection_cost
id
class ScucContingency:
components
id
class ScucDevice:
initial_commitment
startup_cost
ramp_limits
startup_limits
on_cost
kind
energy_lower_bounds
initial_on_status
minimum_up_time
periods
minimum_down_time
reserve_limits
energy_upper_bounds
reactive_capability
id
startup_cost_adjustments
shutdown_cost
class ScucDeviceOutputs:
p_nsyn_res
q
p_ramp_res_up_offline
on_status
shutdown_status
p_reg_res_down
q_res_up
q_res_down
p_ramp_res_up_online
p_ramp_res_down_online
p_reg_res_up
p_syn_res
p_ramp_res_down_offline
startup_status
p_on
class ScucDevicePeriod:
active_power_min
on_status_min
active_power_max
reserve_costs
reactive_power_min
on_status_max
reactive_power_max
energy_cost_blocks
class ScucEnergyCostBlock:
block_size
marginal_cost
class ScucEnergyRequirement:
end_time
energy
start_time
class ScucInitialCommitment:
accumulated_down_time
accumulated_up_time
class ScucInputs:
transformer_controls
active_reserve_zones
shunts
reactive_reserve_zones
interval_durations
violation_costs
contingencies
devices
branch_switching_costs
class ScucNetworkOutputs:
dc_line_pdc_fr
dc_line_qdc_to
bus_va
bus_vm
transformer_on_status
transformer_tm
shunt_step
dc_line_qdc_fr
transformer_ta
ac_line_on_status
class ScucRampLimits:
shutdown
up
startup
down
class ScucReactiveCapability:
reactive_power_at_zero_active_power
slope_max
kind
reactive_power_at_zero_active_power_max
slope
reactive_power_at_zero_active_power_min
slope_min
class ScucReactiveReserveZone:
reactive_down_violation_cost
reactive_up_violation_cost
reactive_up_requirement
id
buses
reactive_down_requirement
class ScucReserveCosts:
reactive_down
ramping_up_online
ramping_down_online
regulation_down
ramping_down_offline
regulation_up
synchronized
nonsynchronized
reactive_up
ramping_up_offline
class ScucReserveLimits:
regulation_down
ramping_down_offline
regulation_up
ramping_up_online
ramping_up_offline
nonsynchronized
synchronized
ramping_down_online
class ScucShunt:
initial_step
step_min
id
conductance_per_step
susceptance_per_step
step_max
class ScucStartupCostAdjustment:
maximum_down_time
cost
class ScucStartupLimit:
start_time
end_time
maximum_startups
class ScucTransformerControl:
id
phase_shift_max
tap_ratio_max
phase_shift_min
tap_ratio_min
class ScucViolationCosts:
active_power_balance
energy_requirement
branch_thermal_limit
reactive_power_balance
class SocwrOpfSolution(_BalancedCalculation, _CalculationSolution):
1253class SocwrOpfSolution(_BalancedCalculation, _CalculationSolution):
1254    """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.

byte_start
byte_end
source
class SubsystemSet(_TypedValue):
1370@_guard_class
1371class SubsystemSet(_TypedValue):
1372    """A PSS/E subsystem description file: the named bus groups a contingency
1373    run and a monitored element file draw on.
1374
1375    :func:`parse` returns it for a ``.sub`` file or for text declared as
1376    ``psse-sub``, and ``network.select_subsystem_buses(groups.text, name)``
1377    names the buses one subsystem selects.
1378    """
1379
1380    @property
1381    def text(self) -> str:
1382        """The ``.sub`` text for this set."""
1383        return _emitted_text(self.module, "psse-sub")

A PSS/E subsystem description file: the named bus groups a contingency run and a monitored element file draw on.

parse() returns it for a .sub file or for text declared as psse-sub, and network.select_subsystem_buses(groups.text, name) names the buses one subsystem selects.

text: str
1380    @property
1381    def text(self) -> str:
1382        """The ``.sub`` text for this set."""
1383        return _emitted_text(self.module, "psse-sub")

The .sub text for this set.

Inherited Members
_TypedValue
_TypedValue
module
@dataclass(frozen=True)
class TimePoint:
983@dataclass(frozen=True)
984class TimePoint:
985    label: str
986    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):
1010@_guard_class
1011class TimeSeries(_TypedValue, Sequence):
1012    """Values of one type ordered in time."""
1013
1014    def __init__(
1015        self,
1016        values: Sequence[Any],
1017        *,
1018        time_points: Sequence[TimePoint],
1019    ) -> None:
1020        if isinstance(values, (str, bytes, bytearray)) or not isinstance(
1021            values, Sequence
1022        ):
1023            raise TypeError("TimeSeries values must be a sequence of PowerIO values")
1024        if not isinstance(time_points, Sequence):
1025            raise TypeError("time_points must be a sequence of TimePoint values")
1026        points = tuple(time_points)
1027        if not all(isinstance(point, TimePoint) for point in points):
1028            raise TypeError("time_points must contain only TimePoint values")
1029        modules = [PioModule.from_value(value)._inner for value in values]
1030        inner = _powerio._PioModule._from_time_series(
1031            modules,
1032            [(point.label, point.duration_seconds) for point in points],
1033        )
1034        super().__init__(PioModule(inner))
1035
1036    @classmethod
1037    def _from_module(cls, module: "PioModule") -> "TimeSeries":
1038        value = object.__new__(cls)
1039        _TypedValue.__init__(value, module)
1040        return value
1041
1042    @property
1043    def time_points(self) -> tuple[TimePoint, ...]:
1044        return tuple(TimePoint(*point) for point in self.module._inner._time_series_points())
1045
1046    def __len__(self) -> int:
1047        return self.module._inner._time_series_len()
1048
1049    def __getitem__(self, index):
1050        if isinstance(index, slice):
1051            return [self[position] for position in range(*index.indices(len(self)))]
1052        try:
1053            position = _operator.index(index)
1054        except TypeError:
1055            raise TypeError("time series indices must be integers") from None
1056        if position < 0:
1057            position += len(self)
1058        if position < 0 or position >= len(self):
1059            raise IndexError("time series index out of range")
1060        current = self._collection_entry or _CollectionEntry(self.module)
1061        if current.time_index is not None:
1062            raise TypeError("nested TimeSeries values are not supported")
1063        value = PioModule(self.module._inner._time_series_get(position)).value
1064        return _bind_collection_entry(
1065            value,
1066            _CollectionEntry(
1067                root=current.root,
1068                time_index=position,
1069                scenario_id=current.scenario_id,
1070            ),
1071        )
1072
1073    def __iter__(self):
1074        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])
1014    def __init__(
1015        self,
1016        values: Sequence[Any],
1017        *,
1018        time_points: Sequence[TimePoint],
1019    ) -> None:
1020        if isinstance(values, (str, bytes, bytearray)) or not isinstance(
1021            values, Sequence
1022        ):
1023            raise TypeError("TimeSeries values must be a sequence of PowerIO values")
1024        if not isinstance(time_points, Sequence):
1025            raise TypeError("time_points must be a sequence of TimePoint values")
1026        points = tuple(time_points)
1027        if not all(isinstance(point, TimePoint) for point in points):
1028            raise TypeError("time_points must contain only TimePoint values")
1029        modules = [PioModule.from_value(value)._inner for value in values]
1030        inner = _powerio._PioModule._from_time_series(
1031            modules,
1032            [(point.label, point.duration_seconds) for point in points],
1033        )
1034        super().__init__(PioModule(inner))
time_points: Tuple[TimePoint, ...]
1042    @property
1043    def time_points(self) -> tuple[TimePoint, ...]:
1044        return tuple(TimePoint(*point) for point in self.module._inner._time_series_points())
Inherited Members
_TypedValue
module
class UpdateChange:
field
component_id
terminal
class UpdateReport:
changes
connectivity_changed
__version__: str = '0.11.3'
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:
1593@_guard
1594def apply_bus_load_active_power(
1595    module: PioModule,
1596    bus_id: int,
1597    total: ActivePower,
1598    *,
1599    allocation: str = "proportional_to_current_active_power",
1600) -> UpdateReport:
1601    """Replace aggregate bus demand through an explicit PowerIO allocation rule.
1602
1603    ``"proportional_to_current_active_power"`` preserves each participating
1604    load's current share. ``"equal"`` gives every participating load the same
1605    share, including when their current aggregate demand is zero. PowerIO
1606    requires stable load IDs and reports each load changed.
1607    """
1608    if not isinstance(module, PioModule):
1609        raise TypeError("module must be a PioModule")
1610    if not isinstance(total, ActivePower):
1611        raise TypeError("total must be an ActivePower")
1612    return module._inner._apply_bus_load_active_power(
1613        bus_id,
1614        total,
1615        allocation=allocation,
1616    )

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:
1559@_guard
1560def apply_updates(
1561    target: Any,
1562    updates: Union[
1563        Iterable[OperatingPointUpdate],
1564        Iterable[NetworkUpdate],
1565        Iterable[CalculationUpdate],
1566    ],
1567) -> UpdateReport:
1568    """Validate and apply one batch of typed updates atomically.
1569
1570    ``updates`` contains one update class: :class:`OperatingPointUpdate`,
1571    :class:`NetworkUpdate`, or :class:`CalculationUpdate`. Values are absolute
1572    replacements and power quantities carry their units in the typed value.
1573    ``target`` is a module or a value obtained by indexing a :class:`TimeSeries`
1574    or :class:`ScenarioSet`. If validation fails, the module is unchanged.
1575    """
1576    batch = list(updates)
1577    if isinstance(target, PioModule):
1578        return target._inner._apply_updates(batch)
1579    location = getattr(target, "_collection_entry", None)
1580    if not isinstance(location, _CollectionEntry):
1581        raise TypeError(
1582            "target must be a PioModule or a TimeSeries/ScenarioSet entry"
1583        )
1584    report = location.root._inner._apply_collection_updates(
1585        batch,
1586        time_index=location.time_index,
1587        scenario_id=location.scenario_id,
1588    )
1589    _refresh_collection_entry(target, location)
1590    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]:
1754@_guard
1755def deserialize(source: Any) -> PioModule:
1756    """Deserialize PowerIO IR from a path, file object, or bytes-like source."""
1757    path = _path_from_source(source)
1758    if path is not None:
1759        return PioModule(_powerio._PioModule._deserialize_path(path))
1760    data, _ = _memory_from_source(source, None)
1761    return PioModule(_powerio._PioModule._deserialize_memory(data))

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

def diagnostic_record(diagnostic: Diagnostic) -> Dict[str, Any]:
1781@_guard
1782def diagnostic_record(diagnostic: Diagnostic) -> dict[str, Any]:
1783    """One diagnostic as a JSON-ready dictionary.
1784
1785    ``code``, ``severity``, ``message``, and ``target`` are always present, in
1786    that order. ``id``, ``suggested_action``, and ``related`` follow when the
1787    diagnostic sets them, ``details`` when it is not ``None``, and ``spans``
1788    as ``source``, ``byte_start``, ``byte_end`` dictionaries when the
1789    diagnostic carries at least one span.
1790    """
1791    record: dict[str, Any] = {
1792        "code": diagnostic.code,
1793        "severity": diagnostic.severity,
1794        "message": diagnostic.message,
1795        "target": diagnostic.target,
1796    }
1797    if diagnostic.id:
1798        record["id"] = diagnostic.id
1799    if diagnostic.suggested_action:
1800        record["suggested_action"] = diagnostic.suggested_action
1801    if diagnostic.related:
1802        record["related"] = list(diagnostic.related)
1803    if diagnostic.details is not None:
1804        record["details"] = diagnostic.details
1805    if diagnostic.spans:
1806        record["spans"] = [
1807            {
1808                "source": span.source,
1809                "byte_start": span.byte_start,
1810                "byte_end": span.byte_end,
1811            }
1812            for span in diagnostic.spans
1813        ]
1814    return record

One diagnostic as a JSON-ready dictionary.

code, severity, message, and target are always present, in that order. id, suggested_action, and related follow when the diagnostic sets them, details when it is not None, and spans as source, byte_start, byte_end dictionaries when the diagnostic carries at least one span.

def diagnostic_records( diagnostics: Iterable[Diagnostic]) -> List[Dict[str, Any]]:
1817@_guard
1818def diagnostic_records(diagnostics: Iterable[Diagnostic]) -> list[dict[str, Any]]:
1819    """Every diagnostic as a JSON-ready dictionary, in the order given."""
1820    return [diagnostic_record(item) for item in diagnostics]

Every diagnostic as a JSON-ready dictionary, in the order given.

def emit( module: PioModule[typing.Any], format: str, destination: Optional[Any] = Ellipsis) -> EmitResult:
1732@_guard
1733def emit(module: PioModule, format: str, destination: Optional[Any] = None) -> EmitResult:
1734    """Emit a module as one grid exchange format."""
1735    return _emit_to_destination(
1736        module,
1737        destination,
1738        lambda: module._inner._emit_memory(format),
1739        lambda path: module._inner._emit_path(format, path),
1740    )

Emit a module as one grid exchange format.

def features() -> Dict[str, bool]:
1764@_guard
1765def features() -> dict[str, bool]:
1766    """The build-time features compiled into this powerio installation.
1767
1768    ``matrix``, ``dist``, and ``prob`` are unconditional dependencies of the
1769    extension and are always ``True``. ``gridfm`` reports whether GridFM
1770    Parquet parsing and emission were compiled in; the published wheel
1771    includes them, while a custom source build can omit them.
1772    """
1773    return {
1774        "matrix": True,
1775        "gridfm": bool(getattr(_powerio, "_has_gridfm", False)),
1776        "dist": True,
1777        "prob": True,
1778    }

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:
935@_guard
936def from_ppc(ppc) -> BalancedNetwork:
937    """Case from a PYPOWER dict (``ppc``); the inverse of :meth:`BalancedNetwork.to_ppc`.
938
939    The tables route through the MATPOWER reader, so the semantics match a
940    ``.m`` case exactly: bus ``PD``/``QD`` become loads, ``GS``/``BS`` become
941    shunts, and ``gencost`` is read when present. Result columns past the
942    MATPOWER input widths are dropped. A 10 column ``gen`` table (the layout
943    without the OPF capability columns) passes through at its own width, so
944    the generators come back with no capability limits rather than eleven
945    zero ones. Raises :class:`ValueError` when a required table is absent,
946    when a ``bus`` or ``branch`` row is below its 13 column width, when a row
947    is not a sequence of numbers, or when a cell is not numeric; the message
948    names the table and the row.
949    """
950    value = parse(
951        _io.StringIO(_ppc_to_matpower_text(ppc)),
952        format="matpower",
953        name="from_ppc.m",
954    ).value
955    assert isinstance(value, BalancedNetwork)
956    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]:
1661@_guard
1662def parse(
1663    source: Any,
1664    *,
1665    format: Optional[str] = None,
1666    name: Optional[str] = None,
1667) -> PioModule:
1668    """Parse a path, file object, or bytes-like source.
1669
1670    A string is always a path. Pass raw text through ``io.StringIO`` or
1671    another file object.
1672    """
1673    path = _path_from_source(source)
1674    if path is not None:
1675        if name is not None:
1676            raise ValueError("name is only valid for memory and file object sources")
1677        return PioModule(_powerio._PioModule._parse_path(path, format))
1678    data, source_name = _memory_from_source(source, name)
1679    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:
823@_guard
824def parse_display(path: Any, format: Optional[str] = None) -> DisplayData:
825    """Parse a display artifact such as a PowerWorld ``.pwd`` file."""
826    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]:
836@_guard
837def parse_geo(text: str, name_hint: Optional[str] = None) -> dict[str, Any]:
838    """Tolerantly read a geographic sidecar and return its canonical form.
839
840    Accepts headerless buscoords CSV, aliased CSV/JSON records, and GeoJSON
841    Point/LineString features. Returns ``{"geojson": <FeatureCollection dict>,
842    "diagnostics": [...]}``; ``name_hint`` (a file name) picks CSV against JSON
843    when the content alone is ambiguous. Input with no usable coordinates
844    raises :class:`PowerIOParseError`.
845    """
846    parsed = _powerio.parse_geo(text, name_hint)
847    parsed["geojson"] = _json.loads(parsed["geojson"])
848    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]:
829@_guard
830def resolve_format(name: str) -> Optional[FormatInfo]:
831    """Resolve a format token or common alias to its canonical metadata."""
832    resolved = _powerio.resolve_format(name)
833    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:
1743@_guard
1744def serialize(module: PioModule, destination: Optional[Any] = None) -> EmitResult:
1745    """Serialize a module as PowerIO IR."""
1746    return _emit_to_destination(
1747        module,
1748        destination,
1749        module._inner._serialize_memory,
1750        module._inner._serialize_path,
1751    )

Serialize a module as PowerIO IR.

def versions() -> Any:
964@_guard
965def versions() -> Any:
966    """Return the PowerIO release, sole IR identity, and BMOPF schema."""
967    return _json.loads(_powerio.versions_json())

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