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