Object-Oriented Programming and DC Power Flow#

Information

Details

Lead Author

Hantao Cui

Learning Objectives

• Understand OOP basics through power system components
• Create Bus, Branch, and Generator classes
• Build system-level analysis using component classes
• Parse power system data from Excel files
• Implement DC power flow analysis

Prerequisites

• Python and NumPy basics
• Power system fundamentals

Estimated Time

90-150 minutes

Topics

• OOP in power systems
• Component modeling
• Data parsing
• DC power flow
• System analysis

Required Packages

• NumPy
• Pandas

Development Status

Completed

Last Updated on

April 24, 2025

OOP Introduction in Power Systems Context#

Object-oriented programming (OOP) is a programming paradigm that uses “objects” – instances of classes – to structure code. In power systems analysis, OOP offers significant advantages by allowing us to model physical components (buses, generators, lines) as software objects that correspond naturally to real-world entities.

Power systems are inherently complex networks of interconnected components with specific behaviors. OOP helps manage this complexity by:

  1. Encapsulating data and functionality together in classes

  2. Abstracting away details so we can focus on the important aspects

  3. Enabling inheritance to share common functionality

  4. Providing polymorphism for specialized implementations of common interfaces

For instance, a transmission line in a power grid has properties (resistance, reactance) and behaviors (power flow). Using OOP, we can create a Line class that contains both these properties and methods that calculate relevant values.

Practical Considerations

When modeling power systems with OOP, try to match your class structures to physical components. This makes your code more intuitive and easier to maintain.

Designing Power System Component Classes#

Let’s create a set of classes to model a power system. We’ll start with a BaseModel abstract class that our other classes will inherit from:

BaseModel Class#

import numpy as np
from abc import ABC, abstractmethod

class BaseModel(ABC):
    """
    Base class for all power system model components.
    Provides common functionality and enforces interface requirements.
    """
    def __init__(self):
        # Basic tracking of object properties
        self.addresses = {}
        self.values = {}

    @abstractmethod
    def get_count(self):
        """Return the number of components in the model."""
        raise NotImplementedError

    def str(self):
        """Return a string representation of the model component."""
        return f"{self.__class__.__name__} object"

    def repr(self):
        """Return a detailed string representation of the model component."""
        return f"{self.__class__.__name__}"

The BaseModel class serves as a foundation for all power system components in our code. Think of it as a blueprint that defines what every power system component (like buses, generators, or transmission lines) should be able to do. It uses Python’s Abstract Base Class (ABC) feature, which is like creating a template that other classes must follow.

Let’s break it down with a real-world analogy: Imagine you’re designing different types of vehicles. Every vehicle (car, truck, motorcycle) needs certain basic features - they all need to know how many wheels they have (like our get_count() method), and they all need to be describable (like our str() and repr() methods). The @abstractmethod on get_count() is like saying “every vehicle MUST have a way to count its wheels, but each type will count them differently.”

The addresses and values dictionaries in __init__() are like a vehicle’s registration book. They keep track of important information about the component. When we create specific components (like a Bus or Generator class), they will inherit from this BaseModel, just like how a Car class inherits from a Vehicle class - they get all these basic features and must implement the required methods.

Required abstract methods

When you create a new class that inherits from BaseModel, you must implement the get_count() method, or Python will give you an error. This helps ensure that all your power system components behave consistently.

Now, let’s implement the Bus class to store bus data:

Bus Class#

import numpy as np

class Bus(BaseModel):
    """
    Bus class to store bus data.

    Attributes
    ----------
    bus_i : int
        Bus number
    bus_type : int
        Bus type (1=PQ, 2=PV, 3=slack, 4=isolated)
    Pd : float
        Active power demand (MW)
    Qd : float
        Reactive power demand (MVAr)
    Gs : float
        Shunt conductance (MW demanded at V = 1.0 p.u.)
    Bs : float
        Shunt susceptance (MVAr injected at V = 1.0 p.u.)
    area : int
        Area number
    Vm : float
        Voltage magnitude (p.u.)
    Va : float
        Voltage angle (degrees)
    baseKV : float
        Base voltage (kV)
    zone : int
        Loss zone
    Vmax : float
        Maximum voltage magnitude (p.u.)
    Vmin : float
        Minimum voltage magnitude (p.u.)
    """

    def __init__(self, bus_i, type, Pd, Qd, Gs, Bs,
                 area, Vm, Va, baseKV, zone, Vmax, Vmin):
        """Initialize the Bus class."""
        super().__init__()
        self.bus_i = bus_i
        self.bus_type = np.array(type)
        self.Pd = np.array(Pd)
        self.Qd = np.array(Qd)
        self.Gs = np.array(Gs)
        self.Bs = np.array(Bs)
        self.area = area
        self.Vm = np.array(Vm)
        self.Va = np.array(Va)
        self.baseKV = np.array(baseKV)
        self.zone = zone
        self.Vmax = np.array(Vmax)
        self.Vmin = np.array(Vmin)

    def get_count(self):
        """Return the number of buses."""
        if isinstance(self.bus_i, list):
            return len(self.bus_i)
        else:
            return 1

    def is_slack(self):
        """Check if the bus is a slack bus (type 3)."""
        return self.bus_type == 3

    def is_pv(self):
        """Check if the bus is a PV bus (type 2)."""
        return self.bus_type == 2

    def is_pq(self):
        """Check if the bus is a PQ bus (type 1)."""
        return self.bus_type == 1

The Bus class represents an electrical bus (node) in a power system, inheriting from our BaseModel blueprint. Each bus has specific characteristics like its number (bus_i), type (PQ, PV, or slack), power demands (Pd, Qd), and voltage parameters (Vm, Va). When we create a new bus, the __init__ method sets up all these properties, using NumPy arrays for values that need mathematical operations. The super().__init__() call is crucial.It’s like telling the bus “before you set up your own properties, make sure you have all the basic features from BaseModel.” Think of it as building a specialized electric vehicle: first you need the basic vehicle frame (called by super()), then you add the electric-specific components.

The class also provides helper methods to identify the bus type (is_slack(), is_pv(), is_pq()), which is essential for power flow calculations. These methods make the code more readable and maintainable: instead of checking bus_type == 3 everywhere in your code, you can use the more descriptive is_slack(). The get_count() method, which we inherited from BaseModel, is implemented to handle both single buses and lists of buses, making the class flexible for different use cases.

Understanding super()

The super().__init__() call is like building a house: you need the foundation (BaseModel) before adding the rooms and furniture (Bus-specific features). Without this call, you’d lose all the basic functionality that BaseModel provides. Always call super().__init__() when inheriting from another class!

Power System Bus Types

  • PQ Bus (Type 1): Known real and reactive power

  • PV Bus (Type 2): Known real power and voltage magnitude (typically generators)

  • Slack Bus (Type 3): Reference bus with known voltage magnitude and angle

  • Isolated Bus (Type 4): Not connected to the system

These are the definitions from the MATPOWER format, which may have been inspired by earlier data formats.

Branch Class#

import numpy as np

class Branch(BaseModel):
    """
    Branch class to store branch data (transmission lines and transformers).

    Attributes
    ----------
    fbus : int
        "From" bus number
    tbus : int
        "To" bus number
    r : float
        Resistance (p.u.)
    x : float
        Reactance (p.u.)
    b : float
        Total line charging susceptance (p.u.)
    rateA : float
        MVA rating A (long term rating)
    rateB : float
        MVA rating B (short term rating)
    rateC : float
        MVA rating C (emergency rating)
    ratio : float
        Transformer off nominal turns ratio
    angle : float
        Transformer phase shift angle (degrees)
    status : int
        Initial branch status, 1 = in-service, 0 = out-of-service
    angmin : float
        Minimum angle difference (degrees)
    angmax : float
        Maximum angle difference (degrees)
    """

    def __init__(self, fbus, tbus, r, x, b, rateA, rateB, rateC, ratio, angle, status, angmin, angmax):
        """Initialize the Branch class."""
        super().__init__()
        self.fbus = fbus
        self.tbus = tbus
        self.r = np.array(r)
        self.x = np.array(x)
        self.b = np.array(b)
        self.rateA = rateA
        self.rateB = rateB
        self.rateC = rateC
        self.ratio = ratio
        self.angle = angle
        self.status = status
        self.angmin = angmin
        self.angmax = angmax

    def get_count(self):
        """Return the number of branches."""
        return len(self.fbus) if isinstance(self.fbus, list) else 1

Generator Class#

import numpy as np

class Generator(BaseModel):
    """
    Generator class to store generator data.

    Attributes
    ----------
    bus : np.ndarray
        Bus number
    Pg : np.ndarray
        Active power output (MW)
    Qg : np.ndarray
        Reactive power output (MVAr)
    Qmax : np.ndarray
        Maximum reactive power output (MVAr)
    Qmin : np.ndarray
        Minimum reactive power output (MVAr)
    Vg : np.ndarray
        Voltage magnitude setpoint (p.u.)
    mBase : np.ndarray
        Total MVA base of machine (MVA)
    status : np.ndarray
        Machine status, 1 = in-service, 0 = out-of-service
    Pmax : np.ndarray
        Maximum active power output (MW)
    Pmin : np.ndarray
        Minimum active power output (MW)
    """

    def __init__(self, bus, Pg, Qg, Qmax, Qmin, Vg, mBase, status, Pmax, Pmin,
                 Pc1=0, Pc2=0, Qc1min=0, Qc1max=0, Qc2min=0, Qc2max=0,
                 ramp_agc=0, ramp_10=0, ramp_30=0, ramp_q=0, apf=0):
        """Initialize the Generator class with all fields as NumPy arrays."""
        super().__init__()
        self.bus = np.array(bus)
        self.Pg = np.array(Pg)
        self.Qg = np.array(Qg)
        self.Qmax = np.array(Qmax)
        self.Qmin = np.array(Qmin)
        self.Vg = np.array(Vg)
        self.mBase = np.array(mBase)
        self.status = np.array(status)
        self.Pmax = np.array(Pmax)
        self.Pmin = np.array(Pmin)

        # Optional cost and ramp rate parameters (scalar or vector)
        self.Pc1 = np.array(Pc1)
        self.Pc2 = np.array(Pc2)
        self.Qc1min = np.array(Qc1min)
        self.Qc1max = np.array(Qc1max)
        self.Qc2min = np.array(Qc2min)
        self.Qc2max = np.array(Qc2max)
        self.ramp_agc = np.array(ramp_agc)
        self.ramp_10 = np.array(ramp_10)
        self.ramp_30 = np.array(ramp_30)
        self.ramp_q = np.array(ramp_q)
        self.apf = np.array(apf)

    def get_count(self):
        """Return the number of generators."""
        return len(self.bus)

    def is_active(self):
        """
        Return a boolean array indicating which generators are active.

        Returns
        -------
        np.ndarray
            Boolean array of active status flags.
        """
        return self.status == 1

Format

The classes above follow the MATPOEWR convention for power system data. MATPOWER is a widely used open-source package for power system analysis in MATLAB.

Now we’ll create a PowerSystem class to contain these components and provide system-level functionality:

PowerSystem Class#

import numpy as np

class PowerSystem:
    """
    PowerSystem class to store and manage power system data.

    This class contains buses, branches (lines/transformers), and generators.
    It provides methods for system-wide operations like power flow analysis.
    """

    def __init__(self, bus=None, branch=None, gen=None):
        """Initialize the PowerSystem class."""
        self.bus = bus
        self.branch = branch
        self.gen = gen
        self.baseMVA = 100.0  # Default base MVA

    def get_count(self):
        """
        Return the number of components in the system.

        Returns
        -------
        dict
            Dictionary with counts of buses, branches, and generators.
        """
        return {
            'buses': self.bus.get_count() if self.bus else 0,
            'branches': self.branch.get_count() if self.branch else 0,
            'generators': self.gen.get_count() if self.gen else 0
        }

    def get_adjacency_matrix(self):
        """
        Create an adjacency matrix for the network topology.

        Returns
        -------
        np.ndarray
            Adjacency matrix representing connectivity between buses.
        """
        n_bus = self.bus.get_count()
        adj_matrix = np.zeros((n_bus, n_bus))

        for i in range(self.branch.get_count()):
            from_bus = self.branch.fbus[i] - 1  # Convert to 0-indexing
            to_bus = self.branch.tbus[i] - 1
            if self.branch.status[i] == 1:  # Only in-service branches
                adj_matrix[from_bus, to_bus] = 1
                adj_matrix[to_bus, from_bus] = 1  # Undirected graph

        return adj_matrix

    def __str__(self):
        """
        Return a string representation of the power system.

        Returns
        -------
        str
            Summary of the power system components.
        """
        counts = self.get_count()
        return (
            f"PowerSystem with {counts['buses']} buses, "
            f"{counts['branches']} branches, and "
            f"{counts['generators']} generators"
        )

The PowerSystem class serves as the central orchestrator in our power system analysis framework, bringing together all the individual components (buses, branches, and generators) into a cohesive system. Think of it as a conductor of an orchestra - while each musician (component) knows how to play their instrument, the conductor ensures they work together harmoniously. The class maintains these components and provides a common interface to access and manipulate them. The baseMVA attribute sets the system’s base power, which is crucial for per-unit calculations, defaulting to the industry-standard value of 100 MVA.

Beyond just storing components, the PowerSystem class provides system-wide analysis capabilities. Its get_adjacency_matrix() method creates a mathematical representation of how buses are connected through branches, which is fundamental for network analysis. This matrix representation is like a map showing which buses are connected, with 1’s indicating connections and 0’s indicating no direct connection.

The class’s __str__() method provides a human-readable summary of the system’s size, making it easy to quickly understand the scale of the power system being analyzed or other general information. This orchestration role makes PowerSystem the primary interface through which most power system analyses (like power flow studies) will be conducted.

Adjacency Matrix

The adjacency matrix is a fundamental tool in network analysis:

# For a 3-bus system with branches between buses 1-2 and 2-3:
[[0, 1, 0],  # Bus 1's connections
 [1, 0, 1],  # Bus 2's connections
 [0, 1, 0]]  # Bus 3's connections

Each row and column represents a bus, and 1’s indicate direct connections.

Data Parsing from XLSX (MATPOWER Format)#

To use our classes, we need to load data from Excel files. Let’s implement functions to read data from Excel and convert it to our class objects:

Data Parsing Functions#

import pandas as pd

def read_from_excel(path):
    """
    Read power system data from Excel file.

    Parameters
    ----------
    path : str
        Path to the Excel file

    Returns
    -------
    dict
        Dictionary containing pandas DataFrames for bus, gen, and branch data
    """
    # Read all sheets from the Excel file
    data_dict = pd.read_excel(path, sheet_name=None)

    # Verify that the required sheets exist
    required_sheets = ['bus', 'gen', 'branch']
    for sheet in required_sheets:
        if sheet not in data_dict:
            raise ValueError(f"Required sheet '{sheet}' not found in Excel file")

    return data_dict


def column_to_obj(df, cls):
    """
    Convert a DataFrame to an object.

    Parameters
    ----------
    df : pd.DataFrame
        DataFrame to be converted
    cls : class
        Class to be instantiated

    Returns
    -------
    object
        Object of the specified class
    """
    # Create a dictionary of column names and lists of values
    obj_dict = {col: df[col].tolist() for col in df.columns}
    return cls(**obj_dict)


# Map sheet names to their corresponding classes
name_to_class = {
    'bus': Bus,
    'branch': Branch,
    'gen': Generator
}


def create_system_from_data(data):
    """
    Create a PowerSystem object from a dictionary of data.

    Parameters
    ----------
    data : dict
        Dictionary of DataFrames

    Returns
    -------
    PowerSystem
        Instantiated PowerSystem object
    """
    model_objects = {key: column_to_obj(data[key], cls) for key, cls in name_to_class.items()}
    return PowerSystem(**model_objects)


def load_data(path):
    """
    Load data from an Excel file and create a PowerSystem object.

    Parameters
    ----------
    path : str
        Path to the Excel file

    Returns
    -------
    PowerSystem
        PowerSystem object
    """
    data = read_from_excel(path)
    return create_system_from_data(data)

Let’s see how this works with a simple example:

# Load a power system from an Excel file
system = load_data('case3.xlsx')
print(system)

# Access system components
print(f"Voltage at bus 1: {system.bus.Vm[0]:.4f} p.u.")
print(f"First branch connects bus {int(system.branch.fbus[0])} to bus {int(system.branch.tbus[0])}")
PowerSystem with 3 buses, 3 branches, and 1 generators
Voltage at bus 1: 1.0200 p.u.
First branch connects bus 1 to bus 2

Building the Admittance Matrix#

The admittance matrix (Y-bus) is fundamental in power system analysis. It represents the network’s topology and electrical characteristics. Let’s implement a method to build this matrix in our PowerSystem class:

Y-Bus Matrix Construction#

def build_ybus(self):
    """
    Build the bus admittance matrix (Y-bus) for the power system.

    Returns
    -------
    np.ndarray
        Complex bus admittance matrix (Y-bus)
    """
    n_bus = self.bus.get_count()
    y_bus = np.zeros((n_bus, n_bus), dtype=complex)

    # Add branch admittances
    for i in range(self.branch.get_count()):
        if self.branch.status[i] == 0:  # Skip out-of-service branches
            continue

        # Get bus indices (convert from 1-indexing to 0-indexing)
        f_bus = int(self.branch.fbus[i]) - 1
        t_bus = int(self.branch.tbus[i]) - 1

        # Branch parameters
        r = self.branch.r[i]
        x = self.branch.x[i]
        b = self.branch.b[i]

        # Series admittance
        y_series = 1.0 / complex(r, x)

        # Shunt admittance (half at each end)
        y_shunt = complex(0, b / 2)

        # Off-diagonal elements (mutual admittances)
        y_bus[f_bus, t_bus] -= y_series
        y_bus[t_bus, f_bus] -= y_series

        # Diagonal elements (self admittances)
        y_bus[f_bus, f_bus] += y_series + y_shunt
        y_bus[t_bus, t_bus] += y_series + y_shunt

    # Add bus shunt admittances (from bus data)
    for i in range(n_bus):
        gs = self.bus.Gs[i]
        bs = self.bus.Bs[i]
        y_bus[i, i] += complex(gs, bs) / self.baseMVA

    return y_bus

Add this method to the PowerSystem class.

PowerSystem.build_ybus = build_ybus

The line above assigns the method to the PowerSystem class. This is allowed in Python.

Unit

The Y-bus matrix construction assumes that all values are in per-unit. Make sure your input data is properly converted before using this method.

Let’s look at the Y-bus matrix in more detail:

  1. The diagonal elements of Y-bus (Y_ii) represent the sum of all admittances connected to bus i.

  2. The off-diagonal elements (Y_ij) represent the negative of the admittance between buses i and j.

The Y-bus matrix is used in both power flow and short circuit analysis. For a system with n buses, the Y-bus matrix is an n×n complex-valued matrix.

More Notes on Y-bus Matrix

For large power systems, the Y-bus matrix is typically sparse (most entries are zero), since most buses are connected to only a few other buses. Special sparse matrix data structures should be used for efficient computation.

This admittance matrix does not consider transformers.

Assumption on bus index

This code also makes an assumption about the bus indices. It assumes the indices start at 1 and are contiguous. Otherwise, the code will fail.

A general implementation will convert the user-named bus index to a contiguous 0-based internal index. To focus on conceptual discussion, we will skip that practical part.

DC Power Flow Analysis#

Now, let’s implement DC power flow analysis, which is a linearized approximation of the AC power flow. It’s useful for quick analysis and is computationally efficient. DC power flow makes the following assumptions:

  1. Flat voltage profile (all voltages are 1.0 p.u.)

  2. Line resistances are negligible (r << x)

  3. Voltage angle differences are small (sin(θ) ≈ θ)

The resulting DC power flow equation becomes:

\[ P = B\theta \]

where:

  • \(P\) is the vector of real power injections

  • \(B\) is the susceptance matrix

  • \(\theta\) is the vector of voltage angles

Solving for Voltage Angles#

To solve for voltage angles, we need to:

  • Remove the slack bus row and column from B (creating \(B'\))

  • Remove the slack bus element from P (creating \(P'\))

  • Solve the linear system:

\[ \theta' = (B')^{-1}P' \]

Branch Power Flows#

Once voltage angles are known, the power flow on a branch between buses \(i\) and \(j\) can be calculated as:

\[ P_{ij} = \frac{\theta_i - \theta_j}{x_{ij}} \]

This linear relationship between power flows and angle differences is what makes DC power flow analysis computationally efficient and suitable for many planning and operational studies.

Implementation#

The function implementation is broken down into several parts, each led by an explanation. To follow this training, you are encouraged to type up the code in your local Jupyter Notebook.

def run_dc_power_flow(self):
    """
    Perform DC power flow analysis.

    DC power flow is a linearized approximation of the AC power flow equations,
    based on the following assumptions:
        - Flat voltage profile (all voltages = 1.0 p.u.)
        - Line resistances are negligible (r << x)
        - Voltage angle differences are small (sin(θ) ≈ θ)

    Returns
    -------
    dict
        Dictionary containing results of the DC power flow:
            - voltage_angles : np.ndarray
            - branch_flows : np.ndarray
            - slack_bus_injection : float
    """
    n_bus = self.bus.get_count()

In DC power flow analysis, we derive a simplified linear relationship between power injections and voltage angles. The B matrix, also known as the susceptance matrix, plays a central role in this formulation.

From Y Matrix to B Matrix#

The complex admittance matrix (Y matrix) for a power system is given by:

\[ Y_{ij} = G_{ij} + jB_{ij} \]

where:

  • \(G_{ij}\) is the conductance between buses \(i\) and \(j\)

  • \(B_{ij}\) is the susceptance between buses \(i\) and \(j\)

Under the three assumptions, the B matrix is constructed from the imaginary part of the Y matrix:

\[\begin{split} B_{ij} = \begin{cases} -\frac{1}{x_{ij}} & \text{for } i \neq j \text{ (off-diagonal elements)} \\ \sum_{k \neq i} \frac{1}{x_{ik}} & \text{for } i = j \text{ (diagonal elements)} \end{cases} \end{split}\]

where \(x_{ij}\) is the reactance of the branch between buses \(i\) and \(j\).

    # Step 1: Build the B matrix (imaginary part of Y-bus)
    y_bus = self.build_ybus()
    b_matrix = np.imag(y_bus)

    # Step 2: Identify the slack bus and remove it from the matrices
    slack_bus_idx = None
    for i in range(n_bus):
        if self.bus.bus_type[i] == 3:  # Slack bus
            slack_bus_idx = i
            break
    if slack_bus_idx is None:
        raise ValueError("No slack bus found in the system")

    # Remove slack bus row and column from B matrix
    b_reduced = np.delete(b_matrix, slack_bus_idx, axis=0)
    b_reduced = np.delete(b_reduced, slack_bus_idx, axis=1)

Next, we will form the power injections. Note that the slack needs to be removed, because the full system is rank deficient (all power injections add up to zero).

# Step 3: Calculate net power injections (P = Pg - Pd)
    p_injection = np.zeros(n_bus)

    # Add generation
    for i in range(self.gen.get_count()):
        if self.gen.status[i] == 1:  # In-service generators only
            bus_idx = int(self.gen.bus[i]) - 1  # Convert to 0-indexing
            p_injection[bus_idx] += self.gen.Pg[i]

    # Subtract load
    for i in range(n_bus):
        p_injection[i] -= self.bus.Pd[i]

    # Convert to per-unit
    p_injection /= self.baseMVA

    # Remove slack bus injection
    p_reduced = np.delete(p_injection, slack_bus_idx)

With the injections calculated, we can solve the linear equation for the phase angles.

    # Step 4: Solve for voltage angles (Bθ = P)
    try:
        theta_reduced = np.linalg.solve(b_reduced, p_reduced)
    except np.linalg.LinAlgError as e:
        raise ValueError(f"Failed to solve DC power flow: {str(e)}")

    # Step 5: Re-insert the slack bus angle (0)
    theta = np.zeros(n_bus)
    j = 0
    for i in range(n_bus):
        if i != slack_bus_idx:
            theta[i] = theta_reduced[j]
            j += 1

Then, we can use the DC power flow equation \(P_{ij} = (\theta_i - \theta_j)/X\) to calculate the branch flows:


    # Step 6: Calculate branch flows
    branch_flows = np.zeros(self.branch.get_count())
    for i in range(self.branch.get_count()):
        if self.branch.status[i] == 1:
            f_bus = int(self.branch.fbus[i]) - 1
            t_bus = int(self.branch.tbus[i]) - 1
            x = self.branch.x[i]
            branch_flows[i] = (theta[f_bus] - theta[t_bus]) / x

    # Convert branch flows back to MW
    branch_flows *= self.baseMVA

There are multiple ways to calculate the slack injections. The following shows a branch-based approach. One can also calculate the total power injection using the \(B\) matrix without the slack bus to obtain the missing active power, and then flip the sign to get the slack injection.


    # Step 7: Compute slack bus injection
    slack_injection = 0
    for i in range(self.branch.get_count()):
        if self.branch.status[i] == 1:
            f_bus = int(self.branch.fbus[i]) - 1
            t_bus = int(self.branch.tbus[i]) - 1
            if f_bus == slack_bus_idx:
                slack_injection += branch_flows[i]
            elif t_bus == slack_bus_idx:
                slack_injection -= branch_flows[i]

    # Return results
    results = {
        'voltage_angles': theta,
        'branch_flows': branch_flows,
        'slack_bus_injection': slack_injection
    }

    return results

PowerSystem.run_dc_power_flow = run_dc_power_flow
system = load_data('case3.xlsx')
dc_results = system.run_dc_power_flow()

Let’s use the DC power flow method on our example system:

# Run DC power flow analysis
system = load_data('case3.xlsx')
dc_results = system.run_dc_power_flow()

# Print results
print("\nVoltage Angles (radians):")
for i, angle in enumerate(dc_results['voltage_angles']):
    print(f"Bus {i+1}: {angle:.6f}")

print("\nBranch Flows (MW):")
for i, flow in enumerate(dc_results['branch_flows']):
    from_bus = int(system.branch.fbus[i])
    to_bus = int(system.branch.tbus[i])
    print(f"Branch {i+1} (Bus {from_bus} to Bus {to_bus}): {flow:.2f} MW")

print(f"\nSlack Bus Injection: {dc_results['slack_bus_injection']:.2f} MW")
Voltage Angles (radians):
Bus 1: 0.000000
Bus 2: 0.012692
Bus 3: 0.067286

Branch Flows (MW):
Branch 1 (Bus 1 to Bus 2): -4.23 MW
Branch 2 (Bus 2 to Bus 3): -54.59 MW
Branch 3 (Bus 3 to Bus 1): 67.29 MW

Slack Bus Injection: -71.52 MW

Notes

DC power flow is a great tool for: Initial screening of potential system violations Transmission planning studies Generator dispatch optimization Contingency analysis where speed is more important than accuracy However, it doesn’t consider reactive power flows or voltage magnitudes, which are important for a complete system analysis.

Advanced OOP Concepts in Practice#

Now that we’ve implemented the basic classes and functionality, let’s explore some advanced OOP concepts that can enhance our power system analysis toolkit.

Inheritance and Polymorphism#

We’ve already seen inheritance in action with our classes inheriting from BaseModel. We can extend this concept by creating specialized versions of our analysis tools:

Advanced OOP Example - Contingency Analysis#

import copy

class ContingencyAnalysis:
    """
    Class to perform contingency analysis on power systems.

    Contingency analysis simulates the outage of system components
    (lines, generators, etc.) to assess system reliability.
    """

    def __init__(self, power_system):
        """Initialize with a power system."""
        self.original_system = power_system
        self.results = {}

    def run_n_1_analysis(self, contingency_type='branch'):
        """
        Perform N-1 contingency analysis.

        Parameters
        ----------
        contingency_type : str
            Type of contingency to analyze ('branch' or 'gen')

        Returns
        -------
        dict
            Dictionary containing results of each contingency
        """
        self.results = {}

        if contingency_type == 'branch':
            # Analyze branch (line) outages
            for i in range(self.original_system.branch.get_count()):
                if self.original_system.branch.status[i] == 0:
                    continue  # Skip already out-of-service branches

                system_copy = copy.deepcopy(self.original_system)
                system_copy.branch.status[i] = 0

                try:
                    results = system_copy.run_dc_power_flow()
                    overloads = []

                    for j, flow in enumerate(results['branch_flows']):
                        if system_copy.branch.status[j] == 1:
                            limit = system_copy.branch.rateA[j]
                            if limit > 0 and abs(flow) > limit:
                                overloads.append((j, flow, limit))

                    from_bus = int(self.original_system.branch.fbus[i])
                    to_bus = int(self.original_system.branch.tbus[i])
                    key = f"Branch_{i+1}_{from_bus}_{to_bus}"

                    self.results[key] = {
                        'flow_results': results,
                        'overloads': overloads,
                        'is_converged': True
                    }

                except Exception as e:
                    from_bus = int(self.original_system.branch.fbus[i])
                    to_bus = int(self.original_system.branch.tbus[i])
                    key = f"Branch_{i+1}_{from_bus}_{to_bus}"

                    self.results[key] = {
                        'error': str(e),
                        'is_converged': False
                    }

        elif contingency_type == 'gen':
            # Analyze generator outages
            for i in range(self.original_system.gen.get_count()):
                if self.original_system.gen.status[i] == 0:
                    continue  # Skip already out-of-service generators

                system_copy = copy.deepcopy(self.original_system)
                system_copy.gen.status[i] = 0

                try:
                    results = system_copy.run_dc_power_flow()
                    overloads = []

                    for j, flow in enumerate(results['branch_flows']):
                        limit = system_copy.branch.rateA[j]
                        if limit > 0 and abs(flow) > limit:
                            overloads.append((j, flow, limit))

                    bus_id = int(self.original_system.gen.bus[i])
                    key = f"Gen_{i+1}_Bus{bus_id}"

                    self.results[key] = {
                        'flow_results': results,
                        'overloads': overloads,
                        'is_converged': True
                    }

                except Exception as e:
                    bus_id = int(self.original_system.gen.bus[i])
                    key = f"Gen_{i+1}_Bus{bus_id}"

                    self.results[key] = {
                        'error': str(e),
                        'is_converged': False
                    }

        return self.results

    def get_critical_contingencies(self):
        """
        Identify critical contingencies that result in overloads.

        Returns
        -------
        list
            List of tuples: (contingency name, list of overloads)
        """
        if not self.results:
            raise ValueError("No contingency analysis results available")

        critical = []
        for contingency, result in self.results.items():
            if result.get('is_converged') and result.get('overloads'):
                critical.append((contingency, result['overloads']))

        return critical

This ContingencyAnalysis class demonstrates several advanced OOP concepts:

  1. Composition - This class contains a PowerSystem object rather than inheriting from it

  2. Encapsulation - The analysis logic is contained within its own class

  3. Polymorphism - The analysis can handle different types of contingencies (branch or generator)

“Has-a” versus “Is-a”

When designing OOP systems, consider “has-a” relationships (composition) versus “is-a” relationships (inheritance). In this case, a contingency analysis has a power system, it isn’t a power system itself.

Decorator Pattern#

Another advanced OOP concept is the decorator pattern, which allows adding new behavior to objects dynamically. Let’s implement a simple monitor decorator:

def add_monitoring(system):
    """
    Decorator function that adds monitoring capabilities to a PowerSystem.

    This wraps the `run_dc_power_flow` method with additional logging and
    branch flow monitoring.

    Parameters
    ----------
    system : PowerSystem
        The power system to enhance.

    Returns
    -------
    PowerSystem
        The enhanced power system with monitoring.
    """
    import time

    original_run_dc_power_flow = system.run_dc_power_flow

    def monitored_dc_power_flow(*args, **kwargs):
        """
        Enhanced DC power flow with timing and branch loading warnings.

        Returns
        -------
        dict
            DC power flow results with monitoring.
        """
        print("Starting DC power flow computation...")
        start_time = time.time()

        # Call the original method
        results = original_run_dc_power_flow(*args, **kwargs)

        elapsed_time = time.time() - start_time
        print(f"DC power flow completed in {elapsed_time:.4f} seconds")

        # Add monitoring for branch loading
        branch_flows = results['branch_flows']
        for i, flow in enumerate(branch_flows):
            if system.branch.status[i] == 1:  # Only in-service branches
                limit = system.branch.rateA[i]
                if limit > 0:  # Only branches with defined limits
                    loading_percent = abs(flow) / limit * 100
                    if loading_percent > 90:
                        print(f"WARNING: Branch {i + 1} loaded at {loading_percent:.1f}% of capacity")

        return results

    # Replace the original method with the enhanced version
    system.run_dc_power_flow = monitored_dc_power_flow
    return system

Let’s see the decorator in action:

# Load a power system
system = load_data('case3.xlsx')

# Add monitoring capabilities
monitored_system = add_monitoring(system)

# Run DC power flow with monitoring
results = monitored_system.run_dc_power_flow()
Starting DC power flow computation...
DC power flow completed in 0.0001 seconds

Conclusion#

In this tutorial, we’ve developed a comprehensive OOP framework for power systems analysis. We’ve covered:

  1. Basic power system component classes (buses, branches, generators)

  2. Data parsing from Excel files in MatPower format

  3. Building the admittance matrix for network representation

  4. DC power flow analysis for linear power flow solutions

  5. Advanced OOP concepts like inheritance, composition, and decorators

This framework provides a solid foundation for power system analysis using Python and object-oriented programming. You can extend it further with AC power flow, optimal power flow, stability analysis, and other advanced power system studies.

Quizzes#