powerio
powerio: lossless power system case file IO, conversion, and matrices.
Parse MATPOWER, PSS/E, PowerWorld, PowerModels JSON, egret JSON, pandapower JSON, and PyPSA CSV folders into one format-neutral case; write retained text formats back byte exact; convert between formats; and pull the sparse matrices and graph views solvers need::
import powerio as pio
net = pio.parse_file("case9.m") # format inferred from the extension
print(net.n_buses, net.base_mva) # 9 100.0
text = net.to_matpower() # byte-exact MATPOWER echo
raw, warnings = pio.convert_file("case9.m", "psse")
pp_json, warnings = pio.convert_file("case9.m", "pandapower-json")
pypsa_out = net.write_pypsa_csv_folder("case9-pypsa")
B = net.bprime() # scipy.sparse, the FDPF B'
Y = net.ybus() # complex csr, G + jB
G = net.to_networkx() # networkx.Graph keyed by bus id
PyPSA CSV folders carry the static network topology (PyPSA's native component format for network definition); time series NetCDF/HDF5 scenarios are out of scope for now (https://github.com/eigenergy/powerio/issues/107).
import powerio and parsing/writing/converting pull in nothing but the
interpreter. The matrix methods need scipy/numpy and the graph view needs networkx; add them
with pip install 'powerio[matrix]', [graph], or [all]. A missing
extra raises a clear ImportError, never a link error: the compiled core
(powerio._powerio) returns COO triplets as plain Python lists, and the
wrappers here assemble scipy matrices and networkx graphs lazily.
1"""powerio: lossless power system case file IO, conversion, and matrices. 2 3Parse MATPOWER, PSS/E, PowerWorld, PowerModels JSON, egret JSON, pandapower 4JSON, and PyPSA CSV folders into one format-neutral case; write retained text 5formats back byte exact; convert between formats; and pull the sparse matrices 6and graph views solvers need:: 7 8 import powerio as pio 9 10 net = pio.parse_file("case9.m") # format inferred from the extension 11 print(net.n_buses, net.base_mva) # 9 100.0 12 text = net.to_matpower() # byte-exact MATPOWER echo 13 raw, warnings = pio.convert_file("case9.m", "psse") 14 pp_json, warnings = pio.convert_file("case9.m", "pandapower-json") 15 pypsa_out = net.write_pypsa_csv_folder("case9-pypsa") 16 17 B = net.bprime() # scipy.sparse, the FDPF B' 18 Y = net.ybus() # complex csr, G + jB 19 G = net.to_networkx() # networkx.Graph keyed by bus id 20 21PyPSA CSV folders carry the static network topology (PyPSA's native component 22format for network definition); time series NetCDF/HDF5 scenarios are out of 23scope for now (https://github.com/eigenergy/powerio/issues/107). 24 25``import powerio`` and parsing/writing/converting pull in nothing but the 26interpreter. The matrix methods need scipy/numpy and the graph view needs networkx; add them 27with ``pip install 'powerio[matrix]'``, ``[graph]``, or ``[all]``. A missing 28extra raises a clear ImportError, never a link error: the compiled core 29(``powerio._powerio``) returns COO triplets as plain Python lists, and the 30wrappers here assemble scipy matrices and networkx graphs lazily. 31""" 32 33from __future__ import annotations 34 35import importlib 36from collections import namedtuple 37from typing import Any, Optional 38 39from . import _powerio 40from ._powerio import PowerIODataError, PowerIOError, PowerIOParseError, __version__ 41 42__all__ = [ 43 "Network", 44 "Case", 45 "Incidence", 46 "YbusParts", 47 "Conversion", 48 "DisplayData", 49 "PwdDisplay", 50 "PwdSubstation", 51 "DenseNetwork", 52 "DenseBranch", 53 "DenseGen", 54 "DenseDemand", 55 "DenseShunt", 56 "PowerIOError", 57 "PowerIOParseError", 58 "PowerIODataError", 59 "parse_file", 60 "parse_display_file", 61 "parse_display_bytes", 62 "parse_str", 63 "from_json", 64 "convert_file", 65 "convert_str", 66 "to_format", 67 "to_matpower", 68 "to_json", 69 "to_dense", 70 "write_gridfm_batch", 71 "read_gridfm", 72 "read_gridfm_scenarios", 73 "read_pypsa_csv_folder", 74 "GridfmRead", 75 "__version__", 76] 77 78Conversion = namedtuple("Conversion", ["text", "warnings"]) 79Conversion.__doc__ = """Output of :func:`convert_file`. 80 81``text`` is the converted file contents; ``warnings`` lists the fields the 82target format could not represent (empty for a faithful conversion). 83""" 84 85GridfmRead = namedtuple("GridfmRead", ["network", "scenario", "warnings"]) 86GridfmRead.__doc__ = """Output of :func:`read_gridfm` / :func:`read_gridfm_scenarios`. 87 88``network`` is the reconstructed :class:`Network`; ``scenario`` is the scenario 89id these rows came from; ``warnings`` lists what the gridfm schema could not 90round-trip (synthesized bus ids, folded per-bus load/shunt, dropped HVDC/storage, 91piecewise costs). The read is lossy but recovers everything a power flow needs. 92""" 93 94DisplayData = namedtuple("DisplayData", ["kind", "data"]) 95DisplayData.__doc__ = """Output of :func:`parse_display_file` / :func:`parse_display_bytes`. 96 97``kind`` names the display format. For v0.2.2, ``kind == "powerworld"`` and 98``data`` is a :class:`PwdDisplay`. 99""" 100 101PwdDisplay = namedtuple( 102 "PwdDisplay", ["canvas_width", "canvas_height", "stamp", "substations"] 103) 104PwdDisplay.__doc__ = """Decoded PowerWorld ``.pwd`` display metadata.""" 105 106PwdSubstation = namedtuple("PwdSubstation", ["number", "name", "x", "y"]) 107PwdSubstation.__doc__ = """One decoded PowerWorld display substation.""" 108 109Incidence = namedtuple("Incidence", ["A", "b", "p_shift", "branch_of_col"]) 110Incidence.__doc__ = """Output of :meth:`Network.incidence`. 111 112Shapes, with ``n`` buses and ``m`` in-service branches: 113- ``A``: signed incidence csr_matrix, ``(n, m)``. 114- ``b``: branch susceptances, ``(m,)``; ``b[k]`` is column ``k``. 115- ``p_shift``: phase-shift injection, ``(n,)`` (all zero unless 116 ``convention="matpower"``). 117- ``branch_of_col``: column→branch index map, ``(m,)``; ``branch_of_col[k]`` 118 and ``b[k]`` are co-indexed by incidence column ``k``. 119""" 120 121YbusParts = namedtuple("YbusParts", ["g", "b"]) 122YbusParts.__doc__ = ( 123 "Output of :meth:`Network.ybus_parts`: ``g`` = Re(Y_bus), ``b`` = Im(Y_bus), " 124 "each a real csr_matrix. ``Network.ybus()`` returns ``g + 1j*b``." 125) 126 127DenseBranch = namedtuple( 128 "DenseBranch", ["from_id", "to_id", "r", "x", "b", "tap", "shift", "in_service"] 129) 130DenseBranch.__doc__ = """Branch arrays in source order.""" 131 132DenseGen = namedtuple("DenseGen", ["bus", "pg", "pmax", "pmin", "in_service"]) 133DenseGen.__doc__ = """Generator arrays in source order.""" 134 135DenseDemand = namedtuple("DenseDemand", ["pd", "qd"]) 136DenseDemand.__doc__ = """Nodal active and reactive demand arrays in bus order.""" 137 138DenseShunt = namedtuple("DenseShunt", ["gs", "bs"]) 139DenseShunt.__doc__ = """Nodal shunt conductance and susceptance arrays in bus order.""" 140 141DenseNetwork = namedtuple( 142 "DenseNetwork", 143 [ 144 "n", 145 "m", 146 "ng", 147 "base_mva", 148 "bus_ids", 149 "branch", 150 "gen", 151 "demand", 152 "shunt", 153 "reference_bus", 154 "n_components", 155 "is_radial", 156 ], 157) 158DenseNetwork.__doc__ = """Dense NumPy table view of a parsed :class:`Network`.""" 159 160 161def _require(module: str, extra: str): 162 """Import ``module`` or raise a clear ImportError naming the extra to install.""" 163 try: 164 return importlib.import_module(module) 165 except ImportError as exc: 166 # Only rewrite "module is absent". A present-but-broken install (e.g. a 167 # failed C-extension load) raises ImportError from a sub-import; let its 168 # own traceback through instead of misdirecting the user to reinstall. 169 if getattr(exc, "name", None) not in (module, module.split(".")[0]): 170 raise 171 raise ImportError( 172 f"powerio needs {module!r} for this call; install it with " 173 f"`pip install 'powerio[{extra}]'`" 174 ) from exc 175 176 177def _to_csr(coo): 178 """Assemble a ``(data, row, col, shape)`` COO tuple into a csr_matrix.""" 179 sparse = _require("scipy.sparse", "matrix") 180 data, row, col, shape = coo 181 return sparse.coo_matrix((data, (row, col)), shape=shape).tocsr() 182 183 184def _require_gridfm() -> None: 185 """Raise a clear ImportError if the extension lacks the gridfm Parquet surface. 186 187 Published wheels include this surface. A custom source build can omit the 188 Rust feature, in which case the method names still raise a direct error 189 instead of failing with ``AttributeError``. 190 """ 191 if not getattr(_powerio, "_has_gridfm", False): 192 raise ImportError( 193 "powerio was built without the gridfm Parquet surface; reinstall a " 194 "wheel built with gridfm support or rebuild from source with " 195 "`maturin develop --features gridfm`." 196 ) 197 198 199def _wrap_display(raw) -> DisplayData: 200 kind, payload = raw 201 if kind == "powerworld": 202 substations = [ 203 PwdSubstation( 204 row["number"], 205 row["name"], 206 row["x"], 207 row["y"], 208 ) 209 for row in payload["substations"] 210 ] 211 payload = PwdDisplay( 212 payload["canvas_width"], 213 payload["canvas_height"], 214 payload["stamp"], 215 substations, 216 ) 217 return DisplayData(kind, payload) 218 219 220class Network: 221 """A parsed power network case. 222 223 The data attributes (``buses``, ``branches``, ``gens``, ``loads``, 224 ``shunts``) and the non-matrix methods (``write``, ``reference_bus_index``, 225 ``connectivity_report``, ``write_dcopf_bundle``) delegate to the compiled 226 handle; the matrix methods below return ``scipy.sparse`` objects. Read 227 fidelity warnings from parse time are on ``read_warnings`` (empty for 228 readers that don't report any; currently all but pandapower JSON and 229 PyPSA CSV). 230 231 Errors: a bad file path raises the standard ``OSError`` subclass 232 (``FileNotFoundError``); a malformed case raises :class:`PowerIOParseError` 233 and an unmet builder precondition (no generators, no reference bus) raises 234 :class:`PowerIODataError`; both subclass :class:`PowerIOError`, so 235 ``except PowerIOError`` catches either; an unknown 236 ``scheme``/``convention``/``units`` string raises ``ValueError``. 237 """ 238 239 def __init__(self, inner: "_powerio.PyCase"): 240 self._inner = inner 241 242 def __getattr__(self, name: str): 243 # Reached only when normal lookup misses, so the matrix methods below 244 # win. Guard underscore names so a lookup before _inner exists raises 245 # AttributeError instead of recursing forever. 246 if name.startswith("_"): 247 raise AttributeError( 248 f"{type(self).__name__!r} object has no attribute {name!r}" 249 ) 250 return getattr(self._inner, name) 251 252 def __repr__(self) -> str: 253 return repr(self._inner).replace("PyCase", "Network", 1) 254 255 # --- canonical format and table views ------------------------------- 256 257 def to_matpower(self) -> str: 258 """Serialize to MATPOWER ``.m`` text. 259 260 A case parsed from MATPOWER keeps its original source, so this returns a 261 byte-exact echo. Derived cases serialize from the format-neutral model. 262 """ 263 return self._inner.to_matpower() 264 265 def to_json(self) -> str: 266 """Serialize to the JSON transport.""" 267 return self._inner.to_json() 268 269 def to_format(self, to: str) -> Conversion: 270 """Serialize this parsed case to another format. 271 272 ``to`` is one of the format names accepted by :func:`convert_file`. 273 Returns a :class:`Conversion` with output text and fidelity warnings. 274 """ 275 text, warnings = self._inner.to_format(to) 276 return Conversion(text, warnings) 277 278 def to_dense(self) -> DenseNetwork: 279 """Dense NumPy arrays for solver and adapter code. 280 281 This view preserves bus and branch source order. Loads and shunts are 282 summed per bus, matching the Rust indexed analysis view. 283 """ 284 np = _require("numpy", "matrix") 285 buses = self._inner.buses 286 branches = self._inner.branches 287 generators = self._inner.generators 288 bus_ids = np.asarray([b["id"] for b in buses], dtype=np.int64) 289 id_to_idx = {int(bus_id): idx for idx, bus_id in enumerate(bus_ids)} 290 291 pd = np.zeros(len(buses), dtype=float) 292 qd = np.zeros(len(buses), dtype=float) 293 for load in self._inner.loads: 294 idx = id_to_idx.get(load["bus"]) 295 if idx is not None: 296 pd[idx] += load["p"] 297 qd[idx] += load["q"] 298 299 gs = np.zeros(len(buses), dtype=float) 300 bs = np.zeros(len(buses), dtype=float) 301 for shunt in self._inner.shunts: 302 idx = id_to_idx.get(shunt["bus"]) 303 if idx is not None: 304 gs[idx] += shunt["g"] 305 bs[idx] += shunt["b"] 306 307 branch = DenseBranch( 308 from_id=np.asarray([br["from_id"] for br in branches], dtype=np.int64), 309 to_id=np.asarray([br["to_id"] for br in branches], dtype=np.int64), 310 r=np.asarray([br["r"] for br in branches], dtype=float), 311 x=np.asarray([br["x"] for br in branches], dtype=float), 312 b=np.asarray([br["b"] for br in branches], dtype=float), 313 tap=np.asarray([br["tap"] for br in branches], dtype=float), 314 shift=np.asarray([br["shift"] for br in branches], dtype=float), 315 in_service=np.asarray([br["in_service"] for br in branches], dtype=bool), 316 ) 317 gen = DenseGen( 318 bus=np.asarray([g["bus"] for g in generators], dtype=np.int64), 319 pg=np.asarray([g["pg"] for g in generators], dtype=float), 320 pmax=np.asarray([g["pmax"] for g in generators], dtype=float), 321 pmin=np.asarray([g["pmin"] for g in generators], dtype=float), 322 in_service=np.asarray([g["in_service"] for g in generators], dtype=bool), 323 ) 324 refs = self.reference_bus_indices() 325 return DenseNetwork( 326 n=len(buses), 327 m=len(branches), 328 ng=len(generators), 329 base_mva=self.base_mva, 330 bus_ids=bus_ids, 331 branch=branch, 332 gen=gen, 333 demand=DenseDemand(pd=pd, qd=qd), 334 shunt=DenseShunt(gs=gs, bs=bs), 335 reference_bus=refs[0] if len(refs) == 1 else None, 336 n_components=self.n_connected_components, 337 is_radial=self.is_radial, 338 ) 339 340 # --- matrix builders (scipy.sparse) --------------------------------- 341 342 def bprime(self, scheme: str = "bx"): 343 """FDPF B' (shuntless). ``scheme`` is ``"bx"`` or ``"xb"``.""" 344 return _to_csr(self._inner.bprime(scheme)) 345 346 def bdoubleprime(self, scheme: str = "bx"): 347 """FDPF B'' (with shunts and taps; shifts zeroed). ``scheme`` is 348 ``"bx"`` or ``"xb"``; taps are always kept (MATPOWER ``makeB``).""" 349 return _to_csr(self._inner.bdoubleprime(scheme)) 350 351 def lacpf(self, include_taps: bool = True, include_shifts: bool = True): 352 """LACPF 2n×2n block ``[[G, -B], [-B, -G]]``.""" 353 return _to_csr(self._inner.lacpf(include_taps, include_shifts)) 354 355 def adjacency(self): 356 """0/1 bus adjacency matrix.""" 357 return _to_csr(self._inner.adjacency()) 358 359 def ybus_parts(self, include_taps: bool = True, include_shifts: bool = True): 360 """:class:`YbusParts` ``(g, b)`` = ``(Re(Y_bus), Im(Y_bus))``, two real 361 csr_matrix.""" 362 g, b = self._inner.ybus_parts(include_taps, include_shifts) 363 return YbusParts(g=_to_csr(g), b=_to_csr(b)) 364 365 def ybus(self, include_taps: bool = True, include_shifts: bool = True): 366 """``Y_bus = G + jB`` as a complex csr_matrix.""" 367 g, b = self.ybus_parts(include_taps, include_shifts) 368 return (g + 1j * b).tocsr() 369 370 def ptdf(self, convention: str = "paper"): 371 """DC PTDF (m×n). ``convention`` is ``"paper"`` or ``"matpower"``.""" 372 return _to_csr(self._inner.ptdf(convention)) 373 374 def lodf(self, convention: str = "paper"): 375 """DC LODF (m×m).""" 376 return _to_csr(self._inner.lodf(convention)) 377 378 def weighted_laplacian(self, convention: str = "paper"): 379 """Weighted Laplacian ``L = A diag(b) Aᵀ``.""" 380 return _to_csr(self._inner.weighted_laplacian(convention)) 381 382 def incidence(self, convention: str = "paper") -> "Incidence": 383 """Signed incidence factorization as an :data:`Incidence` tuple.""" 384 np = _require("numpy", "matrix") 385 a, b, p_shift, branch_of_col = self._inner.incidence(convention) 386 return Incidence( 387 A=_to_csr(a), 388 b=np.asarray(b, dtype=float), 389 p_shift=np.asarray(p_shift, dtype=float), 390 branch_of_col=np.asarray(branch_of_col, dtype=np.int64), 391 ) 392 393 def write_gridfm( 394 self, 395 out_dir: Any, 396 scenario: int = 0, 397 include_y_bus: bool = True, 398 include_taps: bool = True, 399 include_shifts: bool = True, 400 ) -> dict: 401 """Write the gridfm-datakit Parquet dataset for this case under 402 ``<out_dir>/<case>/raw/``. 403 404 Returns a dict with ``dir``, ``files``, ``dropped_zero_impedance``, and 405 ``degenerate_cost_gens``. Published wheels include the native writer; 406 custom source builds without the Rust ``gridfm`` feature raise 407 ``ImportError``. For many perturbed snapshots in one dataset, see 408 :func:`write_gridfm_batch`. 409 """ 410 _require_gridfm() 411 return self._inner.write_gridfm( 412 str(out_dir), scenario, include_y_bus, include_taps, include_shifts 413 ) 414 415 def write_pypsa_csv_folder(self, out_dir: Any) -> dict: 416 """Write this case as a PyPSA CSV folder. 417 418 The folder contains static PyPSA component CSVs and can be imported with 419 ``pypsa.Network().import_from_csv_folder(path)``. Returns a dict with 420 ``dir``, ``files``, and fidelity ``warnings``. 421 """ 422 return self._inner.write_pypsa_csv_folder(str(out_dir)) 423 424 def to_normalized(self) -> "Network": 425 """A normalized, computation-ready copy of this case: per unit, radians, 426 out-of-service filtered, source bus ids preserved, bus types 427 canonicalized. The original case is unchanged; the result carries no 428 retained source, so :meth:`write` serializes the per-unit model rather 429 than echoing it. Raises :class:`PowerIODataError` if the case can't be 430 normalized (no reference bus can be chosen, or a non-positive base MVA). 431 """ 432 return Network(self._inner.to_normalized()) 433 434 def to_networkx(self): 435 """Undirected networkx graph keyed by bus id. 436 437 In-service branches become edges carrying ``branch`` (index), ``r``, 438 ``x``, and ``b``. 439 """ 440 nx = _require("networkx", "graph") 441 g = nx.Graph() 442 g.add_nodes_from(bus["id"] for bus in self._inner.buses) 443 for k, br in enumerate(self._inner.branches): 444 if br["in_service"]: 445 g.add_edge( 446 br["from_id"], 447 br["to_id"], 448 branch=k, 449 r=br["r"], 450 x=br["x"], 451 b=br["b"], 452 ) 453 return g 454 455 456Case = Network 457 458 459def parse_file(path: Any, from_: Optional[str] = None) -> Network: 460 """Parse a case file from a path, inferring the format from the extension. 461 462 Read fidelity warnings are on ``Network.read_warnings`` (empty for readers 463 that don't report any; currently all but pandapower JSON and PyPSA CSV). 464 """ 465 return Network(_powerio.parse_file(str(path), from_)) 466 467 468def parse_display_file(path: Any, from_: Optional[str] = None) -> DisplayData: 469 """Parse a display artifact such as a PowerWorld ``.pwd`` file.""" 470 return _wrap_display(_powerio.parse_display_file(str(path), from_)) 471 472 473def parse_display_bytes(data: bytes, format: str) -> DisplayData: 474 """Parse display bytes in the named display format.""" 475 return _wrap_display(_powerio.parse_display_bytes(data, format)) 476 477 478def parse_str(text: str, format: str = "matpower") -> Network: 479 """Parse a case from in-memory text in the named ``format``.""" 480 return Network(_powerio.parse_str(text, format)) 481 482 483def from_json(text: str) -> Network: 484 """Rebuild a case from JSON produced by :meth:`Network.to_json`.""" 485 return Network(_powerio.from_json(text)) 486 487 488def convert_file(path: Any, to: str, from_: Optional[str] = None) -> Conversion: 489 """Convert a case file to another format through the neutral hub. 490 491 ``to`` / ``from_`` are format names: ``matpower``, ``powermodels-json``, 492 ``egret-json``, ``pandapower-json``, ``psse``, ``powerworld`` (aliases 493 ``m``, ``pm``, ``egret``, ``pp``, ``raw``, ``aux``). The input format is 494 inferred from the file extension unless ``from_`` overrides it. PyPSA CSV 495 folders are read with ``from_="pypsa-csv"`` and written with 496 :meth:`Network.write_pypsa_csv_folder`. Returns a :class:`Conversion` with 497 the text and any fidelity warnings. 498 """ 499 text, warnings = _powerio.convert_file(str(path), to, from_) 500 return Conversion(text, warnings) 501 502 503def convert_str(text: str, to: str, format: str = "matpower") -> Conversion: 504 """Convert in-memory case ``text`` to another format through the neutral 505 hub, with no file staging. 506 507 ``to`` and ``format`` are format names as in :func:`convert_file`; 508 ``format`` names the input (default ``matpower``). Returns a 509 :class:`Conversion` with the converted text and any fidelity warnings. 510 """ 511 out, warnings = _powerio.convert_str(text, to, format) 512 return Conversion(out, warnings) 513 514 515def to_format(case: Network, to: str) -> Conversion: 516 """Serialize ``case`` to another format.""" 517 return case.to_format(to) 518 519 520def to_matpower(case: Network) -> str: 521 """Serialize ``case`` to MATPOWER ``.m`` text.""" 522 return case.to_matpower() 523 524 525def to_json(case: Network) -> str: 526 """Serialize ``case`` to the JSON transport.""" 527 return case.to_json() 528 529 530def to_dense(case: Network) -> DenseNetwork: 531 """Return the dense NumPy table view of ``case``.""" 532 return case.to_dense() 533 534 535def write_gridfm_batch( 536 cases: "list[Network]", 537 out_dir: Any, 538 base_scenario: int = 0, 539 include_y_bus: bool = True, 540 include_taps: bool = True, 541 include_shifts: bool = True, 542) -> dict: 543 """Write several cases as one gridfm-datakit dataset, row-stacked and keyed by 544 the ``scenario`` column. 545 546 Each case is one snapshot; the k-th is stamped ``base_scenario + k``. The 547 cases must share a base element set: the same bus/branch/gen counts and 548 bus id order (otherwise :class:`PowerIODataError` is raised). Load, dispatch, 549 branch status, and costs may vary per scenario. Returns the same dict as 550 :meth:`Network.write_gridfm`. Published wheels include the native writer; 551 custom source builds without the Rust ``gridfm`` feature raise 552 ``ImportError``. 553 """ 554 _require_gridfm() 555 inners = [c._inner for c in cases] 556 return _powerio.write_gridfm_batch( 557 inners, str(out_dir), base_scenario, include_y_bus, include_taps, include_shifts 558 ) 559 560 561def read_gridfm(dir: Any, scenario: int = 0) -> GridfmRead: 562 """Read one scenario of a gridfm-datakit Parquet dataset back into a case. 563 564 The inverse of :meth:`Network.write_gridfm`. ``dir`` is resolved leniently: 565 the ``raw/`` directory holding the parquet files, a ``<case>/`` directory with 566 a ``raw/`` child, or a parent directory with one ``*/raw/`` child all work. 567 ``scenario`` selects one snapshot from a batch (``0``, the base case, by 568 default). Returns a :class:`GridfmRead` ``(network, scenario, warnings)``. 569 570 The read is lossy but recovers everything a power flow needs: bus types, 571 voltages and limits, nodal load and shunt totals, generator dispatch and 572 bounds, branch ``r/x/b/tap/shift/rate_a``/angle limits, and ``baseMVA``, 573 enough to write a runnable case. It cannot recover original bus ids, 574 per-element load/shunt granularity, piecewise/cubic costs, or HVDC/storage; 575 what it can't recover is listed in ``warnings``. Published wheels include the 576 native reader; custom source builds without the Rust ``gridfm`` feature raise 577 ``ImportError``. 578 """ 579 _require_gridfm() 580 case, scen, warnings = _powerio.read_gridfm(str(dir), scenario) 581 return GridfmRead(Network(case), scen, warnings) 582 583 584def read_gridfm_scenarios(dir: Any) -> "list[GridfmRead]": 585 """Read every scenario of a gridfm dataset, one :class:`GridfmRead` per 586 scenario id (ascending) over the shared topology, the read side of 587 :func:`write_gridfm_batch`. 588 589 Each scenario is rebuilt independently, so two scenarios may differ in branch 590 status, bus types, and reference bus. See :func:`read_gridfm` for the lenient 591 directory resolution and the fidelity contract. 592 """ 593 _require_gridfm() 594 return [ 595 GridfmRead(Network(case), scen, warnings) 596 for case, scen, warnings in _powerio.read_gridfm_scenarios(str(dir)) 597 ] 598 599 600def read_pypsa_csv_folder(path: Any) -> Network: 601 """Read a PyPSA CSV folder into a :class:`Network`.""" 602 return Network(_powerio.read_pypsa_csv_folder(str(path)))
221class Network: 222 """A parsed power network case. 223 224 The data attributes (``buses``, ``branches``, ``gens``, ``loads``, 225 ``shunts``) and the non-matrix methods (``write``, ``reference_bus_index``, 226 ``connectivity_report``, ``write_dcopf_bundle``) delegate to the compiled 227 handle; the matrix methods below return ``scipy.sparse`` objects. Read 228 fidelity warnings from parse time are on ``read_warnings`` (empty for 229 readers that don't report any; currently all but pandapower JSON and 230 PyPSA CSV). 231 232 Errors: a bad file path raises the standard ``OSError`` subclass 233 (``FileNotFoundError``); a malformed case raises :class:`PowerIOParseError` 234 and an unmet builder precondition (no generators, no reference bus) raises 235 :class:`PowerIODataError`; both subclass :class:`PowerIOError`, so 236 ``except PowerIOError`` catches either; an unknown 237 ``scheme``/``convention``/``units`` string raises ``ValueError``. 238 """ 239 240 def __init__(self, inner: "_powerio.PyCase"): 241 self._inner = inner 242 243 def __getattr__(self, name: str): 244 # Reached only when normal lookup misses, so the matrix methods below 245 # win. Guard underscore names so a lookup before _inner exists raises 246 # AttributeError instead of recursing forever. 247 if name.startswith("_"): 248 raise AttributeError( 249 f"{type(self).__name__!r} object has no attribute {name!r}" 250 ) 251 return getattr(self._inner, name) 252 253 def __repr__(self) -> str: 254 return repr(self._inner).replace("PyCase", "Network", 1) 255 256 # --- canonical format and table views ------------------------------- 257 258 def to_matpower(self) -> str: 259 """Serialize to MATPOWER ``.m`` text. 260 261 A case parsed from MATPOWER keeps its original source, so this returns a 262 byte-exact echo. Derived cases serialize from the format-neutral model. 263 """ 264 return self._inner.to_matpower() 265 266 def to_json(self) -> str: 267 """Serialize to the JSON transport.""" 268 return self._inner.to_json() 269 270 def to_format(self, to: str) -> Conversion: 271 """Serialize this parsed case to another format. 272 273 ``to`` is one of the format names accepted by :func:`convert_file`. 274 Returns a :class:`Conversion` with output text and fidelity warnings. 275 """ 276 text, warnings = self._inner.to_format(to) 277 return Conversion(text, warnings) 278 279 def to_dense(self) -> DenseNetwork: 280 """Dense NumPy arrays for solver and adapter code. 281 282 This view preserves bus and branch source order. Loads and shunts are 283 summed per bus, matching the Rust indexed analysis view. 284 """ 285 np = _require("numpy", "matrix") 286 buses = self._inner.buses 287 branches = self._inner.branches 288 generators = self._inner.generators 289 bus_ids = np.asarray([b["id"] for b in buses], dtype=np.int64) 290 id_to_idx = {int(bus_id): idx for idx, bus_id in enumerate(bus_ids)} 291 292 pd = np.zeros(len(buses), dtype=float) 293 qd = np.zeros(len(buses), dtype=float) 294 for load in self._inner.loads: 295 idx = id_to_idx.get(load["bus"]) 296 if idx is not None: 297 pd[idx] += load["p"] 298 qd[idx] += load["q"] 299 300 gs = np.zeros(len(buses), dtype=float) 301 bs = np.zeros(len(buses), dtype=float) 302 for shunt in self._inner.shunts: 303 idx = id_to_idx.get(shunt["bus"]) 304 if idx is not None: 305 gs[idx] += shunt["g"] 306 bs[idx] += shunt["b"] 307 308 branch = DenseBranch( 309 from_id=np.asarray([br["from_id"] for br in branches], dtype=np.int64), 310 to_id=np.asarray([br["to_id"] for br in branches], dtype=np.int64), 311 r=np.asarray([br["r"] for br in branches], dtype=float), 312 x=np.asarray([br["x"] for br in branches], dtype=float), 313 b=np.asarray([br["b"] for br in branches], dtype=float), 314 tap=np.asarray([br["tap"] for br in branches], dtype=float), 315 shift=np.asarray([br["shift"] for br in branches], dtype=float), 316 in_service=np.asarray([br["in_service"] for br in branches], dtype=bool), 317 ) 318 gen = DenseGen( 319 bus=np.asarray([g["bus"] for g in generators], dtype=np.int64), 320 pg=np.asarray([g["pg"] for g in generators], dtype=float), 321 pmax=np.asarray([g["pmax"] for g in generators], dtype=float), 322 pmin=np.asarray([g["pmin"] for g in generators], dtype=float), 323 in_service=np.asarray([g["in_service"] for g in generators], dtype=bool), 324 ) 325 refs = self.reference_bus_indices() 326 return DenseNetwork( 327 n=len(buses), 328 m=len(branches), 329 ng=len(generators), 330 base_mva=self.base_mva, 331 bus_ids=bus_ids, 332 branch=branch, 333 gen=gen, 334 demand=DenseDemand(pd=pd, qd=qd), 335 shunt=DenseShunt(gs=gs, bs=bs), 336 reference_bus=refs[0] if len(refs) == 1 else None, 337 n_components=self.n_connected_components, 338 is_radial=self.is_radial, 339 ) 340 341 # --- matrix builders (scipy.sparse) --------------------------------- 342 343 def bprime(self, scheme: str = "bx"): 344 """FDPF B' (shuntless). ``scheme`` is ``"bx"`` or ``"xb"``.""" 345 return _to_csr(self._inner.bprime(scheme)) 346 347 def bdoubleprime(self, scheme: str = "bx"): 348 """FDPF B'' (with shunts and taps; shifts zeroed). ``scheme`` is 349 ``"bx"`` or ``"xb"``; taps are always kept (MATPOWER ``makeB``).""" 350 return _to_csr(self._inner.bdoubleprime(scheme)) 351 352 def lacpf(self, include_taps: bool = True, include_shifts: bool = True): 353 """LACPF 2nĂ—2n block ``[[G, -B], [-B, -G]]``.""" 354 return _to_csr(self._inner.lacpf(include_taps, include_shifts)) 355 356 def adjacency(self): 357 """0/1 bus adjacency matrix.""" 358 return _to_csr(self._inner.adjacency()) 359 360 def ybus_parts(self, include_taps: bool = True, include_shifts: bool = True): 361 """:class:`YbusParts` ``(g, b)`` = ``(Re(Y_bus), Im(Y_bus))``, two real 362 csr_matrix.""" 363 g, b = self._inner.ybus_parts(include_taps, include_shifts) 364 return YbusParts(g=_to_csr(g), b=_to_csr(b)) 365 366 def ybus(self, include_taps: bool = True, include_shifts: bool = True): 367 """``Y_bus = G + jB`` as a complex csr_matrix.""" 368 g, b = self.ybus_parts(include_taps, include_shifts) 369 return (g + 1j * b).tocsr() 370 371 def ptdf(self, convention: str = "paper"): 372 """DC PTDF (mĂ—n). ``convention`` is ``"paper"`` or ``"matpower"``.""" 373 return _to_csr(self._inner.ptdf(convention)) 374 375 def lodf(self, convention: str = "paper"): 376 """DC LODF (mĂ—m).""" 377 return _to_csr(self._inner.lodf(convention)) 378 379 def weighted_laplacian(self, convention: str = "paper"): 380 """Weighted Laplacian ``L = A diag(b) Aáµ€``.""" 381 return _to_csr(self._inner.weighted_laplacian(convention)) 382 383 def incidence(self, convention: str = "paper") -> "Incidence": 384 """Signed incidence factorization as an :data:`Incidence` tuple.""" 385 np = _require("numpy", "matrix") 386 a, b, p_shift, branch_of_col = self._inner.incidence(convention) 387 return Incidence( 388 A=_to_csr(a), 389 b=np.asarray(b, dtype=float), 390 p_shift=np.asarray(p_shift, dtype=float), 391 branch_of_col=np.asarray(branch_of_col, dtype=np.int64), 392 ) 393 394 def write_gridfm( 395 self, 396 out_dir: Any, 397 scenario: int = 0, 398 include_y_bus: bool = True, 399 include_taps: bool = True, 400 include_shifts: bool = True, 401 ) -> dict: 402 """Write the gridfm-datakit Parquet dataset for this case under 403 ``<out_dir>/<case>/raw/``. 404 405 Returns a dict with ``dir``, ``files``, ``dropped_zero_impedance``, and 406 ``degenerate_cost_gens``. Published wheels include the native writer; 407 custom source builds without the Rust ``gridfm`` feature raise 408 ``ImportError``. For many perturbed snapshots in one dataset, see 409 :func:`write_gridfm_batch`. 410 """ 411 _require_gridfm() 412 return self._inner.write_gridfm( 413 str(out_dir), scenario, include_y_bus, include_taps, include_shifts 414 ) 415 416 def write_pypsa_csv_folder(self, out_dir: Any) -> dict: 417 """Write this case as a PyPSA CSV folder. 418 419 The folder contains static PyPSA component CSVs and can be imported with 420 ``pypsa.Network().import_from_csv_folder(path)``. Returns a dict with 421 ``dir``, ``files``, and fidelity ``warnings``. 422 """ 423 return self._inner.write_pypsa_csv_folder(str(out_dir)) 424 425 def to_normalized(self) -> "Network": 426 """A normalized, computation-ready copy of this case: per unit, radians, 427 out-of-service filtered, source bus ids preserved, bus types 428 canonicalized. The original case is unchanged; the result carries no 429 retained source, so :meth:`write` serializes the per-unit model rather 430 than echoing it. Raises :class:`PowerIODataError` if the case can't be 431 normalized (no reference bus can be chosen, or a non-positive base MVA). 432 """ 433 return Network(self._inner.to_normalized()) 434 435 def to_networkx(self): 436 """Undirected networkx graph keyed by bus id. 437 438 In-service branches become edges carrying ``branch`` (index), ``r``, 439 ``x``, and ``b``. 440 """ 441 nx = _require("networkx", "graph") 442 g = nx.Graph() 443 g.add_nodes_from(bus["id"] for bus in self._inner.buses) 444 for k, br in enumerate(self._inner.branches): 445 if br["in_service"]: 446 g.add_edge( 447 br["from_id"], 448 br["to_id"], 449 branch=k, 450 r=br["r"], 451 x=br["x"], 452 b=br["b"], 453 ) 454 return g
A parsed power network case.
The data attributes (buses, branches, gens, loads,
shunts) and the non-matrix methods (write, reference_bus_index,
connectivity_report, write_dcopf_bundle) delegate to the compiled
handle; the matrix methods below return scipy.sparse objects. Read
fidelity warnings from parse time are on read_warnings (empty for
readers that don't report any; currently all but pandapower JSON and
PyPSA CSV).
Errors: a bad file path raises the standard OSError subclass
(FileNotFoundError); a malformed case raises PowerIOParseError
and an unmet builder precondition (no generators, no reference bus) raises
PowerIODataError; both subclass PowerIOError, so
except PowerIOError catches either; an unknown
scheme/convention/units string raises ValueError.
258 def to_matpower(self) -> str: 259 """Serialize to MATPOWER ``.m`` text. 260 261 A case parsed from MATPOWER keeps its original source, so this returns a 262 byte-exact echo. Derived cases serialize from the format-neutral model. 263 """ 264 return self._inner.to_matpower()
Serialize to MATPOWER .m text.
A case parsed from MATPOWER keeps its original source, so this returns a byte-exact echo. Derived cases serialize from the format-neutral model.
266 def to_json(self) -> str: 267 """Serialize to the JSON transport.""" 268 return self._inner.to_json()
Serialize to the JSON transport.
270 def to_format(self, to: str) -> Conversion: 271 """Serialize this parsed case to another format. 272 273 ``to`` is one of the format names accepted by :func:`convert_file`. 274 Returns a :class:`Conversion` with output text and fidelity warnings. 275 """ 276 text, warnings = self._inner.to_format(to) 277 return Conversion(text, warnings)
Serialize this parsed case to another format.
to is one of the format names accepted by convert_file().
Returns a Conversion with output text and fidelity warnings.
279 def to_dense(self) -> DenseNetwork: 280 """Dense NumPy arrays for solver and adapter code. 281 282 This view preserves bus and branch source order. Loads and shunts are 283 summed per bus, matching the Rust indexed analysis view. 284 """ 285 np = _require("numpy", "matrix") 286 buses = self._inner.buses 287 branches = self._inner.branches 288 generators = self._inner.generators 289 bus_ids = np.asarray([b["id"] for b in buses], dtype=np.int64) 290 id_to_idx = {int(bus_id): idx for idx, bus_id in enumerate(bus_ids)} 291 292 pd = np.zeros(len(buses), dtype=float) 293 qd = np.zeros(len(buses), dtype=float) 294 for load in self._inner.loads: 295 idx = id_to_idx.get(load["bus"]) 296 if idx is not None: 297 pd[idx] += load["p"] 298 qd[idx] += load["q"] 299 300 gs = np.zeros(len(buses), dtype=float) 301 bs = np.zeros(len(buses), dtype=float) 302 for shunt in self._inner.shunts: 303 idx = id_to_idx.get(shunt["bus"]) 304 if idx is not None: 305 gs[idx] += shunt["g"] 306 bs[idx] += shunt["b"] 307 308 branch = DenseBranch( 309 from_id=np.asarray([br["from_id"] for br in branches], dtype=np.int64), 310 to_id=np.asarray([br["to_id"] for br in branches], dtype=np.int64), 311 r=np.asarray([br["r"] for br in branches], dtype=float), 312 x=np.asarray([br["x"] for br in branches], dtype=float), 313 b=np.asarray([br["b"] for br in branches], dtype=float), 314 tap=np.asarray([br["tap"] for br in branches], dtype=float), 315 shift=np.asarray([br["shift"] for br in branches], dtype=float), 316 in_service=np.asarray([br["in_service"] for br in branches], dtype=bool), 317 ) 318 gen = DenseGen( 319 bus=np.asarray([g["bus"] for g in generators], dtype=np.int64), 320 pg=np.asarray([g["pg"] for g in generators], dtype=float), 321 pmax=np.asarray([g["pmax"] for g in generators], dtype=float), 322 pmin=np.asarray([g["pmin"] for g in generators], dtype=float), 323 in_service=np.asarray([g["in_service"] for g in generators], dtype=bool), 324 ) 325 refs = self.reference_bus_indices() 326 return DenseNetwork( 327 n=len(buses), 328 m=len(branches), 329 ng=len(generators), 330 base_mva=self.base_mva, 331 bus_ids=bus_ids, 332 branch=branch, 333 gen=gen, 334 demand=DenseDemand(pd=pd, qd=qd), 335 shunt=DenseShunt(gs=gs, bs=bs), 336 reference_bus=refs[0] if len(refs) == 1 else None, 337 n_components=self.n_connected_components, 338 is_radial=self.is_radial, 339 )
Dense NumPy arrays for solver and adapter code.
This view preserves bus and branch source order. Loads and shunts are summed per bus, matching the Rust indexed analysis view.
343 def bprime(self, scheme: str = "bx"): 344 """FDPF B' (shuntless). ``scheme`` is ``"bx"`` or ``"xb"``.""" 345 return _to_csr(self._inner.bprime(scheme))
FDPF B' (shuntless). scheme is "bx" or "xb".
347 def bdoubleprime(self, scheme: str = "bx"): 348 """FDPF B'' (with shunts and taps; shifts zeroed). ``scheme`` is 349 ``"bx"`` or ``"xb"``; taps are always kept (MATPOWER ``makeB``).""" 350 return _to_csr(self._inner.bdoubleprime(scheme))
FDPF B'' (with shunts and taps; shifts zeroed). scheme is
"bx" or "xb"; taps are always kept (MATPOWER makeB).
352 def lacpf(self, include_taps: bool = True, include_shifts: bool = True): 353 """LACPF 2nĂ—2n block ``[[G, -B], [-B, -G]]``.""" 354 return _to_csr(self._inner.lacpf(include_taps, include_shifts))
LACPF 2nĂ—2n block [[G, -B], [-B, -G]].
356 def adjacency(self): 357 """0/1 bus adjacency matrix.""" 358 return _to_csr(self._inner.adjacency())
0/1 bus adjacency matrix.
360 def ybus_parts(self, include_taps: bool = True, include_shifts: bool = True): 361 """:class:`YbusParts` ``(g, b)`` = ``(Re(Y_bus), Im(Y_bus))``, two real 362 csr_matrix.""" 363 g, b = self._inner.ybus_parts(include_taps, include_shifts) 364 return YbusParts(g=_to_csr(g), b=_to_csr(b))
YbusParts (g, b) = (Re(Y_bus), Im(Y_bus)), two real
csr_matrix.
366 def ybus(self, include_taps: bool = True, include_shifts: bool = True): 367 """``Y_bus = G + jB`` as a complex csr_matrix.""" 368 g, b = self.ybus_parts(include_taps, include_shifts) 369 return (g + 1j * b).tocsr()
Y_bus = G + jB as a complex csr_matrix.
371 def ptdf(self, convention: str = "paper"): 372 """DC PTDF (mĂ—n). ``convention`` is ``"paper"`` or ``"matpower"``.""" 373 return _to_csr(self._inner.ptdf(convention))
DC PTDF (mĂ—n). convention is "paper" or "matpower".
375 def lodf(self, convention: str = "paper"): 376 """DC LODF (mĂ—m).""" 377 return _to_csr(self._inner.lodf(convention))
DC LODF (mĂ—m).
379 def weighted_laplacian(self, convention: str = "paper"): 380 """Weighted Laplacian ``L = A diag(b) Aáµ€``.""" 381 return _to_csr(self._inner.weighted_laplacian(convention))
Weighted Laplacian L = A diag(b) Aáµ€.
383 def incidence(self, convention: str = "paper") -> "Incidence": 384 """Signed incidence factorization as an :data:`Incidence` tuple.""" 385 np = _require("numpy", "matrix") 386 a, b, p_shift, branch_of_col = self._inner.incidence(convention) 387 return Incidence( 388 A=_to_csr(a), 389 b=np.asarray(b, dtype=float), 390 p_shift=np.asarray(p_shift, dtype=float), 391 branch_of_col=np.asarray(branch_of_col, dtype=np.int64), 392 )
Signed incidence factorization as an Incidence tuple.
394 def write_gridfm( 395 self, 396 out_dir: Any, 397 scenario: int = 0, 398 include_y_bus: bool = True, 399 include_taps: bool = True, 400 include_shifts: bool = True, 401 ) -> dict: 402 """Write the gridfm-datakit Parquet dataset for this case under 403 ``<out_dir>/<case>/raw/``. 404 405 Returns a dict with ``dir``, ``files``, ``dropped_zero_impedance``, and 406 ``degenerate_cost_gens``. Published wheels include the native writer; 407 custom source builds without the Rust ``gridfm`` feature raise 408 ``ImportError``. For many perturbed snapshots in one dataset, see 409 :func:`write_gridfm_batch`. 410 """ 411 _require_gridfm() 412 return self._inner.write_gridfm( 413 str(out_dir), scenario, include_y_bus, include_taps, include_shifts 414 )
Write the gridfm-datakit Parquet dataset for this case under
<out_dir>/<case>/raw/.
Returns a dict with dir, files, dropped_zero_impedance, and
degenerate_cost_gens. Published wheels include the native writer;
custom source builds without the Rust gridfm feature raise
ImportError. For many perturbed snapshots in one dataset, see
write_gridfm_batch().
416 def write_pypsa_csv_folder(self, out_dir: Any) -> dict: 417 """Write this case as a PyPSA CSV folder. 418 419 The folder contains static PyPSA component CSVs and can be imported with 420 ``pypsa.Network().import_from_csv_folder(path)``. Returns a dict with 421 ``dir``, ``files``, and fidelity ``warnings``. 422 """ 423 return self._inner.write_pypsa_csv_folder(str(out_dir))
Write this case as a PyPSA CSV folder.
The folder contains static PyPSA component CSVs and can be imported with
pypsa.Network().import_from_csv_folder(path). Returns a dict with
dir, files, and fidelity warnings.
425 def to_normalized(self) -> "Network": 426 """A normalized, computation-ready copy of this case: per unit, radians, 427 out-of-service filtered, source bus ids preserved, bus types 428 canonicalized. The original case is unchanged; the result carries no 429 retained source, so :meth:`write` serializes the per-unit model rather 430 than echoing it. Raises :class:`PowerIODataError` if the case can't be 431 normalized (no reference bus can be chosen, or a non-positive base MVA). 432 """ 433 return Network(self._inner.to_normalized())
A normalized, computation-ready copy of this case: per unit, radians,
out-of-service filtered, source bus ids preserved, bus types
canonicalized. The original case is unchanged; the result carries no
retained source, so write() serializes the per-unit model rather
than echoing it. Raises PowerIODataError if the case can't be
normalized (no reference bus can be chosen, or a non-positive base MVA).
435 def to_networkx(self): 436 """Undirected networkx graph keyed by bus id. 437 438 In-service branches become edges carrying ``branch`` (index), ``r``, 439 ``x``, and ``b``. 440 """ 441 nx = _require("networkx", "graph") 442 g = nx.Graph() 443 g.add_nodes_from(bus["id"] for bus in self._inner.buses) 444 for k, br in enumerate(self._inner.branches): 445 if br["in_service"]: 446 g.add_edge( 447 br["from_id"], 448 br["to_id"], 449 branch=k, 450 r=br["r"], 451 x=br["x"], 452 b=br["b"], 453 ) 454 return g
Undirected networkx graph keyed by bus id.
In-service branches become edges carrying branch (index), r,
x, and b.
Incidence(A, b, p_shift, branch_of_col)
YbusParts(g, b)
Conversion(text, warnings)
DisplayData(kind, data)
Create new instance of DisplayData(kind, data)
PwdDisplay(canvas_width, canvas_height, stamp, substations)
Create new instance of PwdDisplay(canvas_width, canvas_height, stamp, substations)
PwdSubstation(number, name, x, y)
DenseNetwork(n, m, ng, base_mva, bus_ids, branch, gen, demand, shunt, reference_bus, n_components, is_radial)
Create new instance of DenseNetwork(n, m, ng, base_mva, bus_ids, branch, gen, demand, shunt, reference_bus, n_components, is_radial)
DenseBranch(from_id, to_id, r, x, b, tap, shift, in_service)
DenseGen(bus, pg, pmax, pmin, in_service)
DenseDemand(pd, qd)
DenseShunt(gs, bs)
Base error from the powerio parser, converter, or matrix builders.
A case file is malformed or unparseable.
A well-formed case cannot satisfy a requested operation.
460def parse_file(path: Any, from_: Optional[str] = None) -> Network: 461 """Parse a case file from a path, inferring the format from the extension. 462 463 Read fidelity warnings are on ``Network.read_warnings`` (empty for readers 464 that don't report any; currently all but pandapower JSON and PyPSA CSV). 465 """ 466 return Network(_powerio.parse_file(str(path), from_))
Parse a case file from a path, inferring the format from the extension.
Read fidelity warnings are on Network.read_warnings (empty for readers
that don't report any; currently all but pandapower JSON and PyPSA CSV).
469def parse_display_file(path: Any, from_: Optional[str] = None) -> DisplayData: 470 """Parse a display artifact such as a PowerWorld ``.pwd`` file.""" 471 return _wrap_display(_powerio.parse_display_file(str(path), from_))
Parse a display artifact such as a PowerWorld .pwd file.
474def parse_display_bytes(data: bytes, format: str) -> DisplayData: 475 """Parse display bytes in the named display format.""" 476 return _wrap_display(_powerio.parse_display_bytes(data, format))
Parse display bytes in the named display format.
479def parse_str(text: str, format: str = "matpower") -> Network: 480 """Parse a case from in-memory text in the named ``format``.""" 481 return Network(_powerio.parse_str(text, format))
Parse a case from in-memory text in the named format.
484def from_json(text: str) -> Network: 485 """Rebuild a case from JSON produced by :meth:`Network.to_json`.""" 486 return Network(_powerio.from_json(text))
Rebuild a case from JSON produced by Network.to_json().
489def convert_file(path: Any, to: str, from_: Optional[str] = None) -> Conversion: 490 """Convert a case file to another format through the neutral hub. 491 492 ``to`` / ``from_`` are format names: ``matpower``, ``powermodels-json``, 493 ``egret-json``, ``pandapower-json``, ``psse``, ``powerworld`` (aliases 494 ``m``, ``pm``, ``egret``, ``pp``, ``raw``, ``aux``). The input format is 495 inferred from the file extension unless ``from_`` overrides it. PyPSA CSV 496 folders are read with ``from_="pypsa-csv"`` and written with 497 :meth:`Network.write_pypsa_csv_folder`. Returns a :class:`Conversion` with 498 the text and any fidelity warnings. 499 """ 500 text, warnings = _powerio.convert_file(str(path), to, from_) 501 return Conversion(text, warnings)
Convert a case file to another format through the neutral hub.
to / from_ are format names: matpower, powermodels-json,
egret-json, pandapower-json, psse, powerworld (aliases
m, pm, egret, pp, raw, aux). The input format is
inferred from the file extension unless from_ overrides it. PyPSA CSV
folders are read with from_="pypsa-csv" and written with
Network.write_pypsa_csv_folder(). Returns a Conversion with
the text and any fidelity warnings.
504def convert_str(text: str, to: str, format: str = "matpower") -> Conversion: 505 """Convert in-memory case ``text`` to another format through the neutral 506 hub, with no file staging. 507 508 ``to`` and ``format`` are format names as in :func:`convert_file`; 509 ``format`` names the input (default ``matpower``). Returns a 510 :class:`Conversion` with the converted text and any fidelity warnings. 511 """ 512 out, warnings = _powerio.convert_str(text, to, format) 513 return Conversion(out, warnings)
Convert in-memory case text to another format through the neutral
hub, with no file staging.
to and format are format names as in convert_file();
format names the input (default matpower). Returns a
Conversion with the converted text and any fidelity warnings.
516def to_format(case: Network, to: str) -> Conversion: 517 """Serialize ``case`` to another format.""" 518 return case.to_format(to)
Serialize case to another format.
521def to_matpower(case: Network) -> str: 522 """Serialize ``case`` to MATPOWER ``.m`` text.""" 523 return case.to_matpower()
Serialize case to MATPOWER .m text.
526def to_json(case: Network) -> str: 527 """Serialize ``case`` to the JSON transport.""" 528 return case.to_json()
Serialize case to the JSON transport.
531def to_dense(case: Network) -> DenseNetwork: 532 """Return the dense NumPy table view of ``case``.""" 533 return case.to_dense()
Return the dense NumPy table view of case.
536def write_gridfm_batch( 537 cases: "list[Network]", 538 out_dir: Any, 539 base_scenario: int = 0, 540 include_y_bus: bool = True, 541 include_taps: bool = True, 542 include_shifts: bool = True, 543) -> dict: 544 """Write several cases as one gridfm-datakit dataset, row-stacked and keyed by 545 the ``scenario`` column. 546 547 Each case is one snapshot; the k-th is stamped ``base_scenario + k``. The 548 cases must share a base element set: the same bus/branch/gen counts and 549 bus id order (otherwise :class:`PowerIODataError` is raised). Load, dispatch, 550 branch status, and costs may vary per scenario. Returns the same dict as 551 :meth:`Network.write_gridfm`. Published wheels include the native writer; 552 custom source builds without the Rust ``gridfm`` feature raise 553 ``ImportError``. 554 """ 555 _require_gridfm() 556 inners = [c._inner for c in cases] 557 return _powerio.write_gridfm_batch( 558 inners, str(out_dir), base_scenario, include_y_bus, include_taps, include_shifts 559 )
Write several cases as one gridfm-datakit dataset, row-stacked and keyed by
the scenario column.
Each case is one snapshot; the k-th is stamped base_scenario + k. The
cases must share a base element set: the same bus/branch/gen counts and
bus id order (otherwise PowerIODataError is raised). Load, dispatch,
branch status, and costs may vary per scenario. Returns the same dict as
Network.write_gridfm(). Published wheels include the native writer;
custom source builds without the Rust gridfm feature raise
ImportError.
562def read_gridfm(dir: Any, scenario: int = 0) -> GridfmRead: 563 """Read one scenario of a gridfm-datakit Parquet dataset back into a case. 564 565 The inverse of :meth:`Network.write_gridfm`. ``dir`` is resolved leniently: 566 the ``raw/`` directory holding the parquet files, a ``<case>/`` directory with 567 a ``raw/`` child, or a parent directory with one ``*/raw/`` child all work. 568 ``scenario`` selects one snapshot from a batch (``0``, the base case, by 569 default). Returns a :class:`GridfmRead` ``(network, scenario, warnings)``. 570 571 The read is lossy but recovers everything a power flow needs: bus types, 572 voltages and limits, nodal load and shunt totals, generator dispatch and 573 bounds, branch ``r/x/b/tap/shift/rate_a``/angle limits, and ``baseMVA``, 574 enough to write a runnable case. It cannot recover original bus ids, 575 per-element load/shunt granularity, piecewise/cubic costs, or HVDC/storage; 576 what it can't recover is listed in ``warnings``. Published wheels include the 577 native reader; custom source builds without the Rust ``gridfm`` feature raise 578 ``ImportError``. 579 """ 580 _require_gridfm() 581 case, scen, warnings = _powerio.read_gridfm(str(dir), scenario) 582 return GridfmRead(Network(case), scen, warnings)
Read one scenario of a gridfm-datakit Parquet dataset back into a case.
The inverse of Network.write_gridfm(). dir is resolved leniently:
the raw/ directory holding the parquet files, a <case>/ directory with
a raw/ child, or a parent directory with one */raw/ child all work.
scenario selects one snapshot from a batch (0, the base case, by
default). Returns a GridfmRead (network, scenario, warnings).
The read is lossy but recovers everything a power flow needs: bus types,
voltages and limits, nodal load and shunt totals, generator dispatch and
bounds, branch r/x/b/tap/shift/rate_a/angle limits, and baseMVA,
enough to write a runnable case. It cannot recover original bus ids,
per-element load/shunt granularity, piecewise/cubic costs, or HVDC/storage;
what it can't recover is listed in warnings. Published wheels include the
native reader; custom source builds without the Rust gridfm feature raise
ImportError.
585def read_gridfm_scenarios(dir: Any) -> "list[GridfmRead]": 586 """Read every scenario of a gridfm dataset, one :class:`GridfmRead` per 587 scenario id (ascending) over the shared topology, the read side of 588 :func:`write_gridfm_batch`. 589 590 Each scenario is rebuilt independently, so two scenarios may differ in branch 591 status, bus types, and reference bus. See :func:`read_gridfm` for the lenient 592 directory resolution and the fidelity contract. 593 """ 594 _require_gridfm() 595 return [ 596 GridfmRead(Network(case), scen, warnings) 597 for case, scen, warnings in _powerio.read_gridfm_scenarios(str(dir)) 598 ]
Read every scenario of a gridfm dataset, one GridfmRead per
scenario id (ascending) over the shared topology, the read side of
write_gridfm_batch().
Each scenario is rebuilt independently, so two scenarios may differ in branch
status, bus types, and reference bus. See read_gridfm() for the lenient
directory resolution and the fidelity contract.
601def read_pypsa_csv_folder(path: Any) -> Network: 602 """Read a PyPSA CSV folder into a :class:`Network`.""" 603 return Network(_powerio.read_pypsa_csv_folder(str(path)))
Read a PyPSA CSV folder into a Network.
GridfmRead(network, scenario, warnings)