Symbolic Analysis with SymPy#

Information

Details

Lead Author

Hantao Cui

Learning Objectives

• Understand symbolic computation in power systems
• Create symbolic expressions for power flow equations
• Derive Jacobian matrices symbolically
• Convert symbolic expressions to numerical code
• Perform sensitivity analysis using SymPy

Prerequisites

• Python, and NumPy basics
• Power system fundamentals
• Basic understanding of power flow equations

Estimated Time

90-240 minutes

Topics

• Symbolic computation basics
• Power flow equation formulation
• Symbolic sensitivity analysis
• DC power flow derivation
• Numerical code generation

Difficulty

Advanced (4/5)

Development Status

Completed

Last Updated on

April 22, 2025

Intro to Symbolic Computation in Power Systems#

Power system analysis often involves complex mathematical equations that describe the behavior of electrical networks. While numerical methods are commonly used to solve these equations, symbolic computation offers a more powerful approach for understanding the fundamental relationships between system variables and parameters.

Symbolic computation allows us to work with mathematical expressions in their exact form, maintaining variables as symbols rather than specific numerical values. This approach provides several key advantages for power system engineers:

  1. It reveals the underlying structure of equations, making it easier to understand how different parameters affect system behavior.

  2. It enables analytical derivation of sensitivities and relationships that might be obscured in purely numerical approaches.

  3. It allows for automatic differentiation and simplification of complex expressions.

  4. It can generate optimized code for numerical evaluation, potentially improving computational efficiency.

SymPy is a Python library for symbolic mathematics that provides these capabilities. It enables us to create symbolic variables, perform algebraic manipulations, differentiate expressions, solve equations symbolically, and convert symbolic expressions into efficient numerical code.

In power systems, symbolic computation can be particularly valuable for tasks such as:

  • Deriving the Jacobian matrix for power flow analysis

  • Understanding the sensitivity of system states to parameter changes

  • Deriving analytical expressions for contingency analysis

  • Simplifying complex network equations to gain insights

This module demonstrates how to integrate SymPy with object-oriented power system models to enhance both understanding and computational capabilities.

Learning Objectives

By the end of this module, you will be able to:

  • Create symbolic representations of power system components and equations

  • Derive power flow equations and Jacobian matrices symbolically

  • Understand how symbolic computation can provide insights into system behavior

  • Convert symbolic expressions to efficient numerical code

  • Perform sensitivity analysis using symbolic differentiation

Setting Up the Environment#

Before we begin working with symbolic computation, we need to import the necessary libraries and review the power system component classes we’ll be using.

%run oop-and-dc-power-flow.ipynb
import numpy as np
import sympy as sp
from sympy import symbols, Matrix, cos, sin, lambdify

The power system component classes (Bus, Branch, and Generator) that we developed in the previous module provide an object-oriented representation of the physical components in a power system. The %run magic brought these classes into this notebook, so we won’t need to redefine them.

In this module, we’ll enhance these classes by incorporating symbolic computation to gain deeper insights into system behavior and relationships.

Introduction to SymPy#

Before diving into symbolic analysis of power systems, it is essential to understand the basics of SymPy, the symbolic mathematics library in Python that allows us to work with mathematical expressions in their exact, symbolic form rather than with numerical approximations.

Key SymPy Components#

Creating Symbolic Variables#

import sympy as sp

# Create a single symbolic variable
x = sp.Symbol('x')
y = sp.Symbol('y')

# Create multiple symbolic variables at once
a, b, c = sp.symbols('a b c')

# Create indexed variables
theta = sp.symbols('theta_1:4')  # Creates theta_1, theta_2, theta_3

The Symbol and symbols functions create symbolic variables that can be manipulated algebraically. Unlike numerical variables, these maintain their symbolic nature through mathematical operations.

Symbolic Matrices#

# Create a 2x2 symbolic matrix
A = sp.Matrix([[a, b], [c, 0]])

# Matrix operations
A_inverse = A.inv()  # Symbolic matrix inverse
det_A = A.det()      # Symbolic determinant

print(f"A's determinant: {det_A}")
A's determinant: -b*c

SymPy’s Matrix class allows creation and manipulation of matrices containing symbolic expressions. This is essential for power system analysis, where we work with matrices like the admittance matrix (Y-bus) and Jacobian.

Mathematical Functions#

# Trigonometric functions
expr1 = sp.sin(x) * sp.cos(y)

# Complex numbers
z = a + sp.I * b       # I is the imaginary unit
real_part = sp.re(z)   # Extract real part
imag_part = sp.im(z)   # Extract imaginary part

SymPy provides symbolic versions of mathematical functions that differ from NumPy or Python’s built-in functions. These maintain symbolic expressions rather than computing numerical values. The sp.I represents the imaginary unit (√-1), and functions like re and im extract real and imaginary parts of complex expressions.

Differentiation#

# Differentiate an expression with respect to a variable
f = x**2 * sp.sin(y)
df_dx = sp.diff(f, x)      # Partial derivative with respect to x
df_dy = sp.diff(f, y)      # Partial derivative with respect to y

print(f"df/dx = {df_dx}")
print(f"df/dy = {df_dy}")
df/dx = 2*x*sin(y)
df/dy = x**2*cos(y)

The diff function computes symbolic derivatives, which is essential for deriving the Jacobian matrix in power flow analysis.

Simplification and Substitution#

# Simplify complex expressions
expr = sp.sin(x)**2 + sp.cos(x)**2
simplified = sp.simplify(expr)  # Simplifies to 1

# Substitute values or expressions
expr = a*x + b*y
subs_expr = expr.subs([(x, 2), (y, a)])  # Substitutes x=2 and y=a

print(f"expr = {expr}")
print(f"subs_expr = {subs_expr}")
expr = a*x + b*y
subs_expr = a*b + 2*a

SymPy can simplify expressions and substitute values or other expressions into symbolic formulas. This is useful for applying assumptions (like in DC power flow) or evaluating expressions at specific points.

Converting to Numerical Functions#

# Create a symbolic expression
expr = sp.sin(x) * sp.cos(y) + x**2

# Convert to a numerical function
f_numeric = sp.lambdify([x, y], expr, 'numpy')

# Now can be evaluated with numerical values
result = f_numeric(0.5, 1.0)  # Fast numerical evaluation

print(f"expr = {expr}")
print(f"f_numeric = {f_numeric}")
expr = x**2 + sin(x)*cos(y)
f_numeric = <function _lambdifygenerated at 0x7fcc0b0d2020>
print("Source code of `f_numeric` using `inspect`:")
from inspect import getsource
print(getsource(f_numeric))
Source code of `f_numeric` using `inspect`:
def _lambdifygenerated(x, y):
    return x**2 + sin(x)*cos(y)

The lambdify function converts symbolic expressions to fast numerical functions using libraries like NumPy. The name lambdify means turning a symbolic expression into a lambda function. This bridges the gap between symbolic analysis and efficient numerical computation, allowing us to derive expressions symbolically but evaluate them quickly.

Differences from NumPy Functions#

A key distinction is that SymPy functions (like sin, cos) maintain expressions symbolically:

import numpy as np

# NumPy computes a numerical value
np_result = np.sin(0.5)  # Returns 0.479425...

# SymPy maintains a symbolic expression
x = sp.Symbol('x')
sp_result = sp.sin(x)    # Returns sin(x) as a symbolic expression
sp_eval = sp.sin(0.5)    # Returns sin(0.5) exactly or as a high-precision approximation

In power system analysis, this symbolic approach helps us:

  1. Derive general formulas that apply to any system

  2. Understand the mathematical structure of power flow equations

  3. Perform analytical sensitivity analysis

  4. Generate optimized code for specific numerical problems

With this foundation in SymPy, we can now apply these techniques to symbolic analysis of power systems, gaining deeper insights while maintaining computational efficiency.

Symbolic Representation of Power System Components#

One of the key advantages of symbolic computation is the ability to work with variables as abstract symbols rather than specific numerical values. This allows us to derive general expressions that hold true for any valid input values.

Let’s start by creating symbolic variables for the key quantities in power system analysis:

# Create symbolic variables for bus voltages and angles
def create_symbolic_bus_variables(bus_count):
    # Create voltage magnitude symbols for each bus
    V = [symbols(f'V_{i}') for i in range(1, bus_count + 1)]
    
    # Create voltage angle symbols for each bus
    theta = [symbols(f'θ_{i}') for i in range(1, bus_count + 1)]
    
    return V, theta

# Create symbolic variables for branch parameters
def create_symbolic_branch_parameters(branch_count):
    # Create resistance symbols
    R = [symbols(f'R_{i}') for i in range(1, branch_count + 1)]
    
    # Create reactance symbols
    X = [symbols(f'X_{i}') for i in range(1, branch_count + 1)]
    
    # Create susceptance symbols
    B = [symbols(f'B_{i}') for i in range(1, branch_count + 1)]
    
    return R, X, B

With these symbolic variables, we can extend our power system component classes to work with both numerical and symbolic representations. For example, we can create a symbolic version of the power flow equations:

def symbolic_power_flow_equations(V, theta, Y_bus):
    """
    Create symbolic expressions for the active and reactive power flow equations.
    
    Parameters:
    -----------
    V : list of sympy symbols
        Bus voltage magnitudes
    theta : list of sympy symbols
        Bus voltage angles
    Y_bus : Matrix of complex values or symbols
        Bus admittance matrix (can be numeric or symbolic)
    
    Returns:
    --------
    P, Q : lists of symbolic expressions
        Active and reactive power injections at each bus
    """
    n = len(V)
    P = []
    Q = []
    
    for i in range(n):
        p_i = 0
        q_i = 0
        
        for j in range(n):
            # Extract magnitude and angle from complex Y_bus
            if isinstance(Y_bus[i, j], complex):
                G_ij = sp.re(Y_bus[i, j])
                B_ij = sp.im(Y_bus[i, j])
            else:
                # If Y_bus is symbolic
                G_ij = sp.symbols(f'G_{i+1}{j+1}')
                B_ij = sp.symbols(f'B_{i+1}{j+1}')
            
            # Active power equation term
            p_term = V[i] * V[j] * (G_ij * cos(theta[i] - theta[j]) + 
                                    B_ij * sin(theta[i] - theta[j]))
            
            # Reactive power equation term
            q_term = V[i] * V[j] * (G_ij * sin(theta[i] - theta[j]) - 
                                    B_ij * cos(theta[i] - theta[j]))
            
            p_i += p_term
            q_i += q_term
        
        P.append(p_i)
        Q.append(q_i)
    
    return P, Q
# Example: Creating symbolic variables for a 3-bus system
bus_count = 3
V, theta = create_symbolic_bus_variables(bus_count)

# Display the created symbolic variables
print("Voltage magnitude symbols:")
for v in V:
    print(f"  {v}")

print("\nVoltage angle symbols:")
for t in theta:
    print(f"  {t}")

# Create symbolic branch parameters for a system with 2 branches
branch_count = 2
R, X, B = create_symbolic_branch_parameters(branch_count)

# Display the branch parameters
print("\nBranch resistance symbols:")
for r in R:
    print(f"  {r}")

print("\nBranch reactance symbols:")
for x in X:
    print(f"  {x}")
Voltage magnitude symbols:
  V_1
  V_2
  V_3

Voltage angle symbols:
  θ_1
  θ_2
  θ_3

Branch resistance symbols:
  R_1
  R_2

Branch reactance symbols:
  X_1
  X_2

This function creates symbolic expressions for the active and reactive power injections at each bus, based on the standard AC power flow equations. These expressions maintain the mathematical relationships in symbolic form, allowing us to analyze them analytically.

Symbolic vs. Numeric Representation

When working with power systems, it’s valuable to understand the distinction between symbolic and numerical representations:

  • Symbolic representation: Maintains variables as abstract symbols, allowing for algebraic manipulation and exact mathematical operations.

  • Numeric representation: Substitutes specific values for variables, allowing for numerical calculations and specific system solutions.

SymPy allows us to seamlessly transition between these two representations, giving us the best of both worlds: the clarity and insight of symbolic analysis and the computational efficiency of numerical methods.

Admittance Matrix Formation with SymPy#

The admittance matrix (Y-bus) is a fundamental component of power system analysis, representing the network connectivity and impedance characteristics. Let’s see how we can create a symbolic representation of the Y-bus matrix:

# Example: Creating symbolic Y-bus matrix using the PowerSystem class
import numpy as np
import sympy as sp
from sympy import symbols, Matrix, I, pprint

def create_symbolic_y_bus_vectorized(system):
    """
    Create a symbolic admittance matrix using vectorized operations.
    
    Parameters:
    -----------
    system : PowerSystem object
        The power system data
    
    Returns:
    --------
    Y_bus : Matrix of symbolic expressions
        Symbolic bus admittance matrix
    """
    n_bus = system.bus.get_count()
    Y_bus = sp.zeros(n_bus, n_bus)
    
    # Create a mapping from bus numbers to indices
    bus_idx = {int(bus_i): i for i, bus_i in enumerate(system.bus.bus_i)}
    
    # Process all branches at once using vectorized operations
    for idx in range(system.branch.get_count()):
        # Get from and to bus indices
        from_idx = bus_idx[int(system.branch.fbus[idx])]
        to_idx = bus_idx[int(system.branch.tbus[idx])]
        
        # Create symbolic impedance parameters
        r = sp.symbols(f'r_{from_idx+1}_{to_idx+1}')
        x = sp.symbols(f'x_{from_idx+1}_{to_idx+1}')
        b = sp.symbols(f'b_{from_idx+1}_{to_idx+1}')
        
        # Calculate admittance
        y = 1 / (r + I * x)
        
        # Add to Y-bus matrix
        Y_bus[from_idx, to_idx] -= y
        Y_bus[to_idx, from_idx] -= y  # Maintain symmetry
        
        # Add shunt admittance
        Y_bus[from_idx, from_idx] += y + I * b / 2
        Y_bus[to_idx, to_idx] += y + I * b / 2
    
    return Y_bus

# Load the system data using the existing parser
system = load_data('case3.xlsx')

# Create symbolic Y-bus matrix
Y_bus_symbolic = create_symbolic_y_bus_vectorized(system)

# Display the symbolic Y-bus matrix
print("Symbolic Y-bus matrix using the PowerSystem class:")
pprint(Y_bus_symbolic)
Symbolic Y-bus matrix using the PowerSystem class:
⎡ⅈ⋅b₁ ₂   ⅈ⋅b₃ ₁         1               1                              -1     ↪
⎢────── + ────── + ───────────── + ─────────────                   ─────────── ↪
⎢  2        2      r₃ ₁ + ⅈ⋅x₃ ₁   r₁ ₂ + ⅈ⋅x₁ ₂                   r₁ ₂ + ⅈ⋅x₁ ↪
⎢                                                                              ↪
⎢                      -1                         ⅈ⋅b₁ ₂   ⅈ⋅b₂ ₃         1    ↪
⎢                 ─────────────                   ────── + ────── + ────────── ↪
⎢                 r₁ ₂ + ⅈ⋅x₁ ₂                     2        2      r₂ ₃ + ⅈ⋅x ↪
⎢                                                                              ↪
⎢                      -1                                               -1     ↪
⎢                 ─────────────                                    ─────────── ↪
⎣                 r₃ ₁ + ⅈ⋅x₃ ₁                                    r₂ ₃ + ⅈ⋅x₂ ↪

↪                                            -1                       ⎤
↪ ──                                    ─────────────                 ⎥
↪  ₂                                    r₃ ₁ + ⅈ⋅x₃ ₁                 ⎥
↪                                                                     ⎥
↪             1                              -1                       ⎥
↪ ─── + ─────────────                   ─────────────                 ⎥
↪ ₂ ₃   r₁ ₂ + ⅈ⋅x₁ ₂                   r₂ ₃ + ⅈ⋅x₂ ₃                 ⎥
↪                                                                     ⎥
↪                      ⅈ⋅b₂ ₃   ⅈ⋅b₃ ₁         1               1      ⎥
↪ ──                   ────── + ────── + ───────────── + ─────────────⎥
↪  ₃                     2        2      r₃ ₁ + ⅈ⋅x₃ ₁   r₂ ₃ + ⅈ⋅x₂ ₃⎦

This function creates a symbolic representation of the Y-bus matrix, using symbols to represent impedances and admittances. The resulting matrix maintains the mathematical structure while allowing for symbolic manipulation.

We can also create a more specific symbolic Y-bus by incorporating the actual branch parameters:

# Can also create a numerical version with specific parameter values
def create_numerical_y_bus(system):
    """
    Create a numerical admittance matrix using the system data.
    
    Parameters:
    -----------
    system : PowerSystem object
        The power system data
    
    Returns:
    --------
    Y_bus : numpy array of complex values
        Numerical bus admittance matrix
    """
    n_bus = system.bus.get_count()
    Y_bus = np.zeros((n_bus, n_bus), dtype=complex)
    
    # Create a mapping from bus numbers to indices
    bus_idx = {int(bus_i): i for i, bus_i in enumerate(system.bus.bus_i)}
    
    # Process all branches at once
    for idx in range(system.branch.get_count()):
        # Get from and to bus indices
        from_idx = bus_idx[int(system.branch.fbus[idx])]
        to_idx = bus_idx[int(system.branch.tbus[idx])]
        
        # Calculate admittance
        z = complex(system.branch.r[idx], system.branch.x[idx])
        y = 1 / z
        
        # Add to Y-bus matrix
        Y_bus[from_idx, to_idx] -= y
        Y_bus[to_idx, from_idx] -= y  # Maintain symmetry
        
        # Add shunt admittance
        Y_bus[from_idx, from_idx] += y + 1j * system.branch.b[idx] / 2
        Y_bus[to_idx, to_idx] += y + 1j * system.branch.b[idx] / 2
    
    return Y_bus

# Create numerical Y-bus matrix
Y_bus_numerical = create_numerical_y_bus(system)

# Display the numerical Y-bus matrix
print("\nNumerical Y-bus matrix using the system data:")
print(np.round(Y_bus_numerical, 4))  # Round for cleaner display
Numerical Y-bus matrix using the system data:
[[ 1.2113-13.0946j -0.2212 +3.3186j -0.9901 +9.901j ]
 [-0.2212 +3.3186j  1.2113-13.0946j -0.9901 +9.901j ]
 [-0.9901 +9.901j  -0.9901 +9.901j   1.9802-19.702j ]]
# Now combine symbolic and numerical approaches
def create_symbolic_from_numerical(Y_bus_numerical):
    """
    Create a symbolic Y-bus with numerical values substituted.
    
    Parameters:
    -----------
    Y_bus_numerical : numpy array
        Numerical Y-bus matrix
    
    Returns:
    --------
    Y_bus_symbolic : Matrix of symbolic expressions
        Symbolic Y-bus matrix with numerical values
    """
    n = Y_bus_numerical.shape[0]
    Y_bus_symbolic = sp.zeros(n, n)
    
    for i in range(n):
        for j in range(n):
            if Y_bus_numerical[i, j] != 0:
                # Create symbolic variable for non-zero elements
                var_name = f'Y_{i+1}{j+1}'
                Y_ij = sp.symbols(var_name)
                
                # Substitute with numerical value
                Y_bus_symbolic[i, j] = Y_ij
    
    # Create substitution dictionary
    subs_dict = {}
    for i in range(n):
        for j in range(n):
            if Y_bus_numerical[i, j] != 0:
                var_name = f'Y_{i+1}{j+1}'
                subs_dict[sp.symbols(var_name)] = complex(np.round(Y_bus_numerical[i, j].real, 4), 
                                                         np.round(Y_bus_numerical[i, j].imag, 4))
    
    return Y_bus_symbolic, subs_dict

# Create symbolic Y-bus with numerical values
Y_bus_sym_with_num, subs_dict = create_symbolic_from_numerical(Y_bus_numerical)

print("\nSymbolic Y-bus matrix with variables:")
pprint(Y_bus_sym_with_num)

print("\nSubstitution dictionary for numerical values:")
for key, value in subs_dict.items():
    print(f"{key} = {value}")
Symbolic Y-bus matrix with variables:
⎡Y₁₁  Y₁₂  Y₁₃⎤
⎢             ⎥
⎢Y₂₁  Y₂₂  Y₂₃⎥
⎢             ⎥
⎣Y₃₁  Y₃₂  Y₃₃⎦

Substitution dictionary for numerical values:
Y_11 = (1.2113-13.0946j)
Y_12 = (-0.2212+3.3186j)
Y_13 = (-0.9901+9.901j)
Y_21 = (-0.2212+3.3186j)
Y_22 = (1.2113-13.0946j)
Y_23 = (-0.9901+9.901j)
Y_31 = (-0.9901+9.901j)
Y_32 = (-0.9901+9.901j)
Y_33 = (1.9802-19.702j)

The symbolic Y-bus matrix provides insights into the network structure and can be manipulated symbolically to understand the relationships between network parameters and system behavior.

# Example: Calculate symbolic power flow using the Y-bus
def symbolic_power_injections(system, Y_bus_symbolic):
    """
    Calculate symbolic power injections using the Y-bus matrix.
    
    Parameters:
    -----------
    system : PowerSystem object
        The power system data
    Y_bus_symbolic : Matrix of symbolic expressions
        Symbolic bus admittance matrix
    
    Returns:
    --------
    P, Q : lists of symbolic expressions
        Active and reactive power injections at each bus
    """
    n_bus = system.bus.get_count()
    
    # Create symbolic variables for voltages and angles
    V = [sp.symbols(f'V_{i+1}') for i in range(n_bus)]
    theta = [sp.symbols(f'theta_{i+1}') for i in range(n_bus)]
    
    # Calculate power injections
    P = []
    Q = []
    
    for i in range(n_bus):
        p_i = 0
        q_i = 0
        
        for j in range(n_bus):
            # Extract symbolic Y-bus elements
            y_ij = Y_bus_symbolic[i, j]
            
            if y_ij != 0:
                # Express in terms of magnitude and angle
                if isinstance(y_ij, complex):
                    G_ij = float(y_ij.real)
                    B_ij = float(y_ij.imag)
                else:
                    G_ij = sp.re(y_ij)
                    B_ij = sp.im(y_ij)
                
                # Calculate power terms
                p_term = V[i] * V[j] * (G_ij * sp.cos(theta[i] - theta[j]) + 
                                      B_ij * sp.sin(theta[i] - theta[j]))
                q_term = V[i] * V[j] * (G_ij * sp.sin(theta[i] - theta[j]) - 
                                      B_ij * sp.cos(theta[i] - theta[j]))
                
                p_i += p_term
                q_i += q_term
        
        P.append(p_i)
        Q.append(q_i)
    
    return P, Q

# Calculate symbolic power injections
P_symbolic, Q_symbolic = symbolic_power_injections(system, Y_bus_symbolic)

# Display the first elements of P and Q to see their structure
print("\nSymbolic active power injection at bus 1:")
pprint(P_symbolic[0])

print("\nSymbolic reactive power injection at bus 1:")
pprint(Q_symbolic[0])
Symbolic active power injection at bus 1:
  2 ⎛  im(b₁ ₂)   im(b₃ ₁)                 re(r₃ ₁) - im(x₃ ₁)                 ↪
V₁ ⋅⎜- ──────── - ──────── + ─────────────────────────────────────────────── + ↪
    ⎜     2          2                            2                        2   ↪
    ⎝                        (re(r₃ ₁) - im(x₃ ₁))  + (re(x₃ ₁) + im(r₃ ₁))    ↪

↪                re(r₁ ₂) - im(x₁ ₂)              ⎞         ⎛        (re(r₁ ₂) ↪
↪  ───────────────────────────────────────────────⎟ + V₁⋅V₂⋅⎜- ─────────────── ↪
↪                       2                        2⎟         ⎜                  ↪
↪  (re(r₁ ₂) - im(x₁ ₂))  + (re(x₁ ₂) + im(r₁ ₂)) ⎠         ⎝  (re(r₁ ₂) - im( ↪

↪  - im(x₁ ₂))⋅cos(θ₁ - θ₂)                (-re(x₁ ₂) - im(r₁ ₂))⋅sin(θ₁ - θ₂) ↪
↪ ──────────────────────────────── - ───────────────────────────────────────── ↪
↪       2                        2                        2                    ↪
↪ x₁ ₂))  + (re(x₁ ₂) + im(r₁ ₂))    (re(r₁ ₂) - im(x₁ ₂))  + (re(x₁ ₂) + im(r ↪

↪       ⎞         ⎛        (re(r₃ ₁) - im(x₃ ₁))⋅cos(θ₁ - θ₃)                ( ↪
↪ ──────⎟ + V₁⋅V₃⋅⎜- ─────────────────────────────────────────────── - ─────── ↪
↪      2⎟         ⎜                       2                        2           ↪
↪ ₁ ₂)) ⎠         ⎝  (re(r₃ ₁) - im(x₃ ₁))  + (re(x₃ ₁) + im(r₃ ₁))    (re(r₃  ↪

↪ -re(x₃ ₁) - im(r₃ ₁))⋅sin(θ₁ - θ₃)      ⎞
↪ ────────────────────────────────────────⎟
↪               2                        2⎟
↪ ₁) - im(x₃ ₁))  + (re(x₃ ₁) + im(r₃ ₁)) ⎠

Symbolic reactive power injection at bus 1:
  2 ⎛  re(b₁ ₂)   re(b₃ ₁)                -re(x₃ ₁) - im(r₃ ₁)                 ↪
V₁ ⋅⎜- ──────── - ──────── - ─────────────────────────────────────────────── - ↪
    ⎜     2          2                            2                        2   ↪
    ⎝                        (re(r₃ ₁) - im(x₃ ₁))  + (re(x₃ ₁) + im(r₃ ₁))    ↪

↪               -re(x₁ ₂) - im(r₁ ₂)              ⎞         ⎛        (re(r₁ ₂) ↪
↪  ───────────────────────────────────────────────⎟ + V₁⋅V₂⋅⎜- ─────────────── ↪
↪                       2                        2⎟         ⎜                  ↪
↪  (re(r₁ ₂) - im(x₁ ₂))  + (re(x₁ ₂) + im(r₁ ₂)) ⎠         ⎝  (re(r₁ ₂) - im( ↪

↪  - im(x₁ ₂))⋅sin(θ₁ - θ₂)                (-re(x₁ ₂) - im(r₁ ₂))⋅cos(θ₁ - θ₂) ↪
↪ ──────────────────────────────── + ───────────────────────────────────────── ↪
↪       2                        2                        2                    ↪
↪ x₁ ₂))  + (re(x₁ ₂) + im(r₁ ₂))    (re(r₁ ₂) - im(x₁ ₂))  + (re(x₁ ₂) + im(r ↪

↪       ⎞         ⎛        (re(r₃ ₁) - im(x₃ ₁))⋅sin(θ₁ - θ₃)                ( ↪
↪ ──────⎟ + V₁⋅V₃⋅⎜- ─────────────────────────────────────────────── + ─────── ↪
↪      2⎟         ⎜                       2                        2           ↪
↪ ₁ ₂)) ⎠         ⎝  (re(r₃ ₁) - im(x₃ ₁))  + (re(x₃ ₁) + im(r₃ ₁))    (re(r₃  ↪

↪ -re(x₃ ₁) - im(r₃ ₁))⋅cos(θ₁ - θ₃)      ⎞
↪ ────────────────────────────────────────⎟
↪               2                        2⎟
↪ ₁) - im(x₃ ₁))  + (re(x₃ ₁) + im(r₃ ₁)) ⎠

Symbolic Network Insights

Working with a symbolic Y-bus matrix can provide insights that might be difficult to see in a purely numerical representation:

  1. It reveals the structural zeros in the matrix, showing which buses are not directly connected.

  2. It shows how the network connectivity affects the mathematical formulation of the power flow problem.

  3. It allows for analytical derivation of sensitivity factors, such as how changes in line impedances affect power flows.

Power Flow Equations in Symbolic Form#

Power flow analysis is a fundamental technique in power systems engineering, used to determine the state of a power system under steady-state conditions. The power flow equations relate the power injections at each bus to the bus voltages and network parameters.

With SymPy, we can express these equations symbolically and gain insights into their structure and relationships:

def symbolic_ac_power_flow_equations(system):
    """
    Create symbolic AC power flow equations for a given power system.
    
    Parameters:
    -----------
    system : PowerSystem object
        The power system containing bus and branch data
    
    Returns:
    --------
    P_eq, Q_eq : lists of symbolic expressions
        Active and reactive power balance equations
    V, theta : lists of symbolic variables
        Bus voltage magnitudes and angles
    """
    n_bus = system.bus.get_count()
    
    # Create symbolic variables for bus voltages and angles
    V = [sp.symbols(f'V_{i+1}') for i in range(n_bus)]
    theta = [sp.symbols(f'theta_{i+1}') for i in range(n_bus)]
    
    # Create symbolic Y-bus using the vectorized method
    Y_bus = create_symbolic_y_bus_vectorized(system)
    
    # Calculate power injections
    P = []
    Q = []
    
    for i in range(n_bus):
        p_i = 0
        q_i = 0
        
        for j in range(n_bus):
            # Extract Y-bus components
            if Y_bus[i, j] != 0:
                if isinstance(Y_bus[i, j], complex):
                    G_ij = float(Y_bus[i, j].real)
                    B_ij = float(Y_bus[i, j].imag)
                else:
                    G_ij = sp.re(Y_bus[i, j])
                    B_ij = sp.im(Y_bus[i, j])
                
                # Calculate power terms
                angle_diff = theta[i] - theta[j]
                p_term = V[i] * V[j] * (G_ij * sp.cos(angle_diff) + B_ij * sp.sin(angle_diff))
                q_term = V[i] * V[j] * (G_ij * sp.sin(angle_diff) - B_ij * sp.cos(angle_diff))
                
                p_i += p_term
                q_i += q_term
        
        P.append(p_i)
        Q.append(q_i)
    
    # Create power balance equations
    P_eq = []
    Q_eq = []
    
    # Dictionary to map bus numbers to indices
    bus_idx = {int(b_i): i for i, b_i in enumerate(system.bus.bus_i)}
    
    for i in range(n_bus):
        # Get bus index
        bus_num = system.bus.bus_i[i]
        
        # Get generation at this bus
        gen_at_bus = [j for j, g_bus in enumerate(system.gen.bus) if int(g_bus) == int(bus_num)]
        P_gen = sum(system.gen.Pg[j] for j in gen_at_bus) if gen_at_bus else 0
        Q_gen = sum(system.gen.Qg[j] for j in gen_at_bus) if gen_at_bus else 0
        
        # Get load at this bus
        P_load = system.bus.Pd[i]
        Q_load = system.bus.Qd[i]
        
        # Create power balance equations
        P_eq.append(P_gen - P_load - P[i])
        Q_eq.append(Q_gen - Q_load - Q[i])
    
    return P_eq, Q_eq, V, theta

This function creates symbolic expressions for the power balance equations at each bus. These equations represent the conservation of power: the power injected at a bus (from generation) minus the power consumed (by loads) must equal the power flowing out through the connected branches.

We can also derive the Jacobian matrix symbolically, which is essential for the Newton-Raphson method used to solve the power flow equations:

def symbolic_power_flow_jacobian(system, P_eq, Q_eq, V, theta):
    """
    Create symbolic Jacobian matrix for power flow equations.
    
    Parameters:
    -----------
    system : PowerSystem object
        The power system data
    P_eq, Q_eq : lists of symbolic expressions
        Active and reactive power balance equations
    V, theta : lists of symbolic variables
        Bus voltage magnitudes and angles
    
    Returns:
    --------
    J : Matrix of symbolic expressions
        Symbolic Jacobian matrix
    """
    n_bus = system.bus.get_count()
    
    # Identify bus types and indices
    slack_idx = next(i for i, type_val in enumerate(system.bus.bus_type) if type_val == 3)
    pv_idx = [i for i, type_val in enumerate(system.bus.bus_type) if type_val == 2]
    pq_idx = [i for i, type_val in enumerate(system.bus.bus_type) if type_val == 1]
    
    # Extract unknown variables based on bus types
    unknown_theta = [theta[i] for i in range(n_bus) if i != slack_idx]
    unknown_V = [V[i] for i in pq_idx]
    
    # Extract relevant equations
    active_eqs = [P_eq[i] for i in range(n_bus) if i != slack_idx]
    reactive_eqs = [Q_eq[i] for i in pq_idx]
    
    # Combine equations and variables
    all_eqs = active_eqs + reactive_eqs
    all_vars = unknown_theta + unknown_V
    
    # Create Jacobian matrix
    J = sp.zeros(len(all_eqs), len(all_vars))
    
    # Fill Jacobian matrix
    for i, eq in enumerate(all_eqs):
        for j, var in enumerate(all_vars):
            J[i, j] = sp.diff(eq, var)
    
    return J

The symbolic Jacobian matrix reveals the sensitivity of the power balance equations to changes in the unknown variables (voltage magnitudes and angles). This can provide insights into the coupling between active and reactive power and the behavior of the power flow solution.

# Example: Creating symbolic power flow equations for a small power system
# Load the system data
system = load_data('case3.xlsx')

# Create symbolic power flow equations
P_eq, Q_eq, V, theta = symbolic_ac_power_flow_equations(system)

# Display the active power equations
print("Symbolic active power equations:")
for i, eq in enumerate(P_eq):
    print(f"P_eq[{i+1}] = {eq}")

# Display the reactive power equations
print("\nSymbolic reactive power equations:")
for i, eq in enumerate(Q_eq):
    print(f"Q_eq[{i+1}] = {eq}")

# Create symbolic Jacobian matrix
J = symbolic_power_flow_jacobian(system, P_eq, Q_eq, V, theta)

# Display the Jacobian matrix shape
print(f"\nJacobian matrix shape: {J.shape}")

# Display a portion of the Jacobian matrix (first 2x2 block)
print("\nA portion of the symbolic Jacobian matrix:")
for i in range(min(2, J.shape[0])):
    for j in range(min(2, J.shape[1])):
        print(f"J[{i},{j}] = {J[i,j]}")

# Simplify one element of the Jacobian to see its structure
if J.shape[0] > 0 and J.shape[1] > 0:
    simple_elem = sp.simplify(J[0, 0])
    print("\nSimplified first element of the Jacobian:")
    print(simple_elem)
Symbolic active power equations:
P_eq[1] = -V_1**2*(-im(b_1_2)/2 - im(b_3_1)/2 + (re(r_3_1) - im(x_3_1))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) + (re(r_1_2) - im(x_1_2))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)) - V_1*V_2*(-(re(r_1_2) - im(x_1_2))*cos(theta_1 - theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) - (-re(x_1_2) - im(r_1_2))*sin(theta_1 - theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)) - V_1*V_3*(-(re(r_3_1) - im(x_3_1))*cos(theta_1 - theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) - (-re(x_3_1) - im(r_3_1))*sin(theta_1 - theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2))
P_eq[2] = -V_1*V_2*(-(re(r_1_2) - im(x_1_2))*cos(theta_1 - theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) + (-re(x_1_2) - im(r_1_2))*sin(theta_1 - theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)) - V_2**2*(-im(b_1_2)/2 - im(b_2_3)/2 + (re(r_2_3) - im(x_2_3))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (re(r_1_2) - im(x_1_2))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)) - V_2*V_3*(-(re(r_2_3) - im(x_2_3))*cos(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_2_3) - im(r_2_3))*sin(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)) + 50
P_eq[3] = -V_1*V_3*(-(re(r_3_1) - im(x_3_1))*cos(theta_1 - theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) + (-re(x_3_1) - im(r_3_1))*sin(theta_1 - theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)) - V_2*V_3*(-(re(r_2_3) - im(x_2_3))*cos(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*sin(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)) - V_3**2*(-im(b_2_3)/2 - im(b_3_1)/2 + (re(r_3_1) - im(x_3_1))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) + (re(r_2_3) - im(x_2_3))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)) - 120

Symbolic reactive power equations:
Q_eq[1] = -V_1**2*(-re(b_1_2)/2 - re(b_3_1)/2 - (-re(x_3_1) - im(r_3_1))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) - (-re(x_1_2) - im(r_1_2))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)) - V_1*V_2*(-(re(r_1_2) - im(x_1_2))*sin(theta_1 - theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) + (-re(x_1_2) - im(r_1_2))*cos(theta_1 - theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)) - V_1*V_3*(-(re(r_3_1) - im(x_3_1))*sin(theta_1 - theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) + (-re(x_3_1) - im(r_3_1))*cos(theta_1 - theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2))
Q_eq[2] = -V_1*V_2*((re(r_1_2) - im(x_1_2))*sin(theta_1 - theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) + (-re(x_1_2) - im(r_1_2))*cos(theta_1 - theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)) - V_2**2*(-re(b_1_2)/2 - re(b_2_3)/2 - (-re(x_2_3) - im(r_2_3))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_1_2) - im(r_1_2))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)) - V_2*V_3*(-(re(r_2_3) - im(x_2_3))*sin(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*cos(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))
Q_eq[3] = -V_1*V_3*((re(r_3_1) - im(x_3_1))*sin(theta_1 - theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) + (-re(x_3_1) - im(r_3_1))*cos(theta_1 - theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)) - V_2*V_3*((re(r_2_3) - im(x_2_3))*sin(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*cos(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)) - V_3**2*(-re(b_2_3)/2 - re(b_3_1)/2 - (-re(x_3_1) - im(r_3_1))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) - (-re(x_2_3) - im(r_2_3))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)) - 50

Jacobian matrix shape: (3, 3)

A portion of the symbolic Jacobian matrix:
J[0,0] = -V_1*V_2*(-(re(r_1_2) - im(x_1_2))*sin(theta_1 - theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) - (-re(x_1_2) - im(r_1_2))*cos(theta_1 - theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)) - V_2*V_3*((re(r_2_3) - im(x_2_3))*sin(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_2_3) - im(r_2_3))*cos(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))
J[0,1] = -V_2*V_3*(-(re(r_2_3) - im(x_2_3))*sin(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*cos(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))
J[1,0] = -V_2*V_3*((re(r_2_3) - im(x_2_3))*sin(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*cos(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))
J[1,1] = -V_1*V_3*(-(re(r_3_1) - im(x_3_1))*sin(theta_1 - theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) - (-re(x_3_1) - im(r_3_1))*cos(theta_1 - theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)) - V_2*V_3*(-(re(r_2_3) - im(x_2_3))*sin(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_2_3) - im(r_2_3))*cos(theta_2 - theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))

Simplified first element of the Jacobian:
V_2*(V_1*((re(r_1_2) - im(x_1_2))*sin(theta_1 - theta_2) - (re(x_1_2) + im(r_1_2))*cos(theta_1 - theta_2))*((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - V_3*((re(r_2_3) - im(x_2_3))*sin(theta_2 - theta_3) + (re(x_2_3) + im(r_2_3))*cos(theta_2 - theta_3))*((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2))/(((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)*((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))

DC Power Flow with Symbolic Variables#

DC power flow is a simplified version of the full AC power flow, making several assumptions to linearize the equations. With symbolic computation, we can derive the DC power flow equations directly from the AC equations, providing insights into the approximations involved:

def symbolic_dc_power_flow_derivation(system):
    """
    Derive DC power flow equations symbolically from AC equations.
    
    Parameters:
    -----------
    system : PowerSystem object
        The power system data
    
    Returns:
    --------
    P_dc : list of symbolic expressions
        DC power flow equations
    """
    n_bus = system.bus.get_count()
    
    # Create symbolic variables
    V = [sp.symbols(f'V_{i+1}') for i in range(n_bus)]
    theta = [sp.symbols(f'theta_{i+1}') for i in range(n_bus)]
    
    # Create symbolic Y-bus
    Y_bus = create_symbolic_y_bus_vectorized(system)
    
    # Calculate AC power injections
    P_ac = []
    
    for i in range(n_bus):
        p_i = 0
        
        for j in range(n_bus):
            if Y_bus[i, j] != 0:
                if isinstance(Y_bus[i, j], complex):
                    G_ij = float(Y_bus[i, j].real)
                    B_ij = float(Y_bus[i, j].imag)
                else:
                    G_ij = sp.re(Y_bus[i, j])
                    B_ij = sp.im(Y_bus[i, j])
                
                angle_diff = theta[i] - theta[j]
                p_term = V[i] * V[j] * (G_ij * sp.cos(angle_diff) + B_ij * sp.sin(angle_diff))
                p_i += p_term
        
        P_ac.append(p_i)
    
    # Apply DC power flow assumptions
    # 1. V_i = V_j = 1.0 (flat voltage profile)
    # 2. sin(theta_i - theta_j) ≈ (theta_i - theta_j)
    # 3. cos(theta_i - theta_j) ≈ 1.0
    # 4. G_ij << B_ij (ignore conductance)
    
    P_dc = []
    for i in range(n_bus):
        # Start with AC active power equation
        p_i = P_ac[i]
        
        # Apply assumption 1: V_i = V_j = 1.0
        p_i = p_i.subs([(v, 1.0) for v in V])
        
        # Apply assumption 3: cos(theta_i - theta_j) ≈ 1.0
        p_i = p_i.subs([(sp.cos(theta[i] - theta[j]), 1.0) for j in range(n_bus)])
        
        # Apply assumption 2: sin(theta_i - theta_j) ≈ (theta_i - theta_j)
        p_i = p_i.subs([(sp.sin(theta[i] - theta[j]), theta[i] - theta[j]) for j in range(n_bus)])
        
        # Apply assumption 4: Ignore conductance (G_ij = 0)
        if isinstance(p_i, sp.Expr):
            p_i = p_i.subs([(sp.re(Y_bus[i, j]), 0) for j in range(n_bus) if Y_bus[i, j] != 0])
        
        P_dc.append(sp.simplify(p_i))
    
    return P_dc

This function symbolically derives the DC power flow equations by applying the standard DC approximations to the full AC equations. The resulting expressions show how the DC power flow relates to the angle differences and network susceptances, providing a clear understanding of the linearization process.

We can also create the DC power flow matrix (B’) symbolically:

def symbolic_dc_power_flow_matrix(system):
    """
    Create symbolic DC power flow matrix (B').
    
    Parameters:
    -----------
    system : PowerSystem object
        The power system data
    
    Returns:
    --------
    B_prime : Matrix of symbolic expressions
        Symbolic DC power flow matrix
    """
    n_bus = system.bus.get_count()
    B_prime = sp.zeros(n_bus, n_bus)
    
    # Dictionary to map bus numbers to indices
    bus_idx = {int(b_i): i for i, b_i in enumerate(system.bus.bus_i)}
    
    # Add branch susceptances to B'
    for idx in range(system.branch.get_count()):
        fbus = int(system.branch.fbus[idx])
        tbus = int(system.branch.tbus[idx])
        i = bus_idx[fbus]
        j = bus_idx[tbus]
        
        # Create symbolic reactance
        x_ij = sp.symbols(f'x_{i+1}_{j+1}')
        
        # Calculate susceptance
        b_ij = 1 / x_ij
        
        # Add off-diagonal elements
        B_prime[i, j] -= b_ij
        B_prime[j, i] -= b_ij
        
        # Add diagonal elements
        B_prime[i, i] += b_ij
        B_prime[j, j] += b_ij
    
    return B_prime
# Example: Creating DC power flow equations for the test system
# Load the system data
system = load_data('case3.xlsx')

# Create symbolic DC power flow equations
P_dc = symbolic_dc_power_flow_derivation(system)

# Display the DC power flow equations
print("Symbolic DC power flow equations:")
for i, eq in enumerate(P_dc):
    print(f"P_dc[{i+1}] = {eq}")

# Create symbolic DC power flow matrix (B')
B_prime = symbolic_dc_power_flow_matrix(system)

# Display the DC power flow matrix
print("\nSymbolic DC power flow matrix (B'):")
for i in range(B_prime.shape[0]):
    for j in range(B_prime.shape[1]):
        if B_prime[i, j] != 0:
            print(f"B'[{i+1},{j+1}] = {B_prime[i, j]}")

# Substitute actual reactance values to get a numerical B' matrix
B_prime_numeric = B_prime.copy()
for idx in range(system.branch.get_count()):
    fbus = int(system.branch.fbus[idx])
    tbus = int(system.branch.tbus[idx])
    i = next(i for i, b_i in enumerate(system.bus.bus_i) if int(b_i) == fbus)
    j = next(j for j, b_j in enumerate(system.bus.bus_i) if int(b_j) == tbus)
    
    # Substitute reactance value
    x_ij = sp.symbols(f'x_{i+1}_{j+1}')
    B_prime_numeric = B_prime_numeric.subs(x_ij, float(system.branch.x[idx]))

print("\nB' matrix with numerical values:")
for i in range(B_prime_numeric.shape[0]):
    print([float(B_prime_numeric[i, j]) if B_prime_numeric[i, j].is_number else 0 
           for j in range(B_prime_numeric.shape[1])])

# Compare with direct calculation of DC power flow
# For a 3-bus system, let's create a simple example with angles
theta_vals = [0.0, 0.1, -0.05]  # Example angle values
theta_sym = [sp.symbols(f'theta_{i+1}') for i in range(len(theta_vals))]

# Calculate P_dc with these angle values
P_dc_numeric = []
for eq in P_dc:
    # Substitute angle values
    eq_subs = eq
    for i, theta_val in enumerate(theta_vals):
        eq_subs = eq_subs.subs(theta_sym[i], theta_val)
    
    # Try to evaluate to a float
    try:
        P_dc_numeric.append(float(eq_subs))
    except:
        P_dc_numeric.append(eq_subs)

print("\nDC power flow results with angle values:")
for i, p in enumerate(P_dc_numeric):
    print(f"P_dc[{i+1}] = {p}")
Symbolic DC power flow equations:
P_dc[1] = (-0.5*((re(r_1_2) - 1.0*im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)**1.0*((re(r_3_1) - 1.0*im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)**1.0*(im(b_1_2) + im(b_3_1)) + 1.0*((re(r_1_2) - 1.0*im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)**1.0*((-re(r_3_1) + 1.0*im(x_3_1))*cos(theta_1 - 1.0*theta_3) + (re(x_3_1) + im(r_3_1))*sin(theta_1 - 1.0*theta_3) + re(r_3_1) - im(x_3_1)) + 1.0*((re(r_3_1) - 1.0*im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)**1.0*((-re(r_1_2) + 1.0*im(x_1_2))*cos(theta_1 - 1.0*theta_2) + (re(x_1_2) + im(r_1_2))*sin(theta_1 - 1.0*theta_2) + re(r_1_2) - im(x_1_2)))/(((re(r_1_2) - 1.0*im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)**1.0*((re(r_3_1) - 1.0*im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)**1.0)
P_dc[2] = (-0.5*((re(r_1_2) - 1.0*im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)**1.0*((re(r_2_3) - 1.0*im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)**1.0*(im(b_1_2) + im(b_2_3)) + 1.0*((re(r_1_2) - 1.0*im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)**1.0*((-re(r_2_3) + 1.0*im(x_2_3))*cos(theta_2 - 1.0*theta_3) + (re(x_2_3) + im(r_2_3))*sin(theta_2 - 1.0*theta_3) + re(r_2_3) - im(x_2_3)) - 1.0*((re(r_2_3) - 1.0*im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)**1.0*(re(x_1_2) + im(r_1_2))*sin(theta_1 - 1.0*theta_2))/(((re(r_1_2) - 1.0*im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)**1.0*((re(r_2_3) - 1.0*im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)**1.0)
P_dc[3] = -1.0*sin(theta_2 - 1.0*theta_3)*re(x_2_3)/((re(r_2_3) - 1.0*im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)**1.0 - 1.0*sin(theta_2 - 1.0*theta_3)*im(r_2_3)/((re(r_2_3) - 1.0*im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)**1.0 - 1.0*sin(theta_1 - 1.0*theta_3)*re(x_3_1)/((re(r_3_1) - 1.0*im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)**1.0 - 1.0*sin(theta_1 - 1.0*theta_3)*im(r_3_1)/((re(r_3_1) - 1.0*im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)**1.0 - 0.5*im(b_2_3) - 0.5*im(b_3_1)

Symbolic DC power flow matrix (B'):
B'[1,1] = 1/x_3_1 + 1/x_1_2
B'[1,2] = -1/x_1_2
B'[1,3] = -1/x_3_1
B'[2,1] = -1/x_1_2
B'[2,2] = 1/x_2_3 + 1/x_1_2
B'[2,3] = -1/x_2_3
B'[3,1] = -1/x_3_1
B'[3,2] = -1/x_2_3
B'[3,3] = 1/x_3_1 + 1/x_2_3

B' matrix with numerical values:
[13.333333333333334, -3.3333333333333335, -10.0]
[-3.3333333333333335, 13.333333333333334, -10.0]
[-10.0, -10.0, 20.0]

DC power flow results with angle values:
P_dc[1] = (-0.5*((re(r_1_2) - 1.0*im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)**1.0*((re(r_3_1) - 1.0*im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)**1.0*(im(b_1_2) + im(b_3_1)) + 1.0*((re(r_1_2) - 1.0*im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)**1.0*(0.00124973960503372*re(r_3_1) + 0.0499791692706783*re(x_3_1) + 0.0499791692706783*im(r_3_1) - 0.00124973960503372*im(x_3_1)) + 1.0*((re(r_3_1) - 1.0*im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)**1.0*(0.00499583472197429*re(r_1_2) - 0.0998334166468282*re(x_1_2) - 0.0998334166468282*im(r_1_2) - 0.00499583472197429*im(x_1_2)))/(((re(r_1_2) - 1.0*im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)**1.0*((re(r_3_1) - 1.0*im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)**1.0)
P_dc[2] = (-0.5*((re(r_1_2) - 1.0*im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)**1.0*((re(r_2_3) - 1.0*im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)**1.0*(im(b_1_2) + im(b_2_3)) + 1.0*((re(r_1_2) - 1.0*im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)**1.0*(0.0112289220639578*re(r_2_3) + 0.149438132473599*re(x_2_3) + 0.149438132473599*im(r_2_3) - 0.0112289220639578*im(x_2_3)) + 0.0998334166468282*((re(r_2_3) - 1.0*im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)**1.0*(re(x_1_2) + im(r_1_2)))/(((re(r_1_2) - 1.0*im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)**1.0*((re(r_2_3) - 1.0*im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)**1.0)
P_dc[3] = -0.149438132473599*re(x_2_3)/((re(r_2_3) - 1.0*im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)**1.0 - 0.149438132473599*im(r_2_3)/((re(r_2_3) - 1.0*im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)**1.0 - 0.0499791692706783*re(x_3_1)/((re(r_3_1) - 1.0*im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)**1.0 - 0.0499791692706783*im(r_3_1)/((re(r_3_1) - 1.0*im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)**1.0 - 0.5*im(b_2_3) - 0.5*im(b_3_1)

The symbolic DC power flow matrix highlights the network structure and the role of branch reactances in determining power flows. It can be used to analytically derive relationships such as Power Transfer Distribution Factors (PTDFs).

DC Power Flow Advantages

DC power flow offers several advantages:

  1. The equations are linear, making them easier to solve

  2. The solution process doesn’t require iteration

  3. The simplicity makes it useful for applications like contingency analysis and market simulations

However, it also has limitations:

  1. It neglects reactive power flows

  2. It assumes all voltages are 1.0 per unit

  3. It neglects transmission losses

Understanding these trade-offs is easier with symbolic analysis, which clearly shows the simplifications made.

Symbolic Sensitivity Analysis#

Sensitivity analysis is crucial for understanding how changes in system parameters or operating conditions affect system behavior. Symbolic computation provides a powerful tool for deriving sensitivity factors analytically.

One important set of sensitivity factors is the Power Transfer Distribution Factors (PTDFs), which show how a change in power injection affects the flow on transmission lines:

def symbolic_ptdf_matrix(system):
    """
    Calculate Power Transfer Distribution Factors (PTDFs) symbolically.
    
    Parameters:
    -----------
    system : PowerSystem object
        The power system data
    
    Returns:
    --------
    ptdf : Matrix of symbolic expressions
        Symbolic PTDF matrix
    """
    n_bus = system.bus.get_count()
    n_branch = system.branch.get_count()
    
    # Find slack bus index
    slack_idx = next(i for i, type_val in enumerate(system.bus.bus_type) if type_val == 3)
    
    # Create symbolic B' matrix
    B_prime = symbolic_dc_power_flow_matrix(system)
    
    # Create reduced B' matrix by removing slack bus row and column
    B_prime_reduced = B_prime.copy()
    B_prime_reduced.row_del(slack_idx)
    B_prime_reduced.col_del(slack_idx)
    
    # Calculate B' inverse symbolically
    # Note: This can be computationally intensive for large systems
    try:
        B_prime_inv = B_prime_reduced.inv()
    except:
        print("Symbolic inversion failed. Using a simplified approach.")
        # Use a simplified approach for demonstration
        B_prime_inv = sp.zeros(n_bus-1, n_bus-1)
        for i in range(n_bus-1):
            for j in range(n_bus-1):
                B_prime_inv[i, j] = sp.symbols(f'X_{i+1}_{j+1}')
    
    # Create symbolic PTDF matrix
    ptdf = sp.zeros(n_branch, n_bus)
    
    # Dictionary to map bus numbers to indices
    bus_idx = {int(b_i): i for i, b_i in enumerate(system.bus.bus_i)}
    
    # Calculate PTDF for each branch and bus pair
    for l in range(n_branch):
        fbus = int(system.branch.fbus[l])
        tbus = int(system.branch.tbus[l])
        i = bus_idx[fbus]
        j = bus_idx[tbus]
        
        # Create symbolic reactance
        x_l = sp.symbols(f'x_{i+1}_{j+1}')
        
        for k in range(n_bus):
            if k == slack_idx:
                # PTDF for slack bus is zero
                ptdf[l, k] = 0
            else:
                # Adjust index for reduced matrix
                k_adj = k if k < slack_idx else k - 1
                
                if i != slack_idx and j != slack_idx:
                    # Both buses are not slack
                    i_adj = i if i < slack_idx else i - 1
                    j_adj = j if j < slack_idx else j - 1
                    ptdf[l, k] = (1 / x_l) * (B_prime_inv[i_adj, k_adj] - B_prime_inv[j_adj, k_adj])
                elif i == slack_idx:
                    # From bus is slack
                    j_adj = j if j < slack_idx else j - 1
                    ptdf[l, k] = -(1 / x_l) * B_prime_inv[j_adj, k_adj]
                elif j == slack_idx:
                    # To bus is slack
                    i_adj = i if i < slack_idx else i - 1
                    ptdf[l, k] = (1 / x_l) * B_prime_inv[i_adj, k_adj]
    
    return ptdf

This function symbolically derives the PTDF matrix, which relates changes in bus power injections to changes in branch flows. The symbolic derivation reveals how the network structure and impedances determine these sensitivity factors.

We can also derive loss factors symbolically, showing how system losses are affected by changes in bus injections:

def symbolic_loss_factors(system):
    """
    Derive loss factors symbolically.
    
    Parameters:
    -----------
    system : PowerSystem object
        The power system data
    
    Returns:
    --------
    loss_factors : list of symbolic expressions
        Symbolic loss factors for each bus
    """
    n_bus = system.bus.get_count()
    n_branch = system.branch.get_count()
    
    # Create symbolic variables
    V = [sp.symbols(f'V_{i+1}') for i in range(n_bus)]
    theta = [sp.symbols(f'theta_{i+1}') for i in range(n_bus)]
    
    # Dictionary to map bus numbers to indices
    bus_idx = {int(b_i): i for i, b_i in enumerate(system.bus.bus_i)}
    
    # Calculate system losses symbolically
    losses = 0
    
    for idx in range(n_branch):
        fbus = int(system.branch.fbus[idx])
        tbus = int(system.branch.tbus[idx])
        i = bus_idx[fbus]
        j = bus_idx[tbus]
        
        # Create symbolic resistance and reactance
        r_ij = sp.symbols(f'r_{i+1}_{j+1}')
        x_ij = sp.symbols(f'x_{i+1}_{j+1}')
        
        # Calculate impedance and admittance
        z_ij = r_ij + sp.I * x_ij
        y_ij = 1 / z_ij
        
        # Calculate voltage difference
        V_i = V[i] * sp.exp(sp.I * theta[i])
        V_j = V[j] * sp.exp(sp.I * theta[j])
        
        # Calculate branch current
        I_ij = (V_i - V_j) * y_ij
        
        # Calculate branch losses
        branch_loss = r_ij * sp.Abs(I_ij)**2
        losses += branch_loss
    
    # Derive loss factors (partial derivative of losses with respect to bus injections)
    loss_factors = []
    
    for i in range(n_bus):
        # Create symbolic bus injection
        P_i = sp.symbols(f'P_{i+1}')
        
        # Loss factor is the partial derivative of losses with respect to P_i
        # For simplicity, we'll calculate the partial derivative with respect to theta
        loss_factor = 0
        
        # Apply chain rule: ∂Loss/∂P_i = Σj (∂Loss/∂θj) * (∂θj/∂P_i)
        for j in range(n_bus):
            if j != next(i for i, type_val in enumerate(system.bus.bus_type) if type_val == 3):
                # Derivative of losses with respect to theta_j
                dlosses_dtheta_j = sp.diff(losses, theta[j])
                
                # Sensitivity of theta_j to change in P_i (simplified as symbolic)
                dtheta_j_dP_i = sp.symbols(f'S_{j+1}_{i+1}')
                
                loss_factor += dlosses_dtheta_j * dtheta_j_dP_i
        
        loss_factors.append(loss_factor)
    
    return loss_factors
# Example: Calculating PTDF and loss factors symbolically
# Load the system data
system = load_data('case3.xlsx')
# Calculate symbolic PTDF matrix
try:
    ptdf = symbolic_ptdf_matrix(system)
    
    # Display a portion of the PTDF matrix
    print("Symbolic PTDF matrix (partial):")
    for l in range(min(2, ptdf.shape[0])):
        for k in range(min(2, ptdf.shape[1])):
            print(f"PTDF[{l+1},{k+1}] = {ptdf[l,k]}")
except Exception as e:
    print(f"Could not calculate full symbolic PTDF matrix: {e}")
    
    # Alternative: Calculate PTDF for a single branch-bus pair
    print("\nSymbolic PTDF for a single branch-bus pair:")
    # For a 3-bus system with bus 1 as slack
    # PTDF for branch 1-2 and injection at bus 3
    x_12 = sp.symbols('x_1_2')
    X_22 = sp.symbols('X_2_2')  # Element of B' inverse
    ptdf_13_2 = -(1 / x_12) * X_22
    print(f"PTDF[branch 1-2, bus 3] = {ptdf_13_2}")
Symbolic PTDF matrix (partial):
PTDF[1,1] = 0
PTDF[1,2] = -(x_1_2*x_2_3 + x_1_2*x_3_1)/(x_1_2*(x_1_2 + x_2_3 + x_3_1))
PTDF[2,1] = 0
PTDF[2,2] = (-x_1_2*x_3_1/(x_1_2 + x_2_3 + x_3_1) + (x_1_2*x_2_3 + x_1_2*x_3_1)/(x_1_2 + x_2_3 + x_3_1))/x_2_3
# Calculate symbolic loss factors
loss_factors = symbolic_loss_factors(system)

# Display the symbolic loss factors
print("\nSymbolic loss factors:")
for i, lf in enumerate(loss_factors):
    print(f"Loss Factor[{i+1}] = {lf}")
Symbolic loss factors:
Loss Factor[1] = S_2_1*(2*r_1_2*(r_1_2 + I*x_1_2)*(((re(r_1_2) - im(x_1_2))*(re(V_1*exp(I*theta_1)) - re(V_2*exp(I*theta_2)))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) - (-re(x_1_2) - im(r_1_2))*(im(V_1*exp(I*theta_1)) - im(V_2*exp(I*theta_2)))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2))*(-(re(r_1_2) - im(x_1_2))*Derivative(re(V_2*exp(I*theta_2)), theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) + (-re(x_1_2) - im(r_1_2))*Derivative(im(V_2*exp(I*theta_2)), theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)) + ((re(r_1_2) - im(x_1_2))*(im(V_1*exp(I*theta_1)) - im(V_2*exp(I*theta_2)))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) + (-re(x_1_2) - im(r_1_2))*(re(V_1*exp(I*theta_1)) - re(V_2*exp(I*theta_2)))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2))*(-(re(r_1_2) - im(x_1_2))*Derivative(im(V_2*exp(I*theta_2)), theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) - (-re(x_1_2) - im(r_1_2))*Derivative(re(V_2*exp(I*theta_2)), theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)))*Abs((V_1*exp(I*theta_1) - V_2*exp(I*theta_2))/(r_1_2 + I*x_1_2))*sign((V_1*exp(I*theta_1) - V_2*exp(I*theta_2))/(r_1_2 + I*x_1_2))/(V_1*exp(I*theta_1) - V_2*exp(I*theta_2)) + 2*r_2_3*(r_2_3 + I*x_2_3)*(((re(r_2_3) - im(x_2_3))*(re(V_2*exp(I*theta_2)) - re(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_2_3) - im(r_2_3))*(im(V_2*exp(I*theta_2)) - im(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))*((re(r_2_3) - im(x_2_3))*Derivative(re(V_2*exp(I*theta_2)), theta_2)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_2_3) - im(r_2_3))*Derivative(im(V_2*exp(I*theta_2)), theta_2)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)) + ((re(r_2_3) - im(x_2_3))*(im(V_2*exp(I*theta_2)) - im(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*(re(V_2*exp(I*theta_2)) - re(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))*((re(r_2_3) - im(x_2_3))*Derivative(im(V_2*exp(I*theta_2)), theta_2)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*Derivative(re(V_2*exp(I*theta_2)), theta_2)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)))*Abs((V_2*exp(I*theta_2) - V_3*exp(I*theta_3))/(r_2_3 + I*x_2_3))*sign((V_2*exp(I*theta_2) - V_3*exp(I*theta_3))/(r_2_3 + I*x_2_3))/(V_2*exp(I*theta_2) - V_3*exp(I*theta_3))) + S_3_1*(2*r_2_3*(r_2_3 + I*x_2_3)*(((re(r_2_3) - im(x_2_3))*(re(V_2*exp(I*theta_2)) - re(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_2_3) - im(r_2_3))*(im(V_2*exp(I*theta_2)) - im(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))*(-(re(r_2_3) - im(x_2_3))*Derivative(re(V_3*exp(I*theta_3)), theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*Derivative(im(V_3*exp(I*theta_3)), theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)) + ((re(r_2_3) - im(x_2_3))*(im(V_2*exp(I*theta_2)) - im(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*(re(V_2*exp(I*theta_2)) - re(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))*(-(re(r_2_3) - im(x_2_3))*Derivative(im(V_3*exp(I*theta_3)), theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_2_3) - im(r_2_3))*Derivative(re(V_3*exp(I*theta_3)), theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)))*Abs((V_2*exp(I*theta_2) - V_3*exp(I*theta_3))/(r_2_3 + I*x_2_3))*sign((V_2*exp(I*theta_2) - V_3*exp(I*theta_3))/(r_2_3 + I*x_2_3))/(V_2*exp(I*theta_2) - V_3*exp(I*theta_3)) + 2*r_3_1*(r_3_1 + I*x_3_1)*(((re(r_3_1) - im(x_3_1))*(re(V_1*exp(I*theta_1)) - re(V_3*exp(I*theta_3)))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) - (-re(x_3_1) - im(r_3_1))*(im(V_1*exp(I*theta_1)) - im(V_3*exp(I*theta_3)))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2))*(-(re(r_3_1) - im(x_3_1))*Derivative(re(V_3*exp(I*theta_3)), theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) + (-re(x_3_1) - im(r_3_1))*Derivative(im(V_3*exp(I*theta_3)), theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)) + ((re(r_3_1) - im(x_3_1))*(im(V_1*exp(I*theta_1)) - im(V_3*exp(I*theta_3)))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) + (-re(x_3_1) - im(r_3_1))*(re(V_1*exp(I*theta_1)) - re(V_3*exp(I*theta_3)))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2))*(-(re(r_3_1) - im(x_3_1))*Derivative(im(V_3*exp(I*theta_3)), theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) - (-re(x_3_1) - im(r_3_1))*Derivative(re(V_3*exp(I*theta_3)), theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)))*Abs((V_1*exp(I*theta_1) - V_3*exp(I*theta_3))/(r_3_1 + I*x_3_1))*sign((V_1*exp(I*theta_1) - V_3*exp(I*theta_3))/(r_3_1 + I*x_3_1))/(V_1*exp(I*theta_1) - V_3*exp(I*theta_3)))
Loss Factor[2] = S_2_2*(2*r_1_2*(r_1_2 + I*x_1_2)*(((re(r_1_2) - im(x_1_2))*(re(V_1*exp(I*theta_1)) - re(V_2*exp(I*theta_2)))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) - (-re(x_1_2) - im(r_1_2))*(im(V_1*exp(I*theta_1)) - im(V_2*exp(I*theta_2)))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2))*(-(re(r_1_2) - im(x_1_2))*Derivative(re(V_2*exp(I*theta_2)), theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) + (-re(x_1_2) - im(r_1_2))*Derivative(im(V_2*exp(I*theta_2)), theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)) + ((re(r_1_2) - im(x_1_2))*(im(V_1*exp(I*theta_1)) - im(V_2*exp(I*theta_2)))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) + (-re(x_1_2) - im(r_1_2))*(re(V_1*exp(I*theta_1)) - re(V_2*exp(I*theta_2)))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2))*(-(re(r_1_2) - im(x_1_2))*Derivative(im(V_2*exp(I*theta_2)), theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) - (-re(x_1_2) - im(r_1_2))*Derivative(re(V_2*exp(I*theta_2)), theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)))*Abs((V_1*exp(I*theta_1) - V_2*exp(I*theta_2))/(r_1_2 + I*x_1_2))*sign((V_1*exp(I*theta_1) - V_2*exp(I*theta_2))/(r_1_2 + I*x_1_2))/(V_1*exp(I*theta_1) - V_2*exp(I*theta_2)) + 2*r_2_3*(r_2_3 + I*x_2_3)*(((re(r_2_3) - im(x_2_3))*(re(V_2*exp(I*theta_2)) - re(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_2_3) - im(r_2_3))*(im(V_2*exp(I*theta_2)) - im(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))*((re(r_2_3) - im(x_2_3))*Derivative(re(V_2*exp(I*theta_2)), theta_2)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_2_3) - im(r_2_3))*Derivative(im(V_2*exp(I*theta_2)), theta_2)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)) + ((re(r_2_3) - im(x_2_3))*(im(V_2*exp(I*theta_2)) - im(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*(re(V_2*exp(I*theta_2)) - re(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))*((re(r_2_3) - im(x_2_3))*Derivative(im(V_2*exp(I*theta_2)), theta_2)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*Derivative(re(V_2*exp(I*theta_2)), theta_2)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)))*Abs((V_2*exp(I*theta_2) - V_3*exp(I*theta_3))/(r_2_3 + I*x_2_3))*sign((V_2*exp(I*theta_2) - V_3*exp(I*theta_3))/(r_2_3 + I*x_2_3))/(V_2*exp(I*theta_2) - V_3*exp(I*theta_3))) + S_3_2*(2*r_2_3*(r_2_3 + I*x_2_3)*(((re(r_2_3) - im(x_2_3))*(re(V_2*exp(I*theta_2)) - re(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_2_3) - im(r_2_3))*(im(V_2*exp(I*theta_2)) - im(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))*(-(re(r_2_3) - im(x_2_3))*Derivative(re(V_3*exp(I*theta_3)), theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*Derivative(im(V_3*exp(I*theta_3)), theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)) + ((re(r_2_3) - im(x_2_3))*(im(V_2*exp(I*theta_2)) - im(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*(re(V_2*exp(I*theta_2)) - re(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))*(-(re(r_2_3) - im(x_2_3))*Derivative(im(V_3*exp(I*theta_3)), theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_2_3) - im(r_2_3))*Derivative(re(V_3*exp(I*theta_3)), theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)))*Abs((V_2*exp(I*theta_2) - V_3*exp(I*theta_3))/(r_2_3 + I*x_2_3))*sign((V_2*exp(I*theta_2) - V_3*exp(I*theta_3))/(r_2_3 + I*x_2_3))/(V_2*exp(I*theta_2) - V_3*exp(I*theta_3)) + 2*r_3_1*(r_3_1 + I*x_3_1)*(((re(r_3_1) - im(x_3_1))*(re(V_1*exp(I*theta_1)) - re(V_3*exp(I*theta_3)))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) - (-re(x_3_1) - im(r_3_1))*(im(V_1*exp(I*theta_1)) - im(V_3*exp(I*theta_3)))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2))*(-(re(r_3_1) - im(x_3_1))*Derivative(re(V_3*exp(I*theta_3)), theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) + (-re(x_3_1) - im(r_3_1))*Derivative(im(V_3*exp(I*theta_3)), theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)) + ((re(r_3_1) - im(x_3_1))*(im(V_1*exp(I*theta_1)) - im(V_3*exp(I*theta_3)))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) + (-re(x_3_1) - im(r_3_1))*(re(V_1*exp(I*theta_1)) - re(V_3*exp(I*theta_3)))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2))*(-(re(r_3_1) - im(x_3_1))*Derivative(im(V_3*exp(I*theta_3)), theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) - (-re(x_3_1) - im(r_3_1))*Derivative(re(V_3*exp(I*theta_3)), theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)))*Abs((V_1*exp(I*theta_1) - V_3*exp(I*theta_3))/(r_3_1 + I*x_3_1))*sign((V_1*exp(I*theta_1) - V_3*exp(I*theta_3))/(r_3_1 + I*x_3_1))/(V_1*exp(I*theta_1) - V_3*exp(I*theta_3)))
Loss Factor[3] = S_2_3*(2*r_1_2*(r_1_2 + I*x_1_2)*(((re(r_1_2) - im(x_1_2))*(re(V_1*exp(I*theta_1)) - re(V_2*exp(I*theta_2)))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) - (-re(x_1_2) - im(r_1_2))*(im(V_1*exp(I*theta_1)) - im(V_2*exp(I*theta_2)))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2))*(-(re(r_1_2) - im(x_1_2))*Derivative(re(V_2*exp(I*theta_2)), theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) + (-re(x_1_2) - im(r_1_2))*Derivative(im(V_2*exp(I*theta_2)), theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)) + ((re(r_1_2) - im(x_1_2))*(im(V_1*exp(I*theta_1)) - im(V_2*exp(I*theta_2)))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) + (-re(x_1_2) - im(r_1_2))*(re(V_1*exp(I*theta_1)) - re(V_2*exp(I*theta_2)))/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2))*(-(re(r_1_2) - im(x_1_2))*Derivative(im(V_2*exp(I*theta_2)), theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2) - (-re(x_1_2) - im(r_1_2))*Derivative(re(V_2*exp(I*theta_2)), theta_2)/((re(r_1_2) - im(x_1_2))**2 + (re(x_1_2) + im(r_1_2))**2)))*Abs((V_1*exp(I*theta_1) - V_2*exp(I*theta_2))/(r_1_2 + I*x_1_2))*sign((V_1*exp(I*theta_1) - V_2*exp(I*theta_2))/(r_1_2 + I*x_1_2))/(V_1*exp(I*theta_1) - V_2*exp(I*theta_2)) + 2*r_2_3*(r_2_3 + I*x_2_3)*(((re(r_2_3) - im(x_2_3))*(re(V_2*exp(I*theta_2)) - re(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_2_3) - im(r_2_3))*(im(V_2*exp(I*theta_2)) - im(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))*((re(r_2_3) - im(x_2_3))*Derivative(re(V_2*exp(I*theta_2)), theta_2)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_2_3) - im(r_2_3))*Derivative(im(V_2*exp(I*theta_2)), theta_2)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)) + ((re(r_2_3) - im(x_2_3))*(im(V_2*exp(I*theta_2)) - im(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*(re(V_2*exp(I*theta_2)) - re(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))*((re(r_2_3) - im(x_2_3))*Derivative(im(V_2*exp(I*theta_2)), theta_2)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*Derivative(re(V_2*exp(I*theta_2)), theta_2)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)))*Abs((V_2*exp(I*theta_2) - V_3*exp(I*theta_3))/(r_2_3 + I*x_2_3))*sign((V_2*exp(I*theta_2) - V_3*exp(I*theta_3))/(r_2_3 + I*x_2_3))/(V_2*exp(I*theta_2) - V_3*exp(I*theta_3))) + S_3_3*(2*r_2_3*(r_2_3 + I*x_2_3)*(((re(r_2_3) - im(x_2_3))*(re(V_2*exp(I*theta_2)) - re(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_2_3) - im(r_2_3))*(im(V_2*exp(I*theta_2)) - im(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))*(-(re(r_2_3) - im(x_2_3))*Derivative(re(V_3*exp(I*theta_3)), theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*Derivative(im(V_3*exp(I*theta_3)), theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)) + ((re(r_2_3) - im(x_2_3))*(im(V_2*exp(I*theta_2)) - im(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) + (-re(x_2_3) - im(r_2_3))*(re(V_2*exp(I*theta_2)) - re(V_3*exp(I*theta_3)))/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2))*(-(re(r_2_3) - im(x_2_3))*Derivative(im(V_3*exp(I*theta_3)), theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2) - (-re(x_2_3) - im(r_2_3))*Derivative(re(V_3*exp(I*theta_3)), theta_3)/((re(r_2_3) - im(x_2_3))**2 + (re(x_2_3) + im(r_2_3))**2)))*Abs((V_2*exp(I*theta_2) - V_3*exp(I*theta_3))/(r_2_3 + I*x_2_3))*sign((V_2*exp(I*theta_2) - V_3*exp(I*theta_3))/(r_2_3 + I*x_2_3))/(V_2*exp(I*theta_2) - V_3*exp(I*theta_3)) + 2*r_3_1*(r_3_1 + I*x_3_1)*(((re(r_3_1) - im(x_3_1))*(re(V_1*exp(I*theta_1)) - re(V_3*exp(I*theta_3)))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) - (-re(x_3_1) - im(r_3_1))*(im(V_1*exp(I*theta_1)) - im(V_3*exp(I*theta_3)))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2))*(-(re(r_3_1) - im(x_3_1))*Derivative(re(V_3*exp(I*theta_3)), theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) + (-re(x_3_1) - im(r_3_1))*Derivative(im(V_3*exp(I*theta_3)), theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)) + ((re(r_3_1) - im(x_3_1))*(im(V_1*exp(I*theta_1)) - im(V_3*exp(I*theta_3)))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) + (-re(x_3_1) - im(r_3_1))*(re(V_1*exp(I*theta_1)) - re(V_3*exp(I*theta_3)))/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2))*(-(re(r_3_1) - im(x_3_1))*Derivative(im(V_3*exp(I*theta_3)), theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2) - (-re(x_3_1) - im(r_3_1))*Derivative(re(V_3*exp(I*theta_3)), theta_3)/((re(r_3_1) - im(x_3_1))**2 + (re(x_3_1) + im(r_3_1))**2)))*Abs((V_1*exp(I*theta_1) - V_3*exp(I*theta_3))/(r_3_1 + I*x_3_1))*sign((V_1*exp(I*theta_1) - V_3*exp(I*theta_3))/(r_3_1 + I*x_3_1))/(V_1*exp(I*theta_1) - V_3*exp(I*theta_3)))
# Example: Simplify loss factors with assumptions
print("\nSimplified loss factors (partial derivatives only):")
n_bus = system.bus.get_count()
V = [sp.symbols(f'V_{i+1}') for i in range(n_bus)]
theta = [sp.symbols(f'theta_{i+1}') for i in range(n_bus)]

# Create a simplified system loss expression
# L = r_12 * [(V_1*V_2/x_12) * (θ_1 - θ_2)]^2
r_12 = sp.symbols('r_1_2')
x_12 = sp.symbols('x_1_2')
simplified_loss = r_12 * ((V[0]*V[1]/x_12) * (theta[0] - theta[1]))**2

# Calculate partial derivatives
dL_dtheta1 = sp.diff(simplified_loss, theta[0])
dL_dtheta2 = sp.diff(simplified_loss, theta[1])

print(f"∂Loss/∂θ_1 = {dL_dtheta1}")
print(f"∂Loss/∂θ_2 = {dL_dtheta2}")
Simplified loss factors (partial derivatives only):
∂Loss/∂θ_1 = V_1**2*V_2**2*r_1_2*(2*theta_1 - 2*theta_2)/x_1_2**2
∂Loss/∂θ_2 = V_1**2*V_2**2*r_1_2*(-2*theta_1 + 2*theta_2)/x_1_2**2

These symbolic sensitivity factors provide insights into system behavior that might be difficult to obtain through purely numerical methods. They reveal the structural relationships between system parameters and sensitivities.

Practical Considerations

Symbolic computation can be computationally intensive for large systems. For practical applications, it’s often useful to:

  1. Use symbolic computation to derive general formulas

  2. Substitute specific numerical values for final calculations

  3. Generate efficient numerical code from symbolic expressions

  4. Apply simplifications and approximations where appropriate

This approach combines the insight of symbolic analysis with the efficiency of numerical methods.

Converting Symbolic Expressions to Efficient Code#

While symbolic computation provides valuable insights, numerical calculations are often more efficient for practical applications. SymPy provides tools to convert symbolic expressions into efficient numerical functions:

def generate_numerical_functions(symbolic_expressions, symbolic_vars):
    """
    Generate efficient numerical functions from symbolic expressions.
    
    Parameters:
    -----------
    symbolic_expressions : list or Matrix of symbolic expressions
        The symbolic expressions to convert
    symbolic_vars : list of symbolic variables
        The variables in the expressions
    
    Returns:
    --------
    numeric_functions : list of callable functions
        Numerical functions that evaluate the expressions
    """
    # Convert a single expression to a list for consistent handling
    if not isinstance(symbolic_expressions, list) and not isinstance(symbolic_expressions, Matrix):
        symbolic_expressions = [symbolic_expressions]
    
    # Create numerical functions using lambdify
    numeric_functions = []
    
    for expr in symbolic_expressions:
        # lambdify converts a symbolic expression to a numerical function
        numeric_fn = lambdify(symbolic_vars, expr, modules='numpy')
        numeric_functions.append(numeric_fn)
    
    return numeric_functions

This function uses SymPy’s lambdify to convert symbolic expressions into efficient numerical functions. These functions can be used for fast repeated evaluations, such as in iterative algorithms like Newton-Raphson.

We can also use this approach to generate optimized code for power system calculations:

def generate_power_flow_functions(system):
    """
    Generate efficient numerical functions for power flow calculations.
    
    Parameters:
    -----------
    system : PowerSystem object
        The power system data
    
    Returns:
    --------
    power_functions : dict
        Dictionary of numerical functions for power calculations
    jacobian_function : callable
        Function to evaluate the Jacobian matrix
    """
    # Create symbolic power flow equations
    P_eq, Q_eq, V, theta = symbolic_ac_power_flow_equations(system)
    
    # Create symbolic Jacobian
    J = symbolic_power_flow_jacobian(system, P_eq, Q_eq, V, theta)
    
    # Generate numerical functions
    all_vars = V + theta
    
    # Convert active power equations to numerical functions
    P_functions = []
    for eq in P_eq:
        # lambdify converts a symbolic expression to a numerical function
        P_functions.append(lambdify(all_vars, eq, modules='numpy'))
    
    # Convert reactive power equations to numerical functions
    Q_functions = []
    for eq in Q_eq:
        Q_functions.append(lambdify(all_vars, eq, modules='numpy'))
    
    # Convert Jacobian to numerical function
    jacobian_function = lambdify(all_vars, J, modules='numpy')
    
    # Return all functions
    power_functions = {
        'P': P_functions,
        'Q': Q_functions
    }
    
    return power_functions, jacobian_function

This function generates optimized numerical functions for power flow calculations, including the power balance equations and the Jacobian matrix. These functions can be used for efficient iterative solution of the power flow problem.

Let’s compare the performance of symbolic-generated code with hard-coded implementations:

def performance_comparison(system):
    """
    Compare performance of symbolic-generated and hard-coded implementations.
    
    Parameters:
    -----------
    system : PowerSystem object
        The power system data
    
    Returns:
    --------
    None (prints comparison results)
    """
    import time
    import numpy as np
    
    n_bus = system.bus.get_count()
    
    try:
        # Generate functions from symbolic expressions
        power_functions, jacobian_function = generate_power_flow_functions(system)
        
        # Create test data
        V_test = np.ones(n_bus)
        theta_test = np.zeros(n_bus)
        all_vars_test = np.concatenate([V_test, theta_test])
        
        # Hard-coded implementation for comparison
        def hard_coded_power_flow(V, theta, Y_bus):
            n = len(V)
            P = np.zeros(n)
            Q = np.zeros(n)
            
            for i in range(n):
                for j in range(n):
                    if Y_bus[i, j] != 0:
                        # Extract Y_bus components
                        G_ij = np.real(Y_bus[i, j])
                        B_ij = np.imag(Y_bus[i, j])
                        
                        # Angle difference
                        angle_diff = theta[i] - theta[j]
                        
                        # Power calculations
                        P[i] += V[i] * V[j] * (G_ij * np.cos(angle_diff) + 
                                              B_ij * np.sin(angle_diff))
                        Q[i] += V[i] * V[j] * (G_ij * np.sin(angle_diff) - 
                                              B_ij * np.cos(angle_diff))
            
            return P, Q
        
        # Create numerical Y-bus
        Y_bus_numerical = np.zeros((n_bus, n_bus), dtype=complex)
        
        # Map bus numbers to indices
        bus_idx = {int(b_i): i for i, b_i in enumerate(system.bus.bus_i)}
        
        for idx in range(system.branch.get_count()):
            fbus = int(system.branch.fbus[idx])
            tbus = int(system.branch.tbus[idx])
            i = bus_idx[fbus]
            j = bus_idx[tbus]
            
            # Calculate impedance and admittance
            r = float(system.branch.r[idx])
            x = float(system.branch.x[idx])
            b = float(system.branch.b[idx])
            
            z = complex(r, x)
            y = 1 / z
            
            # Add off-diagonal elements
            Y_bus_numerical[i, j] -= y
            Y_bus_numerical[j, i] -= y
            
            # Add diagonal elements
            Y_bus_numerical[i, i] += y + 0.5j * b
            Y_bus_numerical[j, j] += y + 0.5j * b
        
        # Number of iterations for timing
        n_iterations = 100
        
        # Timing for symbolic-generated implementation
        start_time = time.time()
        for _ in range(n_iterations):
            for i in range(n_bus):
                if i < len(power_functions['P']):
                    P_i = power_functions['P'][i](*all_vars_test)
                if i < len(power_functions['Q']):
                    Q_i = power_functions['Q'][i](*all_vars_test)
        symbolic_time = time.time() - start_time
        
        # Timing for hard-coded implementation
        start_time = time.time()
        for _ in range(n_iterations):
            P, Q = hard_coded_power_flow(V_test, theta_test, Y_bus_numerical)
        hard_coded_time = time.time() - start_time
        
        print(f"Performance comparison for {n_iterations} iterations:")
        print(f"Symbolic-generated code time: {symbolic_time:.6f} seconds")
        print(f"Hard-coded implementation time: {hard_coded_time:.6f} seconds")
        print(f"Ratio (symbolic/hard-coded): {symbolic_time/hard_coded_time:.2f}")
        
        # Example: Using symbolic-generated Jacobian
        try:
            J_numeric = jacobian_function(*all_vars_test)
            print("\nJacobian evaluation successful")
            print(f"Jacobian shape: {J_numeric.shape}")
            
            # Simple verification: compare a few entries with numerical derivatives
            print("\nVerification of Jacobian accuracy:")
            
            # Calculate numerical derivatives for comparison
            epsilon = 1e-6
            
            # Choose a variable to perturb (e.g., first theta)
            var_idx = n_bus  # First theta variable (after V variables)
            var_name = "theta_1"
            
            # Make a copy of the test data
            perturbed_vars = all_vars_test.copy()
            
            # Perturb the variable
            perturbed_vars[var_idx] += epsilon
            
            # Calculate power with perturbed variable
            perturbed_P = []
            for i in range(min(2, len(power_functions['P']))):
                perturbed_P.append(power_functions['P'][i](*perturbed_vars))
            
            # Calculate numerical derivatives
            numerical_derivs = []
            for i in range(min(2, len(perturbed_P))):
                original_P = power_functions['P'][i](*all_vars_test)
                numerical_derivs.append((perturbed_P[i] - original_P) / epsilon)
            
            # Compare with Jacobian entries
            for i in range(min(2, len(numerical_derivs))):
                symbolic_deriv = J_numeric[i, 0] if J_numeric.shape[1] > 0 else "N/A"
                print(f"∂P_{i+1}/∂{var_name}:")
                print(f"  Numerical: {numerical_derivs[i]}")
                print(f"  Symbolic:  {symbolic_deriv}")
        except Exception as e:
            print(f"Jacobian evaluation failed: {e}")
    except Exception as e:
        print(f"Performance comparison failed: {e}")
        print("Using simplified example instead...")
        
        # Simplified example with a single symbolic expression
        x, y = sp.symbols('x y')
        expr = sp.sin(x) * sp.cos(y) + x**2 * y
        
        # Convert to numerical function
        f_symbolic = lambdify([x, y], expr, 'numpy')
        
        # Hard-coded equivalent
        def f_hardcoded(x, y):
            return np.sin(x) * np.cos(y) + x**2 * y
        
        # Test values
        x_val, y_val = 0.5, 1.0
        
        # Time symbolic function
        start_time = time.time()
        for _ in range(10000):
            result_symbolic = f_symbolic(x_val, y_val)
        symbolic_time = time.time() - start_time
        
        # Time hard-coded function
        start_time = time.time()
        for _ in range(10000):
            result_hardcoded = f_hardcoded(x_val, y_val)
        hardcoded_time = time.time() - start_time
        
        print(f"Simplified example results:")
        print(f"  Symbolic result: {result_symbolic}")
        print(f"  Hard-coded result: {result_hardcoded}")
        print(f"  Symbolic time: {symbolic_time:.6f} s")
        print(f"  Hard-coded time: {hardcoded_time:.6f} s")
        print(f"  Ratio (symbolic/hard-coded): {symbolic_time/hardcoded_time:.2f}")
system = load_data('case3.xlsx')

# Generate power flow functions
try:
    power_functions, jacobian_function = generate_power_flow_functions(system)
    
    # Create test data
    n_bus = system.bus.get_count()
    V_test = np.ones(n_bus)
    theta_test = np.zeros(n_bus)
    all_vars_test = np.concatenate([V_test, theta_test])
    
    # Evaluate power flow equations
    print("Evaluating symbolic-generated power flow equations:")
    for i in range(n_bus):
        if i < len(power_functions['P']):
            P_i = power_functions['P'][i](*all_vars_test)
            print(f"P_{i+1} = {P_i}")
        if i < len(power_functions['Q']):
            Q_i = power_functions['Q'][i](*all_vars_test)
            print(f"Q_{i+1} = {Q_i}")
    
    # Test Jacobian function
    try:
        J_numeric = jacobian_function(*all_vars_test)
        print(f"\nJacobian shape: {J_numeric.shape}")
        if J_numeric.size > 0:
            print("First Jacobian element:", J_numeric[0, 0])
    except Exception as e:
        print(f"Jacobian evaluation failed: {e}")
except Exception as e:
    print(f"Function generation failed: {e}")

# Compare performance
performance_comparison(system)
Evaluating symbolic-generated power flow equations:
P_1 = 0
Q_1 = 0.5*b_1_2 + 0.5*b_3_1
P_2 = 50
Q_2 = 0.5*b_1_2 + 0.5*b_2_3
P_3 = -120
Q_3 = 0.5*b_2_3 + 0.5*b_3_1 - 50

Jacobian shape: (3, 3)
First Jacobian element: -1.0*x_1_2/(r_1_2**2 + x_1_2**2) - 1.0*x_2_3/(r_2_3**2 + x_2_3**2)
Performance comparison for 100 iterations:
Symbolic-generated code time: 0.129798 seconds
Hard-coded implementation time: 0.002911 seconds
Ratio (symbolic/hard-coded): 44.58

Jacobian evaluation successful
Jacobian shape: (3, 3)

Verification of Jacobian accuracy:
∂P_1/∂theta_1:
  Numerical: -5.00044450291171e-7*r_1_2/(r_1_2**2 + x_1_2**2) - 5.00044450291171e-7*r_3_1/(r_3_1**2 + x_3_1**2) - 0.999999999999833*x_1_2/(r_1_2**2 + x_1_2**2) - 0.999999999999833*x_3_1/(r_3_1**2 + x_3_1**2)
  Symbolic:  -1.0*x_1_2/(r_1_2**2 + x_1_2**2) - 1.0*x_2_3/(r_2_3**2 + x_2_3**2)
∂P_2/∂theta_1:
  Numerical: -5.00044450291171e-7*r_1_2/(r_1_2**2 + x_1_2**2) + 0.999999999999833*x_1_2/(r_1_2**2 + x_1_2**2)
  Symbolic:  1.0*x_2_3/(r_2_3**2 + x_2_3**2)

Optimization Opportunities

When converting symbolic expressions to numerical code, there are several optimization opportunities:

  1. Common subexpression elimination: Identify and reuse common intermediate results

  2. Sparsity exploitation: Take advantage of zeros in the matrices to reduce computations

  3. Parallelization: Independent calculations can be parallelized for improved performance

  4. Vectorization: Use NumPy’s vectorized operations for efficiency

SymPy can help with some of these optimizations automatically, particularly common subexpression elimination.

With these techniques, we can enjoy the benefits of symbolic computation for analysis and insight while maintaining computational efficiency for practical applications.

Quizzes#

display_quiz(git_url + "sympy.json", colors=color_dict)