powerio
Parse, convert, and project power system data.
Readers produce a format neutral network model. Writers return retained source bytes where supported or report fields that a target format cannot represent. Packages, sparse matrices, graphs, and problem instances use the same parsed data::
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, MATPOWER Bp
Y = net.ybus() # complex csr, G + jB
G = net.to_networkx() # networkx.Graph keyed by bus id
PyPSA CSV folders carry static network topology. NetCDF and HDF5 time series are tracked in https://github.com/eigenergy/powerio/issues/107.
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 the base parse, write, and conversion paths require no
third party Python package. Matrix methods require SciPy and NumPy. Graph
methods require NetworkX. Install them with powerio[matrix],
powerio[graph], or powerio[all]. Missing extras raise ImportError.
1"""Parse, convert, and project power system data. 2 3Readers produce a format neutral network model. Writers return retained source 4bytes where supported or report fields that a target format cannot represent. 5Packages, sparse matrices, graphs, and problem instances use the same parsed 6data:: 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 pkg = pio.Package.from_file("goc3_case.json", from_="goc3-json") 17 points = pkg.operating_points() 18 19 B = net.bprime() # scipy.sparse, MATPOWER Bp 20 Y = net.ybus() # complex csr, G + jB 21 G = net.to_networkx() # networkx.Graph keyed by bus id 22 23PyPSA CSV folders carry static network topology. NetCDF and HDF5 time series 24are tracked in https://github.com/eigenergy/powerio/issues/107. 25 26GO Challenge 3 JSON is read as a static balanced network using the first 27interval. When it is parsed as a ``.pio.json`` package, the full source time 28series is exposed as replayable operating points. 29 30``import powerio`` and the base parse, write, and conversion paths require no 31third party Python package. Matrix methods require SciPy and NumPy. Graph 32methods require NetworkX. Install them with ``powerio[matrix]``, 33``powerio[graph]``, or ``powerio[all]``. Missing extras raise ``ImportError``. 34""" 35 36from __future__ import annotations 37 38import importlib 39import json as _json 40from collections import namedtuple 41from typing import Any, Optional 42 43from . import _powerio 44from ._powerio import PowerIODataError, PowerIOError, PowerIOParseError, __version__ 45 46__all__ = [ 47 "BalancedNetwork", 48 "Conversion", 49 "DenseBranch", 50 "DenseDemand", 51 "DenseGen", 52 "DenseNetwork", 53 "DenseShunt", 54 "DisplayData", 55 "GridfmRead", 56 "Incidence", 57 "Package", 58 "PowerIODataError", 59 "PowerIOError", 60 "PowerIOParseError", 61 "PwdDisplay", 62 "PwdSubstation", 63 "YbusParts", 64 "__version__", 65 "convert_file", 66 "convert_str", 67 "dist", 68 "from_json", 69 "from_ppc", 70 "parse_bytes", 71 "parse_display_bytes", 72 "parse_display_file", 73 "parse_file", 74 "parse_geo", 75 "parse_scopf", 76 "parse_str", 77 "read_gridfm", 78 "read_gridfm_scenarios", 79 "read_pypsa_csv_folder", 80 "to_dense", 81 "to_format", 82 "to_json", 83 "to_matpower", 84 "write_gridfm_batch", 85] 86 87Conversion = namedtuple("Conversion", ["text", "warnings"]) 88Conversion.__doc__ = """Output of :func:`convert_file`. 89 90``text`` is the converted file contents; ``warnings`` lists the fields the 91target format could not represent (empty for a faithful conversion). 92""" 93 94GridfmRead = namedtuple("GridfmRead", ["network", "scenario", "warnings"]) 95GridfmRead.__doc__ = """Output of :func:`read_gridfm` / :func:`read_gridfm_scenarios`. 96 97``network`` is the reconstructed :class:`BalancedNetwork`; ``scenario`` is the source 98scenario ID; ``warnings`` lists fields the GridFM schema cannot retain, 99including source bus IDs, per element load and shunt rows, HVDC, storage, and 100piecewise costs. 101""" 102 103DisplayData = namedtuple("DisplayData", ["kind", "data"]) 104DisplayData.__doc__ = """Output of :func:`parse_display_file` / :func:`parse_display_bytes`. 105 106``kind`` names the display format. For PowerWorld PWD data, 107``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:`BalancedNetwork.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:`BalancedNetwork.ybus_parts`: ``g`` = Re(Y_bus), ``b`` = Im(Y_bus), " 134 "each a real csr_matrix. ``BalancedNetwork.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:`BalancedNetwork`.""" 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 BalancedNetwork: 231 """A parsed balanced power network. 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._BalancedNetwork"): 249 self._inner = inner 250 251 def __dir__(self): 252 # The data attributes arrive through __getattr__, so name them here or 253 # they stay invisible to tab completion. 254 return sorted(set(super().__dir__()) | set(dir(self._inner))) 255 256 def __getattr__(self, name: str): 257 # Reached only when normal lookup misses, so the matrix methods below 258 # win. Guard underscore names so a lookup before _inner exists raises 259 # AttributeError instead of recursing forever. 260 if name.startswith("_"): 261 raise AttributeError( 262 f"{type(self).__name__!r} object has no attribute {name!r}" 263 ) 264 return getattr(self._inner, name) 265 266 def __repr__(self) -> str: 267 # The inner handle's __repr__ already renders the public ``BalancedNetwork(...)`` 268 # form, so this is a straight delegate. 269 return repr(self._inner) 270 271 # --- canonical format and table exports ----------------------------- 272 273 def to_matpower(self) -> str: 274 """Serialize to MATPOWER ``.m`` text. 275 276 A case parsed from MATPOWER keeps its original source, so this returns a 277 byte-exact echo. Derived cases serialize from the format neutral model. 278 """ 279 return self._inner.to_matpower() 280 281 def to_json(self) -> str: 282 """Serialize to the JSON transport.""" 283 return self._inner.to_json() 284 285 def geo_layer(self) -> dict[str, Any]: 286 """This case's coordinates as a canonical GeoJSON FeatureCollection. 287 288 Raises :class:`PowerIOError` when the case carries none. 289 """ 290 return _json.loads(self._inner.geo_layer_json()) 291 292 def apply_geo_layer( 293 self, text: str, name_hint: Optional[str] = None 294 ) -> tuple["BalancedNetwork", dict[str, Any]]: 295 """Apply a geographic sidecar and return ``(placed, report)``. 296 297 ``text`` is any form :func:`parse_geo` accepts; this case is 298 unchanged. The report carries ``matched_buses``, ``matched_branches``, 299 ``unmatched_features``, ``unlocated_buses``, ``unlocated_branches``, 300 and ``notes``. The two unlocated counts cover the whole case when the 301 pass ends, so a layer that matched nothing reads apart from a case 302 that needed nothing. The placed copy drops the retained source text, 303 so a same-format write re-serializes. 304 """ 305 inner, report = self._inner.apply_geo_layer(text, name_hint) 306 return BalancedNetwork(inner), report 307 308 def acopf_instance(self, units: Optional[str] = None) -> dict[str, Any]: 309 """The matrix free AC OPF problem instance as Python data. 310 311 Dense 0-based indices; ``units`` is ``"perunit"`` (default) or 312 ``"native"``. 313 """ 314 return _json.loads(self._inner.acopf_json(units)) 315 316 def to_format( 317 self, 318 to: str, 319 missing_gen_cost: Optional[str] = None, 320 default_gen_cost: Optional[str] = None, 321 gen_cost_csv: Optional[Any] = None, 322 ) -> Conversion: 323 """Serialize this parsed case to another format. 324 325 ``to`` is one of the format names accepted by :func:`convert_file`. 326 Returns a :class:`Conversion` with output text and fidelity warnings. 327 """ 328 text, warnings = self._inner.to_format( 329 to, 330 missing_gen_cost=missing_gen_cost, 331 default_gen_cost=default_gen_cost, 332 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 333 ) 334 return Conversion(text, warnings) 335 336 def write_file( 337 self, 338 path: Any, 339 to: str, 340 missing_gen_cost: Optional[str] = None, 341 default_gen_cost: Optional[str] = None, 342 gen_cost_csv: Optional[Any] = None, 343 ) -> list[str]: 344 r"""Serialize this case to ``to`` and write it to ``path`` byte exact. 345 346 Returns the fidelity warnings. Prefer this over writing 347 :meth:`to_format` text through ``open(path, "w")``: Python's text mode 348 translates newlines on Windows, so a case whose retained source has 349 CRLF line endings comes out with doubled carriage returns 350 (``\r\r\n``), which PSS/E family tools reject. 351 """ 352 return self._inner.write_file( 353 str(path), 354 to, 355 missing_gen_cost=missing_gen_cost, 356 default_gen_cost=default_gen_cost, 357 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 358 ) 359 360 def to_dense(self) -> DenseNetwork: 361 """Dense NumPy arrays for solver and adapter code. 362 363 This allocates new arrays, preserves bus and branch source order, and 364 sums loads and shunts per bus to match the Rust indexed analysis view. 365 366 That view is the star-lowered one, so a case with an in-service 367 3-winding transformer reports the star bus and its three branches here 368 even though :attr:`buses` and :attr:`branches` mirror the case file and 369 do not. ``reference_bus``, ``n_components`` and ``is_radial`` are 370 computed over the same lowered space, so all of them agree. 371 """ 372 np = _require("numpy", "matrix") 373 lowered = self._inner.lowered() 374 buses = lowered.buses 375 branches = lowered.branches 376 generators = lowered.generators 377 bus_ids = np.asarray([b["id"] for b in buses], dtype=np.int64) 378 pd, qd, gs, bs = _bus_sums(np, buses, lowered.loads, lowered.shunts) 379 380 branch = DenseBranch( 381 from_id=np.asarray([br["from_id"] for br in branches], dtype=np.int64), 382 to_id=np.asarray([br["to_id"] for br in branches], dtype=np.int64), 383 r=np.asarray([br["r"] for br in branches], dtype=float), 384 x=np.asarray([br["x"] for br in branches], dtype=float), 385 b=np.asarray([br["b"] for br in branches], dtype=float), 386 tap=np.asarray([br["tap"] for br in branches], dtype=float), 387 shift=np.asarray([br["shift"] for br in branches], dtype=float), 388 in_service=np.asarray([br["in_service"] for br in branches], dtype=bool), 389 ) 390 gen = DenseGen( 391 bus=np.asarray([g["bus"] for g in generators], dtype=np.int64), 392 pg=np.asarray([g["pg"] for g in generators], dtype=float), 393 pmax=np.asarray([g["pmax"] for g in generators], dtype=float), 394 pmin=np.asarray([g["pmin"] for g in generators], dtype=float), 395 in_service=np.asarray([g["in_service"] for g in generators], dtype=bool), 396 ) 397 refs = self.reference_bus_indices() 398 return DenseNetwork( 399 n=len(buses), 400 m=len(branches), 401 ng=len(generators), 402 base_mva=self.base_mva, 403 bus_ids=bus_ids, 404 branch=branch, 405 gen=gen, 406 demand=DenseDemand(pd=pd, qd=qd), 407 shunt=DenseShunt(gs=gs, bs=bs), 408 reference_bus=refs[0] if len(refs) == 1 else None, 409 n_components=self.n_connected_components, 410 is_radial=self.is_radial, 411 ) 412 413 # --- matrix builders (scipy.sparse) --------------------------------- 414 415 def bprime(self, scheme: str = "bx"): 416 """MATPOWER FDPF Bp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.""" 417 return _to_csr(self._inner.bprime(scheme)) 418 419 def bdoubleprime(self, scheme: str = "bx"): 420 """MATPOWER FDPF Bpp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.""" 421 return _to_csr(self._inner.bdoubleprime(scheme)) 422 423 def lacpf(self, *, include_taps: bool = True, include_shifts: bool = True): 424 """LACPF 2n×2n block ``[[G, -B], [-B, -G]]``.""" 425 return _to_csr( 426 self._inner.lacpf(include_taps=include_taps, include_shifts=include_shifts) 427 ) 428 429 def adjacency(self): 430 """0/1 bus adjacency matrix.""" 431 return _to_csr(self._inner.adjacency()) 432 433 def ybus_parts(self, *, include_taps: bool = True, include_shifts: bool = True): 434 """:class:`YbusParts` ``(g, b)`` = ``(Re(Y_bus), Im(Y_bus))``, two real 435 csr_matrix.""" 436 g, b = self._inner.ybus_parts( 437 include_taps=include_taps, include_shifts=include_shifts 438 ) 439 return YbusParts(g=_to_csr(g), b=_to_csr(b)) 440 441 def ybus(self, *, include_taps: bool = True, include_shifts: bool = True): 442 """``Y_bus = G + jB`` as a complex csr_matrix.""" 443 g, b = self.ybus_parts( 444 include_taps=include_taps, include_shifts=include_shifts 445 ) 446 return (g + 1j * b).tocsr() 447 448 def ptdf(self, convention: str = "series", solver: str = "auto"): 449 """DC PTDF (m×n). ``convention`` is ``"series"`` or ``"matpower"``. 450 451 ``solver`` is ``"auto"``, ``"dense"``, or ``"iterative"``. ``"auto"`` 452 uses the dense factorization on small cases and the iterative 453 conjugate gradient path on large ones, the same policy as the CLI. 454 """ 455 return _to_csr(self._inner.ptdf(convention, solver)) 456 457 def lodf(self, convention: str = "series", solver: str = "auto"): 458 """DC LODF (m×m). ``solver`` as in :meth:`ptdf`.""" 459 return _to_csr(self._inner.lodf(convention, solver)) 460 461 def weighted_laplacian(self, convention: str = "series"): 462 """Weighted Laplacian ``L = A diag(b) Aᵀ``.""" 463 return _to_csr(self._inner.weighted_laplacian(convention)) 464 465 def incidence(self, convention: str = "series") -> "Incidence": 466 """Signed incidence factorization as an :data:`Incidence` tuple.""" 467 np = _require("numpy", "matrix") 468 a, b, p_shift, branch_of_col = self._inner.incidence(convention) 469 return Incidence( 470 A=_to_csr(a), 471 b=np.asarray(b, dtype=float), 472 p_shift=np.asarray(p_shift, dtype=float), 473 branch_of_col=np.asarray(branch_of_col, dtype=np.int64), 474 ) 475 476 def write_gridfm( 477 self, 478 out_dir: Any, 479 *, 480 scenario: int = 0, 481 include_y_bus: bool = True, 482 include_taps: bool = True, 483 include_shifts: bool = True, 484 missing_gen_cost: Optional[str] = None, 485 default_gen_cost: Optional[str] = None, 486 gen_cost_csv: Optional[Any] = None, 487 ) -> dict: 488 """Write the gridfm-datakit Parquet dataset for this case under 489 ``<out_dir>/<case>/raw/``. 490 491 Returns a dict with ``dir``, ``files``, ``dropped_zero_impedance``, and 492 ``degenerate_cost_gens``. Published wheels include the native writer; 493 custom source builds without the Rust ``gridfm`` feature raise 494 ``ImportError``. For many perturbed snapshots in one dataset, see 495 :func:`write_gridfm_batch`. 496 """ 497 _require_gridfm() 498 return self._inner.write_gridfm( 499 str(out_dir), 500 scenario=scenario, 501 include_y_bus=include_y_bus, 502 include_taps=include_taps, 503 include_shifts=include_shifts, 504 missing_gen_cost=missing_gen_cost, 505 default_gen_cost=default_gen_cost, 506 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 507 ) 508 509 def write_pypsa_csv_folder(self, out_dir: Any) -> dict: 510 """Write this case as a PyPSA CSV folder. 511 512 The folder contains static PyPSA component CSVs and can be imported with 513 ``pypsa.Network().import_from_csv_folder(path)``. Returns a dict with 514 ``dir``, ``files``, and fidelity ``warnings``. 515 """ 516 return self._inner.write_pypsa_csv_folder(str(out_dir)) 517 518 def to_normalized(self) -> "BalancedNetwork": 519 """Return a normalized copy with per unit power and radian angles. 520 521 The result removes out of service elements, preserves source bus IDs, 522 and normalizes bus types. It carries no retained source, so 523 :meth:`write` serializes the derived model. Raises 524 :class:`PowerIODataError` if the network cannot be 525 normalized (no reference bus can be chosen, or a non-positive base MVA). 526 """ 527 return BalancedNetwork(self._inner.to_normalized()) 528 529 def to_normalized_with_options( 530 self, 531 *, 532 clamp_angle_bounds: bool = False, 533 angle_bound_pad: Optional[float] = None, 534 ) -> "BalancedNetwork": 535 """Return a normalized copy with explicit normalization options. 536 537 ``clamp_angle_bounds=True`` applies the PowerModels angle difference 538 bound repair: limits at or beyond ``+/-pi/2`` and zero/zero windows 539 become ``[-angle_bound_pad, angle_bound_pad]``. A repair that would 540 invert the interval widens to that same window. The default pad is 541 1.0472 radians. 542 """ 543 return BalancedNetwork( 544 self._inner.to_normalized_with_options( 545 clamp_angle_bounds=clamp_angle_bounds, angle_bound_pad=angle_bound_pad 546 ) 547 ) 548 549 def to_ppc(self): 550 """PYPOWER case dict (``ppc``) with MATPOWER-style numpy tables. 551 552 Values are emitted as the model holds them, so a case read from a 553 file carries MW, MVAr, and degrees. A network from 554 :meth:`to_normalized` holds per unit and radians, and those are what 555 its tables carry — PYPOWER reads a ppc dict as MW and degrees, so 556 build this from the raw network unless the consumer expects per unit. 557 558 Loads and shunts are summed onto their bus in the 559 ``PD``/``QD``/``GS``/``BS`` columns, the same aggregation 560 :meth:`to_matpower` writes. The bus table has no per element status 561 column, so an element the model marks out of service still 562 contributes its value, and a de-energized bus is carried as type 4. 563 ``gencost`` is present only when every generator carries cost data, 564 because MATPOWER requires cost rows for all generators or none. 565 :func:`from_ppc` reads the tables back. 566 """ 567 np = _require("numpy", "matrix") 568 buses = self._inner.buses 569 bus = np.array( 570 [ 571 ( 572 b["id"], _PPC_BUS_TYPE.get(b["kind"], 1.0), 0.0, 0.0, 0.0, 0.0, 573 b["area"], b["vm"], b["va"], b["base_kv"], b["zone"], 574 b["vmax"], b["vmin"], 575 ) 576 for b in buses 577 ], 578 dtype=float, 579 ).reshape(len(buses), 13) 580 bus[:, 2], bus[:, 3], bus[:, 4], bus[:, 5] = _bus_sums( 581 np, buses, self._inner.loads, self._inner.shunts 582 ) 583 584 # The capability and ramp columns past PMIN are an OPF extension that a 585 # source need not carry. Widen to the full 21 only when a generator 586 # actually states one: a table of zeros there reads back as eleven 587 # explicit zero limits, which a ramp aware solver takes as a generator 588 # that cannot move. 589 gens = self._inner.generators 590 caps = [g["caps"] for g in gens] 591 width = 21 if any(c is not None for row in caps for c in row) else 10 592 gen = np.array( 593 [ 594 [ 595 g["bus"], g["pg"], g["qg"], g["qmax"], g["qmin"], g["vg"], 596 g["mbase"], float(g["in_service"]), g["pmax"], g["pmin"], 597 ] 598 + ([0.0 if c is None else c for c in row] if width == 21 else []) 599 for g, row in zip(gens, caps) 600 ], 601 dtype=float, 602 ).reshape(len(gens), width) 603 604 branches = self._inner.branches 605 branch = np.array( 606 [ 607 ( 608 br["from_id"], br["to_id"], br["r"], br["x"], br["b"], 609 br["rate_a"], br["rate_b"], br["rate_c"], br["tap"], 610 br["shift"], float(br["in_service"]), br["angmin"], 611 br["angmax"], 612 ) 613 for br in branches 614 ], 615 dtype=float, 616 ).reshape(len(branches), 13) 617 618 ppc = { 619 "version": "2", 620 "baseMVA": float(self._inner.base_mva), 621 "bus": bus, 622 "gen": gen, 623 "branch": branch, 624 } 625 626 # Coefficients sit left-aligned after ncost, padded to the widest 627 # row, which is the layout PYPOWER's own loadcase produces. 628 costs = [g["cost"] for g in gens] 629 if costs and all(c is not None for c in costs): 630 gencost = np.zeros( 631 (len(costs), 4 + max(len(c["coeffs"]) for c in costs)) 632 ) 633 for i, c in enumerate(costs): 634 gencost[i, :4] = ( 635 c["model"], c["startup"], c["shutdown"], c["ncost"], 636 ) 637 gencost[i, 4:4 + len(c["coeffs"])] = c["coeffs"] 638 ppc["gencost"] = gencost 639 return ppc 640 641 def to_networkx(self): 642 """Undirected networkx graph keyed by bus id. 643 644 In-service branches become edges carrying ``branch`` (index), ``r``, 645 ``x``, and ``b``. 646 """ 647 nx = _require("networkx", "graph") 648 g = nx.Graph() 649 g.add_nodes_from(bus["id"] for bus in self._inner.buses) 650 for k, br in enumerate(self._inner.branches): 651 if br["in_service"]: 652 g.add_edge( 653 br["from_id"], 654 br["to_id"], 655 branch=k, 656 r=br["r"], 657 x=br["x"], 658 b=br["b"], 659 ) 660 return g 661 662 663def parse_file(path: Any, from_: Optional[str] = None) -> BalancedNetwork: 664 """Parse a case file from a path, inferring the format from the extension. 665 666 Read fidelity warnings are on ``BalancedNetwork.read_warnings`` (empty for readers 667 that don't report any; currently pandapower JSON, PyPSA CSV, and PSLF EPC 668 report them). 669 """ 670 return BalancedNetwork(_powerio.parse_file(str(path), from_)) 671 672 673def parse_display_file(path: Any, from_: Optional[str] = None) -> DisplayData: 674 """Parse a display artifact such as a PowerWorld ``.pwd`` file.""" 675 return _wrap_display(_powerio.parse_display_file(str(path), from_)) 676 677 678def parse_display_bytes(data: bytes, format: str) -> DisplayData: 679 """Parse display bytes in the named display format.""" 680 return _wrap_display(_powerio.parse_display_bytes(data, format)) 681 682 683def parse_str(text: str, format: str = "matpower") -> BalancedNetwork: 684 """Parse a case from in-memory text in the named ``format``.""" 685 return BalancedNetwork(_powerio.parse_str(text, format)) 686 687 688def parse_bytes(data: bytes, format: str) -> BalancedNetwork: 689 """Parse a case from in-memory bytes in the named ``format``. 690 691 Accepts every :func:`parse_str` format name plus ``"pwb"``. PowerWorld 692 binary has no text form, so this is the only way to read one without a 693 file on disk. Text formats must be UTF-8. 694 """ 695 return BalancedNetwork(_powerio.parse_bytes(data, format)) 696 697 698def parse_scopf(text: str, from_: str = "goc3-json") -> dict[str, Any]: 699 """Return a versioned SCOPF problem instance document. 700 701 ``from_`` currently accepts ``"goc3-json"``. The returned dictionary uses 702 the wire schema's declared 1-based indices and retains source identities in 703 separate fields. Parse and assembly failures raise :class:`PowerIOError`. 704 """ 705 return _json.loads(_powerio.parse_scopf(text, from_)) 706 707 708def parse_geo(text: str, name_hint: Optional[str] = None) -> dict[str, Any]: 709 """Tolerantly read a geographic sidecar and return its canonical form. 710 711 Accepts headerless buscoords CSV, aliased CSV/JSON records, and GeoJSON 712 Point/LineString features. Returns ``{"geojson": <FeatureCollection dict>, 713 "warnings": [...]}``; ``name_hint`` (a file name) picks CSV against JSON 714 when the content alone is ambiguous. Input with no usable coordinates 715 raises :class:`PowerIOParseError`. 716 """ 717 parsed = _powerio.parse_geo(text, name_hint) 718 parsed["geojson"] = _json.loads(parsed["geojson"]) 719 return parsed 720 721 722def from_json(text: str) -> BalancedNetwork: 723 """Rebuild a case from JSON produced by :meth:`BalancedNetwork.to_json`.""" 724 return BalancedNetwork(_powerio.from_json(text)) 725 726 727# powerio bus kind -> MATPOWER/PYPOWER BUS_TYPE code. 728def _bus_sums(np, buses, loads, shunts): 729 """Per bus `(pd, qd, gs, bs)` in bus order. 730 731 :meth:`BalancedNetwork.to_dense` and :meth:`BalancedNetwork.to_ppc` both fold the element 732 tables onto their bus the way the Rust indexed analysis view does. This is 733 that fold, once. 734 """ 735 row_of = {b["id"]: i for i, b in enumerate(buses)} 736 pd, qd, gs, bs = (np.zeros(len(buses), dtype=float) for _ in range(4)) 737 for load in loads: 738 i = row_of.get(load["bus"]) 739 if i is not None: 740 pd[i] += load["p"] 741 qd[i] += load["q"] 742 for shunt in shunts: 743 i = row_of.get(shunt["bus"]) 744 if i is not None: 745 gs[i] += shunt["g"] 746 bs[i] += shunt["b"] 747 return pd, qd, gs, bs 748 749 750_PPC_BUS_TYPE = {"PQ": 1.0, "PV": 2.0, "REF": 3.0, "ISOLATED": 4.0} 751 752# MATPOWER case-input table widths. PYPOWER result tables append columns 753# (LAM_P, MU_*) past these; from_ppc drops them. 754_PPC_INPUT_WIDTH = {"bus": 13, "gen": 21, "branch": 13} 755 756# Columns a table must carry, which is what the MATPOWER reader requires. The 757# gen table's capability and ramp columns are an OPF extension, so a 10 column 758# gen table is a complete case and passes through at its own width; padding it 759# would hand the reader eleven explicit zero limits the source never stated. A 760# bus or branch row below 13 is truncated data, and zero padding it would 761# invent a bus at 0 p.u. and 0 kV, so it is refused here as the reader refuses 762# it in a `.m` file. 763_PPC_MIN_WIDTH = {"bus": 13, "gen": 10, "branch": 13} 764 765 766def _ppc_rows(name, table): 767 """The table's rows as float lists, trimmed to the MATPOWER input width.""" 768 width = _PPC_INPUT_WIDTH.get(name) 769 minimum = _PPC_MIN_WIDTH.get(name) 770 out = [] 771 for i, row in enumerate(table): 772 try: 773 vals = [float(v) for v in row] 774 except TypeError as e: 775 raise ValueError( 776 f"ppc table {name!r} row {i} is not a sequence of numbers: " 777 f"pass a 2-D array, one row per element" 778 ) from e 779 except ValueError as e: 780 raise ValueError( 781 f"ppc table {name!r} row {i} has a non-numeric value: {e}" 782 ) from e 783 if minimum is not None and len(vals) < minimum: 784 raise ValueError( 785 f"ppc table {name!r} row {i} has {len(vals)} columns; " 786 f"MATPOWER requires at least {minimum}" 787 ) 788 out.append(vals[:width] if width is not None else vals) 789 return out 790 791 792def _ppc_to_matpower_text(ppc) -> str: 793 missing = [k for k in ("baseMVA", "bus", "gen", "branch") if k not in ppc] 794 if missing: 795 raise ValueError(f"ppc dict is missing required keys: {missing}") 796 lines = [ 797 "function mpc = from_ppc", 798 f"mpc.version = '{ppc.get('version', '2')}';", 799 f"mpc.baseMVA = {float(ppc['baseMVA'])!r};", 800 ] 801 names = ["bus", "gen", "branch"] + (["gencost"] if "gencost" in ppc else []) 802 for name in names: 803 rows = _ppc_rows(name, ppc[name]) 804 lines.append(f"mpc.{name} = [") 805 for vals in rows: 806 lines.append(" " + " ".join(repr(v) for v in vals) + ";") 807 lines.append("];") 808 return "\n".join(lines) + "\n" 809 810 811def from_ppc(ppc) -> BalancedNetwork: 812 """Case from a PYPOWER dict (``ppc``); the inverse of :meth:`BalancedNetwork.to_ppc`. 813 814 The tables route through the MATPOWER reader, so the semantics match a 815 ``.m`` case exactly: bus ``PD``/``QD`` become loads, ``GS``/``BS`` become 816 shunts, and ``gencost`` is read when present. Result columns past the 817 MATPOWER input widths are dropped. A 10 column ``gen`` table (the layout 818 without the OPF capability columns) passes through at its own width, so 819 the generators come back with no capability limits rather than eleven 820 zero ones. Raises :class:`ValueError` when a required table is absent, 821 when a ``bus`` or ``branch`` row is below its 13 column width, when a row 822 is not a sequence of numbers, or when a cell is not numeric; the message 823 names the table and the row. 824 """ 825 return parse_str(_ppc_to_matpower_text(ppc), "matpower") 826 827 828def convert_file( 829 path: Any, 830 to: str, 831 from_: Optional[str] = None, 832 missing_gen_cost: Optional[str] = None, 833 default_gen_cost: Optional[str] = None, 834 gen_cost_csv: Optional[Any] = None, 835 out: Optional[Any] = None, 836) -> Conversion: 837 r"""Convert a case file to another format through the network model. 838 839 ``to`` / ``from_`` are format names: ``matpower``, ``powermodels-json``, 840 ``egret-json``, ``pandapower-json``, ``psse``, ``powerworld``, ``pslf``, 841 ``goc3-json``, ``surge-json``, and ``opfdata-json`` (aliases ``m``, ``pm``, 842 ``egret``, ``pp``, ``raw``, ``aux``, ``epc``, ``goc3``, ``surge``, 843 ``opfdata``, and ``gridopt``). The input format is 844 inferred from the file extension unless ``from_`` overrides it. GO Challenge 845 3 and OPFData JSON are read only. An OPFData input may be an extracted 846 FullTop or N-1 example of any published grid size; its element counts are 847 read from the document. PyPSA CSV folders are read with 848 ``from_="pypsa-csv"`` and written with 849 :meth:`BalancedNetwork.write_pypsa_csv_folder`. Returns a :class:`Conversion` with 850 the text and any fidelity warnings. ``out`` writes the text to a file 851 exactly as produced; prefer it over ``open(out, "w").write(text)``, whose 852 text mode newline translation on Windows doubles the carriage returns of 853 a CRLF source echo into ``\r\r\n``, which PSS/E family tools reject. 854 """ 855 text, warnings = _powerio.convert_file( 856 str(path), 857 to, 858 from_, 859 missing_gen_cost=missing_gen_cost, 860 default_gen_cost=default_gen_cost, 861 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 862 out=None if out is None else str(out), 863 ) 864 return Conversion(text, warnings) 865 866 867def convert_str( 868 text: str, 869 to: str, 870 format: str = "matpower", 871 missing_gen_cost: Optional[str] = None, 872 default_gen_cost: Optional[str] = None, 873 gen_cost_csv: Optional[Any] = None, 874) -> Conversion: 875 """Convert in-memory case ``text`` through the network model without a 876 temporary file. 877 878 ``to`` and ``format`` are format names as in :func:`convert_file`; 879 ``format`` names the input (default ``matpower``). Returns a 880 :class:`Conversion` with the converted text and any fidelity warnings. 881 """ 882 out, warnings = _powerio.convert_str( 883 text, 884 to, 885 format, 886 missing_gen_cost=missing_gen_cost, 887 default_gen_cost=default_gen_cost, 888 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 889 ) 890 return Conversion(out, warnings) 891 892 893def to_format( 894 network: BalancedNetwork, 895 to: str, 896 missing_gen_cost: Optional[str] = None, 897 default_gen_cost: Optional[str] = None, 898 gen_cost_csv: Optional[Any] = None, 899) -> Conversion: 900 """Serialize ``network`` to another format.""" 901 return network.to_format( 902 to, 903 missing_gen_cost=missing_gen_cost, 904 default_gen_cost=default_gen_cost, 905 gen_cost_csv=gen_cost_csv, 906 ) 907 908 909def to_matpower(network: BalancedNetwork) -> str: 910 """Serialize ``network`` to MATPOWER ``.m`` text.""" 911 return network.to_matpower() 912 913 914def to_json(network: BalancedNetwork) -> str: 915 """Serialize ``network`` to the JSON transport.""" 916 return network.to_json() 917 918 919def to_dense(network: BalancedNetwork) -> DenseNetwork: 920 """Return copied dense NumPy tables for ``network``.""" 921 return network.to_dense() 922 923 924def write_gridfm_batch( 925 networks: "list[BalancedNetwork]", 926 out_dir: Any, 927 *, 928 base_scenario: int = 0, 929 include_y_bus: bool = True, 930 include_taps: bool = True, 931 include_shifts: bool = True, 932 missing_gen_cost: Optional[str] = None, 933 default_gen_cost: Optional[str] = None, 934 gen_cost_csv: Optional[Any] = None, 935) -> dict: 936 """Write several networks as one gridfm-datakit dataset, row stacked and 937 keyed by the ``scenario`` column. 938 939 Each network is one snapshot; the k-th is stamped ``base_scenario + k``. The 940 networks must share a base element set: the same bus/branch/gen counts and 941 bus id order (otherwise :class:`PowerIODataError` is raised). Load, dispatch, 942 branch status, and costs may vary per scenario. Returns the same dict as 943 :meth:`BalancedNetwork.write_gridfm`. Published wheels include the native writer; 944 custom source builds without the Rust ``gridfm`` feature raise 945 ``ImportError``. 946 """ 947 _require_gridfm() 948 inners = [c._inner for c in networks] 949 return _powerio.write_gridfm_batch( 950 inners, 951 str(out_dir), 952 base_scenario=base_scenario, 953 include_y_bus=include_y_bus, 954 include_taps=include_taps, 955 include_shifts=include_shifts, 956 missing_gen_cost=missing_gen_cost, 957 default_gen_cost=default_gen_cost, 958 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 959 ) 960 961 962def read_gridfm(dir: Any, scenario: int = 0) -> GridfmRead: 963 """Read one scenario of a gridfm-datakit Parquet dataset back into a case. 964 965 The inverse of :meth:`BalancedNetwork.write_gridfm`. ``dir`` is resolved leniently: 966 the ``raw/`` directory holding the parquet files, a ``<case>/`` directory with 967 a ``raw/`` child, or a parent directory with one ``*/raw/`` child all work. 968 ``scenario`` selects one snapshot from a batch (``0``, the base case, by 969 default). Returns a :class:`GridfmRead` ``(network, scenario, warnings)``. 970 971 The read recovers bus types, voltages and limits, nodal load and shunt 972 totals, generator dispatch and bounds, branch 973 ``r/x/b/tap/shift/rate_a`` values, angle limits, and ``baseMVA``. It cannot 974 recover source bus IDs, per element load/shunt granularity, piecewise or 975 cubic costs, HVDC, or storage; 976 what it can't recover is listed in ``warnings``. Published wheels include the 977 native reader; custom source builds without the Rust ``gridfm`` feature raise 978 ``ImportError``. 979 """ 980 _require_gridfm() 981 inner, scen, warnings = _powerio.read_gridfm(str(dir), scenario) 982 return GridfmRead(BalancedNetwork(inner), scen, warnings) 983 984 985def read_gridfm_scenarios(dir: Any) -> "list[GridfmRead]": 986 """Read every scenario of a gridfm dataset, one :class:`GridfmRead` per 987 scenario id (ascending) over the shared topology, the read side of 988 :func:`write_gridfm_batch`. 989 990 Each scenario is rebuilt independently, so two scenarios may differ in branch 991 status, bus types, and reference bus. See :func:`read_gridfm` for the lenient 992 directory resolution and the fidelity behavior. 993 """ 994 _require_gridfm() 995 return [ 996 GridfmRead(BalancedNetwork(inner), scen, warnings) 997 for inner, scen, warnings in _powerio.read_gridfm_scenarios(str(dir)) 998 ] 999 1000 1001def read_pypsa_csv_folder(path: Any) -> BalancedNetwork: 1002 """Read a PyPSA CSV folder into a :class:`BalancedNetwork`.""" 1003 return BalancedNetwork(_powerio.read_pypsa_csv_folder(str(path))) 1004 1005 1006from . import dist # noqa: E402 (needs Conversion defined above) 1007 1008 1009class Package: 1010 """A parsed ``.pio.json`` package. 1011 1012 Parsing occurs once; every accessor reuses the native handle. 1013 """ 1014 1015 def __init__(self, inner: "_powerio._Package"): 1016 self._inner = inner 1017 1018 @classmethod 1019 def from_file( 1020 cls, path: Any, from_: Optional[str] = None, scenario: int = 0 1021 ) -> "Package": 1022 """Build a package from a case file or folder.""" 1023 return cls(_powerio._Package.from_file(str(path), from_, scenario)) 1024 1025 @classmethod 1026 def from_str(cls, text: str, from_: Optional[str] = None) -> "Package": 1027 """Build a package from in-memory case text.""" 1028 return cls(_powerio._Package.from_str(text, from_)) 1029 1030 @classmethod 1031 def from_json(cls, text: str) -> "Package": 1032 """Parse a ``.pio.json`` document.""" 1033 return cls(_powerio._Package.from_json(text)) 1034 1035 @classmethod 1036 def from_balanced( 1037 cls, network: BalancedNetwork, include_solver_metadata: bool = False 1038 ) -> "Package": 1039 """Wrap a balanced :class:`BalancedNetwork` in a package.""" 1040 return cls( 1041 _powerio._Package.from_balanced(network._inner, include_solver_metadata) 1042 ) 1043 1044 @classmethod 1045 def from_multiconductor(cls, network: "dist.MulticonductorNetwork") -> "Package": 1046 """Wrap a multiconductor network in a package.""" 1047 return cls(_powerio._Package.from_multiconductor(network._inner)) 1048 1049 @property 1050 def model_kind(self) -> str: 1051 """``"balanced"`` or ``"multiconductor"``.""" 1052 return self._inner.model_kind() 1053 1054 def to_json(self) -> str: 1055 """Serialize to pretty ``.pio.json``.""" 1056 return self._inner.to_json() 1057 1058 def as_balanced(self) -> BalancedNetwork: 1059 """Return the balanced payload as a :class:`BalancedNetwork`.""" 1060 return BalancedNetwork(self._inner.as_balanced()) 1061 1062 def as_multiconductor(self) -> "dist.MulticonductorNetwork": 1063 """Return the multiconductor payload.""" 1064 return dist.MulticonductorNetwork(self._inner.as_multiconductor()) 1065 1066 def operating_points(self) -> Any: 1067 """The operating point series as Python data, or ``None``. 1068 1069 GOC3 packages populate this from the source time series. Each point is 1070 a set of field updates over the package's static payload. 1071 """ 1072 return _json.loads(self._inner.operating_points_json()) 1073 1074 def set_operating_points(self, points: Any) -> None: 1075 """Replace the operating point series and rerun package validation. 1076 1077 ``None`` or an empty series clears it. 1078 """ 1079 self._inner.set_operating_points_json(_json.dumps(points)) 1080 1081 def study(self) -> Any: 1082 """The study block as Python data, or ``None``.""" 1083 return _json.loads(self._inner.study_json()) 1084 1085 def materialize_operating_point(self, index: int) -> "Package": 1086 """Materialize one operating point into a new static package.""" 1087 return Package(self._inner.materialize_operating_point(index)) 1088 1089 def materialize_study_commit(self, index: int) -> "Package": 1090 """Materialize one study commit into a new static package.""" 1091 return Package(self._inner.materialize_study_commit(index)) 1092 1093 def validate(self) -> None: 1094 """Run the package semantic validation profile in place.""" 1095 self._inner.validate() 1096 1097 def validation(self) -> Any: 1098 """The validation summary as Python data.""" 1099 return _json.loads(self._inner.validation_json()) 1100 1101 def diagnostics(self) -> Any: 1102 """The structured diagnostics as a list of Python dicts.""" 1103 return _json.loads(self._inner.diagnostics_json()) 1104 1105 def multiconductor_to_balanced_preflight(self, base_mva: float = 100.0) -> Any: 1106 """Readiness report for multiconductor to balanced lowering.""" 1107 return _json.loads( 1108 self._inner.multiconductor_to_balanced_preflight_json(base_mva) 1109 ) 1110 1111 def lower_multiconductor_to_balanced(self, base_mva: float = 100.0) -> "Package": 1112 """Lower a multiconductor package to a new balanced package.""" 1113 return Package(self._inner.lower_multiconductor_to_balanced(base_mva)) 1114 1115 def __repr__(self) -> str: 1116 return repr(self._inner)
231class BalancedNetwork: 232 """A parsed balanced power network. 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._BalancedNetwork"): 250 self._inner = inner 251 252 def __dir__(self): 253 # The data attributes arrive through __getattr__, so name them here or 254 # they stay invisible to tab completion. 255 return sorted(set(super().__dir__()) | set(dir(self._inner))) 256 257 def __getattr__(self, name: str): 258 # Reached only when normal lookup misses, so the matrix methods below 259 # win. Guard underscore names so a lookup before _inner exists raises 260 # AttributeError instead of recursing forever. 261 if name.startswith("_"): 262 raise AttributeError( 263 f"{type(self).__name__!r} object has no attribute {name!r}" 264 ) 265 return getattr(self._inner, name) 266 267 def __repr__(self) -> str: 268 # The inner handle's __repr__ already renders the public ``BalancedNetwork(...)`` 269 # form, so this is a straight delegate. 270 return repr(self._inner) 271 272 # --- canonical format and table exports ----------------------------- 273 274 def to_matpower(self) -> str: 275 """Serialize to MATPOWER ``.m`` text. 276 277 A case parsed from MATPOWER keeps its original source, so this returns a 278 byte-exact echo. Derived cases serialize from the format neutral model. 279 """ 280 return self._inner.to_matpower() 281 282 def to_json(self) -> str: 283 """Serialize to the JSON transport.""" 284 return self._inner.to_json() 285 286 def geo_layer(self) -> dict[str, Any]: 287 """This case's coordinates as a canonical GeoJSON FeatureCollection. 288 289 Raises :class:`PowerIOError` when the case carries none. 290 """ 291 return _json.loads(self._inner.geo_layer_json()) 292 293 def apply_geo_layer( 294 self, text: str, name_hint: Optional[str] = None 295 ) -> tuple["BalancedNetwork", dict[str, Any]]: 296 """Apply a geographic sidecar and return ``(placed, report)``. 297 298 ``text`` is any form :func:`parse_geo` accepts; this case is 299 unchanged. The report carries ``matched_buses``, ``matched_branches``, 300 ``unmatched_features``, ``unlocated_buses``, ``unlocated_branches``, 301 and ``notes``. The two unlocated counts cover the whole case when the 302 pass ends, so a layer that matched nothing reads apart from a case 303 that needed nothing. The placed copy drops the retained source text, 304 so a same-format write re-serializes. 305 """ 306 inner, report = self._inner.apply_geo_layer(text, name_hint) 307 return BalancedNetwork(inner), report 308 309 def acopf_instance(self, units: Optional[str] = None) -> dict[str, Any]: 310 """The matrix free AC OPF problem instance as Python data. 311 312 Dense 0-based indices; ``units`` is ``"perunit"`` (default) or 313 ``"native"``. 314 """ 315 return _json.loads(self._inner.acopf_json(units)) 316 317 def to_format( 318 self, 319 to: str, 320 missing_gen_cost: Optional[str] = None, 321 default_gen_cost: Optional[str] = None, 322 gen_cost_csv: Optional[Any] = None, 323 ) -> Conversion: 324 """Serialize this parsed case to another format. 325 326 ``to`` is one of the format names accepted by :func:`convert_file`. 327 Returns a :class:`Conversion` with output text and fidelity warnings. 328 """ 329 text, warnings = self._inner.to_format( 330 to, 331 missing_gen_cost=missing_gen_cost, 332 default_gen_cost=default_gen_cost, 333 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 334 ) 335 return Conversion(text, warnings) 336 337 def write_file( 338 self, 339 path: Any, 340 to: str, 341 missing_gen_cost: Optional[str] = None, 342 default_gen_cost: Optional[str] = None, 343 gen_cost_csv: Optional[Any] = None, 344 ) -> list[str]: 345 r"""Serialize this case to ``to`` and write it to ``path`` byte exact. 346 347 Returns the fidelity warnings. Prefer this over writing 348 :meth:`to_format` text through ``open(path, "w")``: Python's text mode 349 translates newlines on Windows, so a case whose retained source has 350 CRLF line endings comes out with doubled carriage returns 351 (``\r\r\n``), which PSS/E family tools reject. 352 """ 353 return self._inner.write_file( 354 str(path), 355 to, 356 missing_gen_cost=missing_gen_cost, 357 default_gen_cost=default_gen_cost, 358 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 359 ) 360 361 def to_dense(self) -> DenseNetwork: 362 """Dense NumPy arrays for solver and adapter code. 363 364 This allocates new arrays, preserves bus and branch source order, and 365 sums loads and shunts per bus to match the Rust indexed analysis view. 366 367 That view is the star-lowered one, so a case with an in-service 368 3-winding transformer reports the star bus and its three branches here 369 even though :attr:`buses` and :attr:`branches` mirror the case file and 370 do not. ``reference_bus``, ``n_components`` and ``is_radial`` are 371 computed over the same lowered space, so all of them agree. 372 """ 373 np = _require("numpy", "matrix") 374 lowered = self._inner.lowered() 375 buses = lowered.buses 376 branches = lowered.branches 377 generators = lowered.generators 378 bus_ids = np.asarray([b["id"] for b in buses], dtype=np.int64) 379 pd, qd, gs, bs = _bus_sums(np, buses, lowered.loads, lowered.shunts) 380 381 branch = DenseBranch( 382 from_id=np.asarray([br["from_id"] for br in branches], dtype=np.int64), 383 to_id=np.asarray([br["to_id"] for br in branches], dtype=np.int64), 384 r=np.asarray([br["r"] for br in branches], dtype=float), 385 x=np.asarray([br["x"] for br in branches], dtype=float), 386 b=np.asarray([br["b"] for br in branches], dtype=float), 387 tap=np.asarray([br["tap"] for br in branches], dtype=float), 388 shift=np.asarray([br["shift"] for br in branches], dtype=float), 389 in_service=np.asarray([br["in_service"] for br in branches], dtype=bool), 390 ) 391 gen = DenseGen( 392 bus=np.asarray([g["bus"] for g in generators], dtype=np.int64), 393 pg=np.asarray([g["pg"] for g in generators], dtype=float), 394 pmax=np.asarray([g["pmax"] for g in generators], dtype=float), 395 pmin=np.asarray([g["pmin"] for g in generators], dtype=float), 396 in_service=np.asarray([g["in_service"] for g in generators], dtype=bool), 397 ) 398 refs = self.reference_bus_indices() 399 return DenseNetwork( 400 n=len(buses), 401 m=len(branches), 402 ng=len(generators), 403 base_mva=self.base_mva, 404 bus_ids=bus_ids, 405 branch=branch, 406 gen=gen, 407 demand=DenseDemand(pd=pd, qd=qd), 408 shunt=DenseShunt(gs=gs, bs=bs), 409 reference_bus=refs[0] if len(refs) == 1 else None, 410 n_components=self.n_connected_components, 411 is_radial=self.is_radial, 412 ) 413 414 # --- matrix builders (scipy.sparse) --------------------------------- 415 416 def bprime(self, scheme: str = "bx"): 417 """MATPOWER FDPF Bp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.""" 418 return _to_csr(self._inner.bprime(scheme)) 419 420 def bdoubleprime(self, scheme: str = "bx"): 421 """MATPOWER FDPF Bpp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.""" 422 return _to_csr(self._inner.bdoubleprime(scheme)) 423 424 def lacpf(self, *, include_taps: bool = True, include_shifts: bool = True): 425 """LACPF 2n×2n block ``[[G, -B], [-B, -G]]``.""" 426 return _to_csr( 427 self._inner.lacpf(include_taps=include_taps, include_shifts=include_shifts) 428 ) 429 430 def adjacency(self): 431 """0/1 bus adjacency matrix.""" 432 return _to_csr(self._inner.adjacency()) 433 434 def ybus_parts(self, *, include_taps: bool = True, include_shifts: bool = True): 435 """:class:`YbusParts` ``(g, b)`` = ``(Re(Y_bus), Im(Y_bus))``, two real 436 csr_matrix.""" 437 g, b = self._inner.ybus_parts( 438 include_taps=include_taps, include_shifts=include_shifts 439 ) 440 return YbusParts(g=_to_csr(g), b=_to_csr(b)) 441 442 def ybus(self, *, include_taps: bool = True, include_shifts: bool = True): 443 """``Y_bus = G + jB`` as a complex csr_matrix.""" 444 g, b = self.ybus_parts( 445 include_taps=include_taps, include_shifts=include_shifts 446 ) 447 return (g + 1j * b).tocsr() 448 449 def ptdf(self, convention: str = "series", solver: str = "auto"): 450 """DC PTDF (m×n). ``convention`` is ``"series"`` or ``"matpower"``. 451 452 ``solver`` is ``"auto"``, ``"dense"``, or ``"iterative"``. ``"auto"`` 453 uses the dense factorization on small cases and the iterative 454 conjugate gradient path on large ones, the same policy as the CLI. 455 """ 456 return _to_csr(self._inner.ptdf(convention, solver)) 457 458 def lodf(self, convention: str = "series", solver: str = "auto"): 459 """DC LODF (m×m). ``solver`` as in :meth:`ptdf`.""" 460 return _to_csr(self._inner.lodf(convention, solver)) 461 462 def weighted_laplacian(self, convention: str = "series"): 463 """Weighted Laplacian ``L = A diag(b) Aᵀ``.""" 464 return _to_csr(self._inner.weighted_laplacian(convention)) 465 466 def incidence(self, convention: str = "series") -> "Incidence": 467 """Signed incidence factorization as an :data:`Incidence` tuple.""" 468 np = _require("numpy", "matrix") 469 a, b, p_shift, branch_of_col = self._inner.incidence(convention) 470 return Incidence( 471 A=_to_csr(a), 472 b=np.asarray(b, dtype=float), 473 p_shift=np.asarray(p_shift, dtype=float), 474 branch_of_col=np.asarray(branch_of_col, dtype=np.int64), 475 ) 476 477 def write_gridfm( 478 self, 479 out_dir: Any, 480 *, 481 scenario: int = 0, 482 include_y_bus: bool = True, 483 include_taps: bool = True, 484 include_shifts: bool = True, 485 missing_gen_cost: Optional[str] = None, 486 default_gen_cost: Optional[str] = None, 487 gen_cost_csv: Optional[Any] = None, 488 ) -> dict: 489 """Write the gridfm-datakit Parquet dataset for this case under 490 ``<out_dir>/<case>/raw/``. 491 492 Returns a dict with ``dir``, ``files``, ``dropped_zero_impedance``, and 493 ``degenerate_cost_gens``. Published wheels include the native writer; 494 custom source builds without the Rust ``gridfm`` feature raise 495 ``ImportError``. For many perturbed snapshots in one dataset, see 496 :func:`write_gridfm_batch`. 497 """ 498 _require_gridfm() 499 return self._inner.write_gridfm( 500 str(out_dir), 501 scenario=scenario, 502 include_y_bus=include_y_bus, 503 include_taps=include_taps, 504 include_shifts=include_shifts, 505 missing_gen_cost=missing_gen_cost, 506 default_gen_cost=default_gen_cost, 507 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 508 ) 509 510 def write_pypsa_csv_folder(self, out_dir: Any) -> dict: 511 """Write this case as a PyPSA CSV folder. 512 513 The folder contains static PyPSA component CSVs and can be imported with 514 ``pypsa.Network().import_from_csv_folder(path)``. Returns a dict with 515 ``dir``, ``files``, and fidelity ``warnings``. 516 """ 517 return self._inner.write_pypsa_csv_folder(str(out_dir)) 518 519 def to_normalized(self) -> "BalancedNetwork": 520 """Return a normalized copy with per unit power and radian angles. 521 522 The result removes out of service elements, preserves source bus IDs, 523 and normalizes bus types. It carries no retained source, so 524 :meth:`write` serializes the derived model. Raises 525 :class:`PowerIODataError` if the network cannot be 526 normalized (no reference bus can be chosen, or a non-positive base MVA). 527 """ 528 return BalancedNetwork(self._inner.to_normalized()) 529 530 def to_normalized_with_options( 531 self, 532 *, 533 clamp_angle_bounds: bool = False, 534 angle_bound_pad: Optional[float] = None, 535 ) -> "BalancedNetwork": 536 """Return a normalized copy with explicit normalization options. 537 538 ``clamp_angle_bounds=True`` applies the PowerModels angle difference 539 bound repair: limits at or beyond ``+/-pi/2`` and zero/zero windows 540 become ``[-angle_bound_pad, angle_bound_pad]``. A repair that would 541 invert the interval widens to that same window. The default pad is 542 1.0472 radians. 543 """ 544 return BalancedNetwork( 545 self._inner.to_normalized_with_options( 546 clamp_angle_bounds=clamp_angle_bounds, angle_bound_pad=angle_bound_pad 547 ) 548 ) 549 550 def to_ppc(self): 551 """PYPOWER case dict (``ppc``) with MATPOWER-style numpy tables. 552 553 Values are emitted as the model holds them, so a case read from a 554 file carries MW, MVAr, and degrees. A network from 555 :meth:`to_normalized` holds per unit and radians, and those are what 556 its tables carry — PYPOWER reads a ppc dict as MW and degrees, so 557 build this from the raw network unless the consumer expects per unit. 558 559 Loads and shunts are summed onto their bus in the 560 ``PD``/``QD``/``GS``/``BS`` columns, the same aggregation 561 :meth:`to_matpower` writes. The bus table has no per element status 562 column, so an element the model marks out of service still 563 contributes its value, and a de-energized bus is carried as type 4. 564 ``gencost`` is present only when every generator carries cost data, 565 because MATPOWER requires cost rows for all generators or none. 566 :func:`from_ppc` reads the tables back. 567 """ 568 np = _require("numpy", "matrix") 569 buses = self._inner.buses 570 bus = np.array( 571 [ 572 ( 573 b["id"], _PPC_BUS_TYPE.get(b["kind"], 1.0), 0.0, 0.0, 0.0, 0.0, 574 b["area"], b["vm"], b["va"], b["base_kv"], b["zone"], 575 b["vmax"], b["vmin"], 576 ) 577 for b in buses 578 ], 579 dtype=float, 580 ).reshape(len(buses), 13) 581 bus[:, 2], bus[:, 3], bus[:, 4], bus[:, 5] = _bus_sums( 582 np, buses, self._inner.loads, self._inner.shunts 583 ) 584 585 # The capability and ramp columns past PMIN are an OPF extension that a 586 # source need not carry. Widen to the full 21 only when a generator 587 # actually states one: a table of zeros there reads back as eleven 588 # explicit zero limits, which a ramp aware solver takes as a generator 589 # that cannot move. 590 gens = self._inner.generators 591 caps = [g["caps"] for g in gens] 592 width = 21 if any(c is not None for row in caps for c in row) else 10 593 gen = np.array( 594 [ 595 [ 596 g["bus"], g["pg"], g["qg"], g["qmax"], g["qmin"], g["vg"], 597 g["mbase"], float(g["in_service"]), g["pmax"], g["pmin"], 598 ] 599 + ([0.0 if c is None else c for c in row] if width == 21 else []) 600 for g, row in zip(gens, caps) 601 ], 602 dtype=float, 603 ).reshape(len(gens), width) 604 605 branches = self._inner.branches 606 branch = np.array( 607 [ 608 ( 609 br["from_id"], br["to_id"], br["r"], br["x"], br["b"], 610 br["rate_a"], br["rate_b"], br["rate_c"], br["tap"], 611 br["shift"], float(br["in_service"]), br["angmin"], 612 br["angmax"], 613 ) 614 for br in branches 615 ], 616 dtype=float, 617 ).reshape(len(branches), 13) 618 619 ppc = { 620 "version": "2", 621 "baseMVA": float(self._inner.base_mva), 622 "bus": bus, 623 "gen": gen, 624 "branch": branch, 625 } 626 627 # Coefficients sit left-aligned after ncost, padded to the widest 628 # row, which is the layout PYPOWER's own loadcase produces. 629 costs = [g["cost"] for g in gens] 630 if costs and all(c is not None for c in costs): 631 gencost = np.zeros( 632 (len(costs), 4 + max(len(c["coeffs"]) for c in costs)) 633 ) 634 for i, c in enumerate(costs): 635 gencost[i, :4] = ( 636 c["model"], c["startup"], c["shutdown"], c["ncost"], 637 ) 638 gencost[i, 4:4 + len(c["coeffs"])] = c["coeffs"] 639 ppc["gencost"] = gencost 640 return ppc 641 642 def to_networkx(self): 643 """Undirected networkx graph keyed by bus id. 644 645 In-service branches become edges carrying ``branch`` (index), ``r``, 646 ``x``, and ``b``. 647 """ 648 nx = _require("networkx", "graph") 649 g = nx.Graph() 650 g.add_nodes_from(bus["id"] for bus in self._inner.buses) 651 for k, br in enumerate(self._inner.branches): 652 if br["in_service"]: 653 g.add_edge( 654 br["from_id"], 655 br["to_id"], 656 branch=k, 657 r=br["r"], 658 x=br["x"], 659 b=br["b"], 660 ) 661 return g
A parsed balanced power network.
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.
274 def to_matpower(self) -> str: 275 """Serialize to MATPOWER ``.m`` text. 276 277 A case parsed from MATPOWER keeps its original source, so this returns a 278 byte-exact echo. Derived cases serialize from the format neutral model. 279 """ 280 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.
282 def to_json(self) -> str: 283 """Serialize to the JSON transport.""" 284 return self._inner.to_json()
Serialize to the JSON transport.
286 def geo_layer(self) -> dict[str, Any]: 287 """This case's coordinates as a canonical GeoJSON FeatureCollection. 288 289 Raises :class:`PowerIOError` when the case carries none. 290 """ 291 return _json.loads(self._inner.geo_layer_json())
This case's coordinates as a canonical GeoJSON FeatureCollection.
Raises PowerIOError when the case carries none.
293 def apply_geo_layer( 294 self, text: str, name_hint: Optional[str] = None 295 ) -> tuple["BalancedNetwork", dict[str, Any]]: 296 """Apply a geographic sidecar and return ``(placed, report)``. 297 298 ``text`` is any form :func:`parse_geo` accepts; this case is 299 unchanged. The report carries ``matched_buses``, ``matched_branches``, 300 ``unmatched_features``, ``unlocated_buses``, ``unlocated_branches``, 301 and ``notes``. The two unlocated counts cover the whole case when the 302 pass ends, so a layer that matched nothing reads apart from a case 303 that needed nothing. The placed copy drops the retained source text, 304 so a same-format write re-serializes. 305 """ 306 inner, report = self._inner.apply_geo_layer(text, name_hint) 307 return BalancedNetwork(inner), report
Apply a geographic sidecar and return (placed, report).
text is any form parse_geo() accepts; this case is
unchanged. The report carries matched_buses, matched_branches,
unmatched_features, unlocated_buses, unlocated_branches,
and notes. The two unlocated counts cover the whole case when the
pass ends, so a layer that matched nothing reads apart from a case
that needed nothing. The placed copy drops the retained source text,
so a same-format write re-serializes.
309 def acopf_instance(self, units: Optional[str] = None) -> dict[str, Any]: 310 """The matrix free AC OPF problem instance as Python data. 311 312 Dense 0-based indices; ``units`` is ``"perunit"`` (default) or 313 ``"native"``. 314 """ 315 return _json.loads(self._inner.acopf_json(units))
The matrix free AC OPF problem instance as Python data.
Dense 0-based indices; units is "perunit" (default) or
"native".
317 def to_format( 318 self, 319 to: str, 320 missing_gen_cost: Optional[str] = None, 321 default_gen_cost: Optional[str] = None, 322 gen_cost_csv: Optional[Any] = None, 323 ) -> Conversion: 324 """Serialize this parsed case to another format. 325 326 ``to`` is one of the format names accepted by :func:`convert_file`. 327 Returns a :class:`Conversion` with output text and fidelity warnings. 328 """ 329 text, warnings = self._inner.to_format( 330 to, 331 missing_gen_cost=missing_gen_cost, 332 default_gen_cost=default_gen_cost, 333 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 334 ) 335 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.
337 def write_file( 338 self, 339 path: Any, 340 to: str, 341 missing_gen_cost: Optional[str] = None, 342 default_gen_cost: Optional[str] = None, 343 gen_cost_csv: Optional[Any] = None, 344 ) -> list[str]: 345 r"""Serialize this case to ``to`` and write it to ``path`` byte exact. 346 347 Returns the fidelity warnings. Prefer this over writing 348 :meth:`to_format` text through ``open(path, "w")``: Python's text mode 349 translates newlines on Windows, so a case whose retained source has 350 CRLF line endings comes out with doubled carriage returns 351 (``\r\r\n``), which PSS/E family tools reject. 352 """ 353 return self._inner.write_file( 354 str(path), 355 to, 356 missing_gen_cost=missing_gen_cost, 357 default_gen_cost=default_gen_cost, 358 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 359 )
Serialize this case to to and write it to path byte exact.
Returns the fidelity warnings. Prefer this over writing
to_format() text through open(path, "w"): Python's text mode
translates newlines on Windows, so a case whose retained source has
CRLF line endings comes out with doubled carriage returns
(\r\r\n), which PSS/E family tools reject.
361 def to_dense(self) -> DenseNetwork: 362 """Dense NumPy arrays for solver and adapter code. 363 364 This allocates new arrays, preserves bus and branch source order, and 365 sums loads and shunts per bus to match the Rust indexed analysis view. 366 367 That view is the star-lowered one, so a case with an in-service 368 3-winding transformer reports the star bus and its three branches here 369 even though :attr:`buses` and :attr:`branches` mirror the case file and 370 do not. ``reference_bus``, ``n_components`` and ``is_radial`` are 371 computed over the same lowered space, so all of them agree. 372 """ 373 np = _require("numpy", "matrix") 374 lowered = self._inner.lowered() 375 buses = lowered.buses 376 branches = lowered.branches 377 generators = lowered.generators 378 bus_ids = np.asarray([b["id"] for b in buses], dtype=np.int64) 379 pd, qd, gs, bs = _bus_sums(np, buses, lowered.loads, lowered.shunts) 380 381 branch = DenseBranch( 382 from_id=np.asarray([br["from_id"] for br in branches], dtype=np.int64), 383 to_id=np.asarray([br["to_id"] for br in branches], dtype=np.int64), 384 r=np.asarray([br["r"] for br in branches], dtype=float), 385 x=np.asarray([br["x"] for br in branches], dtype=float), 386 b=np.asarray([br["b"] for br in branches], dtype=float), 387 tap=np.asarray([br["tap"] for br in branches], dtype=float), 388 shift=np.asarray([br["shift"] for br in branches], dtype=float), 389 in_service=np.asarray([br["in_service"] for br in branches], dtype=bool), 390 ) 391 gen = DenseGen( 392 bus=np.asarray([g["bus"] for g in generators], dtype=np.int64), 393 pg=np.asarray([g["pg"] for g in generators], dtype=float), 394 pmax=np.asarray([g["pmax"] for g in generators], dtype=float), 395 pmin=np.asarray([g["pmin"] for g in generators], dtype=float), 396 in_service=np.asarray([g["in_service"] for g in generators], dtype=bool), 397 ) 398 refs = self.reference_bus_indices() 399 return DenseNetwork( 400 n=len(buses), 401 m=len(branches), 402 ng=len(generators), 403 base_mva=self.base_mva, 404 bus_ids=bus_ids, 405 branch=branch, 406 gen=gen, 407 demand=DenseDemand(pd=pd, qd=qd), 408 shunt=DenseShunt(gs=gs, bs=bs), 409 reference_bus=refs[0] if len(refs) == 1 else None, 410 n_components=self.n_connected_components, 411 is_radial=self.is_radial, 412 )
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.
That view is the star-lowered one, so a case with an in-service
3-winding transformer reports the star bus and its three branches here
even though buses and branches mirror the case file and
do not. reference_bus, n_components and is_radial are
computed over the same lowered space, so all of them agree.
416 def bprime(self, scheme: str = "bx"): 417 """MATPOWER FDPF Bp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.""" 418 return _to_csr(self._inner.bprime(scheme))
MATPOWER FDPF Bp matrix. scheme is "bx" or "xb".
420 def bdoubleprime(self, scheme: str = "bx"): 421 """MATPOWER FDPF Bpp matrix. ``scheme`` is ``"bx"`` or ``"xb"``.""" 422 return _to_csr(self._inner.bdoubleprime(scheme))
MATPOWER FDPF Bpp matrix. scheme is "bx" or "xb".
424 def lacpf(self, *, include_taps: bool = True, include_shifts: bool = True): 425 """LACPF 2n×2n block ``[[G, -B], [-B, -G]]``.""" 426 return _to_csr( 427 self._inner.lacpf(include_taps=include_taps, include_shifts=include_shifts) 428 )
LACPF 2n×2n block [[G, -B], [-B, -G]].
430 def adjacency(self): 431 """0/1 bus adjacency matrix.""" 432 return _to_csr(self._inner.adjacency())
0/1 bus adjacency matrix.
434 def ybus_parts(self, *, include_taps: bool = True, include_shifts: bool = True): 435 """:class:`YbusParts` ``(g, b)`` = ``(Re(Y_bus), Im(Y_bus))``, two real 436 csr_matrix.""" 437 g, b = self._inner.ybus_parts( 438 include_taps=include_taps, include_shifts=include_shifts 439 ) 440 return YbusParts(g=_to_csr(g), b=_to_csr(b))
YbusParts (g, b) = (Re(Y_bus), Im(Y_bus)), two real
csr_matrix.
442 def ybus(self, *, include_taps: bool = True, include_shifts: bool = True): 443 """``Y_bus = G + jB`` as a complex csr_matrix.""" 444 g, b = self.ybus_parts( 445 include_taps=include_taps, include_shifts=include_shifts 446 ) 447 return (g + 1j * b).tocsr()
Y_bus = G + jB as a complex csr_matrix.
449 def ptdf(self, convention: str = "series", solver: str = "auto"): 450 """DC PTDF (m×n). ``convention`` is ``"series"`` or ``"matpower"``. 451 452 ``solver`` is ``"auto"``, ``"dense"``, or ``"iterative"``. ``"auto"`` 453 uses the dense factorization on small cases and the iterative 454 conjugate gradient path on large ones, the same policy as the CLI. 455 """ 456 return _to_csr(self._inner.ptdf(convention, solver))
DC PTDF (m×n). convention is "series" or "matpower".
solver is "auto", "dense", or "iterative". "auto"
uses the dense factorization on small cases and the iterative
conjugate gradient path on large ones, the same policy as the CLI.
458 def lodf(self, convention: str = "series", solver: str = "auto"): 459 """DC LODF (m×m). ``solver`` as in :meth:`ptdf`.""" 460 return _to_csr(self._inner.lodf(convention, solver))
DC LODF (m×m). solver as in ptdf().
462 def weighted_laplacian(self, convention: str = "series"): 463 """Weighted Laplacian ``L = A diag(b) Aáµ€``.""" 464 return _to_csr(self._inner.weighted_laplacian(convention))
Weighted Laplacian L = A diag(b) Aáµ€.
466 def incidence(self, convention: str = "series") -> "Incidence": 467 """Signed incidence factorization as an :data:`Incidence` tuple.""" 468 np = _require("numpy", "matrix") 469 a, b, p_shift, branch_of_col = self._inner.incidence(convention) 470 return Incidence( 471 A=_to_csr(a), 472 b=np.asarray(b, dtype=float), 473 p_shift=np.asarray(p_shift, dtype=float), 474 branch_of_col=np.asarray(branch_of_col, dtype=np.int64), 475 )
Signed incidence factorization as an Incidence tuple.
477 def write_gridfm( 478 self, 479 out_dir: Any, 480 *, 481 scenario: int = 0, 482 include_y_bus: bool = True, 483 include_taps: bool = True, 484 include_shifts: bool = True, 485 missing_gen_cost: Optional[str] = None, 486 default_gen_cost: Optional[str] = None, 487 gen_cost_csv: Optional[Any] = None, 488 ) -> dict: 489 """Write the gridfm-datakit Parquet dataset for this case under 490 ``<out_dir>/<case>/raw/``. 491 492 Returns a dict with ``dir``, ``files``, ``dropped_zero_impedance``, and 493 ``degenerate_cost_gens``. Published wheels include the native writer; 494 custom source builds without the Rust ``gridfm`` feature raise 495 ``ImportError``. For many perturbed snapshots in one dataset, see 496 :func:`write_gridfm_batch`. 497 """ 498 _require_gridfm() 499 return self._inner.write_gridfm( 500 str(out_dir), 501 scenario=scenario, 502 include_y_bus=include_y_bus, 503 include_taps=include_taps, 504 include_shifts=include_shifts, 505 missing_gen_cost=missing_gen_cost, 506 default_gen_cost=default_gen_cost, 507 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 508 )
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().
510 def write_pypsa_csv_folder(self, out_dir: Any) -> dict: 511 """Write this case as a PyPSA CSV folder. 512 513 The folder contains static PyPSA component CSVs and can be imported with 514 ``pypsa.Network().import_from_csv_folder(path)``. Returns a dict with 515 ``dir``, ``files``, and fidelity ``warnings``. 516 """ 517 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.
519 def to_normalized(self) -> "BalancedNetwork": 520 """Return a normalized copy with per unit power and radian angles. 521 522 The result removes out of service elements, preserves source bus IDs, 523 and normalizes bus types. It carries no retained source, so 524 :meth:`write` serializes the derived model. Raises 525 :class:`PowerIODataError` if the network cannot be 526 normalized (no reference bus can be chosen, or a non-positive base MVA). 527 """ 528 return BalancedNetwork(self._inner.to_normalized())
Return a normalized copy with per unit power and radian angles.
The result removes out of service elements, preserves source bus IDs,
and normalizes bus types. It carries no retained source, so
write() serializes the derived model. Raises
PowerIODataError if the network cannot be
normalized (no reference bus can be chosen, or a non-positive base MVA).
530 def to_normalized_with_options( 531 self, 532 *, 533 clamp_angle_bounds: bool = False, 534 angle_bound_pad: Optional[float] = None, 535 ) -> "BalancedNetwork": 536 """Return a normalized copy with explicit normalization options. 537 538 ``clamp_angle_bounds=True`` applies the PowerModels angle difference 539 bound repair: limits at or beyond ``+/-pi/2`` and zero/zero windows 540 become ``[-angle_bound_pad, angle_bound_pad]``. A repair that would 541 invert the interval widens to that same window. The default pad is 542 1.0472 radians. 543 """ 544 return BalancedNetwork( 545 self._inner.to_normalized_with_options( 546 clamp_angle_bounds=clamp_angle_bounds, angle_bound_pad=angle_bound_pad 547 ) 548 )
Return a normalized copy with explicit normalization options.
clamp_angle_bounds=True applies the PowerModels angle difference
bound repair: limits at or beyond +/-pi/2 and zero/zero windows
become [-angle_bound_pad, angle_bound_pad]. A repair that would
invert the interval widens to that same window. The default pad is
1.0472 radians.
550 def to_ppc(self): 551 """PYPOWER case dict (``ppc``) with MATPOWER-style numpy tables. 552 553 Values are emitted as the model holds them, so a case read from a 554 file carries MW, MVAr, and degrees. A network from 555 :meth:`to_normalized` holds per unit and radians, and those are what 556 its tables carry — PYPOWER reads a ppc dict as MW and degrees, so 557 build this from the raw network unless the consumer expects per unit. 558 559 Loads and shunts are summed onto their bus in the 560 ``PD``/``QD``/``GS``/``BS`` columns, the same aggregation 561 :meth:`to_matpower` writes. The bus table has no per element status 562 column, so an element the model marks out of service still 563 contributes its value, and a de-energized bus is carried as type 4. 564 ``gencost`` is present only when every generator carries cost data, 565 because MATPOWER requires cost rows for all generators or none. 566 :func:`from_ppc` reads the tables back. 567 """ 568 np = _require("numpy", "matrix") 569 buses = self._inner.buses 570 bus = np.array( 571 [ 572 ( 573 b["id"], _PPC_BUS_TYPE.get(b["kind"], 1.0), 0.0, 0.0, 0.0, 0.0, 574 b["area"], b["vm"], b["va"], b["base_kv"], b["zone"], 575 b["vmax"], b["vmin"], 576 ) 577 for b in buses 578 ], 579 dtype=float, 580 ).reshape(len(buses), 13) 581 bus[:, 2], bus[:, 3], bus[:, 4], bus[:, 5] = _bus_sums( 582 np, buses, self._inner.loads, self._inner.shunts 583 ) 584 585 # The capability and ramp columns past PMIN are an OPF extension that a 586 # source need not carry. Widen to the full 21 only when a generator 587 # actually states one: a table of zeros there reads back as eleven 588 # explicit zero limits, which a ramp aware solver takes as a generator 589 # that cannot move. 590 gens = self._inner.generators 591 caps = [g["caps"] for g in gens] 592 width = 21 if any(c is not None for row in caps for c in row) else 10 593 gen = np.array( 594 [ 595 [ 596 g["bus"], g["pg"], g["qg"], g["qmax"], g["qmin"], g["vg"], 597 g["mbase"], float(g["in_service"]), g["pmax"], g["pmin"], 598 ] 599 + ([0.0 if c is None else c for c in row] if width == 21 else []) 600 for g, row in zip(gens, caps) 601 ], 602 dtype=float, 603 ).reshape(len(gens), width) 604 605 branches = self._inner.branches 606 branch = np.array( 607 [ 608 ( 609 br["from_id"], br["to_id"], br["r"], br["x"], br["b"], 610 br["rate_a"], br["rate_b"], br["rate_c"], br["tap"], 611 br["shift"], float(br["in_service"]), br["angmin"], 612 br["angmax"], 613 ) 614 for br in branches 615 ], 616 dtype=float, 617 ).reshape(len(branches), 13) 618 619 ppc = { 620 "version": "2", 621 "baseMVA": float(self._inner.base_mva), 622 "bus": bus, 623 "gen": gen, 624 "branch": branch, 625 } 626 627 # Coefficients sit left-aligned after ncost, padded to the widest 628 # row, which is the layout PYPOWER's own loadcase produces. 629 costs = [g["cost"] for g in gens] 630 if costs and all(c is not None for c in costs): 631 gencost = np.zeros( 632 (len(costs), 4 + max(len(c["coeffs"]) for c in costs)) 633 ) 634 for i, c in enumerate(costs): 635 gencost[i, :4] = ( 636 c["model"], c["startup"], c["shutdown"], c["ncost"], 637 ) 638 gencost[i, 4:4 + len(c["coeffs"])] = c["coeffs"] 639 ppc["gencost"] = gencost 640 return ppc
PYPOWER case dict (ppc) with MATPOWER-style numpy tables.
Values are emitted as the model holds them, so a case read from a
file carries MW, MVAr, and degrees. A network from
to_normalized() holds per unit and radians, and those are what
its tables carry — PYPOWER reads a ppc dict as MW and degrees, so
build this from the raw network unless the consumer expects per unit.
Loads and shunts are summed onto their bus in the
PD/QD/GS/BS columns, the same aggregation
to_matpower() writes. The bus table has no per element status
column, so an element the model marks out of service still
contributes its value, and a de-energized bus is carried as type 4.
gencost is present only when every generator carries cost data,
because MATPOWER requires cost rows for all generators or none.
from_ppc() reads the tables back.
642 def to_networkx(self): 643 """Undirected networkx graph keyed by bus id. 644 645 In-service branches become edges carrying ``branch`` (index), ``r``, 646 ``x``, and ``b``. 647 """ 648 nx = _require("networkx", "graph") 649 g = nx.Graph() 650 g.add_nodes_from(bus["id"] for bus in self._inner.buses) 651 for k, br in enumerate(self._inner.branches): 652 if br["in_service"]: 653 g.add_edge( 654 br["from_id"], 655 br["to_id"], 656 branch=k, 657 r=br["r"], 658 x=br["x"], 659 b=br["b"], 660 ) 661 return g
Undirected networkx graph keyed by bus id.
In-service branches become edges carrying branch (index), r,
x, and b.
Output of convert_file().
text is the converted file contents; warnings lists the fields the
target format could not represent (empty for a faithful conversion).
Branch arrays in source order.
Nodal active and reactive demand arrays in bus order.
Generator arrays in source order.
Copied dense NumPy table export of a parsed BalancedNetwork.
Nodal shunt conductance and susceptance arrays in bus order.
Output of parse_display_file() / parse_display_bytes().
kind names the display format. For PowerWorld PWD data,
kind == "powerworld" and
data is a PwdDisplay.
Output of read_gridfm() / read_gridfm_scenarios().
network is the reconstructed BalancedNetwork; scenario is the source
scenario ID; warnings lists fields the GridFM schema cannot retain,
including source bus IDs, per element load and shunt rows, HVDC, storage, and
piecewise costs.
Output of BalancedNetwork.incidence().
Shapes, with n buses and m in-service branches:
A: signed incidence csr_matrix,(n, m).b: branch susceptances,(m,);b[k]is columnk.p_shift: phase-shift injection,(n,)(all zero unlessconvention="matpower").branch_of_col: column→branch index map,(m,);branch_of_col[k]andb[k]are co-indexed by incidence columnk.
1010class Package: 1011 """A parsed ``.pio.json`` package. 1012 1013 Parsing occurs once; every accessor reuses the native handle. 1014 """ 1015 1016 def __init__(self, inner: "_powerio._Package"): 1017 self._inner = inner 1018 1019 @classmethod 1020 def from_file( 1021 cls, path: Any, from_: Optional[str] = None, scenario: int = 0 1022 ) -> "Package": 1023 """Build a package from a case file or folder.""" 1024 return cls(_powerio._Package.from_file(str(path), from_, scenario)) 1025 1026 @classmethod 1027 def from_str(cls, text: str, from_: Optional[str] = None) -> "Package": 1028 """Build a package from in-memory case text.""" 1029 return cls(_powerio._Package.from_str(text, from_)) 1030 1031 @classmethod 1032 def from_json(cls, text: str) -> "Package": 1033 """Parse a ``.pio.json`` document.""" 1034 return cls(_powerio._Package.from_json(text)) 1035 1036 @classmethod 1037 def from_balanced( 1038 cls, network: BalancedNetwork, include_solver_metadata: bool = False 1039 ) -> "Package": 1040 """Wrap a balanced :class:`BalancedNetwork` in a package.""" 1041 return cls( 1042 _powerio._Package.from_balanced(network._inner, include_solver_metadata) 1043 ) 1044 1045 @classmethod 1046 def from_multiconductor(cls, network: "dist.MulticonductorNetwork") -> "Package": 1047 """Wrap a multiconductor network in a package.""" 1048 return cls(_powerio._Package.from_multiconductor(network._inner)) 1049 1050 @property 1051 def model_kind(self) -> str: 1052 """``"balanced"`` or ``"multiconductor"``.""" 1053 return self._inner.model_kind() 1054 1055 def to_json(self) -> str: 1056 """Serialize to pretty ``.pio.json``.""" 1057 return self._inner.to_json() 1058 1059 def as_balanced(self) -> BalancedNetwork: 1060 """Return the balanced payload as a :class:`BalancedNetwork`.""" 1061 return BalancedNetwork(self._inner.as_balanced()) 1062 1063 def as_multiconductor(self) -> "dist.MulticonductorNetwork": 1064 """Return the multiconductor payload.""" 1065 return dist.MulticonductorNetwork(self._inner.as_multiconductor()) 1066 1067 def operating_points(self) -> Any: 1068 """The operating point series as Python data, or ``None``. 1069 1070 GOC3 packages populate this from the source time series. Each point is 1071 a set of field updates over the package's static payload. 1072 """ 1073 return _json.loads(self._inner.operating_points_json()) 1074 1075 def set_operating_points(self, points: Any) -> None: 1076 """Replace the operating point series and rerun package validation. 1077 1078 ``None`` or an empty series clears it. 1079 """ 1080 self._inner.set_operating_points_json(_json.dumps(points)) 1081 1082 def study(self) -> Any: 1083 """The study block as Python data, or ``None``.""" 1084 return _json.loads(self._inner.study_json()) 1085 1086 def materialize_operating_point(self, index: int) -> "Package": 1087 """Materialize one operating point into a new static package.""" 1088 return Package(self._inner.materialize_operating_point(index)) 1089 1090 def materialize_study_commit(self, index: int) -> "Package": 1091 """Materialize one study commit into a new static package.""" 1092 return Package(self._inner.materialize_study_commit(index)) 1093 1094 def validate(self) -> None: 1095 """Run the package semantic validation profile in place.""" 1096 self._inner.validate() 1097 1098 def validation(self) -> Any: 1099 """The validation summary as Python data.""" 1100 return _json.loads(self._inner.validation_json()) 1101 1102 def diagnostics(self) -> Any: 1103 """The structured diagnostics as a list of Python dicts.""" 1104 return _json.loads(self._inner.diagnostics_json()) 1105 1106 def multiconductor_to_balanced_preflight(self, base_mva: float = 100.0) -> Any: 1107 """Readiness report for multiconductor to balanced lowering.""" 1108 return _json.loads( 1109 self._inner.multiconductor_to_balanced_preflight_json(base_mva) 1110 ) 1111 1112 def lower_multiconductor_to_balanced(self, base_mva: float = 100.0) -> "Package": 1113 """Lower a multiconductor package to a new balanced package.""" 1114 return Package(self._inner.lower_multiconductor_to_balanced(base_mva)) 1115 1116 def __repr__(self) -> str: 1117 return repr(self._inner)
A parsed .pio.json package.
Parsing occurs once; every accessor reuses the native handle.
1019 @classmethod 1020 def from_file( 1021 cls, path: Any, from_: Optional[str] = None, scenario: int = 0 1022 ) -> "Package": 1023 """Build a package from a case file or folder.""" 1024 return cls(_powerio._Package.from_file(str(path), from_, scenario))
Build a package from a case file or folder.
1026 @classmethod 1027 def from_str(cls, text: str, from_: Optional[str] = None) -> "Package": 1028 """Build a package from in-memory case text.""" 1029 return cls(_powerio._Package.from_str(text, from_))
Build a package from in-memory case text.
1031 @classmethod 1032 def from_json(cls, text: str) -> "Package": 1033 """Parse a ``.pio.json`` document.""" 1034 return cls(_powerio._Package.from_json(text))
Parse a .pio.json document.
1036 @classmethod 1037 def from_balanced( 1038 cls, network: BalancedNetwork, include_solver_metadata: bool = False 1039 ) -> "Package": 1040 """Wrap a balanced :class:`BalancedNetwork` in a package.""" 1041 return cls( 1042 _powerio._Package.from_balanced(network._inner, include_solver_metadata) 1043 )
Wrap a balanced BalancedNetwork in a package.
1045 @classmethod 1046 def from_multiconductor(cls, network: "dist.MulticonductorNetwork") -> "Package": 1047 """Wrap a multiconductor network in a package.""" 1048 return cls(_powerio._Package.from_multiconductor(network._inner))
Wrap a multiconductor network in a package.
1050 @property 1051 def model_kind(self) -> str: 1052 """``"balanced"`` or ``"multiconductor"``.""" 1053 return self._inner.model_kind()
"balanced" or "multiconductor".
1055 def to_json(self) -> str: 1056 """Serialize to pretty ``.pio.json``.""" 1057 return self._inner.to_json()
Serialize to pretty .pio.json.
1059 def as_balanced(self) -> BalancedNetwork: 1060 """Return the balanced payload as a :class:`BalancedNetwork`.""" 1061 return BalancedNetwork(self._inner.as_balanced())
Return the balanced payload as a BalancedNetwork.
1063 def as_multiconductor(self) -> "dist.MulticonductorNetwork": 1064 """Return the multiconductor payload.""" 1065 return dist.MulticonductorNetwork(self._inner.as_multiconductor())
Return the multiconductor payload.
1067 def operating_points(self) -> Any: 1068 """The operating point series as Python data, or ``None``. 1069 1070 GOC3 packages populate this from the source time series. Each point is 1071 a set of field updates over the package's static payload. 1072 """ 1073 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.
1075 def set_operating_points(self, points: Any) -> None: 1076 """Replace the operating point series and rerun package validation. 1077 1078 ``None`` or an empty series clears it. 1079 """ 1080 self._inner.set_operating_points_json(_json.dumps(points))
Replace the operating point series and rerun package validation.
None or an empty series clears it.
1082 def study(self) -> Any: 1083 """The study block as Python data, or ``None``.""" 1084 return _json.loads(self._inner.study_json())
The study block as Python data, or None.
1086 def materialize_operating_point(self, index: int) -> "Package": 1087 """Materialize one operating point into a new static package.""" 1088 return Package(self._inner.materialize_operating_point(index))
Materialize one operating point into a new static package.
1090 def materialize_study_commit(self, index: int) -> "Package": 1091 """Materialize one study commit into a new static package.""" 1092 return Package(self._inner.materialize_study_commit(index))
Materialize one study commit into a new static package.
1094 def validate(self) -> None: 1095 """Run the package semantic validation profile in place.""" 1096 self._inner.validate()
Run the package semantic validation profile in place.
1098 def validation(self) -> Any: 1099 """The validation summary as Python data.""" 1100 return _json.loads(self._inner.validation_json())
The validation summary as Python data.
1102 def diagnostics(self) -> Any: 1103 """The structured diagnostics as a list of Python dicts.""" 1104 return _json.loads(self._inner.diagnostics_json())
The structured diagnostics as a list of Python dicts.
1106 def multiconductor_to_balanced_preflight(self, base_mva: float = 100.0) -> Any: 1107 """Readiness report for multiconductor to balanced lowering.""" 1108 return _json.loads( 1109 self._inner.multiconductor_to_balanced_preflight_json(base_mva) 1110 )
Readiness report for multiconductor to balanced lowering.
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).
Base error raised by the powerio parser, converter, or matrix builders.
Subclasses ValueError: every failure it covers is a statement about a value the caller supplied, and except ValueError was what callers wrote before the hierarchy existed. I/O failures do not reach it; they raise the matching OSError subclass by value.
A case file is malformed or unparseable (missing/short rows, bad numbers, unbalanced brackets, format read failures).
Decoded PowerWorld .pwd display metadata.
One decoded PowerWorld display substation.
Output of BalancedNetwork.ybus_parts(): g = Re(Y_bus), b = Im(Y_bus), each a real csr_matrix. BalancedNetwork.ybus() returns g + 1j*b.
829def convert_file( 830 path: Any, 831 to: str, 832 from_: Optional[str] = None, 833 missing_gen_cost: Optional[str] = None, 834 default_gen_cost: Optional[str] = None, 835 gen_cost_csv: Optional[Any] = None, 836 out: Optional[Any] = None, 837) -> Conversion: 838 r"""Convert a case file to another format through the network model. 839 840 ``to`` / ``from_`` are format names: ``matpower``, ``powermodels-json``, 841 ``egret-json``, ``pandapower-json``, ``psse``, ``powerworld``, ``pslf``, 842 ``goc3-json``, ``surge-json``, and ``opfdata-json`` (aliases ``m``, ``pm``, 843 ``egret``, ``pp``, ``raw``, ``aux``, ``epc``, ``goc3``, ``surge``, 844 ``opfdata``, and ``gridopt``). The input format is 845 inferred from the file extension unless ``from_`` overrides it. GO Challenge 846 3 and OPFData JSON are read only. An OPFData input may be an extracted 847 FullTop or N-1 example of any published grid size; its element counts are 848 read from the document. PyPSA CSV folders are read with 849 ``from_="pypsa-csv"`` and written with 850 :meth:`BalancedNetwork.write_pypsa_csv_folder`. Returns a :class:`Conversion` with 851 the text and any fidelity warnings. ``out`` writes the text to a file 852 exactly as produced; prefer it over ``open(out, "w").write(text)``, whose 853 text mode newline translation on Windows doubles the carriage returns of 854 a CRLF source echo into ``\r\r\n``, which PSS/E family tools reject. 855 """ 856 text, warnings = _powerio.convert_file( 857 str(path), 858 to, 859 from_, 860 missing_gen_cost=missing_gen_cost, 861 default_gen_cost=default_gen_cost, 862 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 863 out=None if out is None else str(out), 864 ) 865 return Conversion(text, warnings)
Convert a case file to another format through the network model.
to / from_ are format names: matpower, powermodels-json,
egret-json, pandapower-json, psse, powerworld, pslf,
goc3-json, surge-json, and opfdata-json (aliases m, pm,
egret, pp, raw, aux, epc, goc3, surge,
opfdata, and gridopt). The input format is
inferred from the file extension unless from_ overrides it. GO Challenge
3 and OPFData JSON are read only. An OPFData input may be an extracted
FullTop or N-1 example of any published grid size; its element counts are
read from the document. PyPSA CSV folders are read with
from_="pypsa-csv" and written with
BalancedNetwork.write_pypsa_csv_folder(). Returns a Conversion with
the text and any fidelity warnings. out writes the text to a file
exactly as produced; prefer it over open(out, "w").write(text), whose
text mode newline translation on Windows doubles the carriage returns of
a CRLF source echo into \r\r\n, which PSS/E family tools reject.
868def convert_str( 869 text: str, 870 to: str, 871 format: str = "matpower", 872 missing_gen_cost: Optional[str] = None, 873 default_gen_cost: Optional[str] = None, 874 gen_cost_csv: Optional[Any] = None, 875) -> Conversion: 876 """Convert in-memory case ``text`` through the network model without a 877 temporary file. 878 879 ``to`` and ``format`` are format names as in :func:`convert_file`; 880 ``format`` names the input (default ``matpower``). Returns a 881 :class:`Conversion` with the converted text and any fidelity warnings. 882 """ 883 out, warnings = _powerio.convert_str( 884 text, 885 to, 886 format, 887 missing_gen_cost=missing_gen_cost, 888 default_gen_cost=default_gen_cost, 889 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 890 ) 891 return Conversion(out, warnings)
Convert in-memory case text through the network model without a
temporary file.
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.
723def from_json(text: str) -> BalancedNetwork: 724 """Rebuild a case from JSON produced by :meth:`BalancedNetwork.to_json`.""" 725 return BalancedNetwork(_powerio.from_json(text))
Rebuild a case from JSON produced by BalancedNetwork.to_json().
812def from_ppc(ppc) -> BalancedNetwork: 813 """Case from a PYPOWER dict (``ppc``); the inverse of :meth:`BalancedNetwork.to_ppc`. 814 815 The tables route through the MATPOWER reader, so the semantics match a 816 ``.m`` case exactly: bus ``PD``/``QD`` become loads, ``GS``/``BS`` become 817 shunts, and ``gencost`` is read when present. Result columns past the 818 MATPOWER input widths are dropped. A 10 column ``gen`` table (the layout 819 without the OPF capability columns) passes through at its own width, so 820 the generators come back with no capability limits rather than eleven 821 zero ones. Raises :class:`ValueError` when a required table is absent, 822 when a ``bus`` or ``branch`` row is below its 13 column width, when a row 823 is not a sequence of numbers, or when a cell is not numeric; the message 824 names the table and the row. 825 """ 826 return parse_str(_ppc_to_matpower_text(ppc), "matpower")
Case from a PYPOWER dict (ppc); the inverse of BalancedNetwork.to_ppc().
The tables route through the MATPOWER reader, so the semantics match a
.m case exactly: bus PD/QD become loads, GS/BS become
shunts, and gencost is read when present. Result columns past the
MATPOWER input widths are dropped. A 10 column gen table (the layout
without the OPF capability columns) passes through at its own width, so
the generators come back with no capability limits rather than eleven
zero ones. Raises ValueError when a required table is absent,
when a bus or branch row is below its 13 column width, when a row
is not a sequence of numbers, or when a cell is not numeric; the message
names the table and the row.
689def parse_bytes(data: bytes, format: str) -> BalancedNetwork: 690 """Parse a case from in-memory bytes in the named ``format``. 691 692 Accepts every :func:`parse_str` format name plus ``"pwb"``. PowerWorld 693 binary has no text form, so this is the only way to read one without a 694 file on disk. Text formats must be UTF-8. 695 """ 696 return BalancedNetwork(_powerio.parse_bytes(data, format))
Parse a case from in-memory bytes in the named format.
Accepts every parse_str() format name plus "pwb". PowerWorld
binary has no text form, so this is the only way to read one without a
file on disk. Text formats must be UTF-8.
679def parse_display_bytes(data: bytes, format: str) -> DisplayData: 680 """Parse display bytes in the named display format.""" 681 return _wrap_display(_powerio.parse_display_bytes(data, format))
Parse display bytes in the named display format.
674def parse_display_file(path: Any, from_: Optional[str] = None) -> DisplayData: 675 """Parse a display artifact such as a PowerWorld ``.pwd`` file.""" 676 return _wrap_display(_powerio.parse_display_file(str(path), from_))
Parse a display artifact such as a PowerWorld .pwd file.
664def parse_file(path: Any, from_: Optional[str] = None) -> BalancedNetwork: 665 """Parse a case file from a path, inferring the format from the extension. 666 667 Read fidelity warnings are on ``BalancedNetwork.read_warnings`` (empty for readers 668 that don't report any; currently pandapower JSON, PyPSA CSV, and PSLF EPC 669 report them). 670 """ 671 return BalancedNetwork(_powerio.parse_file(str(path), from_))
Parse a case file from a path, inferring the format from the extension.
Read fidelity warnings are on BalancedNetwork.read_warnings (empty for readers
that don't report any; currently pandapower JSON, PyPSA CSV, and PSLF EPC
report them).
709def parse_geo(text: str, name_hint: Optional[str] = None) -> dict[str, Any]: 710 """Tolerantly read a geographic sidecar and return its canonical form. 711 712 Accepts headerless buscoords CSV, aliased CSV/JSON records, and GeoJSON 713 Point/LineString features. Returns ``{"geojson": <FeatureCollection dict>, 714 "warnings": [...]}``; ``name_hint`` (a file name) picks CSV against JSON 715 when the content alone is ambiguous. Input with no usable coordinates 716 raises :class:`PowerIOParseError`. 717 """ 718 parsed = _powerio.parse_geo(text, name_hint) 719 parsed["geojson"] = _json.loads(parsed["geojson"]) 720 return parsed
Tolerantly read a geographic sidecar and return its canonical form.
Accepts headerless buscoords CSV, aliased CSV/JSON records, and GeoJSON
Point/LineString features. Returns {"geojson": <FeatureCollection dict>,
"warnings": [...]}; name_hint (a file name) picks CSV against JSON
when the content alone is ambiguous. Input with no usable coordinates
raises PowerIOParseError.
699def parse_scopf(text: str, from_: str = "goc3-json") -> dict[str, Any]: 700 """Return a versioned SCOPF problem instance document. 701 702 ``from_`` currently accepts ``"goc3-json"``. The returned dictionary uses 703 the wire schema's declared 1-based indices and retains source identities in 704 separate fields. Parse and assembly failures raise :class:`PowerIOError`. 705 """ 706 return _json.loads(_powerio.parse_scopf(text, from_))
Return a versioned SCOPF problem instance document.
from_ currently accepts "goc3-json". The returned dictionary uses
the wire schema's declared 1-based indices and retains source identities in
separate fields. Parse and assembly failures raise PowerIOError.
684def parse_str(text: str, format: str = "matpower") -> BalancedNetwork: 685 """Parse a case from in-memory text in the named ``format``.""" 686 return BalancedNetwork(_powerio.parse_str(text, format))
Parse a case from in-memory text in the named format.
963def read_gridfm(dir: Any, scenario: int = 0) -> GridfmRead: 964 """Read one scenario of a gridfm-datakit Parquet dataset back into a case. 965 966 The inverse of :meth:`BalancedNetwork.write_gridfm`. ``dir`` is resolved leniently: 967 the ``raw/`` directory holding the parquet files, a ``<case>/`` directory with 968 a ``raw/`` child, or a parent directory with one ``*/raw/`` child all work. 969 ``scenario`` selects one snapshot from a batch (``0``, the base case, by 970 default). Returns a :class:`GridfmRead` ``(network, scenario, warnings)``. 971 972 The read recovers bus types, voltages and limits, nodal load and shunt 973 totals, generator dispatch and bounds, branch 974 ``r/x/b/tap/shift/rate_a`` values, angle limits, and ``baseMVA``. It cannot 975 recover source bus IDs, per element load/shunt granularity, piecewise or 976 cubic costs, HVDC, or storage; 977 what it can't recover is listed in ``warnings``. Published wheels include the 978 native reader; custom source builds without the Rust ``gridfm`` feature raise 979 ``ImportError``. 980 """ 981 _require_gridfm() 982 inner, scen, warnings = _powerio.read_gridfm(str(dir), scenario) 983 return GridfmRead(BalancedNetwork(inner), scen, warnings)
Read one scenario of a gridfm-datakit Parquet dataset back into a case.
The inverse of BalancedNetwork.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 recovers bus types, voltages and limits, nodal load and shunt
totals, generator dispatch and bounds, branch
r/x/b/tap/shift/rate_a values, angle limits, and baseMVA. It cannot
recover source bus IDs, per element load/shunt granularity, piecewise or
cubic costs, HVDC, or 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.
986def read_gridfm_scenarios(dir: Any) -> "list[GridfmRead]": 987 """Read every scenario of a gridfm dataset, one :class:`GridfmRead` per 988 scenario id (ascending) over the shared topology, the read side of 989 :func:`write_gridfm_batch`. 990 991 Each scenario is rebuilt independently, so two scenarios may differ in branch 992 status, bus types, and reference bus. See :func:`read_gridfm` for the lenient 993 directory resolution and the fidelity behavior. 994 """ 995 _require_gridfm() 996 return [ 997 GridfmRead(BalancedNetwork(inner), scen, warnings) 998 for inner, scen, warnings in _powerio.read_gridfm_scenarios(str(dir)) 999 ]
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.
1002def read_pypsa_csv_folder(path: Any) -> BalancedNetwork: 1003 """Read a PyPSA CSV folder into a :class:`BalancedNetwork`.""" 1004 return BalancedNetwork(_powerio.read_pypsa_csv_folder(str(path)))
Read a PyPSA CSV folder into a BalancedNetwork.
920def to_dense(network: BalancedNetwork) -> DenseNetwork: 921 """Return copied dense NumPy tables for ``network``.""" 922 return network.to_dense()
Return copied dense NumPy tables for network.
894def to_format( 895 network: BalancedNetwork, 896 to: str, 897 missing_gen_cost: Optional[str] = None, 898 default_gen_cost: Optional[str] = None, 899 gen_cost_csv: Optional[Any] = None, 900) -> Conversion: 901 """Serialize ``network`` to another format.""" 902 return network.to_format( 903 to, 904 missing_gen_cost=missing_gen_cost, 905 default_gen_cost=default_gen_cost, 906 gen_cost_csv=gen_cost_csv, 907 )
Serialize network to another format.
915def to_json(network: BalancedNetwork) -> str: 916 """Serialize ``network`` to the JSON transport.""" 917 return network.to_json()
Serialize network to the JSON transport.
910def to_matpower(network: BalancedNetwork) -> str: 911 """Serialize ``network`` to MATPOWER ``.m`` text.""" 912 return network.to_matpower()
Serialize network to MATPOWER .m text.
925def write_gridfm_batch( 926 networks: "list[BalancedNetwork]", 927 out_dir: Any, 928 *, 929 base_scenario: int = 0, 930 include_y_bus: bool = True, 931 include_taps: bool = True, 932 include_shifts: bool = True, 933 missing_gen_cost: Optional[str] = None, 934 default_gen_cost: Optional[str] = None, 935 gen_cost_csv: Optional[Any] = None, 936) -> dict: 937 """Write several networks as one gridfm-datakit dataset, row stacked and 938 keyed by the ``scenario`` column. 939 940 Each network is one snapshot; the k-th is stamped ``base_scenario + k``. The 941 networks must share a base element set: the same bus/branch/gen counts and 942 bus id order (otherwise :class:`PowerIODataError` is raised). Load, dispatch, 943 branch status, and costs may vary per scenario. Returns the same dict as 944 :meth:`BalancedNetwork.write_gridfm`. Published wheels include the native writer; 945 custom source builds without the Rust ``gridfm`` feature raise 946 ``ImportError``. 947 """ 948 _require_gridfm() 949 inners = [c._inner for c in networks] 950 return _powerio.write_gridfm_batch( 951 inners, 952 str(out_dir), 953 base_scenario=base_scenario, 954 include_y_bus=include_y_bus, 955 include_taps=include_taps, 956 include_shifts=include_shifts, 957 missing_gen_cost=missing_gen_cost, 958 default_gen_cost=default_gen_cost, 959 gen_cost_csv=None if gen_cost_csv is None else str(gen_cost_csv), 960 )
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
BalancedNetwork.write_gridfm(). Published wheels include the native writer;
custom source builds without the Rust gridfm feature raise
ImportError.