Analysis routines¶
Higher-level analysis workflows on top of a fully built network: the inverse rho problem at the bus-grounding side, the conductor thermal-limit check that assesses whether a grounding conductor survives the fault current it carries, and its node-side counterpart, the node thermal-limit check for earthing conductors and earth electrodes.
Physical / modelling context¶
A fully built Network solves a forward problem: given the bus
grounding impedances \(Z_{\text{B},i}(\rho_E, f)\), find the EPR at the
fault bus. The natural inverse question — "how poor can the soil
get before the EPR exceeds a touch-voltage limit \(u_{\max}\)?" — is
recurring in safety-engineering workflows: it sets the worst-case
specific earth resistance the network can tolerate while still
satisfying the relevant standard (e.g. EN 50522) at the assumed
fault current.
find_max_rho_scaling solves that problem by log-bisection over
a uniform scaling factor \(c\) of \(\rho_E\) on a user-supplied list of
selected buses. At every trial \(c\), the bus-grounding impedance is
re-evaluated through the bus's own BusType.impedance_formula and
run_fault is invoked. The bracket converges on the largest \(c\) for
which \(|U_\text{EPR}(f)|_{\text{RMS}} \le u_{\max}\) at the fault bus.
The sister routine find_max_rho_f_scaling (and the diagnostic helper
evaluate_max_epr_under_k) extend the same idea to a frequency-
dependent rho-f characteristic: rho is scaled while a separate
factor \(k\) tunes the imaginary, frequency-coupling part of the
formula, so the inverse problem can be posed against a parametric
rho-f curve rather than a single scalar.
The shape of the rho-f curve at each bus is controlled entirely by
the existing BusType.impedance_formula; the analysis routines only
vary the scalar factors, so any user-defined parametric form
(linear, square root, frequency-dependent, …) is supported
transparently. Original rho values are restored via a finally
block, so the network is left untouched even if the bisection fails.
Example¶
import groundinsight as gi
# Assume `net` is a built network with at least one fault and one
# source. Pick the buses whose specific_earth_resistance should be
# scaled jointly — typically all buses sharing a soil environment.
result = gi.find_max_rho_scaling(
network=net,
fault_name="fault1",
bus_names=["bus_substation", "bus_fault"],
u_max=200.0, # touch-voltage limit in volts (RMS)
c_bounds=(0.1, 100.0), # search bracket on the scaling factor
tol_rel=1e-3,
max_iter=40,
)
# `result` is a dict with keys: c_max, u_epr_rms_at_c_max,
# rho_max (per-bus dict), iterations, status, converged,
# c_bracket, bracket_rel_width.
#
# Read `converged` before `c_max`: c_max is always a scaling factor
# whose EPR was measured and found admissible, but only a converged
# search has also shown that nothing meaningfully larger is.
if not result["converged"]:
print(f"not a maximum: {result['status']}, "
f"bracket {result['c_bracket']}")
print(f"c_max = {result['c_max']:.3f}")
print(f"EPR at c_max = {result['u_epr_rms_at_c_max']:.1f} V")
for bus, rho_max in result["rho_max"].items():
print(f" {bus}: rho_max = {rho_max:.0f} Ω·m")
Reading the result¶
c_max carries a one-directional guarantee: the EPR at that scaling
factor was computed and found to satisfy the limit. Whether it is
also the largest such factor depends on how the search ended, and
that is what status reports.
status |
converged |
what c_max means |
|---|---|---|
"converged" |
True |
The bracket closed to within tol_rel. The true threshold lies inside c_bracket. |
"bracket_within_tol_on_entry" |
True |
The supplied bracket was already narrower than tol_rel, so no step was needed. Same guarantee as above. |
"bracket_fully_admissible" |
False |
Every factor in the bracket satisfied the limit, so c_max is the upper bound — a lower bound on the true maximum, since nothing above it was evaluated. c_bracket is (c_hi, inf), so math.isfinite(result["c_bracket"][1]) tests for this case. Widen c_bounds. |
"max_iter_reached" |
False |
The step cap was hit before the bracket closed. c_max is admissible but can be far below the true threshold — on a saturating test network, a cap of 3 instead of 60 came out 81 % low. Raise max_iter, or narrow c_bounds. |
iterations on its own does not identify a case: two of the four
rows above can produce iterations == 0, and their c_max comes from
opposite ends of the bracket. Branch on converged or status, never
on the step count.
bracket_rel_width is (c_hi - c_lo) / c_lo for the reported
bracket — directly comparable against tol_rel, and inf when the
bracket is open upwards.
The arguments are validated strictly, because a limit search that
returns a number nobody can tell apart from an answer is worse than
one that raises. u_max and tol_rel must be finite and strictly
positive — NaN passes a plain <= 0 check and then makes every later
comparison False — c_bounds must be finite, and max_iter must be
an int of at least 1.
For the rho-f variant — useful when the bus impedance carries a
frequency-coupling term whose magnitude should also be probed —
see :func:find_max_rho_f_scaling. It reports the same four
diagnostic keys with the same meaning.
Conductor thermal limits (IEC 60949 / IEC 60909-0)¶
The inverse-rho routines answer a person-safety question (does the
EPR stay below a touch-voltage limit). check_conductor_limits
answers the complementary equipment-integrity question: does the
grounding conductor survive the fault thermally? It compares the
thermally equivalent short-time current
against the adiabatic limit of IEC 60949,
per grounding branch, and reports the utilisation and a pass/fail
flag. A branch is checked only when its BranchType carries both
conductor_material and cross_section_mm2.
What is superposed and what is not¶
This is the modelling rule the whole short-circuit side is built on. The frequency-domain solve superposes the linear AC-RMS currents as always. The IEC 60909 factors \(\kappa\) and \(m\) are non-linear in the fault-loop \(R/X\), so they are applied once, to the already aggregated branch current — \(i_p\) and \(I_{th}\) are never superposed directly.
With several infeeds the DC components add, so the largest possible peak of the total current is the sum of the individual peaks. Written as a single factor on the aggregate that is exactly the current-weighted mean
which resolve_fault_sc_characteristics uses by default
(aggregation="weighted"); it reproduces the sum of the individual
peaks identically. aggregation="max" is the strictly conservative
variant. Reusing one source's \(\kappa\) for all of them is simply
wrong and errs in either direction. Where the simultaneous-peak
assumption is too crude — strongly mixed \(R/X\) infeeds — the
transient solver remains the exact fallback, because it integrates
the actual waveforms instead of applying a standard factor.
import groundinsight as gi
# The characteristics can be set by hand ...
net.sources["src"].r_to_x = 0.1
net.faults["F"].t_k_s = 0.5
# ... or imported from a solved pandapower case, see the I/O page:
# gi.apply_shortcircuit_characteristics(net, net_pp, "F")
gi.run_fault(net, "F")
gi.check_conductor_limits(net, "F").select(
"branch_name", "I_s_rms_A", "i_p_A", "I_th_A",
"I_admissible_A", "utilization", "within_limit",
)
Explicit t_k, kappa / r_to_x and n arguments override whatever
is stored on the model, so sensitivity studies stay possible.
Node thermal limits — earthing conductor vs earth electrode¶
check_conductor_limits assesses the shield / earth wire between
buses. check_node_limits assesses the two grounding elements at a
bus, which EN 50522 / IEC 61936-1 keep strictly apart because they carry
different currents. Confusing them is the classic sizing error, and in a
meshed system it is an order-of-magnitude error in both directions.
| element | German | current | column |
|---|---|---|---|
| earthing conductor | Erdungsleiter | full injected earth-fault current | ResultBus.i_inj |
| earth electrode | Erder | share dissipated into the soil, \(u_\text{EPR}/Z_B\) | ResultBus.ia |
Three physically distinct currents meet at a grounding bus: the lumped
injection i_inj, the electrode current ia, and the branch shield
currents reported per branch on ResultBranch. The nodal balance that
ties them together is
i_inj deliberately excludes the mutual Norton-equivalent injections
of the inductively coupled branches: those model a distributed induced
EMF along the line, not a current entering the node through a lumped
conductor. It is non-zero only at source buses (the infeed) and at the
fault bus (the total fault current).
Data model and current split¶
A bus is assessed per element, and only once its BusType carries both
the material and the cross-section for that element. The two elements are
independent — declaring one does not imply the other.
bt = gi.BusType(
name="tower",
system_type="Tower",
voltage_level=110.0,
impedance_formula="rho * 0.1",
# Erdungsleiter — sized for the full earth-fault current
earthing_conductor_material="Cu",
earthing_conductor_cross_section_mm2=50.0,
earthing_conductor_theta_final_C=gi.final_temperature("Cu", "bare"),
# Erder — sized for what actually reaches the soil, four legs
electrode_material="Steel",
electrode_cross_section_mm2=95.0,
electrode_current_split=0.25,
)
Each element carries a free factor current_split in \((0, 1]\), applied as
\(I_\text{conductor} = I_\text{RMS}\cdot\texttt{current\_split}\). It
expresses how the bus current divides between physically parallel paths
that the nodal model lumps into one: 1.0 (default) for a single
conductor, 1/N for \(N\) parallel legs, 0.5 for a ring fed at one point,
or an IEEE Std 80 division factor. It is deliberately not derived
automatically — the split depends on geometry the nodal model does not
carry — and a value above 1 is rejected at the model level, because that
would not be a split but an error.
gi.run_fault(net, "F")
gi.check_node_limits(net, "F", t_k=0.5, r_to_x=0.1).select(
"bus_name", "element", "I_rms_A", "current_split",
"I_th_A", "I_admissible_A", "utilization", "within_limit",
)
The IEC 60909 excitation (t_k, kappa, m, n) is resolved by the
same helper the branch check uses, so both views of one fault always
agree. Buses whose type declares neither element still appear in the
frame, with the currents filled in and within_limit = None — enough to
size by hand without re-running anything.
Final temperatures¶
FINAL_TEMPERATURES and final_temperature(material, covering) provide
\(\theta_f\) values with the source named inline per entry (National Grid
ETS Table 5a for bare buried conductors, IEC 60364-5-54 Table 54.2 for
PVC / XLPE). The catalog is deliberately incomplete rather than filled
with plausible-looking numbers; final_temperature raises for anything
missing and names EN 50522 Table 2 as the source to consult.
Steel default
The IEC60949_MATERIALS["Steel"] default of 400 °C is higher — i.e.
more permissive, the unsafe direction for a limit check — than the
300 °C the National Grid table gives for bare buried steel. It is left
unchanged because moving a default silently would move every existing
study. Pass theta_final_C explicitly until the value has been checked
against EN 50522 Table 2.
Results computed before this feature
ResultBus.i_inj defaults to 0.0 on results stored by an earlier
version, so the earthing-conductor rows would read as unstressed.
Re-run run_fault after upgrading.
Incomplete results are an error, not a pass¶
Both checks build their frame from the stored result of the fault, so a
missing entry would produce no row — and a missing row is indistinguishable
from a passing one. They therefore verify first that the stored result covers
every branch and every active bus, and raise ValueError otherwise rather
than reporting the missing elements as free of violations.
This matters because the gap is reachable through ordinary use.
ElectricalNetwork.solve_network() replaces network.results[fault] with a
result carrying bus rows only — compute_branch_currents() fills in the
branches, which is why run_fault calls both — so solving a hand-built
ElectricalNetwork to look at \(Y\), \(i\) or \(u\) discards the branch results.
Adding a branch after run_fault leaves the same gap. To inspect the nodal
system without disturbing anything, build the ElectricalNetwork (its
constructor has no side effects) and read the voltages back from
network.results[fault].buses instead of re-solving.
Both frames also carry an explicit schema, so a network genuinely without
branches returns an empty frame that is still selectable and filterable,
rather than a schema-less (0, 0) frame on which pl.col(...) raises.
API reference¶
analysis ¶
Analysis subpackage.
Contains higher-level analysis routines on top of :mod:network_operations,
such as inverse problems for the bus-grounding rho-f characteristic. These
functions assume that the network is already fully built (buses, branches,
faults, sources, paths) and orchestrate repeated :func:run_fault calls.
BusResponse ¶
Bases: BaseModel
Closed-form response of the network to the electrode at one bus.
Built by :func:bus_response. Evaluating it costs no solve.
Attributes:
| Name | Type | Description |
|---|---|---|
fault |
str
|
Fault the response was built for. |
bus |
str
|
Bus whose electrode is the free parameter. |
fault_bus |
str
|
Bus the fault sits on. Used to anchor which group of buses is named as feeding the soil when the earth-return current is split. |
frequencies |
list of float
|
Frequencies covered. |
z_network |
dict of float to complex
|
Driving-point impedance at |
u_open |
dict of float to dict of str to complex
|
Nodal voltages with the local electrode removed. |
z_column |
dict of float to dict of str to complex
|
Voltage at every bus per ampere injected at |
r_epr |
dict of float to float or None
|
The EPR-based reduction factor, carried through because it is constant along the whole curve. |
i_fault |
dict of float to complex
|
Fault current, the negated sum of source injections. Constant. |
admittance ¶
Translate an electrode spelling into a shunt admittance.
None and an infinite impedance both mean no electrode
(Y_B = 0); a zero impedance means an ideal one and is returned as
inf, which :meth:voltages handles as a limit rather than a
division.
Source code in src/groundinsight/analysis/response.py
driving_point ¶
Z_dp = 1 / (Y_B + 1/Z_net) -- the impedance seen at bus.
Source code in src/groundinsight/analysis/response.py
evaluate ¶
Everything the location does for one electrode, as one frame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z_bus
|
(complex, float or None)
|
The electrode. |
required |
label
|
str
|
Value of the |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
One row per frequency: |
Source code in src/groundinsight/analysis/response.py
extremes ¶
The bracket: no electrode, ideal electrode, and the passive worst case.
Three rows per frequency, labelled "open", "ideal" and
"worst_passive". The first two are the endpoints of the curve; the
third is the reactive electrode that maximises the driving-point
magnitude (see :meth:worst_case_electrode).
Source code in src/groundinsight/analysis/response.py
sweep ¶
sweep(
z_values: Sequence[ElectrodeSpec],
*,
labels: Optional[Sequence[str]] = None
) -> pl.DataFrame
Evaluate many electrodes at once. No solve, whatever the length.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z_values
|
sequence
|
Electrodes, in the spellings :meth: |
required |
labels
|
sequence of str
|
One label per value. Defaults to a rendering of each value. |
None
|
Source code in src/groundinsight/analysis/response.py
voltages ¶
Nodal voltages for one electrode at one frequency.
Both endpoints are evaluated as limits, so an ideal electrode gives
exactly zero at bus instead of a very small number.
Source code in src/groundinsight/analysis/response.py
worst_case_electrode ¶
The passive electrode that maximises |Z_dp|, and the value it gives.
Over the closed right half-plane of Y_B the magnitude of
1/(Y_B + Y_net) is largest where the imaginary parts cancel and the
real part is as small as it can be, i.e. at Y_B = -j*Im(Y_net). The
result exceeds |Z_net| only slightly in a cable network, but it is
the true bound rather than an assumed one.
Returns:
| Type | Description |
|---|---|
dict
|
|
Source code in src/groundinsight/analysis/response.py
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
FaultShortCircuitData ¶
Bases: BaseModel
Resolved IEC 60909 characteristics of one fault.
The result of aggregating the per-source data
(:attr:Source.i_k_a <groundinsight.models.core_models.Source.i_k_a>,
r_to_x, kappa) with the fault-level data
(:attr:Fault.t_k_s <groundinsight.models.core_models.Fault.t_k_s>,
n_factor) into the single set of numbers the non-linear 60909
factors are evaluated with.
Attributes:
| Name | Type | Description |
|---|---|---|
fault_name |
str
|
Name of the fault these characteristics belong to. |
frequency |
float
|
Frequency in Hz at which the source currents were weighted. |
kappa |
(float, optional)
|
Effective peak factor, or |
r_to_x |
(float, optional)
|
Effective |
t_k_s |
(float, optional)
|
Fault duration |
n_factor |
float
|
AC-decay heat factor |
m |
(float, optional)
|
DC heat-effect factor at |
i_k_a |
float
|
Arithmetic sum of the participating source injection magnitudes at
|
i_p_a |
(float, optional)
|
|
aggregation |
str
|
|
homogeneous |
bool
|
|
sources |
list of str
|
Names of the sources that contributed a |
sources_without_kappa |
list of str
|
Names of sources that inject current at |
ReferenceCase ¶
ReferenceCase(
name: str,
quantity: str,
conditions: str,
tolerance: float,
run: Callable[[], Tuple[float, float]],
)
One closed-form case and the model run that has to match it.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Short identifier, used as the row key. |
quantity |
str
|
What is being compared, with its unit. |
conditions |
str
|
The boundary conditions under which the closed form holds. A deviation outside tolerance means either the model is wrong or a condition here was not met -- the second is the more common finding. |
tolerance |
float
|
Relative deviation still counted as agreement. |
Source code in src/groundinsight/analysis/reference.py
evaluate ¶
Run the case and return its row.
Source code in src/groundinsight/analysis/reference.py
admissible_short_circuit_current ¶
Adiabatic admissible short-circuit current I_adm = k*S/sqrt(t_k).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
k
|
float
|
Material constant from :func: |
required |
cross_section_mm2
|
float
|
Conductor cross-section |
required |
t_k
|
float
|
Fault duration in seconds. Must be strictly positive. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The admissible short-circuit current in amperes. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/groundinsight/analysis/thermal.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 | |
bus_response ¶
Build the closed-form response of the network to the electrode at one bus.
Requires a solved fault: the assembled nodal system is reused, the bus's own shunt is taken back out of it, and two systems are solved once each -- the fault with the electrode removed, and a unit injection at the bus. Neither depends on the electrode, which is why the result covers every electrode.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
network
|
Network
|
A network with |
required |
fault
|
str
|
The solved fault. |
required |
bus
|
str
|
Bus whose electrode is the free parameter. Defaults to the fault bus. |
None
|
Returns:
| Type | Description |
|---|---|
BusResponse
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the fault has not been solved, if the bus is unknown or inactive, or if the network without that electrode has no path to reference earth -- in which case there is nothing to characterise, because the location's behaviour is its own electrode. |
Examples:
>>> gi.run_fault(net, "F1")
>>> response = gi.bus_response(net, fault="F1")
>>> response.extremes()
>>> response.evaluate(7.5) # any electrode, no solve
Source code in src/groundinsight/analysis/response.py
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 462 463 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 601 602 603 604 605 | |
check_conductor_limits ¶
check_conductor_limits(
network: Network,
fault_name: str,
t_k: Optional[float] = None,
*,
kappa: Optional[float] = None,
r_to_x: Optional[float] = None,
n: Optional[float] = None,
f: Optional[float] = None,
aggregation: str = "weighted"
) -> pl.DataFrame
Check every grounding branch against its adiabatic thermal limit.
For the given fault the RMS shield current of every branch (from the
most recent :func:groundinsight.run_fault) is converted into the
thermally equivalent short-time current I_th = I_s_rms * sqrt(m + n)
(IEC 60909-0) and compared against the admissible adiabatic current
I_adm = k * S / sqrt(t_k) (IEC 60949) of the branch conductor. A
branch is checked only if its :class:BranchType carries both
conductor_material and cross_section_mm2; the others are
reported with within_limit = None.
.. note::
The thermal data is opt-in. A network that declares none of it
still solves and is still reported here — every branch appears with
its currents (I_s_rms_A, i_p_A, I_th_A) and only the
judgement columns are None. Declaring half of it
(a material without a cross-section or the reverse) is the one case
that is announced with a logging.WARNING, because such a row is
indistinguishable from a passing one at a glance.
All three IEC 60909 inputs — t_k, kappa and n — can either
be passed explicitly or be left to the network data:
t_kfalls back toFault.t_k_s,nfalls back toFault.n_factor(default1.0),kappafalls back to the current-weighted aggregation over the feeding sources'kappa/r_to_x, see :func:~groundinsight.analysis.shortcircuit.resolve_fault_sc_characteristics.
An explicit argument always wins over the stored value, so a study can override a single scenario without editing the network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
network
|
Network
|
A network that already has results for |
required |
fault_name
|
str
|
Name of the solved fault whose branch currents are checked. |
required |
t_k
|
float
|
Fault duration |
None
|
kappa
|
float
|
Peak factor |
None
|
r_to_x
|
float
|
|
None
|
n
|
float
|
AC-decay heat factor |
None
|
f
|
float
|
System frequency in Hz used in the |
None
|
aggregation
|
(weighted, max)
|
Only relevant when |
'weighted'
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
One row per branch with the columns |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> gi.check_conductor_limits(net, "F1", t_k=0.5, r_to_x=0.1)
>>> gi.check_conductor_limits(net, "F1") # all inputs from the model
Source code in src/groundinsight/analysis/thermal.py
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 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 | |
check_node_limits ¶
check_node_limits(
network: Network,
fault_name: str,
t_k: Optional[float] = None,
*,
kappa: Optional[float] = None,
r_to_x: Optional[float] = None,
n: Optional[float] = None,
f: Optional[float] = None,
aggregation: str = "weighted",
elements: Sequence[str] = (
"earthing_conductor",
"electrode",
)
) -> pl.DataFrame
Check every bus against the adiabatic thermal limits of its grounding.
The node-side counterpart of :func:check_conductor_limits. Where the
branch check assesses the shield / earth wire between buses, this one
assesses the two elements at a bus, which EN 50522 / IEC 61936-1 keep
strictly apart because they carry different currents:
earthing_conductor
The earthing conductor (Erdungsleiter): the lumped connection
that brings the earth-fault current from the installation into the
grounding system. It carries the full injected current
ResultBus.i_inj — at a source bus the source infeed, at the
fault bus the total fault current — and is therefore the more
heavily stressed of the two.
electrode
The earth electrode (Erder): the buried part, which only
carries the share actually dissipated into the soil at this bus,
ResultBus.ia = u_EPR / Z_B. In a well-meshed system that is a
small fraction of the injection, so sizing the electrode for the
full fault current is wasteful — and sizing the earthing conductor
for the electrode current is dangerous.
Both are reported separately, one row each, in a long-format frame.
An element is assessed only if its :class:BusType carries both the
*_material and the *_cross_section_mm2 field of that prefix;
otherwise the row is reported with within_limit = None.
.. note::
The thermal data is opt-in. A network that declares none of it
still solves and is still reported here — every active bus appears
with its currents and only the judgement columns are None.
Declaring half of it (a material without a cross-section or the
reverse) is the one case that is announced with a
logging.WARNING, because such a row is indistinguishable from a
passing one at a glance.
Current split
Each element carries a free factor current_split in (0, 1]
(BusType.earthing_conductor_current_split /
BusType.electrode_current_split), applied as
I_conductor = I_rms * current_split. It expresses how the bus
current divides between physically parallel paths that the nodal model
lumps into one:
1.0(default) — one conductor / one electrode carries everything.1/N—Nequal parallel paths, e.g.0.25for four down-conductors from a substation steelwork to the grid.0.5— a ring electrode fed at a single point: the current splits into the two halves of the ring.- any IEEE Std 80 style split / division factor, entered directly.
The factor is deliberately not derived automatically: the split
depends on the physical arrangement inside the substation, which the
nodal grounding model does not resolve. Values above 1 are rejected
at the model level — more current than the bus carries is an error, not
a split.
Superposition
Identical to the branch check: the frequency-domain solve superposes
the linear AC RMS currents, and the non-linear IEC 60909 factors
kappa / m are applied once to the aggregate. i_p and
I_th are never superposed directly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
network
|
Network
|
A network that already has results for |
required |
fault_name
|
str
|
Name of the solved fault whose bus currents are checked. |
required |
t_k
|
float
|
Fault duration |
None
|
kappa
|
float
|
Peak factor |
None
|
r_to_x
|
float
|
|
None
|
n
|
float
|
AC-decay heat factor |
None
|
f
|
float
|
System frequency in Hz used in the |
None
|
aggregation
|
(weighted, max)
|
Only relevant when |
'weighted'
|
elements
|
sequence of str
|
Which of the two elements to report. Pass a single-element sequence to restrict the frame. |
``('earthing_conductor', 'electrode')``
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Long format, one row per bus and requested element, with the
columns |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown entry in |
Notes
A bus that is both the fault bus and the only source bus gets
i_inj = 0: the current never enters the grounding system, it
circulates in the phase conductor. That is physically correct and not
a missing value.
Results computed before ResultBus.i_inj existed default to 0
for the injection. Re-run :func:run_fault if an earthing-conductor
row reads I_rms_A = 0 at a bus that should be carrying current.
Examples:
>>> gi.run_fault(net, "F1")
>>> gi.check_node_limits(net, "F1").filter(
... pl.col("element") == "electrode"
... )
Source code in src/groundinsight/analysis/thermal.py
763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 | |
classify ¶
classify(
frame: pl.DataFrame,
value: str,
edges: Sequence[float],
*,
labels: Optional[Sequence[str]] = None,
name: str = "class"
) -> pl.DataFrame
Add a class column by binning a numeric column at the given edges.
edges are the interior boundaries: n edges make n + 1 classes.
Bins are closed on the right, so a value exactly on an edge falls into the
lower class -- the conservative reading when the edge is a limit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frame
|
DataFrame
|
Frame to extend. |
required |
value
|
str
|
Numeric column to classify. |
required |
edges
|
sequence of float
|
Strictly increasing interior boundaries, e.g. |
required |
labels
|
sequence of str
|
Names for the classes, |
None
|
name
|
str
|
Name of the added column. Defaults to |
'class'
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
The input with one column added. Rows whose value is null get a null class rather than being forced into the lowest band. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the column is missing or not numeric, if |
Examples:
Source code in src/groundinsight/analysis/statistics.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | |
evaluate_max_epr_under_k ¶
evaluate_max_epr_under_k(
network: Network,
bus_names: List[str],
k: KVector,
*,
fault_scalings: Optional[Dict[float, float]] = None,
run_fault_kwargs: Optional[Dict[str, Any]] = None
) -> Dict[str, float]
Evaluate the RMS EPR at each selected bus for a given k vector.
For every name in bus_names the bus grounding impedance is
overwritten with the rho-f linear form
Z(rho, f) = k1*rho + (k2+jk3)*f + (k4+jk5)*rho*f — using the
bus' own :attr:Bus.specific_earth_resistance for rho — and
the bus is swept as the active fault. An existing fault at that bus
is reused; otherwise a temporary one is created with
fault_scalings (or 1.0 at every simulation frequency by
default) and removed at the end. The original bus impedances, the
temporary faults and the previous active_fault are restored in a
finally block, so the network is left exactly as it was on entry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
network
|
Network
|
The simulation network. Sources, branches, paths and the buses to be swept must already be configured. |
required |
bus_names
|
list of str
|
Buses whose impedance is rewritten and which are swept as fault
locations one by one. Must be non-empty and refer to buses in
|
required |
k
|
tuple of (float, float, float, float, float)
|
Real model parameters |
required |
fault_scalings
|
dict of float to float
|
Frequency-resolved scalings used for any fault that has to be
created on the fly (no pre-existing fault at the swept bus).
Defaults to |
None
|
run_fault_kwargs
|
dict
|
Extra keyword arguments forwarded to :func: |
None
|
Returns:
| Type | Description |
|---|---|
dict of str to float
|
Mapping of each |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/groundinsight/analysis/inverse_rho_f.py
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 | |
final_temperature ¶
Look up a tabulated maximum final temperature theta_f.
Convenience accessor for :data:FINAL_TEMPERATURES, meant to be used when
building a BranchType or BusType so that the source of the
number stays visible in the model::
gi.BusType(..., electrode_theta_final_C=gi.final_temperature("Steel", "bare"))
Which standard the answer comes from depends on the covering: EN 50522 Table 2 for an uninsulated conductor, IEC 60364-5-54 Table 54.2 for an insulated one. The insulated caps are much lower, so the two must not be confused — hence one accessor over one catalogue rather than a per-source lookup the caller has to choose between.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
material
|
str
|
One of the keys of :data: |
required |
covering
|
str
|
Covering / surface treatment key. Uninsulated: |
required |
Returns:
| Type | Description |
|---|---|
float
|
Maximum permissible final temperature in °C. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the material or the material / covering combination is not
tabulated. The message lists what is available — the table is
intentionally incomplete rather than populated with unsourced
values, since an over-estimated |
See Also
CABLE_INSULATION_LIMITS : the insulated subset, on its own, when the conductor material is not known or not relevant.
Examples:
>>> final_temperature("Cu", "bare")
300.0
>>> final_temperature("Cu", "tinned")
150.0
>>> final_temperature("Cu", "PVC")
160.0
Source code in src/groundinsight/analysis/thermal.py
find_max_rho_f_scaling ¶
find_max_rho_f_scaling(
network: Network,
bus_names: List[str],
u_limit: float,
k_ref: KVector,
*,
c_bounds: Tuple[float, float] = (0.001, 1000.0),
tol_rel: float = 0.001,
max_iter: int = 60,
fault_scalings: Optional[Dict[float, float]] = None,
run_fault_kwargs: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]
Find the largest scaling factor c of a reference k-vector.
Performs a log-scale bisection on c so that k = c * k_ref
yields a maximum RMS EPR (across all buses swept as faults) that is
at or below u_limit. k_ref is typically obtained by fitting
the rho-f model above to measured impedance points; this function
answers how much head-room the network has around that
characteristic.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
network
|
Network
|
The simulation network. Buses, branches, sources and paths must already be configured. |
required |
bus_names
|
list of str
|
Buses whose impedance is parameterised by |
required |
u_limit
|
float
|
Upper bound on the RMS EPR (in volts) at any swept bus. Must be finite and strictly positive. |
required |
k_ref
|
tuple of (float, float, float, float, float)
|
Reference 5-tuple |
required |
c_bounds
|
tuple of (float, float)
|
Search interval for |
(0.001, 1000.0)
|
tol_rel
|
float
|
Bisection tolerance on the relative bracket width
|
0.001
|
max_iter
|
int
|
Hard cap on bisection steps. Must be an |
60
|
fault_scalings
|
dict of float to float
|
See :func: |
None
|
run_fault_kwargs
|
dict
|
Forwarded to :func: |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
Mapping with keys
As in :func: |
Raises:
| Type | Description |
|---|---|
ValueError
|
For invalid input -- |
Examples:
>>> import groundinsight as gi
>>> res = gi.find_max_rho_f_scaling(
... network=net, bus_names=["b0", "b1"], u_limit=200.0,
... k_ref=(0.01, 0.0, 0.0, 0.0, 0.0),
... )
Source code in src/groundinsight/analysis/inverse_rho_f.py
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 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 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 | |
find_max_rho_scaling ¶
find_max_rho_scaling(
network: Network,
fault_name: str,
bus_names: List[str],
u_max: float,
*,
c_bounds: Tuple[float, float] = (0.001, 1000.0),
tol_rel: float = 0.001,
max_iter: int = 60,
run_fault_kwargs: Dict[str, Any] = None
) -> Dict[str, Any]
Find the largest uniform rho-scaling factor compatible with an EPR limit.
Bisects the scalar c such that scaling every selected bus'
specific earth resistance to c * rho_0 yields an RMS earth
potential rise at the fault bus that just satisfies
|u_EPR|_rms <= u_max. The bus impedance formula is re-evaluated
through the existing :meth:Bus.calculate_impedance machinery, so
any user-defined rho-f characteristic is honoured.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
network
|
Network
|
The simulation network. Must already contain the named fault, the
sources, the buses listed in |
required |
fault_name
|
str
|
Name of the fault to evaluate. Used as |
required |
bus_names
|
list of str
|
Names of the buses whose specific earth resistance is uniformly
scaled by the same factor |
required |
u_max
|
float
|
Upper bound on the RMS earth potential rise at the fault bus, in
volts. Must be finite and strictly positive. The RMS is taken
over all simulation frequencies, matching :attr: |
required |
c_bounds
|
tuple of (float, float)
|
Search interval for the scaling factor |
(0.001, 1000.0)
|
tol_rel
|
float
|
Relative tolerance on the bracket width
|
0.001
|
max_iter
|
int
|
Hard cap on the number of bisection steps. Must be an |
60
|
run_fault_kwargs
|
dict
|
Extra keyword arguments forwarded to :func: |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
Mapping with keys
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> import groundinsight as gi
>>> res = gi.find_max_rho_scaling(
... network=net, fault_name="flt",
... bus_names=["b0", "b1"], u_max=200.0,
... )
>>> if not res["converged"]:
... print(res["status"], res["c_bracket"])
Source code in src/groundinsight/analysis/inverse_rho.py
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | |
iec60909_m ¶
Heat-effect factor m of the aperiodic (DC) short-circuit component.
m = (exp(4*f*Tk*ln(kappa-1)) - 1) / (2*f*Tk*ln(kappa-1)) per
IEC 60909-0. The limits are handled explicitly: kappa -> 1 (no DC
offset) gives m -> 0; kappa -> 2 (non-decaying DC, X/R -> inf)
gives m -> 2.
Notes
The kappa -> 2 limit is worth spelling out because it is easy to get
backwards. Substituting a = ln(kappa - 1) -> 0 and expanding,
(exp(4 f Tk a) - 1) / (2 f Tk a) -> 4 f Tk a / (2 f Tk a) = 2. A
vanishing-resistance fault therefore carries the maximum DC heat, not
none. pandapower's _calc_ith sets m = 0 for kappa > 1.99,
which is why :func:groundinsight.io.pandapower_sc.read_shortcircuit_results
recomputes I_th with this function instead of copying pandapower's
ith_ka.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kappa
|
float
|
Peak factor |
required |
f
|
float
|
System frequency in Hz (50 or 60). |
required |
t_k
|
float
|
Fault duration |
required |
Returns:
| Type | Description |
|---|---|
float
|
The dimensionless factor |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
Source code in src/groundinsight/analysis/shortcircuit.py
iec60949_k ¶
iec60949_k(
material: str,
theta_initial_C: float = 20.0,
theta_final_C: Optional[float] = None,
) -> float
Material constant k of the adiabatic short-circuit equation.
k = K * sqrt(ln((theta_f + beta) / (theta_i + beta))) per IEC 60949 /
IEC 60364-5-54, with the base constant K and beta taken from
:data:IEC60949_MATERIALS.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
material
|
str
|
One of the keys of :data: |
required |
theta_initial_C
|
float
|
Initial conductor temperature |
20.0
|
theta_final_C
|
float
|
Final (maximum permissible) conductor temperature |
None
|
Returns:
| Type | Description |
|---|---|
float
|
The material constant |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
Source code in src/groundinsight/analysis/thermal.py
kappa_from_r_to_x ¶
Peak factor kappa from the R/X ratio (IEC 60909-0).
kappa = 1.02 + 0.98 * exp(-3 * R/X), bounded to (1, 2].
The R/X to insert is the ratio of the fault loop, not of the
positive-sequence system alone. For a three-phase fault the two
coincide; for the line-to-earth fault that dominates grounding studies
the loop impedance is 2*Z1 + Z0, hence
R/X = (2*R1 + R0) / (2*X1 + X0)
(see :func:groundinsight.io.pandapower_sc.read_shortcircuit_results).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
r_to_x
|
float
|
The |
required |
Returns:
| Type | Description |
|---|---|
float
|
The peak factor |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
Source code in src/groundinsight/analysis/shortcircuit.py
peak_short_circuit_current ¶
Peak short-circuit current i_p = kappa * sqrt(2) * I_k'' (IEC 60909-0).
i_p drives the electrodynamic (mechanical) stress on conductors and
supports; the thermal counterpart is
:func:thermal_equivalent_current.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
i_k
|
float
|
RMS short-circuit current |
required |
kappa
|
float
|
Peak factor from :func: |
required |
Returns:
| Type | Description |
|---|---|
float
|
The peak current in amperes. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
Source code in src/groundinsight/analysis/shortcircuit.py
resolve_fault_sc_characteristics ¶
resolve_fault_sc_characteristics(
network: Network,
fault_name: str,
*,
frequency: Optional[float] = None,
aggregation: str = "weighted"
) -> FaultShortCircuitData
Aggregate the IEC 60909 data of a fault and its sources into one set of characteristics.
This is the executable form of the project's superposition rule (see the
module docstring): the linear RMS currents stay superposed by the
solve, and one effective kappa is resolved here so that the
non-linear 60909 factors are applied exactly once, to the aggregate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
network
|
Network
|
The network holding the fault and its sources. |
required |
fault_name
|
str
|
Name of the fault to resolve. |
required |
frequency
|
float
|
Frequency in Hz at which the source injections are weighted.
Defaults to the lowest positive frequency of |
None
|
aggregation
|
(weighted, max)
|
|
'weighted'
|
Returns:
| Type | Description |
|---|---|
FaultShortCircuitData
|
The resolved characteristics. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> import groundinsight as gi
>>> net = gi.create_network(name="demo", frequencies=[50.0])
>>> bt = gi.BusType(name="b", system_type="s", voltage_level=20.0,
... impedance_formula="rho*0 + 1.0")
>>> _ = gi.create_bus(name="B1", type=bt, network=net)
>>> _ = gi.create_source(name="S1", bus="B1", values={50.0: 1000.0},
... network=net, r_to_x=0.1)
>>> _ = gi.create_fault(name="F1", bus="B1", scalings={50.0: 1.0},
... network=net, t_k_s=0.5)
>>> data = gi.resolve_fault_sc_characteristics(net, "F1")
>>> round(data.kappa, 4)
1.746
Source code in src/groundinsight/analysis/shortcircuit.py
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 462 463 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 | |
run_reference_cases ¶
Run the closed-form reference cases and report the comparison.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cases
|
list of ReferenceCase
|
Defaults to :data: |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
One row per case: |
Examples:
Source code in src/groundinsight/analysis/reference.py
select_rho_f_from_catalog ¶
select_rho_f_from_catalog(
network: Network,
bus_names: List[str],
u_limit: float,
candidates: Dict[str, KVector],
*,
fault_scalings: Optional[Dict[float, float]] = None,
run_fault_kwargs: Optional[Dict[str, Any]] = None,
sort_by: Literal[
"max_epr_asc", "max_epr_desc", "name"
] = "max_epr_asc"
) -> pl.DataFrame
Pick admissible rho-f characteristics from a user-provided catalog.
Evaluates every candidate k in the catalog with
:func:evaluate_max_epr_under_k and reports, per candidate, the
maximum RMS EPR observed across the bus sweep, the per-bus EPRs and
whether the candidate satisfies the limit u_limit. The catalog is
typically a hand-curated list of soil scenarios (e.g. dry sand, wet
clay, permafrost) or rho-f fits from previous measurements.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
network
|
Network
|
The simulation network. Sources, branches and the buses to be swept must already be configured. |
required |
bus_names
|
List[str]
|
Buses whose impedance is parameterised by every
candidate |
required |
u_limit
|
float
|
Upper bound on the RMS EPR (in volts) at any swept bus. Must be strictly positive. |
required |
candidates
|
Dict[str, KVector]
|
Mapping |
required |
fault_scalings
|
Optional[Dict[float, float]]
|
Frequency-resolved scalings for any fault that
has to be created on the fly. See
:func: |
None
|
run_fault_kwargs
|
Optional[Dict[str, Any]]
|
Forwarded to :func: |
None
|
sort_by
|
Literal['max_epr_asc', 'max_epr_desc', 'name']
|
How to sort the result rows.
|
'max_epr_asc'
|
Returns:
| Type | Description |
|---|---|
A Polars DataFrame with one row per candidate and the columns
|
|
- ``"name"`` (str)
|
|
- ``"k1", "k2", "k3", "k4", "k5"`` (float)
|
|
- ``"max_epr_rms_V"`` (float)
|
maximum RMS EPR across the bus sweep, in volts. |
- ``"admissible"`` (bool)
|
|
- one ``"epr_<bus>_V"`` column per swept bus, holding the RMS
|
EPR at that bus when it is the active fault, in volts. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> import groundinsight as gi
>>> from groundinsight.models.core_models import BusType, BranchType
>>> from groundinsight.analysis import select_rho_f_from_catalog
>>> bt = BusType(name="BT", system_type="Grounded",
... voltage_level=20.0,
... impedance_formula="rho * 0.01 + 0*f")
>>> brt = BranchType(name="BRT", grounding_conductor=True,
... self_impedance_formula="(0.25 + I*0.6)*l",
... mutual_impedance_formula="(0.0 + I*0.6)*l")
>>> net = gi.create_network(name="N", frequencies=[50])
>>> _ = gi.create_bus(name="b0", type=bt, network=net)
>>> _ = gi.create_bus(name="b1", type=bt, network=net)
>>> _ = gi.create_branch(name="br", type=brt, from_bus="b0",
... to_bus="b1", length=1.0, network=net)
>>> _ = gi.create_source(name="src", bus="b0",
... values={50: 100.0}, network=net)
>>> catalog = {
... "low": (0.005, 0.0, 0.0, 0.0, 0.0),
... "med": (0.01, 0.0, 0.0, 0.0, 0.0),
... "high": (0.05, 0.0, 0.0, 0.0, 0.0),
... }
>>> df = select_rho_f_from_catalog(
... net, ["b0", "b1"], u_limit=20.0, candidates=catalog,
... )
>>> set(df.columns) >= {"name", "k1", "max_epr_rms_V", "admissible"}
True
Source code in src/groundinsight/analysis/inverse_rho_f.py
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 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 | |
summarize ¶
summarize(
frame: pl.DataFrame,
value: str,
*,
by: Optional[Sequence[str]] = None,
quantiles: Sequence[float] = DEFAULT_QUANTILES
) -> pl.DataFrame
Reduce one column to count, spread, quantiles and extremes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frame
|
DataFrame
|
Long-format results, e.g. from
:meth: |
required |
value
|
str
|
Numeric column to summarise. |
required |
by
|
sequence of str
|
Grouping columns. Without them the whole frame is one group. |
None
|
quantiles
|
sequence of float
|
Quantiles in |
DEFAULT_QUANTILES
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
One row per group: the grouping columns, then |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a named column is missing, if the value column is not numeric, or if
a quantile lies outside |
Examples:
Source code in src/groundinsight/analysis/statistics.py
thermal_equivalent_current ¶
Thermally equivalent short-time current I_th = I_k'' * sqrt(m + n).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
i_k
|
float
|
RMS short-circuit current |
required |
m
|
float
|
DC heat-effect factor from :func: |
required |
n
|
float
|
AC-decay heat factor in |
1.0
|
Returns:
| Type | Description |
|---|---|
float
|
The thermally equivalent current in amperes. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Notes
The bounds are enforced rather than clipped because violating them
fails silently downwards: a negative m, or an n above one
supplied in place of a correct smaller value, changes sqrt(m + n)
without any visible symptom, and a too-small result under-estimates the
thermal stress -- the unsafe direction for a limit check. A caller that
hands in such a value has a bug upstream and should hear about it.
Examples: