Network decomposition at the fault location¶
Split the grounding network at the faulted station into named directions, and quantify each of them: what parallel grounding impedance it contributes, and what share of the fault current leaves through it.
The impedances come from source-free current division and add up to the driving-point impedance of the whole network exactly, in a radial network and in a ring alike. The currents come from a solved fault and describe how the fault current actually divides. See Concepts for the definitions and their limits.
decomposition ¶
Split a grounding network at the fault location into parallel sides.
The question this module answers is the one a cable-network study starts from: of everything that is bolted onto the faulted station, how much grounding does each direction actually contribute, and how does the fault current divide between them?
parallel impedance left --- fault location --- parallel impedance right
A cut is a named set of branches, all of them incident to the fault bus. Removing the cut separates a far side from the near side that holds the fault bus. Two families of quantity are then computed for each cut, and they answer different questions:
Impedances -- a property of the network, not of the fault.
Z_side is obtained by current division, which keeps it exact in any
topology. One ampere is injected at the fault bus into the network with all
sources and all mutual injections removed; the share of it that leaves through
each cut is read off, and Z_side = u_fault / i_cut. Because the shares and
the local electrode current add up to the injected ampere, the decomposition
closes by construction:
.. math::
\frac{1}{Z_\text{driving point}} = \frac{1}{Z_\text{local}}
+ \sum_\text{sides} \frac{1}{Z_\text{side}}
and the residual of that identity is reported next to the values, so a study
never has to take it on trust. Because Z_side is source-free it does not
change when the fault bus's own rho-f characteristic is varied -- which is the
useful statement: the network's parallel contribution is a fixed property, the
local electrode is the variable, and the total is their parallel combination.
An earlier formulation isolated each side into its own sub-network instead. That
is equivalent wherever the sides are galvanically separate, and wrong in a
ring: removing one branch of a ring separates nothing, the far side comes out
empty and the impedance comes out infinite. Current division has no such blind
spot -- in a ring both directions simply see much of the same network, which is
reported through sides_are_disjoint.
Currents -- how the fault current actually divides. Taken from a solved fault. Across a cut, the current crossing from the far side splits into a metallic part through the cable shields and a part travelling through the soil:
.. math::
\underline{I}_\text{total} = \underline{I}_\text{shield}
+ \underline{I}_\text{earth}
where I_earth is the sum of the electrode currents of every bus on the far
side and I_total is the sum of the source injections there. The reduction
factor of that side is r = |I_earth| / |I_total| -- the share of the current
crossing the cut that returns through the earth rather than through the shields.
Unlike the EPR-based reduction factor on Result, this one does respond to
the impedance at the fault bus, because the split between electrode and shields
is decided by that impedance.
A cut whose far side carries no source has I_total = 0: nothing has to
cross it, and whatever does is driven purely by the coupling. r is then
None rather than a division by zero, and Z_side is the quantity that
carries the meaning for that side.
I_earth, I_total and r all need a far side that belongs to one cut
alone. Where the sides overlap -- a ring -- they are None and
current_share = |I_shield| / |I_inj at the fault bus| is the quantity that
still holds: the fraction of the fault current that leaves the fault location
metallically in that direction. Together with the local share it always adds up
to the injected current.
Contract
Every branch of a cut must be incident to the fault bus. That is what makes the sides parallel elements at the fault bus and the identity above exact; a cut placed further out is a legitimate thing to want, but it is not a parallel element of the fault location and the sum would not close. Branches incident to the fault bus that no cut claims are collected into an implicit side so the decomposition always covers the whole network.
Cut ¶
Bases: BaseModel
A named set of branches separating one side of the network from the fault.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Label used in the result frame. Must not collide with the reserved
name |
branches |
list of str
|
Names of the branches forming the cut. All of them must exist, be active, carry a grounding conductor and be incident to the fault bus. |
description |
(str, optional)
|
Free text carried through to the result frame. |
CutAnalysis ¶
Bases: BaseModel
Result of :func:analyze_cuts for one fault.
Attributes:
| Name | Type | Description |
|---|---|---|
fault |
str
|
Name of the analysed fault. |
fault_bus |
str
|
Bus the fault sits on -- the reference point of the decomposition. |
frequencies |
list of float
|
Frequencies the analysis was run at. |
sides |
dict of str to list of str
|
Buses visible from the fault bus when looking out through that cut, with the other cuts' branches removed. In a ring the entries overlap. |
sides_are_disjoint |
bool
|
|
branches |
dict of str to list of str
|
The branches of each cut, including the implicit |
z_local |
dict of float to complex
|
Impedance of the fault bus's own electrode per frequency. |
z_side |
dict of str to dict of float to complex
|
Impedance of each direction, from source-free current division. |
z_parallel |
dict of float to complex
|
|
z_driving_point |
dict of float to complex
|
Driving-point impedance of the complete network at the fault bus,
computed independently. Equal to |
identity_residual |
dict of float to float
|
|
i_shield |
dict of str to dict of float to complex
|
Shield current leaving the fault bus in each direction. Empty when no solved result was available. |
i_fault, i_local |
dict of float to complex
|
Injection at the fault bus and the part of it taken by the fault bus's own electrode. |
current_share |
dict of str to dict of float to float or None
|
|
i_earth, i_total |
dict of str to dict of float to complex or None
|
Soil and total current crossing the cut, summed over the far side.
|
r_side |
dict of str to dict of float to float or None
|
|
kcl_residual |
dict of float to float
|
|
to_polars ¶
Return the analysis as one long-format frame, one row per direction and frequency.
Columns
fault, fault_bus, cut, side_buses, n_side_buses, branches, sides_are_disjoint, frequency_Hz, Z_side_Ohm, Z_side_deg, Z_local_Ohm, Z_parallel_Ohm, Z_driving_point_Ohm, identity_residual, I_fault_A, I_local_A, I_shield_A, I_shield_deg, current_share, I_earth_A, I_total_A, r_side, kcl_residual
Returns:
| Type | Description |
|---|---|
DataFrame
|
One row per (cut, frequency). |
Source code in src/groundinsight/analysis/decomposition.py
analyze_cuts ¶
analyze_cuts(
network: Network,
*,
fault: str,
cuts: Sequence[Cut],
include_currents: bool = True
) -> CutAnalysis
Split the network at the fault bus and quantify each direction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
network
|
Network
|
The network. Bus and branch impedances must have been evaluated at
|
required |
fault
|
str
|
Name of the fault whose bus is the reference of the decomposition. |
required |
cuts
|
sequence of Cut
|
Named sets of branches, each incident to the fault bus and disjoint from
the others. Incident branches that no cut claims form the implicit side
|
required |
include_currents
|
bool
|
If |
True
|
Returns:
| Type | Description |
|---|---|
CutAnalysis
|
Impedances, currents and the residual of the parallel identity. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the fault is unknown, if a cut names a branch that is not an active grounding branch at the fault bus, or if two cuts claim the same branch. |
Examples:
>>> import groundinsight as gi
>>> gi.run_fault(net, "F1")
>>> analysis = gi.analyze_cuts(
... net,
... fault="F1",
... cuts=[gi.Cut(name="left", branches=["L12"]),
... gi.Cut(name="right", branches=["L23"])],
... )
>>> analysis.to_polars()
Source code in src/groundinsight/analysis/decomposition.py
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 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 | |