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

An AC optimal power flow calculation instance.

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

An AC optimal power flow solution.

class AcPfInstance(_BalancedCalculation):
1199class AcPfInstance(_BalancedCalculation):
1200    """An AC power flow calculation instance."""

An AC power flow calculation instance.

class AcPfSolution(_BalancedCalculation, _CalculationSolution):
1253class AcPfSolution(_BalancedCalculation, _CalculationSolution):
1254    """An AC power flow solution."""

An AC power flow solution.

class AcScucInstance(_BalancedCalculation):
1239@_guard_class
1240class AcScucInstance(_BalancedCalculation):
1241    """An AC security constrained unit commitment calculation instance."""
1242
1243    @property
1244    def inputs(self) -> ScucInputs:
1245        """Scheduling, reserve, and contingency inputs."""
1246        return self.module._inner._ac_scuc_inputs()

An AC security constrained unit commitment calculation instance.

inputs: ScucInputs
1243    @property
1244    def inputs(self) -> ScucInputs:
1245        """Scheduling, reserve, and contingency inputs."""
1246        return self.module._inner._ac_scuc_inputs()

Scheduling, reserve, and contingency inputs.

class AcScucSolution(_BalancedCalculation, _CalculationSolution):
1315@_guard_class
1316class AcScucSolution(_BalancedCalculation, _CalculationSolution):
1317    """An AC security constrained unit commitment solution."""
1318
1319    @property
1320    def termination(self) -> str:
1321        """How the calculation ended."""
1322        return self.module._inner._ac_scuc_solution_termination()
1323
1324    @property
1325    def residuals(self) -> Residuals:
1326        """Reported active and reactive power balance residuals."""
1327        return self.module._inner._ac_scuc_solution_residuals()
1328
1329    @property
1330    def producer(self) -> Optional[str]:
1331        """Producer or solver identity, when recorded."""
1332        return self.module._inner._ac_scuc_solution_producer()
1333
1334    @property
1335    def network_outputs(self) -> ScucNetworkOutputs:
1336        """Per interval network outputs."""
1337        return self.module._inner._ac_scuc_solution_network_outputs()
1338
1339    @property
1340    def device_outputs(self) -> ScucDeviceOutputs:
1341        """Per interval dispatchable device outputs."""
1342        return self.module._inner._ac_scuc_solution_device_outputs()
1343
1344    @property
1345    def objective(self) -> Optional[float]:
1346        """Reported objective value, when present."""
1347        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']
1319    @property
1320    def termination(self) -> str:
1321        """How the calculation ended."""
1322        return self.module._inner._ac_scuc_solution_termination()

How the calculation ended.

residuals: Residuals
1324    @property
1325    def residuals(self) -> Residuals:
1326        """Reported active and reactive power balance residuals."""
1327        return self.module._inner._ac_scuc_solution_residuals()

Reported active and reactive power balance residuals.

producer: Optional[str]
1329    @property
1330    def producer(self) -> Optional[str]:
1331        """Producer or solver identity, when recorded."""
1332        return self.module._inner._ac_scuc_solution_producer()

Producer or solver identity, when recorded.

network_outputs: ScucNetworkOutputs
1334    @property
1335    def network_outputs(self) -> ScucNetworkOutputs:
1336        """Per interval network outputs."""
1337        return self.module._inner._ac_scuc_solution_network_outputs()

Per interval network outputs.

device_outputs: ScucDeviceOutputs
1339    @property
1340    def device_outputs(self) -> ScucDeviceOutputs:
1341        """Per interval dispatchable device outputs."""
1342        return self.module._inner._ac_scuc_solution_device_outputs()

Per interval dispatchable device outputs.

objective: Optional[float]
1344    @property
1345    def objective(self) -> Optional[float]:
1346        """Reported objective value, when present."""
1347        return self.module._inner._ac_scuc_solution_objective()

Reported objective value, when present.

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

Decode an in-memory UTF-8 artifact.

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

Calculate the in-service topology summary.

def to_geo_layer(self) -> Dict[str, Any]:
377    def to_geo_layer(self) -> dict[str, Any]:
378        """Transform coordinates to a canonical GeoJSON FeatureCollection.
379
380        A case without coordinates produces an empty feature collection.
381        """
382        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]]:
384    def apply_geo_layer(
385        self, text: str, name_hint: Optional[str] = None
386    ) -> tuple["BalancedNetwork", dict[str, Any]]:
387        """Apply a geographic sidecar and return ``(placed, report)``.
388
389        ``text`` is any form :func:`parse_geo` accepts; this case is
390        unchanged. The report carries ``matched_buses``, ``matched_branches``,
391        ``unmatched_features``, ``unlocated_buses``, ``unlocated_branches``,
392        and ``notes``. The two unlocated counts cover the whole case when the
393        pass ends, so a layer that matched nothing reads apart from a case
394        that needed nothing. The placed copy drops the retained source text,
395        so a same-format emission re-serializes.
396        """
397        inner, report = self._inner.apply_geo_layer(text, name_hint)
398        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]:
400    def resolve_contingencies(self, text: str) -> dict[str, Any]:
401        """Read PSS/E contingency text and bind every case to this network.
402
403        ``text`` is the content of a ``.con`` file, such as
404        ``parse("cases.con").value.text``. The result carries ``cases``,
405        ``resolved``, ``unresolved``, and ``unrecognized_statements`` as
406        counts; ``case_results``, one entry per case in the file's order with
407        its ``name``, whether it ``resolved``, the ``components`` it bound to,
408        and the actions that bound to nothing as ``{"action", "reason"}``; and
409        ``diagnostics``, the reader's notes on statements it kept as text.
410
411        Each component states the ``type`` naming the table, the ``row`` it
412        occupies there, the element's own ``in_service`` flag, and the ``id``
413        the network states for it. ``id`` is ``None`` when the network states
414        no identity for that row; ``type`` and ``row`` name the element either
415        way.
416
417        Binding reports rather than refuses, so a case naming an element this
418        network does not hold is counted unresolved and listed.
419        """
420        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]]:
422    def expand_contingencies(
423        self, con_text: str, sub_text: str
424    ) -> tuple[str, list[Diagnostic]]:
425        """Turn automatic contingency specifications into explicit cases.
426
427        ``con_text`` and ``sub_text`` are the contents of a ``.con`` and a
428        ``.sub`` file. A specification such as ``SINGLE BRANCH IN SUBSYSTEM
429        'A1'`` states a rule, so expanding it needs both the network and the
430        subsystem the ``.sub`` file names. Returns the expanded ``.con`` text,
431        which states every outage explicitly, and the notes from the two
432        readers followed by the expansion's own notes. Only elements this
433        network states in service expand into cases.
434        """
435        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]:
437    def select_subsystem_buses(self, sub_text: str, name: str) -> list[int]:
438        """The bus numbers one named subsystem of ``sub_text`` selects.
439
440        ``sub_text`` is the content of a ``.sub`` file. The numbers come back
441        in ascending order. A name the file does not state raises
442        ``ValueError``.
443        """
444        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:
448    def calc_bprime_matrix(
449        self, scheme: str = "bx", *, skip_zero_impedance: bool = False
450    ):
451        """MATPOWER FDPF Bp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.
452
453        ``skip_zero_impedance=False`` refuses a zero impedance branch
454        (``r`` and ``x`` both zero); pass ``True`` to drop it instead.
455        """
456        return _to_csr(
457            self._inner.bprime(scheme, skip_zero_impedance=skip_zero_impedance)
458        )

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

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

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:
490    def calc_branch_flow_matrix(
491        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
492    ):
493        """Return ``Bf = diag(b) A`` as a CSR matrix (branches by buses)."""
494        return _to_csr(
495            self._inner.calc_branch_flow_matrix(
496                formula, skip_zero_impedance=skip_zero_impedance
497            )
498        )

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:
500    def calc_bus_susceptance_matrix(
501        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
502    ):
503        """Return ``B = A.T diag(b) A`` as a CSR matrix (buses by buses)."""
504        return _to_csr(
505            self._inner.calc_bus_susceptance_matrix(
506                formula, skip_zero_impedance=skip_zero_impedance
507            )
508        )

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:
510    def calc_branch_phase_shift_injection(
511        self, formula: str = "series_susceptance", *, skip_zero_impedance: bool = False
512    ):
513        """Return ``b * shift`` over the branch axis."""
514        np = _require("numpy", "matrix")
515        return np.asarray(
516            self._inner.calc_branch_phase_shift_injection(
517                formula, skip_zero_impedance=skip_zero_impedance
518            ),
519            dtype=float,
520        )

Return b * shift over the branch axis.

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

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

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

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

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:
585    def calc_bdoubleprime_matrix(
586        self, scheme: str = "bx", *, skip_zero_impedance: bool = False
587    ):
588        """MATPOWER FDPF Bpp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.
589        ``skip_zero_impedance`` as in :meth:`calc_bprime_matrix`.
590        """
591        return _to_csr(
592            self._inner.bdoubleprime(scheme, skip_zero_impedance=skip_zero_impedance)
593        )

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

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

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

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

Undirected networkx graph keyed by bus id.

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

class CalculationUpdate:
data_role
class ComponentId:
local_id
component_type
class ContingencySet(_TypedValue):
1382@_guard_class
1383class ContingencySet(_TypedValue):
1384    """A PSS/E contingency description file: the cases to run, the automatic
1385    specifications that state cases by rule, and the ``SKIP`` rules.
1386
1387    :func:`parse` returns it for a ``.con`` file or for text declared as
1388    ``psse-con``, :func:`powerio.emit` writes it back under that token, and
1389    :func:`serialize` carries it through PowerIO IR. Bind a set to a case with
1390    ``network.resolve_contingencies(cases.text)`` and turn its automatic
1391    specifications into explicit cases with
1392    ``network.expand_contingencies(cases.text, groups.text)``.
1393    """
1394
1395    @property
1396    def text(self) -> str:
1397        """The ``.con`` text for this set."""
1398        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
1395    @property
1396    def text(self) -> str:
1397        """The ``.con`` text for this set."""
1398        return _emitted_text(self.module, "psse-con")

The .con text for this set.

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

A DC optimal power flow calculation instance.

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

A DC optimal power flow solution.

class DcPfInstance(_BalancedCalculation):
1195class DcPfInstance(_BalancedCalculation):
1196    """A DC power flow calculation instance."""

A DC power flow calculation instance.

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

code
details

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

target
severity

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

message
id
suggested_action
related
spans
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:
198@dataclass(frozen=True)
199class EmitResult:
200    """Artifact inventory and diagnostics from an emission or serialization."""
201
202    artifacts: tuple[Artifact, ...]
203    layout: str
204    fidelity: str
205    diagnostics: tuple[Diagnostic, ...]
206
207    @property
208    def text(self) -> Optional[str]:
209        """The sole UTF-8 memory artifact, or ``None`` for other inventories."""
210        if len(self.artifacts) != 1 or self.artifacts[0].data is None:
211            return None
212        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]
207    @property
208    def text(self) -> Optional[str]:
209        """The sole UTF-8 memory artifact, or ``None`` for other inventories."""
210        if len(self.artifacts) != 1 or self.artifacts[0].data is None:
211            return None
212        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):
1350@_guard_class
1351class GeoLayer(_TypedValue):
1352    """A standalone geographic document: element points and routes keyed by
1353    element identity, in one coordinate space.
1354
1355    :func:`parse` returns it for the canonical ``.geo.json``, GeoJSON, aliased
1356    CSV or JSON records, headerless buscoords CSV, and a PowerWorld ``.pwd``
1357    display. :func:`powerio.emit` writes the canonical document as
1358    ``geo-json``, and :func:`serialize` carries the layer through PowerIO IR.
1359    Place a layer onto a case with
1360    ``network.apply_geo_layer(layer.geojson)``.
1361    """
1362
1363    @property
1364    def geojson(self) -> str:
1365        """The canonical ``.geo.json`` document for this layer."""
1366        result = emit(self.module, "geo-json")
1367        data = result.artifacts[0].data
1368        if data is None:
1369            raise ValueError("the layer emission returned no artifact bytes")
1370        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
1363    @property
1364    def geojson(self) -> str:
1365        """The canonical ``.geo.json`` document for this layer."""
1366        result = emit(self.module, "geo-json")
1367        data = result.artifacts[0].data
1368        if data is None:
1369            raise ValueError("the layer emission returned no artifact bytes")
1370        return data.decode("utf-8")

The canonical .geo.json document for this layer.

Inherited Members
_TypedValue
_TypedValue
module
class LinDist3FlowOpfInstance(_MulticonductorCalculation):
1225@_guard_class
1226class LinDist3FlowOpfInstance(_MulticonductorCalculation):
1227    """A radial fixed-reference multiconductor linear OPF instance."""
1228
1229    @property
1230    def metadata(self) -> dict[str, Any]:
1231        """Node and conductor axes, roots, and reference phasors in volts/radians."""
1232        return self.module._inner._lindist3flow_metadata()

A radial fixed-reference multiconductor linear OPF instance.

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

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

class LinDist3FlowOpfSolution(_MulticonductorCalculation, _CalculationSolution):
1292@_guard_class
1293class LinDist3FlowOpfSolution(_MulticonductorCalculation, _CalculationSolution):
1294    """LinDist3Flow primal values in squared volts, watts, and vars."""
1295
1296    @property
1297    def termination(self) -> str:
1298        """How the calculation ended."""
1299        return self.module._inner._lindist3flow_solution_termination()
1300
1301    @property
1302    def objective(self) -> float:
1303        """The reported objective value."""
1304        return self.module._inner._lindist3flow_solution_objective()
1305
1306    def __getitem__(self, quantity: str) -> list[float]:
1307        """Copy one named primal column in its physical instance order."""
1308        return self.module._inner._lindist3flow_solution_values(quantity)

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

termination: str
1296    @property
1297    def termination(self) -> str:
1298        """How the calculation ended."""
1299        return self.module._inner._lindist3flow_solution_termination()

How the calculation ended.

objective: float
1301    @property
1302    def objective(self) -> float:
1303        """The reported objective value."""
1304        return self.module._inner._lindist3flow_solution_objective()

The reported objective value.

class LinDist3FlowPfInstance(_MulticonductorCalculation):
1215@_guard_class
1216class LinDist3FlowPfInstance(_MulticonductorCalculation):
1217    """A fixed-dispatch LinDist3Flow power-flow instance."""
1218
1219    @property
1220    def metadata(self) -> dict[str, Any]:
1221        """Node/conductor axes, preparation actions, and reference phasors."""
1222        return self.module._inner._lindist3flow_metadata()

A fixed-dispatch LinDist3Flow power-flow instance.

metadata: dict[str, typing.Any]
1219    @property
1220    def metadata(self) -> dict[str, Any]:
1221        """Node/conductor axes, preparation actions, and reference phasors."""
1222        return self.module._inner._lindist3flow_metadata()

Node/conductor axes, preparation actions, and reference phasors.

class LinDist3FlowPfSolution(_MulticonductorCalculation, _CalculationSolution):
1273@_guard_class
1274class LinDist3FlowPfSolution(_MulticonductorCalculation, _CalculationSolution):
1275    """Fixed-dispatch LinDist3Flow values and monitored SI line limits."""
1276
1277    @property
1278    def termination(self) -> str:
1279        """How the calculation ended."""
1280        return self.module._inner._lindist3flow_solution_termination()
1281
1282    @property
1283    def limit_checks(self) -> list[dict[str, Any]]:
1284        """Monitored line ratings, values, loading ratios, and overload flags."""
1285        return self.module._inner._lindist3flow_limit_checks()
1286
1287    def __getitem__(self, quantity: str) -> list[float]:
1288        """Copy one named primal column in its physical instance order."""
1289        return self.module._inner._lindist3flow_solution_values(quantity)

Fixed-dispatch LinDist3Flow values and monitored SI line limits.

termination: str
1277    @property
1278    def termination(self) -> str:
1279        """How the calculation ended."""
1280        return self.module._inner._lindist3flow_solution_termination()

How the calculation ended.

limit_checks: list[dict[str, typing.Any]]
1282    @property
1283    def limit_checks(self) -> list[dict[str, Any]]:
1284        """Monitored line ratings, values, loading ratios, and overload flags."""
1285        return self.module._inner._lindist3flow_limit_checks()

Monitored line ratings, values, loading ratios, and overload flags.

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

A multiconductor AC optimal power flow calculation instance.

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

A multiconductor AC optimal power flow solution.

class McAcPfInstance(_MulticonductorCalculation):
1211class McAcPfInstance(_MulticonductorCalculation):
1212    """A multiconductor AC power flow calculation instance."""

A multiconductor AC power flow calculation instance.

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

A multiconductor AC power flow solution.

class MonitoredSet(_TypedValue):
1417@_guard_class
1418class MonitoredSet(_TypedValue):
1419    """A PSS/E monitored element file: the branches, interfaces, and voltage
1420    scopes a contingency run reports on.
1421
1422    :func:`parse` returns it for a ``.mon`` file or for text declared as
1423    ``psse-mon``.
1424    """
1425
1426    @property
1427    def text(self) -> str:
1428        """The ``.mon`` text for this set."""
1429        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
1426    @property
1427    def text(self) -> str:
1428        """The ``.mon`` text for this set."""
1429        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):
1142@_guard_class
1143class OperatingPoint(_TypedValue):
1144    """A possibly partial assignment over fixed equipment identities."""
1145
1146    @property
1147    def network(self) -> "BalancedNetwork":
1148        """The balanced network this operating point is stated over.
1149
1150        The point's own values are applied to the returned copy, so it is the
1151        network a solver receives for this entry; the collection's shared
1152        base network is not changed. A multiconductor operating point raises
1153        :class:`PowerIOError`.
1154
1155        Net bus injection quantities have no balanced network field, so they
1156        are dropped here and the property reports nothing. Emit the
1157        collection when you need that omission reported: `emit` warns
1158        `EMIT.OPERATING_POINT.DATA_OMITTED` for the same point.
1159        """
1160        return BalancedNetwork(self.module._inner._operating_point_network())

A possibly partial assignment over fixed equipment identities.

network: BalancedNetwork
1146    @property
1147    def network(self) -> "BalancedNetwork":
1148        """The balanced network this operating point is stated over.
1149
1150        The point's own values are applied to the returned copy, so it is the
1151        network a solver receives for this entry; the collection's shared
1152        base network is not changed. A multiconductor operating point raises
1153        :class:`PowerIOError`.
1154
1155        Net bus injection quantities have no balanced network field, so they
1156        are dropped here and the property reports nothing. Emit the
1157        collection when you need that omission reported: `emit` warns
1158        `EMIT.OPERATING_POINT.DATA_OMITTED` for the same point.
1159        """
1160        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:
1461@_guard_class
1462class PioModule:
1463    """One typed value with diagnostics, producer, sources, source mappings,
1464    history, and extensions.
1465    """
1466
1467    def __init__(self, inner: "_powerio._PioModule"):
1468        self._inner = inner
1469
1470    @classmethod
1471    def from_value(cls, value: Any) -> "PioModule":
1472        """Wrap an existing typed value without serializing it."""
1473        if isinstance(value, BalancedNetwork):
1474            return cls(_powerio._PioModule.from_balanced_network(value._inner))
1475        if isinstance(value, dist.MulticonductorNetwork):
1476            return cls(_powerio._PioModule.from_multiconductor_network(value._inner))
1477        if isinstance(value, _TypedValue):
1478            location = value._collection_entry
1479            inner = value.module._inner
1480            if location is not None:
1481                inner = location.root._inner
1482                if location.scenario_id is not None:
1483                    inner = inner._scenario_get(location.scenario_id)
1484                if location.time_index is not None:
1485                    inner = inner._time_series_get(location.time_index)
1486            return cls(inner._copy())
1487        raise TypeError("PioModule.from_value expects a typed PowerIO value")
1488
1489    @property
1490    def value(self) -> Any:
1491        """The contained typed value."""
1492        type_name = self._inner._type_name
1493        if type_name == "powerio.BalancedNetwork":
1494            return BalancedNetwork(self._inner.as_balanced_network())
1495        if type_name == "powerio.MulticonductorNetwork":
1496            return dist.MulticonductorNetwork(self._inner.as_multiconductor_network())
1497        if type_name.startswith("powerio.TimeSeries<"):
1498            return TimeSeries._from_module(self)
1499        if type_name.startswith("powerio.ScenarioSet<"):
1500            return ScenarioSet._from_module(self)
1501        value_class = _VALUE_CLASSES.get(type_name)
1502        if value_class is None:
1503            raise RuntimeError(f"this binding has no Python class for {type_name}")
1504        return value_class(self)
1505
1506    @property
1507    def type_name(self) -> str:
1508        """The canonical structural name of the contained value, such as
1509        ``powerio.OperatingPoint<powerio.BalancedNetwork>``. The C ABI and
1510        PowerIO IR name the same type with the same string.
1511        """
1512        return str(self._inner._type_name)
1513
1514    @property
1515    def diagnostics(self) -> list[Diagnostic]:
1516        """The diagnostics stored on this module, in encounter order."""
1517        return list(self._inner.diagnostics)
1518
1519    def to_balanced_report(self, base_mva: float = 100.0) -> Any:
1520        """Report whether a multiconductor network can become balanced."""
1521        return _json.loads(self._inner.lowering_readiness_json(base_mva))
1522
1523    def to_balanced(self, base_mva: float = 100.0) -> "PioModule":
1524        """Transform a multiconductor network to a balanced module."""
1525        return PioModule(self._inner.lower_to_balanced(base_mva))
1526
1527    def to_dc_pf_instance(self) -> "PioModule":
1528        """Build a DC power flow instance from a balanced network module."""
1529        return PioModule(self._inner._to_dc_pf_instance())
1530
1531    def to_ac_pf_instance(self) -> "PioModule":
1532        """Build an AC power flow instance from a balanced network module."""
1533        return PioModule(self._inner._to_ac_pf_instance())
1534
1535    def to_dc_opf_instance(self) -> "PioModule":
1536        """Build a DC optimal power flow instance from a balanced network module."""
1537        return PioModule(self._inner._to_dc_opf_instance())
1538
1539    def to_ac_opf_instance(self) -> "PioModule":
1540        """Build an AC optimal power flow instance from a balanced network module."""
1541        return PioModule(self._inner._to_ac_opf_instance())
1542
1543    def to_mc_ac_pf_instance(self) -> "PioModule":
1544        """Build a multiconductor AC power flow instance from a network module."""
1545        return PioModule(self._inner._to_mc_ac_pf_instance())
1546
1547    def to_mc_ac_opf_instance(self) -> "PioModule":
1548        """Build a multiconductor AC optimal power flow instance from a network module."""
1549        return PioModule(self._inner._to_mc_ac_opf_instance())
1550
1551    def to_lindist3flow_opf_instance(self) -> "PioModule":
1552        """Build a LinDist3Flow optimal power flow instance from a network module."""
1553        return PioModule(self._inner._to_lindist3flow_opf_instance())
1554
1555    def to_lindist3flow_pf_instance(self) -> "PioModule":
1556        """Build a fixed-dispatch LinDist3Flow instance from a network module."""
1557        return PioModule(self._inner._to_lindist3flow_pf_instance())
1558
1559    def __repr__(self) -> str:
1560        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)
1467    def __init__(self, inner: "_powerio._PioModule"):
1468        self._inner = inner
@classmethod
def from_value(*args, **kwds):
1470    @classmethod
1471    def from_value(cls, value: Any) -> "PioModule":
1472        """Wrap an existing typed value without serializing it."""
1473        if isinstance(value, BalancedNetwork):
1474            return cls(_powerio._PioModule.from_balanced_network(value._inner))
1475        if isinstance(value, dist.MulticonductorNetwork):
1476            return cls(_powerio._PioModule.from_multiconductor_network(value._inner))
1477        if isinstance(value, _TypedValue):
1478            location = value._collection_entry
1479            inner = value.module._inner
1480            if location is not None:
1481                inner = location.root._inner
1482                if location.scenario_id is not None:
1483                    inner = inner._scenario_get(location.scenario_id)
1484                if location.time_index is not None:
1485                    inner = inner._time_series_get(location.time_index)
1486            return cls(inner._copy())
1487        raise TypeError("PioModule.from_value expects a typed PowerIO value")

Wrap an existing typed value without serializing it.

value: ~_T
1489    @property
1490    def value(self) -> Any:
1491        """The contained typed value."""
1492        type_name = self._inner._type_name
1493        if type_name == "powerio.BalancedNetwork":
1494            return BalancedNetwork(self._inner.as_balanced_network())
1495        if type_name == "powerio.MulticonductorNetwork":
1496            return dist.MulticonductorNetwork(self._inner.as_multiconductor_network())
1497        if type_name.startswith("powerio.TimeSeries<"):
1498            return TimeSeries._from_module(self)
1499        if type_name.startswith("powerio.ScenarioSet<"):
1500            return ScenarioSet._from_module(self)
1501        value_class = _VALUE_CLASSES.get(type_name)
1502        if value_class is None:
1503            raise RuntimeError(f"this binding has no Python class for {type_name}")
1504        return value_class(self)

The contained typed value.

type_name: str
1506    @property
1507    def type_name(self) -> str:
1508        """The canonical structural name of the contained value, such as
1509        ``powerio.OperatingPoint<powerio.BalancedNetwork>``. The C ABI and
1510        PowerIO IR name the same type with the same string.
1511        """
1512        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]
1514    @property
1515    def diagnostics(self) -> list[Diagnostic]:
1516        """The diagnostics stored on this module, in encounter order."""
1517        return list(self._inner.diagnostics)

The diagnostics stored on this module, in encounter order.

def to_balanced_report(self, base_mva: float = Ellipsis) -> Any:
1519    def to_balanced_report(self, base_mva: float = 100.0) -> Any:
1520        """Report whether a multiconductor network can become balanced."""
1521        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]:
1523    def to_balanced(self, base_mva: float = 100.0) -> "PioModule":
1524        """Transform a multiconductor network to a balanced module."""
1525        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]:
1527    def to_dc_pf_instance(self) -> "PioModule":
1528        """Build a DC power flow instance from a balanced network module."""
1529        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]:
1531    def to_ac_pf_instance(self) -> "PioModule":
1532        """Build an AC power flow instance from a balanced network module."""
1533        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]:
1535    def to_dc_opf_instance(self) -> "PioModule":
1536        """Build a DC optimal power flow instance from a balanced network module."""
1537        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]:
1539    def to_ac_opf_instance(self) -> "PioModule":
1540        """Build an AC optimal power flow instance from a balanced network module."""
1541        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]:
1543    def to_mc_ac_pf_instance(self) -> "PioModule":
1544        """Build a multiconductor AC power flow instance from a network module."""
1545        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]:
1547    def to_mc_ac_opf_instance(self) -> "PioModule":
1548        """Build a multiconductor AC optimal power flow instance from a network module."""
1549        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]:
1551    def to_lindist3flow_opf_instance(self) -> "PioModule":
1552        """Build a LinDist3Flow optimal power flow instance from a network module."""
1553        return PioModule(self._inner._to_lindist3flow_opf_instance())

Build a LinDist3Flow optimal power flow instance from a network module.

def to_lindist3flow_pf_instance(self) -> PioModule[LinDist3FlowPfInstance]:
1555    def to_lindist3flow_pf_instance(self) -> "PioModule":
1556        """Build a fixed-dispatch LinDist3Flow instance from a network module."""
1557        return PioModule(self._inner._to_lindist3flow_pf_instance())

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

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])
1083    def __init__(
1084        self,
1085        values: Mapping[str, Any],
1086        *,
1087        probabilities: Optional[Mapping[str, float]] = None,
1088    ) -> None:
1089        if not isinstance(values, Mapping):
1090            raise TypeError("ScenarioSet values must be a mapping from IDs to values")
1091        if probabilities is not None and not isinstance(probabilities, Mapping):
1092            raise TypeError("probabilities must be a mapping from scenario IDs to numbers")
1093        ids = list(values)
1094        modules = [PioModule.from_value(values[id])._inner for id in ids]
1095        inner = _powerio._PioModule._from_scenario_set(
1096            modules,
1097            ids,
1098            None if probabilities is None else dict(probabilities),
1099        )
1100        super().__init__(PioModule(inner))
scenarios: Tuple[Scenario, ...]
1108    @property
1109    def scenarios(self) -> tuple[Scenario, ...]:
1110        return tuple(Scenario(*entry) for entry in self.module._inner._scenario_entries())
Inherited Members
_TypedValue
module
class ScucActiveReserveZone:
ramping_down_violation_cost
ramping_down_requirement
regulation_up_violation_cost
id
nonsynchronized_requirement_fraction
synchronized_requirement_fraction
regulation_down_requirement_fraction
regulation_down_violation_cost
regulation_up_requirement_fraction
buses
synchronized_violation_cost
nonsynchronized_violation_cost
ramping_up_violation_cost
ramping_up_requirement
class ScucBranchSwitchingCost:
connection_cost
id
disconnection_cost
class ScucContingency:
id
components
class ScucDevice:
startup_limits
startup_cost_adjustments
energy_upper_bounds
periods
shutdown_cost
reserve_limits
startup_cost
minimum_down_time
initial_on_status
minimum_up_time
on_cost
initial_commitment
energy_lower_bounds
id
reactive_capability
ramp_limits
kind
class ScucDeviceOutputs:
p_on
p_reg_res_down
p_ramp_res_down_offline
p_ramp_res_up_offline
p_nsyn_res
q_res_up
shutdown_status
p_reg_res_up
on_status
startup_status
p_syn_res
p_ramp_res_up_online
q
p_ramp_res_down_online
q_res_down
class ScucDevicePeriod:
on_status_min
on_status_max
reserve_costs
active_power_max
reactive_power_max
active_power_min
reactive_power_min
energy_cost_blocks
class ScucEnergyCostBlock:
block_size
marginal_cost
class ScucEnergyRequirement:
energy
start_time
end_time
class ScucInitialCommitment:
accumulated_up_time
accumulated_down_time
class ScucInputs:
branch_switching_costs
contingencies
interval_durations
reactive_reserve_zones
violation_costs
active_reserve_zones
devices
transformer_controls
shunts
class ScucNetworkOutputs:
dc_line_qdc_fr
transformer_ta
shunt_step
ac_line_on_status
dc_line_pdc_fr
dc_line_qdc_to
transformer_tm
transformer_on_status
bus_vm
bus_va
class ScucRampLimits:
shutdown
down
up
startup
class ScucReactiveCapability:
reactive_power_at_zero_active_power
slope_min
kind
reactive_power_at_zero_active_power_max
slope_max
slope
reactive_power_at_zero_active_power_min
class ScucReactiveReserveZone:
id
buses
reactive_down_requirement
reactive_up_violation_cost
reactive_down_violation_cost
reactive_up_requirement
class ScucReserveCosts:
regulation_up
synchronized
nonsynchronized
ramping_up_offline
ramping_up_online
reactive_up
regulation_down
ramping_down_offline
reactive_down
ramping_down_online
class ScucReserveLimits:
ramping_down_online
synchronized
ramping_up_online
regulation_up
regulation_down
ramping_down_offline
ramping_up_offline
nonsynchronized
class ScucShunt:
conductance_per_step
step_max
id
step_min
susceptance_per_step
initial_step
class ScucStartupCostAdjustment:
cost
maximum_down_time
class ScucStartupLimit:
start_time
end_time
maximum_startups
class ScucTransformerControl:
tap_ratio_max
id
tap_ratio_min
phase_shift_max
phase_shift_min
class ScucViolationCosts:
branch_thermal_limit
reactive_power_balance
active_power_balance
energy_requirement
class SocwrOpfSolution(_BalancedCalculation, _CalculationSolution):
1265class SocwrOpfSolution(_BalancedCalculation, _CalculationSolution):
1266    """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_end
byte_start
source
class SubsystemSet(_TypedValue):
1401@_guard_class
1402class SubsystemSet(_TypedValue):
1403    """A PSS/E subsystem description file: the named bus groups a contingency
1404    run and a monitored element file draw on.
1405
1406    :func:`parse` returns it for a ``.sub`` file or for text declared as
1407    ``psse-sub``, and ``network.select_subsystem_buses(groups.text, name)``
1408    names the buses one subsystem selects.
1409    """
1410
1411    @property
1412    def text(self) -> str:
1413        """The ``.sub`` text for this set."""
1414        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
1411    @property
1412    def text(self) -> str:
1413        """The ``.sub`` text for this set."""
1414        return _emitted_text(self.module, "psse-sub")

The .sub text for this set.

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

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:
1596@_guard
1597def apply_updates(
1598    target: Any,
1599    updates: Union[
1600        Iterable[OperatingPointUpdate],
1601        Iterable[NetworkUpdate],
1602        Iterable[CalculationUpdate],
1603    ],
1604) -> UpdateReport:
1605    """Validate and apply one batch of typed updates atomically.
1606
1607    ``updates`` contains one update class: :class:`OperatingPointUpdate`,
1608    :class:`NetworkUpdate`, or :class:`CalculationUpdate`. Values are absolute
1609    replacements and power quantities carry their units in the typed value.
1610    ``target`` is a module or a value obtained by indexing a :class:`TimeSeries`
1611    or :class:`ScenarioSet`. If validation fails, the module is unchanged.
1612    """
1613    batch = list(updates)
1614    if isinstance(target, PioModule):
1615        return target._inner._apply_updates(batch)
1616    location = getattr(target, "_collection_entry", None)
1617    if not isinstance(location, _CollectionEntry):
1618        raise TypeError(
1619            "target must be a PioModule or a TimeSeries/ScenarioSet entry"
1620        )
1621    report = location.root._inner._apply_collection_updates(
1622        batch,
1623        time_index=location.time_index,
1624        scenario_id=location.scenario_id,
1625    )
1626    _refresh_collection_entry(target, location)
1627    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]:
1791@_guard
1792def deserialize(source: Any) -> PioModule:
1793    """Deserialize PowerIO IR from a path, file object, or bytes-like source."""
1794    path = _path_from_source(source)
1795    if path is not None:
1796        return PioModule(_powerio._PioModule._deserialize_path(path))
1797    data, _ = _memory_from_source(source, None)
1798    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]:
1818@_guard
1819def diagnostic_record(diagnostic: Diagnostic) -> dict[str, Any]:
1820    """One diagnostic as a JSON-ready dictionary.
1821
1822    ``code``, ``severity``, ``message``, and ``target`` are always present, in
1823    that order. ``id``, ``suggested_action``, and ``related`` follow when the
1824    diagnostic sets them, ``details`` when it is not ``None``, and ``spans``
1825    as ``source``, ``byte_start``, ``byte_end`` dictionaries when the
1826    diagnostic carries at least one span.
1827    """
1828    record: dict[str, Any] = {
1829        "code": diagnostic.code,
1830        "severity": diagnostic.severity,
1831        "message": diagnostic.message,
1832        "target": diagnostic.target,
1833    }
1834    if diagnostic.id:
1835        record["id"] = diagnostic.id
1836    if diagnostic.suggested_action:
1837        record["suggested_action"] = diagnostic.suggested_action
1838    if diagnostic.related:
1839        record["related"] = list(diagnostic.related)
1840    if diagnostic.details is not None:
1841        record["details"] = diagnostic.details
1842    if diagnostic.spans:
1843        record["spans"] = [
1844            {
1845                "source": span.source,
1846                "byte_start": span.byte_start,
1847                "byte_end": span.byte_end,
1848            }
1849            for span in diagnostic.spans
1850        ]
1851    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]]:
1854@_guard
1855def diagnostic_records(diagnostics: Iterable[Diagnostic]) -> list[dict[str, Any]]:
1856    """Every diagnostic as a JSON-ready dictionary, in the order given."""
1857    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:
1769@_guard
1770def emit(module: PioModule, format: str, destination: Optional[Any] = None) -> EmitResult:
1771    """Emit a module as one grid exchange format."""
1772    return _emit_to_destination(
1773        module,
1774        destination,
1775        lambda: module._inner._emit_memory(format),
1776        lambda path: module._inner._emit_path(format, path),
1777    )

Emit a module as one grid exchange format.

def features() -> Dict[str, bool]:
1801@_guard
1802def features() -> dict[str, bool]:
1803    """The build-time features compiled into this powerio installation.
1804
1805    ``matrix``, ``dist``, and ``prob`` are unconditional dependencies of the
1806    extension and are always ``True``. ``gridfm`` reports whether GridFM
1807    Parquet parsing and emission were compiled in; the published wheel
1808    includes them, while a custom source build can omit them.
1809    """
1810    return {
1811        "matrix": True,
1812        "gridfm": bool(getattr(_powerio, "_has_gridfm", False)),
1813        "dist": True,
1814        "prob": True,
1815    }

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:
937@_guard
938def from_ppc(ppc) -> BalancedNetwork:
939    """Case from a PYPOWER dict (``ppc``); the inverse of :meth:`BalancedNetwork.to_ppc`.
940
941    The tables route through the MATPOWER reader, so the semantics match a
942    ``.m`` case exactly: bus ``PD``/``QD`` become loads, ``GS``/``BS`` become
943    shunts, and ``gencost`` is read when present. Result columns past the
944    MATPOWER input widths are dropped. A 10 column ``gen`` table (the layout
945    without the OPF capability columns) passes through at its own width, so
946    the generators come back with no capability limits rather than eleven
947    zero ones. Raises :class:`ValueError` when a required table is absent,
948    when a ``bus`` or ``branch`` row is below its 13 column width, when a row
949    is not a sequence of numbers, or when a cell is not numeric; the message
950    names the table and the row.
951    """
952    value = parse(
953        _io.StringIO(_ppc_to_matpower_text(ppc)),
954        format="matpower",
955        name="from_ppc.m",
956    ).value
957    assert isinstance(value, BalancedNetwork)
958    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]:
1698@_guard
1699def parse(
1700    source: Any,
1701    *,
1702    format: Optional[str] = None,
1703    name: Optional[str] = None,
1704) -> PioModule:
1705    """Parse a path, file object, or bytes-like source.
1706
1707    A string is always a path. Pass raw text through ``io.StringIO`` or
1708    another file object.
1709    """
1710    path = _path_from_source(source)
1711    if path is not None:
1712        if name is not None:
1713            raise ValueError("name is only valid for memory and file object sources")
1714        return PioModule(_powerio._PioModule._parse_path(path, format))
1715    data, source_name = _memory_from_source(source, name)
1716    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:
825@_guard
826def parse_display(path: Any, format: Optional[str] = None) -> DisplayData:
827    """Parse a display artifact such as a PowerWorld ``.pwd`` file."""
828    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]:
838@_guard
839def parse_geo(text: str, name_hint: Optional[str] = None) -> dict[str, Any]:
840    """Tolerantly read a geographic sidecar and return its canonical form.
841
842    Accepts headerless buscoords CSV, aliased CSV/JSON records, and GeoJSON
843    Point/LineString features. Returns ``{"geojson": <FeatureCollection dict>,
844    "diagnostics": [...]}``; ``name_hint`` (a file name) picks CSV against JSON
845    when the content alone is ambiguous. Input with no usable coordinates
846    raises :class:`PowerIOParseError`.
847    """
848    parsed = _powerio.parse_geo(text, name_hint)
849    parsed["geojson"] = _json.loads(parsed["geojson"])
850    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]:
831@_guard
832def resolve_format(name: str) -> Optional[FormatInfo]:
833    """Resolve a format token or common alias to its canonical metadata."""
834    resolved = _powerio.resolve_format(name)
835    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:
1780@_guard
1781def serialize(module: PioModule, destination: Optional[Any] = None) -> EmitResult:
1782    """Serialize a module as PowerIO IR."""
1783    return _emit_to_destination(
1784        module,
1785        destination,
1786        module._inner._serialize_memory,
1787        module._inner._serialize_path,
1788    )

Serialize a module as PowerIO IR.

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

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