Skip to content

Electrical network

Numerical core of groundinsight. ElectricalNetwork assembles the nodal admittance matrix \(Y(f)\) per frequency, fills the right-hand side with source and mutual-coupling Norton currents, solves the linear system via sparse LU and derives branch currents, reduction factors and grounding impedances.

electrical_network

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_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