Outage and what-if studies¶
The groundinsight.simulation.outage module turns the new active
flag on Bus and Branch into a first-class what-if API. Multiple
topology scenarios (e.g. "cable A out", "substation isolator open",
"shield broken") can be evaluated against one base case in a single
call, with comparison DataFrames produced automatically.
Physical / modelling context¶
The nodal-admittance solver assembles
\(Y(f)\,\underline{u}(f) = \underline{i}(f)\) over active elements
only. An inactive Bus is removed from the system entirely (its row
and column drop from \(Y\)); an inactive Branch contributes no
self-admittance, no mutual-coupling Norton injection, and reports a
zero current in the result — the open-circuit limit. The pathfinder
also skips inactive elements when enumerating source-to-fault paths,
which keeps the per-path Norton bookkeeping consistent.
That makes maintenance scenarios, planned outages, broken shields,
and N-1 contingencies expressible as scenarios without rebuilding
the network. The outage_context context manager flips the listed
elements to active=False for the duration of a with block and
restores the previous state — including the cached path list —
afterwards. run_outage_study orchestrates a base run plus one fault
calculation per scenario and returns long-format Polars DataFrames
with absolute and relative deltas against a reference scenario
(default: the base case).
Example¶
import groundinsight as gi
# Assume a pre-built network `net` with sources, buses, branches and
# at least one fault, e.g. from the quickstart or from
# gi.from_pandapower(...).
scenario_cable_out = gi.Outage(
name="cable_1_oos",
description="MS-cable cable_1 out of service",
disabled_buses=[],
disabled_branches=["cable_1"],
)
scenario_bus_islanded = gi.Outage(
name="bus_x_islanded",
description="Bus bus_x removed from the network",
disabled_buses=["bus_x"],
disabled_branches=[],
)
study = gi.run_outage_study(
network=net,
fault="fault1",
scenarios=[scenario_cable_out, scenario_bus_islanded],
include_base=True,
)
# Per-scenario result DataFrames
print(study.compare_buses()) # EPR per bus, with delta vs. base
print(study.compare_branches()) # branch currents, with delta vs. base
Reading the two delta columns¶
Each comparison carries an absolute delta_vs_<ref> and a relative
delta_pct_vs_<ref>. The relative one is null wherever the
reference value is zero, and that is the ordinary case rather than
an exotic one:
| situation | reference | scenario | delta_vs_base |
delta_pct_vs_base |
|---|---|---|---|---|
| a frequency the fault does not excite | 0 V | 0 V | 0 V | null |
| a station islanded in the reference scenario | 0 V | 57.9 V | 57.9 V | null |
| ordinary change | 800 V | 720 V | −80 V | −10 % |
"Ten per cent of nothing" has no answer, so none is reported. A
zero baseline arises from a scalings={250.0: 0.0} fault, from
comparing against= a scenario that islands the bus in question,
and from any element that carries no current in the reference —
none of which is an error.
The absolute column keeps the full information in all three rows,
so it is the one to rank by when a zero baseline is possible. And
because the marker is null and not NaN, Polars aggregations
skip it:
tbl = study.compare_buses()
tbl["delta_pct_vs_base"].mean() # finite; the null rows are skipped
tbl.filter(pl.col("delta_pct_vs_base").is_null()) # the zero-baseline rows
Use outage_context(network, outage) directly when a single one-off
modification is needed:
with gi.outage_context(net, scenario_cable_out):
gi.run_fault(network=net, fault_name="fault1")
print(net.res_all_impedances())
# net is fully restored on exit — including the cached paths.
API reference¶
outage ¶
Outage / What-If Study Module.
Convenience layer on top of the active flag of
:class:groundinsight.models.core_models.Bus and
:class:groundinsight.models.core_models.Branch. An :class:Outage is a
named scenario that lists buses and/or branches to be deactivated; the
:func:outage_context context manager applies the scenario for the
duration of a with block and restores the previous state on exit, and
:func:run_outage_study runs run_fault for the base case and a list
of scenarios in one go and packages the results into an
:class:OutageStudyResult with ready-made comparison helpers.
Example:
>>> import groundinsight as gi
>>> # build a network, add fault "F1", then:
>>> study = gi.run_outage_study(
... network=net,
... fault="F1",
... scenarios=[
... gi.Outage(name="branch_b12_open", disabled_branches=["B12"]),
... gi.Outage(name="bus5_isolated", disabled_buses=["bus5"]),
... ],
... )
>>> df = study.compare_buses()
>>> df_branch = study.compare_branches()
Inactive elements are physically modelled as open circuits / removed nodes
in the solver (see :mod:groundinsight.electrical_network and
:mod:groundinsight.pathfinder); the outage layer does not duplicate that
logic, it only flips the active flag and re-runs the existing
run_fault pipeline.
Outage ¶
Bases: BaseModel
Definition of a single what-if scenario.
An outage is described by the names of buses and/or branches that should
be deactivated for the duration of the scenario. The names must refer to
elements that exist in the network when the scenario is applied;
unknown names raise ValueError at apply time.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Unique label used in result keys and DataFrame columns. |
disabled_buses |
list of str
|
Names of buses to deactivate. |
disabled_branches |
list of str
|
Names of branches to deactivate. |
description |
(str, optional)
|
Free-form description for documentation. |
OutageStudyResult ¶
Bases: BaseModel
Result of an outage study.
Holds bus and branch result DataFrames per scenario (and for the base
case, if it was included). The compare_* methods produce
long-format Polars DataFrames suitable for plotting or further
aggregation.
Attributes:
| Name | Type | Description |
|---|---|---|
fault |
str
|
Name of the fault that was solved. |
base_label |
str
|
Label of the base scenario (typically |
scenarios |
list of Outage
|
The scenarios in the order they were run. |
bus_results |
dict of str to polars.DataFrame
|
|
branch_results |
dict of str to polars.DataFrame
|
Same as |
compare_branches ¶
compare_branches(
*,
against: Optional[str] = None,
columns: Sequence[str] = ("I_branch_A",)
) -> pl.DataFrame
Long-format branch comparison across scenarios.
Same shape as :meth:compare_buses but keyed by
(branch_name, frequency_Hz).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
against
|
str
|
Reference scenario label. |
None
|
columns
|
sequence of str
|
Branch result metrics to compare. Defaults to
|
('I_branch_A',)
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Long-format comparison frame. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/groundinsight/simulation/outage.py
compare_buses ¶
compare_buses(
*,
against: Optional[str] = None,
columns: Sequence[str] = ("EPR_V",)
) -> pl.DataFrame
Long-format bus comparison across scenarios.
For every (bus_name, frequency_Hz) pair the table contains one
row per scenario per metric, plus the delta_vs_<against> and
delta_pct_vs_<against> columns relative to the reference scenario.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
against
|
str
|
Reference scenario label. Defaults to |
None
|
columns
|
sequence of str
|
Bus result metrics to compare. Defaults to |
('EPR_V',)
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Long-format frame with columns |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/groundinsight/simulation/outage.py
labels ¶
Return the labels of all stored scenarios.
The base label is included first (if present), followed by the user
scenarios in the order they were submitted to run_outage_study.
Returns:
| Type | Description |
|---|---|
list of str
|
Scenario labels. |
Source code in src/groundinsight/simulation/outage.py
outage_context ¶
Apply an :class:Outage to network for the duration of a with block.
On entry, the active flag of the listed buses and branches is set
to False. On exit, the original active values are restored
even if the body raises. Path information is invalidated on entry and
on exit because both the outage and its rollback can change topology.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
network
|
Network
|
The network to mutate in place. |
required |
outage
|
Outage
|
Scenario describing what to deactivate. |
required |
Yields:
| Type | Description |
|---|---|
Network
|
The same |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a name in |
Notes
- The module-level pathfinder cache (see
:mod:
groundinsight.pathfinder) is cleared on exit as well as on entry. Without the exit-side eviction, a multi-scenario outage sweep was accumulating one cache entry per scenario indefinitely; together with the new LRU cap the cache footprint now matches the user-visible state ofnetwork. - The function is now nestable. Nested
withblocks store the outer-block-baseline on a per-(bus, branch) basis usingsetdefaultso the inner block does not capture the already-modified outer state as its own baseline. On outer-block exit the originally-captured baseline is restored, so changes made by the inner block to buses/branches that are not part of the outer outage are correctly preserved through the unwind.
Examples:
>>> with outage_context(net, Outage(name="b12", disabled_branches=["B12"])):
... gi.run_fault(net, "F1")
... df = net.res_buses(fault="F1")
Source code in src/groundinsight/simulation/outage.py
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 | |
run_outage_study ¶
run_outage_study(
network: Network,
*,
fault: str,
scenarios: List[Outage],
include_base: bool = True,
base_label: str = "base",
auto_parallel_coefficients: Optional[bool] = None,
phase_current_mode: str = "auto",
redefine_paths: bool = True
) -> OutageStudyResult
Solve fault for the base case (optional) and a list of outage scenarios.
Each scenario is applied via :func:outage_context, then run_fault
is called. Bus and branch result DataFrames are pulled out of the
network and stored under the scenario label so that the network's own
results dict is left in a single, reproducible state at the end of
the study (the last scenario's solution).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
network
|
Network
|
The network to study. Must contain |
required |
fault
|
str
|
Name of the fault to solve in every scenario. |
required |
scenarios
|
list of Outage
|
Scenarios to run, in order. |
required |
include_base
|
bool
|
If |
True
|
base_label
|
str
|
Label used to identify the base case in the stored DataFrames.
Defaults to |
'base'
|
auto_parallel_coefficients
|
bool
|
Deprecated alias, forwarded to :func: |
None
|
phase_current_mode
|
('auto', 'paths')
|
Forwarded to :func: |
"auto"
|
redefine_paths
|
bool
|
If |
True
|
Returns:
| Type | Description |
|---|---|
OutageStudyResult
|
Aggregated result with comparison helpers. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> study = run_outage_study(
... net, fault="F1",
... scenarios=[
... Outage(name="b12_open", disabled_branches=["B12"]),
... Outage(name="bus5_isolated", disabled_buses=["bus5"]),
... ],
... )
>>> study.compare_buses()
>>> study.compare_branches()
Source code in src/groundinsight/simulation/outage.py
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 | |