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

Network(inner: PyNetwork)
249    def __init__(self, inner: "_powerio.PyNetwork"):
250        self._inner = inner
def to_matpower(self) -> str:
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.

def to_json(self) -> str:
277    def to_json(self) -> str:
278        """Serialize to the JSON transport."""
279        return self._inner.to_json()

Serialize to the JSON transport.

def to_format( self, to: str, missing_gen_cost: Optional[str] = None, default_gen_cost: Optional[str] = None, gen_cost_csv: Optional[Any] = None) -> Conversion:
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.

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

def bprime(self, scheme: str = 'bx'):
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".

def bdoubleprime(self, scheme: str = 'bx'):
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).

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

def adjacency(self):
378    def adjacency(self):
379        """0/1 bus adjacency matrix."""
380        return _to_csr(self._inner.adjacency())

0/1 bus adjacency matrix.

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

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

def ptdf(self, convention: str = 'paper'):
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".

def lodf(self, convention: str = 'paper'):
397    def lodf(self, convention: str = "paper"):
398        """DC LODF (m×m)."""
399        return _to_csr(self._inner.lodf(convention))

DC LODF (m×m).

def weighted_laplacian(self, convention: str = 'paper'):
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áµ€.

def incidence(self, convention: str = 'paper') -> Incidence:
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.

def write_gridfm( self, out_dir: Any, scenario: int = 0, include_y_bus: bool = True, include_taps: bool = True, include_shifts: bool = True, missing_gen_cost: Optional[str] = None, default_gen_cost: Optional[str] = None, gen_cost_csv: Optional[Any] = None) -> dict:
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().

def write_pypsa_csv_folder(self, out_dir: Any) -> dict:
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.

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

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

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

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 column k.
  • p_shift: phase-shift injection, (n,) (all zero unless convention="matpower").
  • branch_of_col: column→branch index map, (m,); branch_of_col[k] and b[k] are co-indexed by incidence column k.
Incidence(A, b, p_shift, branch_of_col)

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

A

Alias for field number 0

b

Alias for field number 1

p_shift

Alias for field number 2

branch_of_col

Alias for field number 3

class YbusParts(builtins.tuple):

Output of Network.ybus_parts(): g = Re(Y_bus), b = Im(Y_bus), each a real csr_matrix. Network.ybus() returns g + 1j*b.

YbusParts(g, b)

Create new instance of YbusParts(g, b)

g

Alias for field number 0

b

Alias for field number 1

class Conversion(builtins.tuple):

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).

Conversion(text, warnings)

Create new instance of Conversion(text, warnings)

text

Alias for field number 0

warnings

Alias for field number 1

class DisplayData(builtins.tuple):

Output of parse_display_file() / parse_display_bytes().

kind names the display format. For v0.2.2, kind == "powerworld" and data is a PwdDisplay.

DisplayData(kind, data)

Create new instance of DisplayData(kind, data)

kind

Alias for field number 0

data

Alias for field number 1

class PwdDisplay(builtins.tuple):

Decoded PowerWorld .pwd display metadata.

PwdDisplay(canvas_width, canvas_height, stamp, substations)

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

canvas_width

Alias for field number 0

canvas_height

Alias for field number 1

stamp

Alias for field number 2

substations

Alias for field number 3

class PwdSubstation(builtins.tuple):

One decoded PowerWorld display substation.

PwdSubstation(number, name, x, y)

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

number

Alias for field number 0

name

Alias for field number 1

x

Alias for field number 2

y

Alias for field number 3

class DenseNetwork(builtins.tuple):

Copied dense NumPy table export of a parsed Network.

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

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

n

Alias for field number 0

m

Alias for field number 1

ng

Alias for field number 2

base_mva

Alias for field number 3

bus_ids

Alias for field number 4

branch

Alias for field number 5

gen

Alias for field number 6

demand

Alias for field number 7

shunt

Alias for field number 8

reference_bus

Alias for field number 9

n_components

Alias for field number 10

is_radial

Alias for field number 11

class DenseBranch(builtins.tuple):

Branch arrays in source order.

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

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

from_id

Alias for field number 0

to_id

Alias for field number 1

r

Alias for field number 2

x

Alias for field number 3

b

Alias for field number 4

tap

Alias for field number 5

shift

Alias for field number 6

in_service

Alias for field number 7

class DenseGen(builtins.tuple):

Generator arrays in source order.

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

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

bus

Alias for field number 0

pg

Alias for field number 1

pmax

Alias for field number 2

pmin

Alias for field number 3

in_service

Alias for field number 4

class DenseDemand(builtins.tuple):

Nodal active and reactive demand arrays in bus order.

DenseDemand(pd, qd)

Create new instance of DenseDemand(pd, qd)

pd

Alias for field number 0

qd

Alias for field number 1

class DenseShunt(builtins.tuple):

Nodal shunt conductance and susceptance arrays in bus order.

DenseShunt(gs, bs)

Create new instance of DenseShunt(gs, bs)

gs

Alias for field number 0

bs

Alias for field number 1

class PowerIOError(builtins.Exception):

Base error raised by the powerio parser, converter, or matrix builders.

class PowerIOParseError(powerio.PowerIOError):

A case file is malformed or unparseable (missing/short rows, bad numbers, unbalanced brackets, format read failures).

class PowerIODataError(powerio.PowerIOError):

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).

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

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

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

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

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

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

def convert_str( text: str, to: str, format: str = 'matpower', missing_gen_cost: Optional[str] = None, default_gen_cost: Optional[str] = None, gen_cost_csv: Optional[Any] = None) -> Conversion:
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.

def to_format( network: Network, to: str, missing_gen_cost: Optional[str] = None, default_gen_cost: Optional[str] = None, gen_cost_csv: Optional[Any] = None) -> Conversion:
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.

def to_matpower(network: Network) -> str:
597def to_matpower(network: Network) -> str:
598    """Serialize ``network`` to MATPOWER ``.m`` text."""
599    return network.to_matpower()

Serialize network to MATPOWER .m text.

def to_json(network: Network) -> str:
602def to_json(network: Network) -> str:
603    """Serialize ``network`` to the JSON transport."""
604    return network.to_json()

Serialize network to the JSON transport.

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

class Package:
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.

Package(inner: _Package)
703    def __init__(self, inner: "_powerio._Package"):
704        self._inner = inner
@classmethod
def from_file( cls, path: Any, from_: Optional[str] = None, scenario: int = 0) -> Package:
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.

@classmethod
def from_str(cls, text: str, from_: Optional[str] = None) -> Package:
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.

@classmethod
def from_json(cls, text: str) -> Package:
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.

@classmethod
def from_balanced( cls, network: Network, include_solver_metadata: bool = False) -> Package:
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.

@classmethod
def from_multiconductor(cls, network: powerio.dist.DistNetwork) -> 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.

model_kind: str
737    @property
738    def model_kind(self) -> str:
739        """``"balanced"`` or ``"multiconductor"``."""
740        return self._inner.model_kind()

"balanced" or "multiconductor".

def to_json(self) -> str:
742    def to_json(self) -> str:
743        """Serialize to pretty ``.pio.json``."""
744        return self._inner.to_json()

Serialize to pretty .pio.json.

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

def as_multiconductor(self) -> powerio.dist.DistNetwork:
750    def as_multiconductor(self) -> "dist.MulticonductorNetwork":
751        """Return the multiconductor payload."""
752        return dist.MulticonductorNetwork(self._inner.as_multiconductor())

Return the multiconductor payload.

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

def materialize_operating_point(self, index: int) -> Package:
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.

def validate(self) -> None:
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.

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

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

def multiconductor_to_balanced_preflight(self, base_mva: float = 100.0) -> Any:
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.

def lower_multiconductor_to_balanced(self, base_mva: float = 100.0) -> Package:
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))

Lower a multiconductor package to a new balanced package.

def write_gridfm_batch( networks: list[Network], out_dir: Any, base_scenario: int = 0, include_y_bus: bool = True, include_taps: bool = True, include_shifts: bool = True, missing_gen_cost: Optional[str] = None, default_gen_cost: Optional[str] = None, gen_cost_csv: Optional[Any] = None) -> dict:
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.

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

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

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

class GridfmRead(builtins.tuple):

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.

GridfmRead(network, scenario, warnings)

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

network

Alias for field number 0

scenario

Alias for field number 1

warnings

Alias for field number 2

__version__ = '0.5.1'