module for creating an electrical network based on the core models
The network is used to perform calculations based on the matrix form of the network:
Y * u = i
u = Y^-1 * i
where:
Y - admittance matrix, branches and buses are used to build this matrix
u - vector of bus voltages (earth potential rise per bus)
i - vector of source injections plus the Norton-equivalent injections that
represent the inductive coupling between phase conductors and shield
(grounding) conductors along each branch.
Sign convention for the mutual coupling (see _add_mutual_currents):
U_from - U_to = Z_self * I_s - Z_mutual * I_p
where I_p is the phase current through the branch in from->to direction and
I_s is the shield current in the same direction. The induced EMF enters the
nodal form as a Norton current i_mut = (Z_mutual / Z_self) * I_p which is
injected as +i_mut out of the from bus (nodal: i_vector[from] -= i_mut) and
+i_mut into the to bus (nodal: i_vector[to] += i_mut).
Two strategies are available to determine the phase current I_p per branch:
1) Path-based (default). For every simple path from source to fault, the
branch direction is derived from the actual path traversal (no index
heuristic). The user may scale the contribution per branch via
Branch.parallel_coefficient, which is the legacy knob for splitting
current between parallel paths.
2) Automatic distribution (auto_phase_currents=True). A reduced phase-only
network is solved per source with +I at the source bus and the fault bus
as reference. The resulting branch currents are used directly. In this
mode parallel_coefficient is ignored. This mode is topology-agnostic and
is the intended integration point for an external phase-current source
(e.g. pandapower single-phase short-circuit results).
ElectricalNetwork
ElectricalNetwork(
network: Network, auto_phase_currents: bool = False
)
Represents the electrical properties of a network, enabling calculations.
This class handles the construction of admittance matrices, voltage and current vectors,
and performs network analysis to compute results such as bus voltages, branch currents,
reduction factors, and grounding impedances.
Initialize the ElectricalNetwork with a given Network model.
Sets up necessary data structures and initializes the network calculations.
Parameters:
| Name |
Type |
Description |
Default |
network
|
Network
|
The Network instance containing buses, branches, sources, and faults.
|
required
|
auto_phase_currents
|
bool
|
If True the phase current through each
branch is computed by solving a reduced phase-only network (Variant B).
If False (default) the phase current is derived from the enumerated
source-to-fault paths using each branch's parallel_coefficient
(Variant A). Defaults to False.
|
False
|
Source code in src/groundinsight/electrical_network.py
| def __init__(self, network: Network, auto_phase_currents: bool = False):
"""
Initialize the ElectricalNetwork with a given Network model.
Sets up necessary data structures and initializes the network calculations.
Args:
network (Network): The Network instance containing buses, branches, sources, and faults.
auto_phase_currents (bool, optional): If True the phase current through each
branch is computed by solving a reduced phase-only network (Variant B).
If False (default) the phase current is derived from the enumerated
source-to-fault paths using each branch's ``parallel_coefficient``
(Variant A). Defaults to False.
"""
self.network = network
self.auto_phase_currents = auto_phase_currents
self.bus_indices = {}
self.Y_matrices = {} # Admittance matrices for each frequency
self.u_vectors = {} # Voltage vectors for each frequency
self.u_vectors_no_mutual = {} # Voltage vectors without mutual currents
self.i_vectors_no_mutual = {} # Current vectors without mutual currents
self.i_vectors = {} # Current vectors for each frequency
self.results: Result = Result() # Stores the calculation results
self.i_mutuals = {} # Store mutual currents per frequency per branch
self.total_source_currents = {} # Store total source currents per frequency
self.phase_currents = {} # Signed phase current per branch per frequency
self._initialize()
|
compute_branch_currents
compute_branch_currents()
Compute branch currents for each frequency and store them in the Result object.
This method calculates the current flowing through each branch based on the bus voltages
and branch impedances. The results are stored as ResultBranch instances within the
network's results object.
Source code in src/groundinsight/electrical_network.py
| def compute_branch_currents(self):
"""
Compute branch currents for each frequency and store them in the Result object.
This method calculates the current flowing through each branch based on the bus voltages
and branch impedances. The results are stored as `ResultBranch` instances within the
network's results object.
"""
fault_name = self.network.active_fault
if fault_name is None:
raise ValueError("No active fault set in the network.")
if fault_name not in self.network.results:
raise ValueError(f"No results available for fault '{fault_name}'.")
result = self.network.results[fault_name]
for branch in self.network.branches.values():
from_idx = self.bus_indices[branch.from_bus]
to_idx = self.bus_indices[branch.to_bus]
i_s_freq = {}
for freq in self.network.frequencies:
from_voltage = self.u_vectors[freq][from_idx]
to_voltage = self.u_vectors[freq][to_idx]
impedance = branch.self_impedance.get(freq)
if (
impedance is not None
and branch.type.grounding_conductor
and complex(impedance.real, impedance.imag) != 0
):
Z_self_complex = complex(impedance.real, impedance.imag)
Y_self_complex = 1 / Z_self_complex
delta_voltage = to_voltage - from_voltage
# Stored mutual term already carries the sign to combine with
# delta_voltage = u_to - u_from. See _add_mutual_currents.
i_mutual = self.i_mutuals.get(freq, {}).get(branch.name, 0)
current = delta_voltage * Y_self_complex + i_mutual
i_s_freq[freq] = ComplexNumber(real=current.real, imag=current.imag)
else:
i_s_freq[freq] = ComplexNumber(real=0.0, imag=0.0)
# Calculate RMS current
rms_current = self._calculate_rms(i_s_freq)
result_branch = ResultBranch(
name=branch.name, i_s=rms_current, i_s_freq=i_s_freq
)
result.branches.append(result_branch)
# Update the result in the network's results dictionary
self.network.results[fault_name] = result
self.results = result # Update self.results
|
compute_grounding_impedance
compute_grounding_impedance()
Compute the grounding impedance for the fault bus.
This method calculates the grounding impedance using the formula
grounding_impedance = uepr / (reduction_factor * sum of all fault currents at the fault bus)
The results are stored in the network's results object.
Source code in src/groundinsight/electrical_network.py
| def compute_grounding_impedance(self):
"""
Compute the grounding impedance for the fault bus.
This method calculates the grounding impedance using the formula:
grounding_impedance = uepr / (reduction_factor * sum of all fault currents at the fault bus)
The results are stored in the network's results object.
"""
fault_name = self.network.active_fault
if fault_name is None:
raise ValueError("No active fault set in the network.")
fault_bus = self.network.faults[fault_name].bus
fault_bus_idx = self.bus_indices[fault_bus]
grounding_impedances = (
{}
) # Dictionary to store grounding impedance per frequency
frequencies = self.network.frequencies
result = self.network.results[fault_name]
# Ensure that reduction factors are computed
if not result.reduction_factor:
raise ValueError(
"Reduction factors not computed. Please compute reduction factors before grounding impedance."
)
for freq in frequencies:
# Get uepr at the fault bus
voltage = self.u_vectors[freq][fault_bus_idx]
uepr = voltage # Complex voltage at the fault bus
# Get reduction factor at this frequency
reduction_factor = result.reduction_factor.value.get(freq)
if reduction_factor is None or reduction_factor == 0:
grounding_impedances[freq] = None # Cannot compute
continue
# Get total source current at this frequency
total_source_current = self.total_source_currents.get(freq)
if total_source_current is None or total_source_current == 0:
grounding_impedances[freq] = None
continue
# The fault current is negative of total_source_current
I_fault = -total_source_current # Current flowing into the fault bus
# Compute grounding impedance
try:
grounding_impedance = uepr / (reduction_factor * I_fault)
grounding_impedances[freq] = ComplexNumber(
real=grounding_impedance.real, imag=grounding_impedance.imag
)
except ZeroDivisionError:
grounding_impedances[freq] = None # Handle division by zero
# Store the grounding impedance in the result
result_grounding_impedance = ResultGroundingImpedance(
fault_bus=fault_bus, value=grounding_impedances
)
result.grounding_impedance = result_grounding_impedance
# Update the result in the network's results dictionary
self.network.results[fault_name] = result
self.results = result # Update self.results
|
compute_reduction_factors
compute_reduction_factors()
Compute the reduction factors by solving the network with and without mutual currents.
This method calculates how much the presence of mutual currents affects the Earth Potential Rise (EPR).
The reduction factors are stored in the network's results object.
Source code in src/groundinsight/electrical_network.py
| def compute_reduction_factors(self):
"""
Compute the reduction factors by solving the network with and without mutual currents.
This method calculates how much the presence of mutual currents affects the Earth Potential Rise (EPR).
The reduction factors are stored in the network's results object.
"""
fault_name = self.network.active_fault
if fault_name is None:
raise ValueError("No active fault set in the network.")
fault_bus = self.network.faults[fault_name].bus
fault_bus_idx = self.bus_indices[fault_bus]
reduction_factors = {}
uepr_with_mutual = {}
uepr_without_mutual = {}
frequencies = self.network.frequencies
# Step 1: Solve network with mutual currents (already done)
# Voltages are stored in self.u_vectors
# Store uepr with mutual currents
for freq in frequencies:
voltage = self.u_vectors[freq][fault_bus_idx]
uepr_with_mutual[freq] = voltage
# Step 2: Create i_vectors without mutual currents
self._construct_vectors_no_mutual()
# Step 3: Solve network without mutual currents
self.u_vectors_no_mutual = {}
for freq in frequencies:
Y_matrix = self.Y_matrices[freq]
i_vector = self.i_vectors_no_mutual[freq]
try:
# Solve for u_vector without mutual currents
u_vector = np.linalg.solve(Y_matrix, i_vector)
self.u_vectors_no_mutual[freq] = u_vector
except np.linalg.LinAlgError as e:
print(
f"Error solving network equations at frequency {freq} without mutual currents: {e}"
)
continue
# Store uepr without mutual currents
for freq in frequencies:
voltage = self.u_vectors_no_mutual[freq][fault_bus_idx]
uepr_without_mutual[freq] = voltage
# Step 4: Compute reduction factors
for freq in frequencies:
v_with = uepr_with_mutual[freq]
v_without = uepr_without_mutual[freq]
# Compute magnitudes
mag_with = abs(v_with)
mag_without = abs(v_without)
if mag_without != 0:
reduction_factor = mag_with / mag_without
else:
reduction_factor = None # Handle division by zero
reduction_factors[freq] = reduction_factor
# Store the reduction factors in the result
result = self.network.results[fault_name]
result_reduction_factor = ResultReductionFactor(
fault_bus=fault_bus, value=reduction_factors
)
result.reduction_factor = result_reduction_factor
# Update the result in the network's results dictionary
self.network.results[fault_name] = result
self.results = result # Update self.results
|
solve_network
Solve the network equations Y * u = i for each frequency.
This method computes the bus voltages by solving the admittance matrix equations for each frequency.
The results are stored in the network's results object.
It uses the csc_matrix and splu functions from scipy, assuming the Y-Matrix is a sparse matrix.
Source code in src/groundinsight/electrical_network.py
| def solve_network(self):
"""
Solve the network equations Y * u = i for each frequency.
This method computes the bus voltages by solving the admittance matrix equations for each frequency.
The results are stored in the network's results object.
It uses the csc_matrix and splu functions from scipy, assuming the Y-Matrix is a sparse matrix.
"""
fault_name = self.network.active_fault
if fault_name is None:
raise ValueError("No active fault set in the network.")
result = Result(buses=[], branches=[], fault=fault_name)
for freq in self.network.frequencies:
Y_matrix = self.Y_matrices[freq]
Y_matrix_sparse = csc_matrix(Y_matrix)
lu = splu(Y_matrix_sparse)
i_vector = self.i_vectors[freq]
try:
# Solve for u_vector
u_vector = lu.solve(i_vector)
self.u_vectors[freq] = u_vector
except np.linalg.LinAlgError as e:
print(f"Error solving network equations at frequency {freq}: {e}")
continue
# Create ResultBus instances
for bus_name, idx in self.bus_indices.items():
uepr_freq = {}
ia_freq = {}
for freq in self.network.frequencies:
voltage = self.u_vectors[freq][idx]
bus = self.network.buses.get(bus_name)
impedance = bus.impedance.get(freq)
if impedance is None:
current = 0
else:
Z_self_complex = complex(impedance.real, impedance.imag)
if Z_self_complex == 0:
current = 0
else:
current = voltage / Z_self_complex
uepr_freq[freq] = ComplexNumber(real=voltage.real, imag=voltage.imag)
ia_freq[freq] = ComplexNumber(
real=complex(current).real, imag=complex(current).imag
)
# Calculate RMS values
rms_voltage = self._calculate_rms(uepr_freq)
rms_current = self._calculate_rms(ia_freq)
result_bus = ResultBus(
name=bus_name,
uepr=rms_voltage,
ia=rms_current,
uepr_freq=uepr_freq,
ia_freq=ia_freq,
)
result.buses.append(result_bus)
# Store the result in the network's results dictionary
self.network.results[fault_name] = result
self.results = result # Also keep a reference in self.results
|