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)))
class Network:
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.

def to_matpower(self) -> str:
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.

def to_json(self) -> str:
266    def to_json(self) -> str:
267        """Serialize to the JSON transport."""
268        return self._inner.to_json()

Serialize to the JSON transport.

def to_format(self, to: str) -> Conversion:
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.

def to_dense(self) -> DenseNetwork:
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.

def bprime(self, scheme: Literal['bx', 'xb'] = Ellipsis) -> Any:
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".

def bdoubleprime(self, scheme: Literal['bx', 'xb'] = Ellipsis) -> Any:
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).

def lacpf( self, include_taps: bool = Ellipsis, include_shifts: bool = Ellipsis) -> Any:
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]].

def adjacency(self) -> Any:
356    def adjacency(self):
357        """0/1 bus adjacency matrix."""
358        return _to_csr(self._inner.adjacency())

0/1 bus adjacency matrix.

def ybus_parts( self, include_taps: bool = Ellipsis, include_shifts: bool = Ellipsis) -> YbusParts:
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.

def ybus( self, include_taps: bool = Ellipsis, include_shifts: bool = Ellipsis) -> Any:
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.

def ptdf(self, convention: Literal['paper', 'matpower'] = Ellipsis) -> Any:
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".

def lodf(self, convention: Literal['paper', 'matpower'] = Ellipsis) -> Any:
375    def lodf(self, convention: str = "paper"):
376        """DC LODF (mĂ—m)."""
377        return _to_csr(self._inner.lodf(convention))

DC LODF (mĂ—m).

def weighted_laplacian(self, convention: Literal['paper', 'matpower'] = Ellipsis) -> Any:
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áµ€.

def incidence( self, convention: Literal['paper', 'matpower'] = Ellipsis) -> Incidence:
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.

def write_gridfm( self, out_dir: Any, scenario: int = Ellipsis, include_y_bus: bool = Ellipsis, include_taps: bool = Ellipsis, include_shifts: bool = Ellipsis) -> Dict[str, Any]:
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().

def write_pypsa_csv_folder(self, out_dir: Any) -> Dict[str, Any]:
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.

def to_normalized(self) -> Network:
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).

def to_networkx(self) -> Any:
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.

Case = <class 'Network'>
class Incidence(builtins.tuple):

Incidence(A, b, p_shift, branch_of_col)

Incidence(A: Any, b: Any, p_shift: Any, branch_of_col: Any)

Create new instance of Incidence(A, b, p_shift, branch_of_col)

A: Any

Alias for field number 0

b: Any

Alias for field number 1

p_shift: Any

Alias for field number 2

branch_of_col: Any

Alias for field number 3

class YbusParts(builtins.tuple):

YbusParts(g, b)

YbusParts(g: Any, b: Any)

Create new instance of YbusParts(g, b)

g: Any

Alias for field number 0

b: Any

Alias for field number 1

class Conversion(builtins.tuple):

Conversion(text, warnings)

Conversion(text: str, warnings: List[str])

Create new instance of Conversion(text, warnings)

text: str

Alias for field number 0

warnings: List[str]

Alias for field number 1

class DisplayData(builtins.tuple):

DisplayData(kind, data)

DisplayData(kind: Literal['powerworld'], data: PwdDisplay)

Create new instance of DisplayData(kind, data)

kind: Literal['powerworld']

Alias for field number 0

data: PwdDisplay

Alias for field number 1

class PwdDisplay(builtins.tuple):

PwdDisplay(canvas_width, canvas_height, stamp, substations)

PwdDisplay( canvas_width: int, canvas_height: int, stamp: int, substations: List[PwdSubstation])

Create new instance of PwdDisplay(canvas_width, canvas_height, stamp, substations)

canvas_width: int

Alias for field number 0

canvas_height: int

Alias for field number 1

stamp: int

Alias for field number 2

substations: List[PwdSubstation]

Alias for field number 3

class PwdSubstation(builtins.tuple):

PwdSubstation(number, name, x, y)

PwdSubstation(number: int, name: str, x: float, y: float)

Create new instance of PwdSubstation(number, name, x, y)

number: int

Alias for field number 0

name: str

Alias for field number 1

x: float

Alias for field number 2

y: float

Alias for field number 3

class DenseNetwork(builtins.tuple):

DenseNetwork(n, m, ng, base_mva, bus_ids, branch, gen, demand, shunt, reference_bus, n_components, is_radial)

DenseNetwork( n: int, m: int, ng: int, base_mva: float, bus_ids: Any, branch: DenseBranch, gen: DenseGen, demand: DenseDemand, shunt: DenseShunt, reference_bus: Optional[int], n_components: int, is_radial: bool)

Create new instance of DenseNetwork(n, m, ng, base_mva, bus_ids, branch, gen, demand, shunt, reference_bus, n_components, is_radial)

n: int

Alias for field number 0

m: int

Alias for field number 1

ng: int

Alias for field number 2

base_mva: float

Alias for field number 3

bus_ids: Any

Alias for field number 4

branch: DenseBranch

Alias for field number 5

gen: DenseGen

Alias for field number 6

demand: DenseDemand

Alias for field number 7

shunt: DenseShunt

Alias for field number 8

reference_bus: Optional[int]

Alias for field number 9

n_components: int

Alias for field number 10

is_radial: bool

Alias for field number 11

class DenseBranch(builtins.tuple):

DenseBranch(from_id, to_id, r, x, b, tap, shift, in_service)

DenseBranch( from_id: Any, to_id: Any, r: Any, x: Any, b: Any, tap: Any, shift: Any, in_service: Any)

Create new instance of DenseBranch(from_id, to_id, r, x, b, tap, shift, in_service)

from_id: Any

Alias for field number 0

to_id: Any

Alias for field number 1

r: Any

Alias for field number 2

x: Any

Alias for field number 3

b: Any

Alias for field number 4

tap: Any

Alias for field number 5

shift: Any

Alias for field number 6

in_service: Any

Alias for field number 7

class DenseGen(builtins.tuple):

DenseGen(bus, pg, pmax, pmin, in_service)

DenseGen(bus: Any, pg: Any, pmax: Any, pmin: Any, in_service: Any)

Create new instance of DenseGen(bus, pg, pmax, pmin, in_service)

bus: Any

Alias for field number 0

pg: Any

Alias for field number 1

pmax: Any

Alias for field number 2

pmin: Any

Alias for field number 3

in_service: Any

Alias for field number 4

class DenseDemand(builtins.tuple):

DenseDemand(pd, qd)

DenseDemand(pd: Any, qd: Any)

Create new instance of DenseDemand(pd, qd)

pd: Any

Alias for field number 0

qd: Any

Alias for field number 1

class DenseShunt(builtins.tuple):

DenseShunt(gs, bs)

DenseShunt(gs: Any, bs: Any)

Create new instance of DenseShunt(gs, bs)

gs: Any

Alias for field number 0

bs: Any

Alias for field number 1

class PowerIOError(builtins.Exception):

Base error from the powerio parser, converter, or matrix builders.

class PowerIOParseError(powerio.PowerIOError):

A case file is malformed or unparseable.

class PowerIODataError(powerio.PowerIOError):

A well-formed case cannot satisfy a requested operation.

def parse_file(path: Any, from_: Optional[str] = Ellipsis) -> Network:
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).

def parse_display_file(path: Any, from_: Optional[str] = Ellipsis) -> DisplayData:
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.

def parse_display_bytes(data: bytes, format: str) -> DisplayData:
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.

def parse_str(text: str, format: str = Ellipsis) -> Network:
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.

def from_json(text: str) -> Network:
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().

def convert_file( path: Any, to: str, from_: Optional[str] = Ellipsis) -> Conversion:
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.

def convert_str(text: str, to: str, format: str = Ellipsis) -> Conversion:
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.

def to_format(case: Network, to: str) -> Conversion:
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.

def to_matpower(case: Network) -> str:
521def to_matpower(case: Network) -> str:
522    """Serialize ``case`` to MATPOWER ``.m`` text."""
523    return case.to_matpower()

Serialize case to MATPOWER .m text.

def to_json(case: Network) -> str:
526def to_json(case: Network) -> str:
527    """Serialize ``case`` to the JSON transport."""
528    return case.to_json()

Serialize case to the JSON transport.

def to_dense(case: Network) -> DenseNetwork:
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.

def write_gridfm_batch( cases: List[Network], out_dir: Any, base_scenario: int = Ellipsis, include_y_bus: bool = Ellipsis, include_taps: bool = Ellipsis, include_shifts: bool = Ellipsis) -> Dict[str, Any]:
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.

def read_gridfm(dir: Any, scenario: int = Ellipsis) -> GridfmRead:
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.

def read_gridfm_scenarios(dir: Any) -> List[GridfmRead]:
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.

def read_pypsa_csv_folder(path: Any) -> Network:
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.

class GridfmRead(builtins.tuple):

GridfmRead(network, scenario, warnings)

GridfmRead(network: ForwardRef('Network'), scenario: int, warnings: List[str])

Create new instance of GridfmRead(network, scenario, warnings)

network: Network

Alias for field number 0

scenario: int

Alias for field number 1

warnings: List[str]

Alias for field number 2

__version__: str = '0.2.3'