powerio
powerio: lossless power system case file IO, conversion, and matrices.
Parse MATPOWER, PSS/E, PowerWorld, PSLF EPC, PowerModels JSON, egret JSON,
pandapower JSON, PyPSA CSV folders, GO Challenge 3 JSON, Surge JSON, GridFM
Parquet datasets, and PowerIO JSON snapshots into one format neutral case; write
retained text formats back byte exact; convert between formats; package cases as
.pio.json; and pull the sparse matrices and graph outputs 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")
pkg = pio.Package.from_file("goc3_case.json", from_="goc3-json")
points = pkg.operating_points()
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).
GO Challenge 3 JSON is read as a static balanced network using the first
interval. When it is parsed as a .pio.json package, the full source time
series is exposed as replayable operating points.
import powerio and parsing/writing/converting pull in nothing but the
interpreter. The matrix methods need scipy/numpy and the graph helper 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, PSLF EPC, PowerModels JSON, egret JSON, 4pandapower JSON, PyPSA CSV folders, GO Challenge 3 JSON, Surge JSON, GridFM 5Parquet datasets, and PowerIO JSON snapshots into one format neutral case; write 6retained text formats back byte exact; convert between formats; package cases as 7``.pio.json``; and pull the sparse matrices and graph outputs solvers need:: 8 9 import powerio as pio 10 11 net = pio.parse_file("case9.m") # format inferred from the extension 12 print(net.n_buses, net.base_mva) # 9 100.0 13 text = net.to_matpower() # byte-exact MATPOWER echo 14 raw, warnings = pio.convert_file("case9.m", "psse") 15 pp_json, warnings = pio.convert_file("case9.m", "pandapower-json") 16 pypsa_out = net.write_pypsa_csv_folder("case9-pypsa") 17 pkg = pio.Package.from_file("goc3_case.json", from_="goc3-json") 18 points = pkg.operating_points() 19 20 B = net.bprime() # scipy.sparse, the FDPF B' 21 Y = net.ybus() # complex csr, G + jB 22 G = net.to_networkx() # networkx.Graph keyed by bus id 23 24PyPSA CSV folders carry the static network topology (PyPSA's native component 25format for network definition); time series NetCDF/HDF5 scenarios are out of 26scope for now (https://github.com/eigenergy/powerio/issues/107). 27 28GO Challenge 3 JSON is read as a static balanced network using the first 29interval. When it is parsed as a ``.pio.json`` package, the full source time 30series is exposed as replayable operating points. 31 32``import powerio`` and parsing/writing/converting pull in nothing but the 33interpreter. The matrix methods need scipy/numpy and the graph helper needs networkx; add them 34with ``pip install 'powerio[matrix]'``, ``[graph]``, or ``[all]``. A missing 35extra raises a clear ImportError, never a link error: the compiled core 36(``powerio._powerio``) returns COO triplets as plain Python lists, and the 37wrappers here assemble scipy matrices and networkx graphs lazily. 38""" 39 40from __future__ import annotations 41 42import importlib 43import json as _json 44from collections import namedtuple 45from typing import Any, Optional 46 47from . import _powerio 48from ._powerio import PowerIODataError, PowerIOError, PowerIOParseError, __version__ 49 50__all__ = [ 51 "Network", 52 "BalancedNetwork", 53 "Incidence", 54 "YbusParts", 55 "Conversion", 56 "DisplayData", 57 "PwdDisplay", 58 "PwdSubstation", 59 "DenseNetwork", 60 "DenseBranch", 61 "DenseGen", 62 "DenseDemand", 63 "DenseShunt", 64 "PowerIOError", 65 "PowerIOParseError", 66 "PowerIODataError", 67 "parse_file", 68 "parse_display_file", 69 "parse_display_bytes", 70 "parse_str", 71 "from_json", 72 "convert_file", 73 "convert_str", 74 "to_format", 75 "to_matpower", 76 "to_json", 77 "to_dense", 78 "Package", 79 "write_gridfm_batch", 80 "read_gridfm", 81 "read_gridfm_scenarios", 82 "read_pypsa_csv_folder", 83 "GridfmRead", 84 "dist", 85 "__version__", 86] 87 88Conversion = namedtuple("Conversion", ["text", "warnings"]) 89Conversion.__doc__ = """Output of :func:`convert_file`. 90 91``text`` is the converted file contents; ``warnings`` lists the fields the 92target format could not represent (empty for a faithful conversion). 93""" 94 95GridfmRead = namedtuple("GridfmRead", ["network", "scenario", "warnings"]) 96GridfmRead.__doc__ = """Output of :func:`read_gridfm` / :func:`read_gridfm_scenarios`. 97 98``network`` is the reconstructed :class:`Network`; ``scenario`` is the scenario 99id these rows came from; ``warnings`` lists what the gridfm schema could not 100round-trip (synthesized bus ids, folded per-bus load/shunt, dropped HVDC/storage, 101piecewise costs). The read is lossy but recovers everything a power flow needs. 102""" 103 104DisplayData = namedtuple("DisplayData", ["kind", "data"]) 105DisplayData.__doc__ = """Output of :func:`parse_display_file` / :func:`parse_display_bytes`. 106 107``kind`` names the display format. For v0.2.2, ``kind == "powerworld"`` and 108``data`` is a :class:`PwdDisplay`. 109""" 110 111PwdDisplay = namedtuple( 112 "PwdDisplay", ["canvas_width", "canvas_height", "stamp", "substations"] 113) 114PwdDisplay.__doc__ = """Decoded PowerWorld ``.pwd`` display metadata.""" 115 116PwdSubstation = namedtuple("PwdSubstation", ["number", "name", "x", "y"]) 117PwdSubstation.__doc__ = """One decoded PowerWorld display substation.""" 118 119Incidence = namedtuple("Incidence", ["A", "b", "p_shift", "branch_of_col"]) 120Incidence.__doc__ = """Output of :meth:`Network.incidence`. 121 122Shapes, with ``n`` buses and ``m`` in-service branches: 123- ``A``: signed incidence csr_matrix, ``(n, m)``. 124- ``b``: branch susceptances, ``(m,)``; ``b[k]`` is column ``k``. 125- ``p_shift``: phase-shift injection, ``(n,)`` (all zero unless 126 ``convention="matpower"``). 127- ``branch_of_col``: column→branch index map, ``(m,)``; ``branch_of_col[k]`` 128 and ``b[k]`` are co-indexed by incidence column ``k``. 129""" 130 131YbusParts = namedtuple("YbusParts", ["g", "b"]) 132YbusParts.__doc__ = ( 133 "Output of :meth:`Network.ybus_parts`: ``g`` = Re(Y_bus), ``b`` = Im(Y_bus), " 134 "each a real csr_matrix. ``Network.ybus()`` returns ``g + 1j*b``." 135) 136 137DenseBranch = namedtuple( 138 "DenseBranch", ["from_id", "to_id", "r", "x", "b", "tap", "shift", "in_service"] 139) 140DenseBranch.__doc__ = """Branch arrays in source order.""" 141 142DenseGen = namedtuple("DenseGen", ["bus", "pg", "pmax", "pmin", "in_service"]) 143DenseGen.__doc__ = """Generator arrays in source order.""" 144 145DenseDemand = namedtuple("DenseDemand", ["pd", "qd"]) 146DenseDemand.__doc__ = """Nodal active and reactive demand arrays in bus order.""" 147 148DenseShunt = namedtuple("DenseShunt", ["gs", "bs"]) 149DenseShunt.__doc__ = """Nodal shunt conductance and susceptance arrays in bus order.""" 150 151DenseNetwork = namedtuple( 152 "DenseNetwork", 153 [ 154 "n", 155 "m", 156 "ng", 157 "base_mva", 158 "bus_ids", 159 "branch", 160 "gen", 161 "demand", 162 "shunt", 163 "reference_bus", 164 "n_components", 165 "is_radial", 166 ], 167) 168DenseNetwork.__doc__ = """Copied dense NumPy table export of a parsed :class:`Network`.""" 169 170 171def _require(module: str, extra: str): 172 """Import ``module`` or raise a clear ImportError naming the extra to install.""" 173 try: 174 return importlib.import_module(module) 175 except ImportError as exc: 176 # Only rewrite "module is absent". A present-but-broken install (e.g. a 177 # failed C-extension load) raises ImportError from a sub-import; let its 178 # own traceback through instead of misdirecting the user to reinstall. 179 if getattr(exc, "name", None) not in (module, module.split(".")[0]): 180 raise 181 raise ImportError( 182 f"powerio needs {module!r} for this call; install it with " 183 f"`pip install 'powerio[{extra}]'`" 184 ) from exc 185 186 187def _to_csr(coo): 188 """Assemble a ``(data, row, col, shape)`` COO tuple into a csr_matrix.""" 189 sparse = _require("scipy.sparse", "matrix") 190 data, row, col, shape = coo 191 return sparse.coo_matrix((data, (row, col)), shape=shape).tocsr() 192 193 194def _require_gridfm() -> None: 195 """Raise a clear ImportError if the extension lacks the gridfm Parquet surface. 196 197 Published wheels include this surface. A custom source build can omit the 198 Rust feature, in which case the method names still raise a direct error 199 instead of failing with ``AttributeError``. 200 """ 201 if not getattr(_powerio, "_has_gridfm", False): 202 raise ImportError( 203 "powerio was built without the gridfm Parquet surface; reinstall a " 204 "wheel built with gridfm support or rebuild from source with " 205 "`maturin develop --features gridfm`." 206 ) 207 208 209def _wrap_display(raw) -> DisplayData: 210 kind, payload = raw 211 if kind == "powerworld": 212 substations = [ 213 PwdSubstation( 214 row["number"], 215 row["name"], 216 row["x"], 217 row["y"], 218 ) 219 for row in payload["substations"] 220 ] 221 payload = PwdDisplay( 222 payload["canvas_width"], 223 payload["canvas_height"], 224 payload["stamp"], 225 substations, 226 ) 227 return DisplayData(kind, payload) 228 229 230class Network: 231 """A parsed power network case. 232 233 The data attributes (``buses``, ``branches``, ``gens``, ``loads``, 234 ``shunts``) and the non-matrix methods (``write``, ``reference_bus_index``, 235 ``connectivity_report``, ``write_dcopf_bundle``) delegate to the compiled 236 handle; the matrix methods below return ``scipy.sparse`` objects. Read 237 fidelity warnings from parse time are on ``read_warnings``. Readers use this 238 for source data they cannot model or assumptions they had to make. 239 240 Errors: a bad file path raises the standard ``OSError`` subclass 241 (``FileNotFoundError``); a malformed case raises :class:`PowerIOParseError` 242 and an unmet builder precondition (no generators, no reference bus) raises 243 :class:`PowerIODataError`; both subclass :class:`PowerIOError`, so 244 ``except PowerIOError`` catches either; an unknown 245 ``scheme``/``convention``/``units`` string raises ``ValueError``. 246 """ 247 248 def __init__(self, inner: "_powerio.PyNetwork"): 249 self._inner = inner 250 251 def __getattr__(self, name: str): 252 # Reached only when normal lookup misses, so the matrix methods below 253 # win. Guard underscore names so a lookup before _inner exists raises 254 # AttributeError instead of recursing forever. 255 if name.startswith("_"): 256 raise AttributeError( 257 f"{type(self).__name__!r} object has no attribute {name!r}" 258 ) 259 return getattr(self._inner, name) 260 261 def __repr__(self) -> str: 262 # The inner handle's __repr__ already renders the public ``Network(...)`` 263 # form, so this is a straight delegate. 264 return repr(self._inner) 265 266 # --- canonical format and table exports ----------------------------- 267 268 def to_matpower(self) -> str: 269 """Serialize to MATPOWER ``.m`` text. 270 271 A case parsed from MATPOWER keeps its original source, so this returns a 272 byte-exact echo. Derived cases serialize from the format-neutral model. 273 """ 274 return self._inner.to_matpower() 275 276 def to_json(self) -> str: 277 """Serialize to the JSON transport.""" 278 return self._inner.to_json() 279 280 def to_format( 281 self, 282 to: str, 283 missing_gen_cost: Optional[str] = None, 284 default_gen_cost: Optional[str] = None, 285 gen_cost_csv: Optional[Any] = None, 286 ) -> Conversion: 287 """Serialize this parsed case to another format. 288 289 ``to`` is one of the format names accepted by :func:`convert_file`. 290 Returns a :class:`Conversion` with output text and fidelity warnings. 291 """ 292 text, warnings = self._inner.to_format( 293 to, 294 missing_gen_cost=missing_gen_cost, 295 default_gen_cost=default_gen_cost, 296 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 297 ) 298 return Conversion(text, warnings) 299 300 def to_dense(self) -> DenseNetwork: 301 """Dense NumPy arrays for solver and adapter code. 302 303 This allocates new arrays, preserves bus and branch source order, and 304 sums loads and shunts per bus to match the Rust indexed analysis view. 305 """ 306 np = _require("numpy", "matrix") 307 buses = self._inner.buses 308 branches = self._inner.branches 309 generators = self._inner.generators 310 bus_ids = np.asarray([b["id"] for b in buses], dtype=np.int64) 311 id_to_idx = {int(bus_id): idx for idx, bus_id in enumerate(bus_ids)} 312 313 pd = np.zeros(len(buses), dtype=float) 314 qd = np.zeros(len(buses), dtype=float) 315 for load in self._inner.loads: 316 idx = id_to_idx.get(load["bus"]) 317 if idx is not None: 318 pd[idx] += load["p"] 319 qd[idx] += load["q"] 320 321 gs = np.zeros(len(buses), dtype=float) 322 bs = np.zeros(len(buses), dtype=float) 323 for shunt in self._inner.shunts: 324 idx = id_to_idx.get(shunt["bus"]) 325 if idx is not None: 326 gs[idx] += shunt["g"] 327 bs[idx] += shunt["b"] 328 329 branch = DenseBranch( 330 from_id=np.asarray([br["from_id"] for br in branches], dtype=np.int64), 331 to_id=np.asarray([br["to_id"] for br in branches], dtype=np.int64), 332 r=np.asarray([br["r"] for br in branches], dtype=float), 333 x=np.asarray([br["x"] for br in branches], dtype=float), 334 b=np.asarray([br["b"] for br in branches], dtype=float), 335 tap=np.asarray([br["tap"] for br in branches], dtype=float), 336 shift=np.asarray([br["shift"] for br in branches], dtype=float), 337 in_service=np.asarray([br["in_service"] for br in branches], dtype=bool), 338 ) 339 gen = DenseGen( 340 bus=np.asarray([g["bus"] for g in generators], dtype=np.int64), 341 pg=np.asarray([g["pg"] for g in generators], dtype=float), 342 pmax=np.asarray([g["pmax"] for g in generators], dtype=float), 343 pmin=np.asarray([g["pmin"] for g in generators], dtype=float), 344 in_service=np.asarray([g["in_service"] for g in generators], dtype=bool), 345 ) 346 refs = self.reference_bus_indices() 347 return DenseNetwork( 348 n=len(buses), 349 m=len(branches), 350 ng=len(generators), 351 base_mva=self.base_mva, 352 bus_ids=bus_ids, 353 branch=branch, 354 gen=gen, 355 demand=DenseDemand(pd=pd, qd=qd), 356 shunt=DenseShunt(gs=gs, bs=bs), 357 reference_bus=refs[0] if len(refs) == 1 else None, 358 n_components=self.n_connected_components, 359 is_radial=self.is_radial, 360 ) 361 362 # --- matrix builders (scipy.sparse) --------------------------------- 363 364 def bprime(self, scheme: str = "bx"): 365 """FDPF B' (shuntless). ``scheme`` is ``"bx"`` or ``"xb"``.""" 366 return _to_csr(self._inner.bprime(scheme)) 367 368 def bdoubleprime(self, scheme: str = "bx"): 369 """FDPF B'' (with shunts and taps; shifts zeroed). ``scheme`` is 370 ``"bx"`` or ``"xb"``; taps are always kept (MATPOWER ``makeB``).""" 371 return _to_csr(self._inner.bdoubleprime(scheme)) 372 373 def lacpf(self, include_taps: bool = True, include_shifts: bool = True): 374 """LACPF 2n×2n block ``[[G, -B], [-B, -G]]``.""" 375 return _to_csr(self._inner.lacpf(include_taps, include_shifts)) 376 377 def adjacency(self): 378 """0/1 bus adjacency matrix.""" 379 return _to_csr(self._inner.adjacency()) 380 381 def ybus_parts(self, include_taps: bool = True, include_shifts: bool = True): 382 """:class:`YbusParts` ``(g, b)`` = ``(Re(Y_bus), Im(Y_bus))``, two real 383 csr_matrix.""" 384 g, b = self._inner.ybus_parts(include_taps, include_shifts) 385 return YbusParts(g=_to_csr(g), b=_to_csr(b)) 386 387 def ybus(self, include_taps: bool = True, include_shifts: bool = True): 388 """``Y_bus = G + jB`` as a complex csr_matrix.""" 389 g, b = self.ybus_parts(include_taps, include_shifts) 390 return (g + 1j * b).tocsr() 391 392 def ptdf(self, convention: str = "paper"): 393 """DC PTDF (m×n). ``convention`` is ``"paper"`` or ``"matpower"``.""" 394 return _to_csr(self._inner.ptdf(convention)) 395 396 def lodf(self, convention: str = "paper"): 397 """DC LODF (m×m).""" 398 return _to_csr(self._inner.lodf(convention)) 399 400 def weighted_laplacian(self, convention: str = "paper"): 401 """Weighted Laplacian ``L = A diag(b) Aᵀ``.""" 402 return _to_csr(self._inner.weighted_laplacian(convention)) 403 404 def incidence(self, convention: str = "paper") -> "Incidence": 405 """Signed incidence factorization as an :data:`Incidence` tuple.""" 406 np = _require("numpy", "matrix") 407 a, b, p_shift, branch_of_col = self._inner.incidence(convention) 408 return Incidence( 409 A=_to_csr(a), 410 b=np.asarray(b, dtype=float), 411 p_shift=np.asarray(p_shift, dtype=float), 412 branch_of_col=np.asarray(branch_of_col, dtype=np.int64), 413 ) 414 415 def write_gridfm( 416 self, 417 out_dir: Any, 418 scenario: int = 0, 419 include_y_bus: bool = True, 420 include_taps: bool = True, 421 include_shifts: bool = True, 422 missing_gen_cost: Optional[str] = None, 423 default_gen_cost: Optional[str] = None, 424 gen_cost_csv: Optional[Any] = None, 425 ) -> dict: 426 """Write the gridfm-datakit Parquet dataset for this case under 427 ``<out_dir>/<case>/raw/``. 428 429 Returns a dict with ``dir``, ``files``, ``dropped_zero_impedance``, and 430 ``degenerate_cost_gens``. Published wheels include the native writer; 431 custom source builds without the Rust ``gridfm`` feature raise 432 ``ImportError``. For many perturbed snapshots in one dataset, see 433 :func:`write_gridfm_batch`. 434 """ 435 _require_gridfm() 436 return self._inner.write_gridfm( 437 str(out_dir), 438 scenario, 439 include_y_bus, 440 include_taps, 441 include_shifts, 442 missing_gen_cost=missing_gen_cost, 443 default_gen_cost=default_gen_cost, 444 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 445 ) 446 447 def write_pypsa_csv_folder(self, out_dir: Any) -> dict: 448 """Write this case as a PyPSA CSV folder. 449 450 The folder contains static PyPSA component CSVs and can be imported with 451 ``pypsa.Network().import_from_csv_folder(path)``. Returns a dict with 452 ``dir``, ``files``, and fidelity ``warnings``. 453 """ 454 return self._inner.write_pypsa_csv_folder(str(out_dir)) 455 456 def to_normalized(self) -> "Network": 457 """A normalized, computation-ready copy of this case: per unit, radians, 458 out-of-service filtered, source bus ids preserved, bus types 459 canonicalized. The original case is unchanged; the result carries no 460 retained source, so :meth:`write` serializes the per-unit model rather 461 than echoing it. Raises :class:`PowerIODataError` if the case can't be 462 normalized (no reference bus can be chosen, or a non-positive base MVA). 463 """ 464 return Network(self._inner.to_normalized()) 465 466 def to_networkx(self): 467 """Undirected networkx graph keyed by bus id. 468 469 In-service branches become edges carrying ``branch`` (index), ``r``, 470 ``x``, and ``b``. 471 """ 472 nx = _require("networkx", "graph") 473 g = nx.Graph() 474 g.add_nodes_from(bus["id"] for bus in self._inner.buses) 475 for k, br in enumerate(self._inner.branches): 476 if br["in_service"]: 477 g.add_edge( 478 br["from_id"], 479 br["to_id"], 480 branch=k, 481 r=br["r"], 482 x=br["x"], 483 b=br["b"], 484 ) 485 return g 486 487 488# v1 name for the scalar positive sequence model. ``Network`` remains the 489# existing Python handle name in 0.4. 490BalancedNetwork = Network 491 492 493def parse_file(path: Any, from_: Optional[str] = None) -> Network: 494 """Parse a case file from a path, inferring the format from the extension. 495 496 Read fidelity warnings are on ``Network.read_warnings`` (empty for readers 497 that don't report any; currently pandapower JSON, PyPSA CSV, and PSLF EPC 498 report them). 499 """ 500 return Network(_powerio.parse_file(str(path), from_)) 501 502 503def parse_display_file(path: Any, from_: Optional[str] = None) -> DisplayData: 504 """Parse a display artifact such as a PowerWorld ``.pwd`` file.""" 505 return _wrap_display(_powerio.parse_display_file(str(path), from_)) 506 507 508def parse_display_bytes(data: bytes, format: str) -> DisplayData: 509 """Parse display bytes in the named display format.""" 510 return _wrap_display(_powerio.parse_display_bytes(data, format)) 511 512 513def parse_str(text: str, format: str = "matpower") -> Network: 514 """Parse a case from in-memory text in the named ``format``.""" 515 return Network(_powerio.parse_str(text, format)) 516 517 518def from_json(text: str) -> Network: 519 """Rebuild a case from JSON produced by :meth:`Network.to_json`.""" 520 return Network(_powerio.from_json(text)) 521 522 523def convert_file( 524 path: Any, 525 to: str, 526 from_: Optional[str] = None, 527 missing_gen_cost: Optional[str] = None, 528 default_gen_cost: Optional[str] = None, 529 gen_cost_csv: Optional[Any] = None, 530) -> Conversion: 531 """Convert a case file to another format through the neutral hub. 532 533 ``to`` / ``from_`` are format names: ``matpower``, ``powermodels-json``, 534 ``egret-json``, ``pandapower-json``, ``psse``, ``powerworld``, ``pslf``, 535 ``goc3-json``, and ``surge-json`` (aliases ``m``, ``pm``, ``egret``, 536 ``pp``, ``raw``, ``aux``, ``epc``, ``goc3``, and ``surge``). The input format is 537 inferred from the file extension unless ``from_`` overrides it. GO Challenge 538 3 JSON is read only. PyPSA CSV folders are read with 539 ``from_="pypsa-csv"`` and written with 540 :meth:`Network.write_pypsa_csv_folder`. Returns a :class:`Conversion` with 541 the text and any fidelity warnings. 542 """ 543 text, warnings = _powerio.convert_file( 544 str(path), 545 to, 546 from_, 547 missing_gen_cost=missing_gen_cost, 548 default_gen_cost=default_gen_cost, 549 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 550 ) 551 return Conversion(text, warnings) 552 553 554def convert_str( 555 text: str, 556 to: str, 557 format: str = "matpower", 558 missing_gen_cost: Optional[str] = None, 559 default_gen_cost: Optional[str] = None, 560 gen_cost_csv: Optional[Any] = None, 561) -> Conversion: 562 """Convert in-memory case ``text`` to another format through the neutral 563 hub, with no file staging. 564 565 ``to`` and ``format`` are format names as in :func:`convert_file`; 566 ``format`` names the input (default ``matpower``). Returns a 567 :class:`Conversion` with the converted text and any fidelity warnings. 568 """ 569 out, warnings = _powerio.convert_str( 570 text, 571 to, 572 format, 573 missing_gen_cost=missing_gen_cost, 574 default_gen_cost=default_gen_cost, 575 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 576 ) 577 return Conversion(out, warnings) 578 579 580def to_format( 581 network: Network, 582 to: str, 583 missing_gen_cost: Optional[str] = None, 584 default_gen_cost: Optional[str] = None, 585 gen_cost_csv: Optional[Any] = None, 586) -> Conversion: 587 """Serialize ``network`` to another format.""" 588 return network.to_format( 589 to, 590 missing_gen_cost=missing_gen_cost, 591 default_gen_cost=default_gen_cost, 592 gen_cost_csv=gen_cost_csv, 593 ) 594 595 596def to_matpower(network: Network) -> str: 597 """Serialize ``network`` to MATPOWER ``.m`` text.""" 598 return network.to_matpower() 599 600 601def to_json(network: Network) -> str: 602 """Serialize ``network`` to the JSON transport.""" 603 return network.to_json() 604 605 606def to_dense(network: Network) -> DenseNetwork: 607 """Return copied dense NumPy tables for ``network``.""" 608 return network.to_dense() 609 610 611def write_gridfm_batch( 612 networks: "list[Network]", 613 out_dir: Any, 614 base_scenario: int = 0, 615 include_y_bus: bool = True, 616 include_taps: bool = True, 617 include_shifts: bool = True, 618 missing_gen_cost: Optional[str] = None, 619 default_gen_cost: Optional[str] = None, 620 gen_cost_csv: Optional[Any] = None, 621) -> dict: 622 """Write several networks as one gridfm-datakit dataset, row-stacked and 623 keyed by the ``scenario`` column. 624 625 Each network is one snapshot; the k-th is stamped ``base_scenario + k``. The 626 networks must share a base element set: the same bus/branch/gen counts and 627 bus id order (otherwise :class:`PowerIODataError` is raised). Load, dispatch, 628 branch status, and costs may vary per scenario. Returns the same dict as 629 :meth:`Network.write_gridfm`. Published wheels include the native writer; 630 custom source builds without the Rust ``gridfm`` feature raise 631 ``ImportError``. 632 """ 633 _require_gridfm() 634 inners = [c._inner for c in networks] 635 return _powerio.write_gridfm_batch( 636 inners, 637 str(out_dir), 638 base_scenario, 639 include_y_bus, 640 include_taps, 641 include_shifts, 642 missing_gen_cost=missing_gen_cost, 643 default_gen_cost=default_gen_cost, 644 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 645 ) 646 647 648def read_gridfm(dir: Any, scenario: int = 0) -> GridfmRead: 649 """Read one scenario of a gridfm-datakit Parquet dataset back into a case. 650 651 The inverse of :meth:`Network.write_gridfm`. ``dir`` is resolved leniently: 652 the ``raw/`` directory holding the parquet files, a ``<case>/`` directory with 653 a ``raw/`` child, or a parent directory with one ``*/raw/`` child all work. 654 ``scenario`` selects one snapshot from a batch (``0``, the base case, by 655 default). Returns a :class:`GridfmRead` ``(network, scenario, warnings)``. 656 657 The read is lossy but recovers everything a power flow needs: bus types, 658 voltages and limits, nodal load and shunt totals, generator dispatch and 659 bounds, branch ``r/x/b/tap/shift/rate_a``/angle limits, and ``baseMVA``, 660 enough to write a runnable case. It cannot recover original bus ids, 661 per-element load/shunt granularity, piecewise/cubic costs, or HVDC/storage; 662 what it can't recover is listed in ``warnings``. Published wheels include the 663 native reader; custom source builds without the Rust ``gridfm`` feature raise 664 ``ImportError``. 665 """ 666 _require_gridfm() 667 inner, scen, warnings = _powerio.read_gridfm(str(dir), scenario) 668 return GridfmRead(Network(inner), scen, warnings) 669 670 671def read_gridfm_scenarios(dir: Any) -> "list[GridfmRead]": 672 """Read every scenario of a gridfm dataset, one :class:`GridfmRead` per 673 scenario id (ascending) over the shared topology, the read side of 674 :func:`write_gridfm_batch`. 675 676 Each scenario is rebuilt independently, so two scenarios may differ in branch 677 status, bus types, and reference bus. See :func:`read_gridfm` for the lenient 678 directory resolution and the fidelity behavior. 679 """ 680 _require_gridfm() 681 return [ 682 GridfmRead(Network(inner), scen, warnings) 683 for inner, scen, warnings in _powerio.read_gridfm_scenarios(str(dir)) 684 ] 685 686 687def read_pypsa_csv_folder(path: Any) -> Network: 688 """Read a PyPSA CSV folder into a :class:`Network`.""" 689 return Network(_powerio.read_pypsa_csv_folder(str(path))) 690 691 692from . import dist # noqa: E402 (needs Conversion defined above) 693 694 695class Package: 696 """A parsed ``.pio.json`` package handle. 697 698 Parses the envelope once; every accessor reuses the handle instead of 699 re-reading the JSON text. 700 """ 701 702 def __init__(self, inner: "_powerio._Package"): 703 self._inner = inner 704 705 @classmethod 706 def from_file( 707 cls, path: Any, from_: Optional[str] = None, scenario: int = 0 708 ) -> "Package": 709 """Build a package from a case file or folder.""" 710 return cls(_powerio._Package.from_file(str(path), from_, scenario)) 711 712 @classmethod 713 def from_str(cls, text: str, from_: Optional[str] = None) -> "Package": 714 """Build a package from in-memory case text.""" 715 return cls(_powerio._Package.from_str(text, from_)) 716 717 @classmethod 718 def from_json(cls, text: str) -> "Package": 719 """Parse ``.pio.json`` envelope text.""" 720 return cls(_powerio._Package.from_json(text)) 721 722 @classmethod 723 def from_balanced( 724 cls, network: Network, include_solver_metadata: bool = False 725 ) -> "Package": 726 """Wrap a balanced :class:`Network` in a package.""" 727 return cls( 728 _powerio._Package.from_balanced(network._inner, include_solver_metadata) 729 ) 730 731 @classmethod 732 def from_multiconductor(cls, network: "dist.MulticonductorNetwork") -> "Package": 733 """Wrap a multiconductor network in a package.""" 734 return cls(_powerio._Package.from_multiconductor(network._inner)) 735 736 @property 737 def model_kind(self) -> str: 738 """``"balanced"`` or ``"multiconductor"``.""" 739 return self._inner.model_kind() 740 741 def to_json(self) -> str: 742 """Serialize to pretty ``.pio.json``.""" 743 return self._inner.to_json() 744 745 def as_balanced(self) -> Network: 746 """Return the balanced payload as a :class:`Network`.""" 747 return Network(self._inner.as_balanced()) 748 749 def as_multiconductor(self) -> "dist.MulticonductorNetwork": 750 """Return the multiconductor payload.""" 751 return dist.MulticonductorNetwork(self._inner.as_multiconductor()) 752 753 def operating_points(self) -> Any: 754 """The operating point series as Python data, or ``None``. 755 756 GOC3 packages populate this from the source time series. Each point is 757 a set of field updates over the package's static payload. 758 """ 759 return _json.loads(self._inner.operating_points_json()) 760 761 def materialize_operating_point(self, index: int) -> "Package": 762 """Materialize one operating point into a new static package.""" 763 return Package(self._inner.materialize_operating_point(index)) 764 765 def validate(self) -> None: 766 """Run the package semantic validation profile in place.""" 767 self._inner.validate() 768 769 def validation(self) -> Any: 770 """The validation summary as Python data.""" 771 return _json.loads(self._inner.validation_json()) 772 773 def diagnostics(self) -> Any: 774 """The structured diagnostics as a list of Python dicts.""" 775 return _json.loads(self._inner.diagnostics_json()) 776 777 def multiconductor_to_balanced_preflight(self, base_mva: float = 100.0) -> Any: 778 """Readiness report for multiconductor to balanced lowering.""" 779 return _json.loads( 780 self._inner.multiconductor_to_balanced_preflight_json(base_mva) 781 ) 782 783 def lower_multiconductor_to_balanced(self, base_mva: float = 100.0) -> "Package": 784 """Lower a multiconductor package to a new balanced package.""" 785 return Package(self._inner.lower_multiconductor_to_balanced(base_mva)) 786 787 def __repr__(self) -> str: 788 return repr(self._inner)
231class Network: 232 """A parsed power network case. 233 234 The data attributes (``buses``, ``branches``, ``gens``, ``loads``, 235 ``shunts``) and the non-matrix methods (``write``, ``reference_bus_index``, 236 ``connectivity_report``, ``write_dcopf_bundle``) delegate to the compiled 237 handle; the matrix methods below return ``scipy.sparse`` objects. Read 238 fidelity warnings from parse time are on ``read_warnings``. Readers use this 239 for source data they cannot model or assumptions they had to make. 240 241 Errors: a bad file path raises the standard ``OSError`` subclass 242 (``FileNotFoundError``); a malformed case raises :class:`PowerIOParseError` 243 and an unmet builder precondition (no generators, no reference bus) raises 244 :class:`PowerIODataError`; both subclass :class:`PowerIOError`, so 245 ``except PowerIOError`` catches either; an unknown 246 ``scheme``/``convention``/``units`` string raises ``ValueError``. 247 """ 248 249 def __init__(self, inner: "_powerio.PyNetwork"): 250 self._inner = inner 251 252 def __getattr__(self, name: str): 253 # Reached only when normal lookup misses, so the matrix methods below 254 # win. Guard underscore names so a lookup before _inner exists raises 255 # AttributeError instead of recursing forever. 256 if name.startswith("_"): 257 raise AttributeError( 258 f"{type(self).__name__!r} object has no attribute {name!r}" 259 ) 260 return getattr(self._inner, name) 261 262 def __repr__(self) -> str: 263 # The inner handle's __repr__ already renders the public ``Network(...)`` 264 # form, so this is a straight delegate. 265 return repr(self._inner) 266 267 # --- canonical format and table exports ----------------------------- 268 269 def to_matpower(self) -> str: 270 """Serialize to MATPOWER ``.m`` text. 271 272 A case parsed from MATPOWER keeps its original source, so this returns a 273 byte-exact echo. Derived cases serialize from the format-neutral model. 274 """ 275 return self._inner.to_matpower() 276 277 def to_json(self) -> str: 278 """Serialize to the JSON transport.""" 279 return self._inner.to_json() 280 281 def to_format( 282 self, 283 to: str, 284 missing_gen_cost: Optional[str] = None, 285 default_gen_cost: Optional[str] = None, 286 gen_cost_csv: Optional[Any] = None, 287 ) -> Conversion: 288 """Serialize this parsed case to another format. 289 290 ``to`` is one of the format names accepted by :func:`convert_file`. 291 Returns a :class:`Conversion` with output text and fidelity warnings. 292 """ 293 text, warnings = self._inner.to_format( 294 to, 295 missing_gen_cost=missing_gen_cost, 296 default_gen_cost=default_gen_cost, 297 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 298 ) 299 return Conversion(text, warnings) 300 301 def to_dense(self) -> DenseNetwork: 302 """Dense NumPy arrays for solver and adapter code. 303 304 This allocates new arrays, preserves bus and branch source order, and 305 sums loads and shunts per bus to match the Rust indexed analysis view. 306 """ 307 np = _require("numpy", "matrix") 308 buses = self._inner.buses 309 branches = self._inner.branches 310 generators = self._inner.generators 311 bus_ids = np.asarray([b["id"] for b in buses], dtype=np.int64) 312 id_to_idx = {int(bus_id): idx for idx, bus_id in enumerate(bus_ids)} 313 314 pd = np.zeros(len(buses), dtype=float) 315 qd = np.zeros(len(buses), dtype=float) 316 for load in self._inner.loads: 317 idx = id_to_idx.get(load["bus"]) 318 if idx is not None: 319 pd[idx] += load["p"] 320 qd[idx] += load["q"] 321 322 gs = np.zeros(len(buses), dtype=float) 323 bs = np.zeros(len(buses), dtype=float) 324 for shunt in self._inner.shunts: 325 idx = id_to_idx.get(shunt["bus"]) 326 if idx is not None: 327 gs[idx] += shunt["g"] 328 bs[idx] += shunt["b"] 329 330 branch = DenseBranch( 331 from_id=np.asarray([br["from_id"] for br in branches], dtype=np.int64), 332 to_id=np.asarray([br["to_id"] for br in branches], dtype=np.int64), 333 r=np.asarray([br["r"] for br in branches], dtype=float), 334 x=np.asarray([br["x"] for br in branches], dtype=float), 335 b=np.asarray([br["b"] for br in branches], dtype=float), 336 tap=np.asarray([br["tap"] for br in branches], dtype=float), 337 shift=np.asarray([br["shift"] for br in branches], dtype=float), 338 in_service=np.asarray([br["in_service"] for br in branches], dtype=bool), 339 ) 340 gen = DenseGen( 341 bus=np.asarray([g["bus"] for g in generators], dtype=np.int64), 342 pg=np.asarray([g["pg"] for g in generators], dtype=float), 343 pmax=np.asarray([g["pmax"] for g in generators], dtype=float), 344 pmin=np.asarray([g["pmin"] for g in generators], dtype=float), 345 in_service=np.asarray([g["in_service"] for g in generators], dtype=bool), 346 ) 347 refs = self.reference_bus_indices() 348 return DenseNetwork( 349 n=len(buses), 350 m=len(branches), 351 ng=len(generators), 352 base_mva=self.base_mva, 353 bus_ids=bus_ids, 354 branch=branch, 355 gen=gen, 356 demand=DenseDemand(pd=pd, qd=qd), 357 shunt=DenseShunt(gs=gs, bs=bs), 358 reference_bus=refs[0] if len(refs) == 1 else None, 359 n_components=self.n_connected_components, 360 is_radial=self.is_radial, 361 ) 362 363 # --- matrix builders (scipy.sparse) --------------------------------- 364 365 def bprime(self, scheme: str = "bx"): 366 """FDPF B' (shuntless). ``scheme`` is ``"bx"`` or ``"xb"``.""" 367 return _to_csr(self._inner.bprime(scheme)) 368 369 def bdoubleprime(self, scheme: str = "bx"): 370 """FDPF B'' (with shunts and taps; shifts zeroed). ``scheme`` is 371 ``"bx"`` or ``"xb"``; taps are always kept (MATPOWER ``makeB``).""" 372 return _to_csr(self._inner.bdoubleprime(scheme)) 373 374 def lacpf(self, include_taps: bool = True, include_shifts: bool = True): 375 """LACPF 2n×2n block ``[[G, -B], [-B, -G]]``.""" 376 return _to_csr(self._inner.lacpf(include_taps, include_shifts)) 377 378 def adjacency(self): 379 """0/1 bus adjacency matrix.""" 380 return _to_csr(self._inner.adjacency()) 381 382 def ybus_parts(self, include_taps: bool = True, include_shifts: bool = True): 383 """:class:`YbusParts` ``(g, b)`` = ``(Re(Y_bus), Im(Y_bus))``, two real 384 csr_matrix.""" 385 g, b = self._inner.ybus_parts(include_taps, include_shifts) 386 return YbusParts(g=_to_csr(g), b=_to_csr(b)) 387 388 def ybus(self, include_taps: bool = True, include_shifts: bool = True): 389 """``Y_bus = G + jB`` as a complex csr_matrix.""" 390 g, b = self.ybus_parts(include_taps, include_shifts) 391 return (g + 1j * b).tocsr() 392 393 def ptdf(self, convention: str = "paper"): 394 """DC PTDF (m×n). ``convention`` is ``"paper"`` or ``"matpower"``.""" 395 return _to_csr(self._inner.ptdf(convention)) 396 397 def lodf(self, convention: str = "paper"): 398 """DC LODF (m×m).""" 399 return _to_csr(self._inner.lodf(convention)) 400 401 def weighted_laplacian(self, convention: str = "paper"): 402 """Weighted Laplacian ``L = A diag(b) Aᵀ``.""" 403 return _to_csr(self._inner.weighted_laplacian(convention)) 404 405 def incidence(self, convention: str = "paper") -> "Incidence": 406 """Signed incidence factorization as an :data:`Incidence` tuple.""" 407 np = _require("numpy", "matrix") 408 a, b, p_shift, branch_of_col = self._inner.incidence(convention) 409 return Incidence( 410 A=_to_csr(a), 411 b=np.asarray(b, dtype=float), 412 p_shift=np.asarray(p_shift, dtype=float), 413 branch_of_col=np.asarray(branch_of_col, dtype=np.int64), 414 ) 415 416 def write_gridfm( 417 self, 418 out_dir: Any, 419 scenario: int = 0, 420 include_y_bus: bool = True, 421 include_taps: bool = True, 422 include_shifts: bool = True, 423 missing_gen_cost: Optional[str] = None, 424 default_gen_cost: Optional[str] = None, 425 gen_cost_csv: Optional[Any] = None, 426 ) -> dict: 427 """Write the gridfm-datakit Parquet dataset for this case under 428 ``<out_dir>/<case>/raw/``. 429 430 Returns a dict with ``dir``, ``files``, ``dropped_zero_impedance``, and 431 ``degenerate_cost_gens``. Published wheels include the native writer; 432 custom source builds without the Rust ``gridfm`` feature raise 433 ``ImportError``. For many perturbed snapshots in one dataset, see 434 :func:`write_gridfm_batch`. 435 """ 436 _require_gridfm() 437 return self._inner.write_gridfm( 438 str(out_dir), 439 scenario, 440 include_y_bus, 441 include_taps, 442 include_shifts, 443 missing_gen_cost=missing_gen_cost, 444 default_gen_cost=default_gen_cost, 445 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 446 ) 447 448 def write_pypsa_csv_folder(self, out_dir: Any) -> dict: 449 """Write this case as a PyPSA CSV folder. 450 451 The folder contains static PyPSA component CSVs and can be imported with 452 ``pypsa.Network().import_from_csv_folder(path)``. Returns a dict with 453 ``dir``, ``files``, and fidelity ``warnings``. 454 """ 455 return self._inner.write_pypsa_csv_folder(str(out_dir)) 456 457 def to_normalized(self) -> "Network": 458 """A normalized, computation-ready copy of this case: per unit, radians, 459 out-of-service filtered, source bus ids preserved, bus types 460 canonicalized. The original case is unchanged; the result carries no 461 retained source, so :meth:`write` serializes the per-unit model rather 462 than echoing it. Raises :class:`PowerIODataError` if the case can't be 463 normalized (no reference bus can be chosen, or a non-positive base MVA). 464 """ 465 return Network(self._inner.to_normalized()) 466 467 def to_networkx(self): 468 """Undirected networkx graph keyed by bus id. 469 470 In-service branches become edges carrying ``branch`` (index), ``r``, 471 ``x``, and ``b``. 472 """ 473 nx = _require("networkx", "graph") 474 g = nx.Graph() 475 g.add_nodes_from(bus["id"] for bus in self._inner.buses) 476 for k, br in enumerate(self._inner.branches): 477 if br["in_service"]: 478 g.add_edge( 479 br["from_id"], 480 br["to_id"], 481 branch=k, 482 r=br["r"], 483 x=br["x"], 484 b=br["b"], 485 ) 486 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. Readers use this
for source data they cannot model or assumptions they had to make.
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.
269 def to_matpower(self) -> str: 270 """Serialize to MATPOWER ``.m`` text. 271 272 A case parsed from MATPOWER keeps its original source, so this returns a 273 byte-exact echo. Derived cases serialize from the format-neutral model. 274 """ 275 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.
277 def to_json(self) -> str: 278 """Serialize to the JSON transport.""" 279 return self._inner.to_json()
Serialize to the JSON transport.
281 def to_format( 282 self, 283 to: str, 284 missing_gen_cost: Optional[str] = None, 285 default_gen_cost: Optional[str] = None, 286 gen_cost_csv: Optional[Any] = None, 287 ) -> Conversion: 288 """Serialize this parsed case to another format. 289 290 ``to`` is one of the format names accepted by :func:`convert_file`. 291 Returns a :class:`Conversion` with output text and fidelity warnings. 292 """ 293 text, warnings = self._inner.to_format( 294 to, 295 missing_gen_cost=missing_gen_cost, 296 default_gen_cost=default_gen_cost, 297 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 298 ) 299 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.
301 def to_dense(self) -> DenseNetwork: 302 """Dense NumPy arrays for solver and adapter code. 303 304 This allocates new arrays, preserves bus and branch source order, and 305 sums loads and shunts per bus to match the Rust indexed analysis view. 306 """ 307 np = _require("numpy", "matrix") 308 buses = self._inner.buses 309 branches = self._inner.branches 310 generators = self._inner.generators 311 bus_ids = np.asarray([b["id"] for b in buses], dtype=np.int64) 312 id_to_idx = {int(bus_id): idx for idx, bus_id in enumerate(bus_ids)} 313 314 pd = np.zeros(len(buses), dtype=float) 315 qd = np.zeros(len(buses), dtype=float) 316 for load in self._inner.loads: 317 idx = id_to_idx.get(load["bus"]) 318 if idx is not None: 319 pd[idx] += load["p"] 320 qd[idx] += load["q"] 321 322 gs = np.zeros(len(buses), dtype=float) 323 bs = np.zeros(len(buses), dtype=float) 324 for shunt in self._inner.shunts: 325 idx = id_to_idx.get(shunt["bus"]) 326 if idx is not None: 327 gs[idx] += shunt["g"] 328 bs[idx] += shunt["b"] 329 330 branch = DenseBranch( 331 from_id=np.asarray([br["from_id"] for br in branches], dtype=np.int64), 332 to_id=np.asarray([br["to_id"] for br in branches], dtype=np.int64), 333 r=np.asarray([br["r"] for br in branches], dtype=float), 334 x=np.asarray([br["x"] for br in branches], dtype=float), 335 b=np.asarray([br["b"] for br in branches], dtype=float), 336 tap=np.asarray([br["tap"] for br in branches], dtype=float), 337 shift=np.asarray([br["shift"] for br in branches], dtype=float), 338 in_service=np.asarray([br["in_service"] for br in branches], dtype=bool), 339 ) 340 gen = DenseGen( 341 bus=np.asarray([g["bus"] for g in generators], dtype=np.int64), 342 pg=np.asarray([g["pg"] for g in generators], dtype=float), 343 pmax=np.asarray([g["pmax"] for g in generators], dtype=float), 344 pmin=np.asarray([g["pmin"] for g in generators], dtype=float), 345 in_service=np.asarray([g["in_service"] for g in generators], dtype=bool), 346 ) 347 refs = self.reference_bus_indices() 348 return DenseNetwork( 349 n=len(buses), 350 m=len(branches), 351 ng=len(generators), 352 base_mva=self.base_mva, 353 bus_ids=bus_ids, 354 branch=branch, 355 gen=gen, 356 demand=DenseDemand(pd=pd, qd=qd), 357 shunt=DenseShunt(gs=gs, bs=bs), 358 reference_bus=refs[0] if len(refs) == 1 else None, 359 n_components=self.n_connected_components, 360 is_radial=self.is_radial, 361 )
Dense NumPy arrays for solver and adapter code.
This allocates new arrays, preserves bus and branch source order, and sums loads and shunts per bus to match the Rust indexed analysis view.
365 def bprime(self, scheme: str = "bx"): 366 """FDPF B' (shuntless). ``scheme`` is ``"bx"`` or ``"xb"``.""" 367 return _to_csr(self._inner.bprime(scheme))
FDPF B' (shuntless). scheme is "bx" or "xb".
369 def bdoubleprime(self, scheme: str = "bx"): 370 """FDPF B'' (with shunts and taps; shifts zeroed). ``scheme`` is 371 ``"bx"`` or ``"xb"``; taps are always kept (MATPOWER ``makeB``).""" 372 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).
374 def lacpf(self, include_taps: bool = True, include_shifts: bool = True): 375 """LACPF 2n×2n block ``[[G, -B], [-B, -G]]``.""" 376 return _to_csr(self._inner.lacpf(include_taps, include_shifts))
LACPF 2n×2n block [[G, -B], [-B, -G]].
378 def adjacency(self): 379 """0/1 bus adjacency matrix.""" 380 return _to_csr(self._inner.adjacency())
0/1 bus adjacency matrix.
382 def ybus_parts(self, include_taps: bool = True, include_shifts: bool = True): 383 """:class:`YbusParts` ``(g, b)`` = ``(Re(Y_bus), Im(Y_bus))``, two real 384 csr_matrix.""" 385 g, b = self._inner.ybus_parts(include_taps, include_shifts) 386 return YbusParts(g=_to_csr(g), b=_to_csr(b))
YbusParts (g, b) = (Re(Y_bus), Im(Y_bus)), two real
csr_matrix.
388 def ybus(self, include_taps: bool = True, include_shifts: bool = True): 389 """``Y_bus = G + jB`` as a complex csr_matrix.""" 390 g, b = self.ybus_parts(include_taps, include_shifts) 391 return (g + 1j * b).tocsr()
Y_bus = G + jB as a complex csr_matrix.
393 def ptdf(self, convention: str = "paper"): 394 """DC PTDF (m×n). ``convention`` is ``"paper"`` or ``"matpower"``.""" 395 return _to_csr(self._inner.ptdf(convention))
DC PTDF (m×n). convention is "paper" or "matpower".
397 def lodf(self, convention: str = "paper"): 398 """DC LODF (m×m).""" 399 return _to_csr(self._inner.lodf(convention))
DC LODF (m×m).
401 def weighted_laplacian(self, convention: str = "paper"): 402 """Weighted Laplacian ``L = A diag(b) Aáµ€``.""" 403 return _to_csr(self._inner.weighted_laplacian(convention))
Weighted Laplacian L = A diag(b) Aáµ€.
405 def incidence(self, convention: str = "paper") -> "Incidence": 406 """Signed incidence factorization as an :data:`Incidence` tuple.""" 407 np = _require("numpy", "matrix") 408 a, b, p_shift, branch_of_col = self._inner.incidence(convention) 409 return Incidence( 410 A=_to_csr(a), 411 b=np.asarray(b, dtype=float), 412 p_shift=np.asarray(p_shift, dtype=float), 413 branch_of_col=np.asarray(branch_of_col, dtype=np.int64), 414 )
Signed incidence factorization as an Incidence tuple.
416 def write_gridfm( 417 self, 418 out_dir: Any, 419 scenario: int = 0, 420 include_y_bus: bool = True, 421 include_taps: bool = True, 422 include_shifts: bool = True, 423 missing_gen_cost: Optional[str] = None, 424 default_gen_cost: Optional[str] = None, 425 gen_cost_csv: Optional[Any] = None, 426 ) -> dict: 427 """Write the gridfm-datakit Parquet dataset for this case under 428 ``<out_dir>/<case>/raw/``. 429 430 Returns a dict with ``dir``, ``files``, ``dropped_zero_impedance``, and 431 ``degenerate_cost_gens``. Published wheels include the native writer; 432 custom source builds without the Rust ``gridfm`` feature raise 433 ``ImportError``. For many perturbed snapshots in one dataset, see 434 :func:`write_gridfm_batch`. 435 """ 436 _require_gridfm() 437 return self._inner.write_gridfm( 438 str(out_dir), 439 scenario, 440 include_y_bus, 441 include_taps, 442 include_shifts, 443 missing_gen_cost=missing_gen_cost, 444 default_gen_cost=default_gen_cost, 445 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 446 )
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().
448 def write_pypsa_csv_folder(self, out_dir: Any) -> dict: 449 """Write this case as a PyPSA CSV folder. 450 451 The folder contains static PyPSA component CSVs and can be imported with 452 ``pypsa.Network().import_from_csv_folder(path)``. Returns a dict with 453 ``dir``, ``files``, and fidelity ``warnings``. 454 """ 455 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.
457 def to_normalized(self) -> "Network": 458 """A normalized, computation-ready copy of this case: per unit, radians, 459 out-of-service filtered, source bus ids preserved, bus types 460 canonicalized. The original case is unchanged; the result carries no 461 retained source, so :meth:`write` serializes the per-unit model rather 462 than echoing it. Raises :class:`PowerIODataError` if the case can't be 463 normalized (no reference bus can be chosen, or a non-positive base MVA). 464 """ 465 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).
467 def to_networkx(self): 468 """Undirected networkx graph keyed by bus id. 469 470 In-service branches become edges carrying ``branch`` (index), ``r``, 471 ``x``, and ``b``. 472 """ 473 nx = _require("networkx", "graph") 474 g = nx.Graph() 475 g.add_nodes_from(bus["id"] for bus in self._inner.buses) 476 for k, br in enumerate(self._inner.branches): 477 if br["in_service"]: 478 g.add_edge( 479 br["from_id"], 480 br["to_id"], 481 branch=k, 482 r=br["r"], 483 x=br["x"], 484 b=br["b"], 485 ) 486 return g
Undirected networkx graph keyed by bus id.
In-service branches become edges carrying branch (index), r,
x, and b.
Output of Network.incidence().
Shapes, with n buses and m in-service branches:
A: signed incidence csr_matrix,(n, m).b: branch susceptances,(m,);b[k]is columnk.p_shift: phase-shift injection,(n,)(all zero unlessconvention="matpower").branch_of_col: column→branch index map,(m,);branch_of_col[k]andb[k]are co-indexed by incidence columnk.
Output of Network.ybus_parts(): g = Re(Y_bus), b = Im(Y_bus), each a real csr_matrix. Network.ybus() returns g + 1j*b.
Output of convert_file().
text is the converted file contents; warnings lists the fields the
target format could not represent (empty for a faithful conversion).
Output of parse_display_file() / parse_display_bytes().
kind names the display format. For v0.2.2, kind == "powerworld" and
data is a PwdDisplay.
Decoded PowerWorld .pwd display metadata.
One decoded PowerWorld display substation.
Copied dense NumPy table export of a parsed Network.
Branch arrays in source order.
Generator arrays in source order.
Nodal active and reactive demand arrays in bus order.
Nodal shunt conductance and susceptance arrays in bus order.
Base error raised by the powerio parser, converter, or matrix builders.
A case file is malformed or unparseable (missing/short rows, bad numbers, unbalanced brackets, format read failures).
A well-formed case cannot satisfy a requested operation (no generators, wrong reference bus count, an unknown bus reference, zero/non-finite branch impedance, a disconnected or singular network, a scenario batch shape mismatch, or a dimension/cost mismatch).
494def parse_file(path: Any, from_: Optional[str] = None) -> Network: 495 """Parse a case file from a path, inferring the format from the extension. 496 497 Read fidelity warnings are on ``Network.read_warnings`` (empty for readers 498 that don't report any; currently pandapower JSON, PyPSA CSV, and PSLF EPC 499 report them). 500 """ 501 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 pandapower JSON, PyPSA CSV, and PSLF EPC
report them).
504def parse_display_file(path: Any, from_: Optional[str] = None) -> DisplayData: 505 """Parse a display artifact such as a PowerWorld ``.pwd`` file.""" 506 return _wrap_display(_powerio.parse_display_file(str(path), from_))
Parse a display artifact such as a PowerWorld .pwd file.
509def parse_display_bytes(data: bytes, format: str) -> DisplayData: 510 """Parse display bytes in the named display format.""" 511 return _wrap_display(_powerio.parse_display_bytes(data, format))
Parse display bytes in the named display format.
514def parse_str(text: str, format: str = "matpower") -> Network: 515 """Parse a case from in-memory text in the named ``format``.""" 516 return Network(_powerio.parse_str(text, format))
Parse a case from in-memory text in the named format.
519def from_json(text: str) -> Network: 520 """Rebuild a case from JSON produced by :meth:`Network.to_json`.""" 521 return Network(_powerio.from_json(text))
Rebuild a case from JSON produced by Network.to_json().
524def convert_file( 525 path: Any, 526 to: str, 527 from_: Optional[str] = None, 528 missing_gen_cost: Optional[str] = None, 529 default_gen_cost: Optional[str] = None, 530 gen_cost_csv: Optional[Any] = None, 531) -> Conversion: 532 """Convert a case file to another format through the neutral hub. 533 534 ``to`` / ``from_`` are format names: ``matpower``, ``powermodels-json``, 535 ``egret-json``, ``pandapower-json``, ``psse``, ``powerworld``, ``pslf``, 536 ``goc3-json``, and ``surge-json`` (aliases ``m``, ``pm``, ``egret``, 537 ``pp``, ``raw``, ``aux``, ``epc``, ``goc3``, and ``surge``). The input format is 538 inferred from the file extension unless ``from_`` overrides it. GO Challenge 539 3 JSON is read only. PyPSA CSV folders are read with 540 ``from_="pypsa-csv"`` and written with 541 :meth:`Network.write_pypsa_csv_folder`. Returns a :class:`Conversion` with 542 the text and any fidelity warnings. 543 """ 544 text, warnings = _powerio.convert_file( 545 str(path), 546 to, 547 from_, 548 missing_gen_cost=missing_gen_cost, 549 default_gen_cost=default_gen_cost, 550 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 551 ) 552 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, pslf,
goc3-json, and surge-json (aliases m, pm, egret,
pp, raw, aux, epc, goc3, and surge). The input format is
inferred from the file extension unless from_ overrides it. GO Challenge
3 JSON is read only. 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.
555def convert_str( 556 text: str, 557 to: str, 558 format: str = "matpower", 559 missing_gen_cost: Optional[str] = None, 560 default_gen_cost: Optional[str] = None, 561 gen_cost_csv: Optional[Any] = None, 562) -> Conversion: 563 """Convert in-memory case ``text`` to another format through the neutral 564 hub, with no file staging. 565 566 ``to`` and ``format`` are format names as in :func:`convert_file`; 567 ``format`` names the input (default ``matpower``). Returns a 568 :class:`Conversion` with the converted text and any fidelity warnings. 569 """ 570 out, warnings = _powerio.convert_str( 571 text, 572 to, 573 format, 574 missing_gen_cost=missing_gen_cost, 575 default_gen_cost=default_gen_cost, 576 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 577 ) 578 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.
581def to_format( 582 network: Network, 583 to: str, 584 missing_gen_cost: Optional[str] = None, 585 default_gen_cost: Optional[str] = None, 586 gen_cost_csv: Optional[Any] = None, 587) -> Conversion: 588 """Serialize ``network`` to another format.""" 589 return network.to_format( 590 to, 591 missing_gen_cost=missing_gen_cost, 592 default_gen_cost=default_gen_cost, 593 gen_cost_csv=gen_cost_csv, 594 )
Serialize network to another format.
597def to_matpower(network: Network) -> str: 598 """Serialize ``network`` to MATPOWER ``.m`` text.""" 599 return network.to_matpower()
Serialize network to MATPOWER .m text.
602def to_json(network: Network) -> str: 603 """Serialize ``network`` to the JSON transport.""" 604 return network.to_json()
Serialize network to the JSON transport.
607def to_dense(network: Network) -> DenseNetwork: 608 """Return copied dense NumPy tables for ``network``.""" 609 return network.to_dense()
Return copied dense NumPy tables for network.
696class Package: 697 """A parsed ``.pio.json`` package handle. 698 699 Parses the envelope once; every accessor reuses the handle instead of 700 re-reading the JSON text. 701 """ 702 703 def __init__(self, inner: "_powerio._Package"): 704 self._inner = inner 705 706 @classmethod 707 def from_file( 708 cls, path: Any, from_: Optional[str] = None, scenario: int = 0 709 ) -> "Package": 710 """Build a package from a case file or folder.""" 711 return cls(_powerio._Package.from_file(str(path), from_, scenario)) 712 713 @classmethod 714 def from_str(cls, text: str, from_: Optional[str] = None) -> "Package": 715 """Build a package from in-memory case text.""" 716 return cls(_powerio._Package.from_str(text, from_)) 717 718 @classmethod 719 def from_json(cls, text: str) -> "Package": 720 """Parse ``.pio.json`` envelope text.""" 721 return cls(_powerio._Package.from_json(text)) 722 723 @classmethod 724 def from_balanced( 725 cls, network: Network, include_solver_metadata: bool = False 726 ) -> "Package": 727 """Wrap a balanced :class:`Network` in a package.""" 728 return cls( 729 _powerio._Package.from_balanced(network._inner, include_solver_metadata) 730 ) 731 732 @classmethod 733 def from_multiconductor(cls, network: "dist.MulticonductorNetwork") -> "Package": 734 """Wrap a multiconductor network in a package.""" 735 return cls(_powerio._Package.from_multiconductor(network._inner)) 736 737 @property 738 def model_kind(self) -> str: 739 """``"balanced"`` or ``"multiconductor"``.""" 740 return self._inner.model_kind() 741 742 def to_json(self) -> str: 743 """Serialize to pretty ``.pio.json``.""" 744 return self._inner.to_json() 745 746 def as_balanced(self) -> Network: 747 """Return the balanced payload as a :class:`Network`.""" 748 return Network(self._inner.as_balanced()) 749 750 def as_multiconductor(self) -> "dist.MulticonductorNetwork": 751 """Return the multiconductor payload.""" 752 return dist.MulticonductorNetwork(self._inner.as_multiconductor()) 753 754 def operating_points(self) -> Any: 755 """The operating point series as Python data, or ``None``. 756 757 GOC3 packages populate this from the source time series. Each point is 758 a set of field updates over the package's static payload. 759 """ 760 return _json.loads(self._inner.operating_points_json()) 761 762 def materialize_operating_point(self, index: int) -> "Package": 763 """Materialize one operating point into a new static package.""" 764 return Package(self._inner.materialize_operating_point(index)) 765 766 def validate(self) -> None: 767 """Run the package semantic validation profile in place.""" 768 self._inner.validate() 769 770 def validation(self) -> Any: 771 """The validation summary as Python data.""" 772 return _json.loads(self._inner.validation_json()) 773 774 def diagnostics(self) -> Any: 775 """The structured diagnostics as a list of Python dicts.""" 776 return _json.loads(self._inner.diagnostics_json()) 777 778 def multiconductor_to_balanced_preflight(self, base_mva: float = 100.0) -> Any: 779 """Readiness report for multiconductor to balanced lowering.""" 780 return _json.loads( 781 self._inner.multiconductor_to_balanced_preflight_json(base_mva) 782 ) 783 784 def lower_multiconductor_to_balanced(self, base_mva: float = 100.0) -> "Package": 785 """Lower a multiconductor package to a new balanced package.""" 786 return Package(self._inner.lower_multiconductor_to_balanced(base_mva)) 787 788 def __repr__(self) -> str: 789 return repr(self._inner)
A parsed .pio.json package handle.
Parses the envelope once; every accessor reuses the handle instead of re-reading the JSON text.
706 @classmethod 707 def from_file( 708 cls, path: Any, from_: Optional[str] = None, scenario: int = 0 709 ) -> "Package": 710 """Build a package from a case file or folder.""" 711 return cls(_powerio._Package.from_file(str(path), from_, scenario))
Build a package from a case file or folder.
713 @classmethod 714 def from_str(cls, text: str, from_: Optional[str] = None) -> "Package": 715 """Build a package from in-memory case text.""" 716 return cls(_powerio._Package.from_str(text, from_))
Build a package from in-memory case text.
718 @classmethod 719 def from_json(cls, text: str) -> "Package": 720 """Parse ``.pio.json`` envelope text.""" 721 return cls(_powerio._Package.from_json(text))
Parse .pio.json envelope text.
723 @classmethod 724 def from_balanced( 725 cls, network: Network, include_solver_metadata: bool = False 726 ) -> "Package": 727 """Wrap a balanced :class:`Network` in a package.""" 728 return cls( 729 _powerio._Package.from_balanced(network._inner, include_solver_metadata) 730 )
Wrap a balanced Network in a package.
732 @classmethod 733 def from_multiconductor(cls, network: "dist.MulticonductorNetwork") -> "Package": 734 """Wrap a multiconductor network in a package.""" 735 return cls(_powerio._Package.from_multiconductor(network._inner))
Wrap a multiconductor network in a package.
737 @property 738 def model_kind(self) -> str: 739 """``"balanced"`` or ``"multiconductor"``.""" 740 return self._inner.model_kind()
"balanced" or "multiconductor".
742 def to_json(self) -> str: 743 """Serialize to pretty ``.pio.json``.""" 744 return self._inner.to_json()
Serialize to pretty .pio.json.
746 def as_balanced(self) -> Network: 747 """Return the balanced payload as a :class:`Network`.""" 748 return Network(self._inner.as_balanced())
Return the balanced payload as a Network.
750 def as_multiconductor(self) -> "dist.MulticonductorNetwork": 751 """Return the multiconductor payload.""" 752 return dist.MulticonductorNetwork(self._inner.as_multiconductor())
Return the multiconductor payload.
754 def operating_points(self) -> Any: 755 """The operating point series as Python data, or ``None``. 756 757 GOC3 packages populate this from the source time series. Each point is 758 a set of field updates over the package's static payload. 759 """ 760 return _json.loads(self._inner.operating_points_json())
The operating point series as Python data, or None.
GOC3 packages populate this from the source time series. Each point is a set of field updates over the package's static payload.
762 def materialize_operating_point(self, index: int) -> "Package": 763 """Materialize one operating point into a new static package.""" 764 return Package(self._inner.materialize_operating_point(index))
Materialize one operating point into a new static package.
766 def validate(self) -> None: 767 """Run the package semantic validation profile in place.""" 768 self._inner.validate()
Run the package semantic validation profile in place.
770 def validation(self) -> Any: 771 """The validation summary as Python data.""" 772 return _json.loads(self._inner.validation_json())
The validation summary as Python data.
774 def diagnostics(self) -> Any: 775 """The structured diagnostics as a list of Python dicts.""" 776 return _json.loads(self._inner.diagnostics_json())
The structured diagnostics as a list of Python dicts.
778 def multiconductor_to_balanced_preflight(self, base_mva: float = 100.0) -> Any: 779 """Readiness report for multiconductor to balanced lowering.""" 780 return _json.loads( 781 self._inner.multiconductor_to_balanced_preflight_json(base_mva) 782 )
Readiness report for multiconductor to balanced lowering.
612def write_gridfm_batch( 613 networks: "list[Network]", 614 out_dir: Any, 615 base_scenario: int = 0, 616 include_y_bus: bool = True, 617 include_taps: bool = True, 618 include_shifts: bool = True, 619 missing_gen_cost: Optional[str] = None, 620 default_gen_cost: Optional[str] = None, 621 gen_cost_csv: Optional[Any] = None, 622) -> dict: 623 """Write several networks as one gridfm-datakit dataset, row-stacked and 624 keyed by the ``scenario`` column. 625 626 Each network is one snapshot; the k-th is stamped ``base_scenario + k``. The 627 networks must share a base element set: the same bus/branch/gen counts and 628 bus id order (otherwise :class:`PowerIODataError` is raised). Load, dispatch, 629 branch status, and costs may vary per scenario. Returns the same dict as 630 :meth:`Network.write_gridfm`. Published wheels include the native writer; 631 custom source builds without the Rust ``gridfm`` feature raise 632 ``ImportError``. 633 """ 634 _require_gridfm() 635 inners = [c._inner for c in networks] 636 return _powerio.write_gridfm_batch( 637 inners, 638 str(out_dir), 639 base_scenario, 640 include_y_bus, 641 include_taps, 642 include_shifts, 643 missing_gen_cost=missing_gen_cost, 644 default_gen_cost=default_gen_cost, 645 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 646 )
Write several networks as one gridfm-datakit dataset, row-stacked and
keyed by the scenario column.
Each network is one snapshot; the k-th is stamped base_scenario + k. The
networks 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.
649def read_gridfm(dir: Any, scenario: int = 0) -> GridfmRead: 650 """Read one scenario of a gridfm-datakit Parquet dataset back into a case. 651 652 The inverse of :meth:`Network.write_gridfm`. ``dir`` is resolved leniently: 653 the ``raw/`` directory holding the parquet files, a ``<case>/`` directory with 654 a ``raw/`` child, or a parent directory with one ``*/raw/`` child all work. 655 ``scenario`` selects one snapshot from a batch (``0``, the base case, by 656 default). Returns a :class:`GridfmRead` ``(network, scenario, warnings)``. 657 658 The read is lossy but recovers everything a power flow needs: bus types, 659 voltages and limits, nodal load and shunt totals, generator dispatch and 660 bounds, branch ``r/x/b/tap/shift/rate_a``/angle limits, and ``baseMVA``, 661 enough to write a runnable case. It cannot recover original bus ids, 662 per-element load/shunt granularity, piecewise/cubic costs, or HVDC/storage; 663 what it can't recover is listed in ``warnings``. Published wheels include the 664 native reader; custom source builds without the Rust ``gridfm`` feature raise 665 ``ImportError``. 666 """ 667 _require_gridfm() 668 inner, scen, warnings = _powerio.read_gridfm(str(dir), scenario) 669 return GridfmRead(Network(inner), 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.
672def read_gridfm_scenarios(dir: Any) -> "list[GridfmRead]": 673 """Read every scenario of a gridfm dataset, one :class:`GridfmRead` per 674 scenario id (ascending) over the shared topology, the read side of 675 :func:`write_gridfm_batch`. 676 677 Each scenario is rebuilt independently, so two scenarios may differ in branch 678 status, bus types, and reference bus. See :func:`read_gridfm` for the lenient 679 directory resolution and the fidelity behavior. 680 """ 681 _require_gridfm() 682 return [ 683 GridfmRead(Network(inner), scen, warnings) 684 for inner, scen, warnings in _powerio.read_gridfm_scenarios(str(dir)) 685 ]
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 behavior.
688def read_pypsa_csv_folder(path: Any) -> Network: 689 """Read a PyPSA CSV folder into a :class:`Network`.""" 690 return Network(_powerio.read_pypsa_csv_folder(str(path)))
Read a PyPSA CSV folder into a Network.
Output of read_gridfm() / read_gridfm_scenarios().
network is the reconstructed Network; scenario is the scenario
id these rows came from; warnings lists what the gridfm schema could not
round-trip (synthesized bus ids, folded per-bus load/shunt, dropped HVDC/storage,
piecewise costs). The read is lossy but recovers everything a power flow needs.