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]
1207class AcOpfInstance(_BalancedCalculation): 1208 """An AC optimal power flow calculation instance."""
An AC optimal power flow calculation instance.
Inherited Members
1261class AcOpfSolution(_BalancedCalculation, _CalculationSolution): 1262 """An AC optimal power flow solution."""
An AC optimal power flow solution.
Inherited Members
An AC power flow calculation instance.
Inherited Members
1253class AcPfSolution(_BalancedCalculation, _CalculationSolution): 1254 """An AC power flow solution."""
An AC power flow solution.
Inherited Members
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.
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.
Inherited Members
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.
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.
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.
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.
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.
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.
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.
Inherited Members
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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().
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).
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).
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.
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.
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.
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.
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().
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().
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().
612 def calc_adjacency_matrix(self): 613 """0/1 bus adjacency matrix.""" 614 return _to_csr(self._inner.adjacency())
0/1 bus adjacency matrix.
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().
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.
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().
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().
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.
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.
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.
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).
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
1203class DcOpfInstance(_BalancedCalculation): 1204 """A DC optimal power flow calculation instance."""
A DC optimal power flow calculation instance.
Inherited Members
1257class DcOpfSolution(_BalancedCalculation, _CalculationSolution): 1258 """A DC optimal power flow solution."""
A DC optimal power flow solution.
Inherited Members
A DC power flow calculation instance.
Inherited Members
1249class DcPfSolution(_BalancedCalculation, _CalculationSolution): 1250 """A DC power flow solution."""
A DC power flow solution.
Inherited Members
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.
DisplayData(kind, data)
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.
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.
FormatInfo(token, extension, is_directory, can_emit)
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).
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
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.
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.
Inherited Members
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.
1296 @property 1297 def termination(self) -> str: 1298 """How the calculation ended.""" 1299 return self.module._inner._lindist3flow_solution_termination()
How the calculation ended.
1301 @property 1302 def objective(self) -> float: 1303 """The reported objective value.""" 1304 return self.module._inner._lindist3flow_solution_objective()
The reported objective value.
Inherited Members
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.
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.
Inherited Members
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.
1277 @property 1278 def termination(self) -> str: 1279 """How the calculation ended.""" 1280 return self.module._inner._lindist3flow_solution_termination()
How the calculation ended.
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.
Inherited Members
1235class McAcOpfInstance(_MulticonductorCalculation): 1236 """A multiconductor AC optimal power flow calculation instance."""
A multiconductor AC optimal power flow calculation instance.
Inherited Members
1311class McAcOpfSolution(_MulticonductorCalculation, _CalculationSolution): 1312 """A multiconductor AC optimal power flow solution."""
A multiconductor AC optimal power flow solution.
Inherited Members
1211class McAcPfInstance(_MulticonductorCalculation): 1212 """A multiconductor AC power flow calculation instance."""
A multiconductor AC power flow calculation instance.
Inherited Members
1269class McAcPfSolution(_MulticonductorCalculation, _CalculationSolution): 1270 """A multiconductor AC power flow solution."""
A multiconductor AC power flow solution.
Inherited Members
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
A case file is malformed or unparseable.
PwdDisplay(canvas_width, canvas_height, stamp, substations)
PwdSubstation(number, name, x, y)
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__.
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))
Inherited Members
1265class SocwrOpfSolution(_BalancedCalculation, _CalculationSolution): 1266 """A PowerModels SOCWR relaxation solution and objective lower bound."""
A PowerModels SOCWR relaxation solution and objective lower bound.
Inherited Members
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.
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.
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
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__.
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))
Inherited Members
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.