Spaces:
Running
Running
import numpy as np | |
import pandas as pd | |
from typing import Dict, List, Tuple, Union | |
from dataclasses import dataclass | |
import matplotlib.pyplot as plt | |
import seaborn as sns | |
class ConductorParams: | |
"""Data class for conductor electrical parameters""" | |
resistance: float # Ω/mile | |
gmr: float # feet (Geometric Mean Radius) | |
class Coordinate: | |
"""Data class for conductor coordinates""" | |
x: float # feet | |
y: float # feet | |
class PowerSystemImpedanceCalculator: | |
""" | |
Advanced impedance calculator for 5-wire power systems using modified Carson's equations | |
and Kron reduction technique. | |
The calculator implements industry-standard methods for computing equivalent impedance | |
matrices in multi-conductor transmission and distribution lines. | |
""" | |
# Carson's equation constants for 60 Hz, 100 Ω⋅m earth resistivity | |
CARSON_REAL_CONSTANT = 0.09530 # Ω/mile | |
CARSON_IMAG_COEFFICIENT = 0.12134 # Ω/mile | |
CARSON_IMAG_CONSTANT = 7.93402 # dimensionless | |
def __init__(self): | |
"""Initialize the calculator with default conductor labels""" | |
self.conductor_labels = ['a', 'b', 'c', 'n', 'pe'] | |
self.phase_labels = ['a', 'b', 'c'] | |
def calculate_distance_from_coordinates(self, coord1: Coordinate, coord2: Coordinate) -> float: | |
""" | |
Calculate Euclidean distance between two conductor coordinates. | |
Args: | |
coord1: First conductor coordinate | |
coord2: Second conductor coordinate | |
Returns: | |
Distance in feet | |
""" | |
dx = coord1.x - coord2.x | |
dy = coord1.y - coord2.y | |
return np.sqrt(dx**2 + dy**2) | |
def calculate_all_distances_from_coordinates(self, coordinates: Dict[str, Coordinate]) -> Dict[str, float]: | |
""" | |
Calculate all pairwise distances from conductor coordinates. | |
Args: | |
coordinates: Dictionary mapping conductor labels to coordinates | |
Returns: | |
Dictionary of pairwise distances with keys like 'ab', 'ac', etc. | |
""" | |
distances = {} | |
conductors = self.conductor_labels | |
# Generate all unique pairs | |
for i, cond1 in enumerate(conductors): | |
for j, cond2 in enumerate(conductors[i+1:], i+1): | |
key = f"{cond1}{cond2}" | |
distance = self.calculate_distance_from_coordinates( | |
coordinates[cond1], coordinates[cond2] | |
) | |
distances[key] = distance | |
return distances | |
def calculate_primitive_impedance_matrix(self, | |
distances: Dict[str, float], | |
conductor_params: Dict[str, ConductorParams]) -> np.ndarray: | |
""" | |
Calculate the 5×5 primitive impedance matrix using modified Carson's equations. | |
The primitive matrix includes all conductors (phases, neutral, PE) before reduction. | |
Self-impedances account for conductor resistance and earth return effects. | |
Mutual impedances account for electromagnetic coupling and earth return effects. | |
Args: | |
distances: Dictionary of pairwise distances between conductors | |
conductor_params: Dictionary of conductor electrical parameters | |
Returns: | |
5×5 complex impedance matrix [Ω/mile] | |
""" | |
n_conductors = len(self.conductor_labels) | |
matrix = np.zeros((n_conductors, n_conductors), dtype=complex) | |
# Calculate self-impedances (diagonal elements) | |
for i, conductor in enumerate(self.conductor_labels): | |
params = conductor_params[conductor] | |
# Modified Carson's equation for self-impedance | |
real_part = params.resistance + self.CARSON_REAL_CONSTANT | |
imag_part = self.CARSON_IMAG_COEFFICIENT * ( | |
np.log(1.0 / params.gmr) + self.CARSON_IMAG_CONSTANT | |
) | |
matrix[i, i] = complex(real_part, imag_part) | |
# Calculate mutual impedances (off-diagonal elements) | |
# Mapping from matrix indices to distance dictionary keys | |
distance_map = { | |
(0, 1): 'ab', (0, 2): 'ac', (0, 3): 'an', (0, 4): 'ape', | |
(1, 2): 'bc', (1, 3): 'bn', (1, 4): 'bpe', | |
(2, 3): 'cn', (2, 4): 'cpe', | |
(3, 4): 'npe' | |
} | |
for (i, j), distance_key in distance_map.items(): | |
distance = distances[distance_key] | |
# Modified Carson's equation for mutual impedance | |
real_part = self.CARSON_REAL_CONSTANT | |
imag_part = self.CARSON_IMAG_COEFFICIENT * ( | |
np.log(1.0 / distance) + self.CARSON_IMAG_CONSTANT | |
) | |
mutual_impedance = complex(real_part, imag_part) | |
matrix[i, j] = mutual_impedance | |
matrix[j, i] = mutual_impedance # Symmetry | |
return matrix | |
def apply_kron_reduction(self, primitive_matrix: np.ndarray) -> np.ndarray: | |
""" | |
Apply Kron reduction to eliminate neutral and PE conductors from the impedance matrix. | |
Kron reduction preserves the electrical behavior of the phase conductors while | |
eliminating the neutral and protective earth conductors from the analysis. | |
The reduction formula is: Z_abc = Z_pp - Z_pq @ Z_qq^(-1) @ Z_qp | |
Args: | |
primitive_matrix: 5×5 primitive impedance matrix | |
Returns: | |
3×3 reduced impedance matrix for phase conductors only [Ω/mile] | |
""" | |
# Extract sub-matrices for Kron reduction | |
# Z_pp: phase-to-phase impedances (3×3) - indices 0,1,2 (a,b,c) | |
# Z_qq: neutral/PE impedances (2×2) - indices 3,4 (n,pe) | |
# Z_pq: phase-to-neutral/PE coupling (3×2) | |
# Z_qp: neutral/PE-to-phase coupling (2×3) - transpose of Z_pq | |
Z_pp = primitive_matrix[0:3, 0:3] # Phase conductors | |
Z_qq = primitive_matrix[3:5, 3:5] # Neutral and PE | |
Z_pq = primitive_matrix[0:3, 3:5] # Phase to neutral/PE coupling | |
Z_qp = primitive_matrix[3:5, 0:3] # Neutral/PE to phase coupling | |
# Calculate Z_qq inverse using robust numerical methods | |
try: | |
Z_qq_inv = np.linalg.inv(Z_qq) | |
except np.linalg.LinAlgError: | |
# Fallback to pseudo-inverse if matrix is singular | |
Z_qq_inv = np.linalg.pinv(Z_qq) | |
print("Warning: Z_qq matrix is singular, using pseudo-inverse") | |
# Apply Kron reduction formula | |
# Z_abc = Z_pp - Z_pq @ Z_qq^(-1) @ Z_qp | |
correction_term = Z_pq @ Z_qq_inv @ Z_qp | |
reduced_matrix = Z_pp - correction_term | |
return reduced_matrix | |
def calculate_impedance_from_distances(self, | |
distances: Dict[str, float], | |
conductor_params: Dict[str, ConductorParams]) -> Tuple[np.ndarray, np.ndarray]: | |
""" | |
Complete impedance calculation from conductor distances. | |
Args: | |
distances: Dictionary of pairwise conductor distances | |
conductor_params: Dictionary of conductor electrical parameters | |
Returns: | |
Tuple of (primitive_matrix, reduced_matrix) | |
""" | |
primitive_matrix = self.calculate_primitive_impedance_matrix(distances, conductor_params) | |
reduced_matrix = self.apply_kron_reduction(primitive_matrix) | |
return primitive_matrix, reduced_matrix | |
def calculate_impedance_from_coordinates(self, | |
coordinates: Dict[str, Coordinate], | |
conductor_params: Dict[str, ConductorParams]) -> Tuple[np.ndarray, np.ndarray, Dict[str, float]]: | |
""" | |
Complete impedance calculation from conductor coordinates. | |
Args: | |
coordinates: Dictionary mapping conductor labels to coordinates | |
conductor_params: Dictionary of conductor electrical parameters | |
Returns: | |
Tuple of (primitive_matrix, reduced_matrix, calculated_distances) | |
""" | |
distances = self.calculate_all_distances_from_coordinates(coordinates) | |
primitive_matrix, reduced_matrix = self.calculate_impedance_from_distances(distances, conductor_params) | |
return primitive_matrix, reduced_matrix, distances | |
def format_complex_matrix(self, matrix: np.ndarray, precision: int = 6) -> List[List[str]]: | |
""" | |
Format complex matrix for readable display. | |
Args: | |
matrix: Complex numpy array | |
precision: Number of decimal places | |
Returns: | |
List of lists containing formatted complex number strings | |
""" | |
formatted = [] | |
for row in matrix: | |
formatted_row = [] | |
for element in row: | |
real = f"{element.real:.{precision}f}" | |
imag = f"{element.imag:.{precision}f}" | |
sign = "+" if element.imag >= 0 else "" | |
formatted_row.append(f"{real} {sign} j{imag}") | |
formatted.append(formatted_row) | |
return formatted | |
def create_impedance_dataframe(self, matrix: np.ndarray, labels: List[str]) -> pd.DataFrame: | |
""" | |
Create a pandas DataFrame from impedance matrix for easier analysis. | |
Args: | |
matrix: Complex impedance matrix | |
labels: Row/column labels | |
Returns: | |
DataFrame with complex impedance values | |
""" | |
return pd.DataFrame(matrix, index=labels, columns=labels) | |
def analyze_matrix_properties(self, matrix: np.ndarray) -> Dict[str, Union[float, bool]]: | |
""" | |
Analyze impedance matrix properties for engineering insights. | |
Args: | |
matrix: Complex impedance matrix | |
Returns: | |
Dictionary containing matrix analysis results | |
""" | |
properties = {} | |
# Basic properties | |
properties['condition_number'] = np.linalg.cond(matrix) | |
properties['determinant'] = np.linalg.det(matrix) | |
properties['is_symmetric'] = np.allclose(matrix, matrix.T, rtol=1e-10) | |
properties['is_positive_definite'] = np.all(np.linalg.eigvals(matrix.real) > 0) | |
# Eigenvalue analysis | |
eigenvalues = np.linalg.eigvals(matrix) | |
properties['min_eigenvalue_real'] = np.min(eigenvalues.real) | |
properties['max_eigenvalue_real'] = np.max(eigenvalues.real) | |
properties['eigenvalue_spread'] = properties['max_eigenvalue_real'] / properties['min_eigenvalue_real'] | |
# Impedance magnitude analysis | |
magnitudes = np.abs(matrix) | |
properties['min_impedance_magnitude'] = np.min(magnitudes) | |
properties['max_impedance_magnitude'] = np.max(magnitudes) | |
properties['avg_self_impedance_magnitude'] = np.mean(np.abs(np.diag(matrix))) | |
return properties | |
def format_results_for_display(self, primitive_matrix: np.ndarray, | |
reduced_matrix: np.ndarray, | |
distances: Dict[str, float]) -> str: | |
""" | |
Format calculation results for display in Gradio interface. | |
Args: | |
primitive_matrix: 5×5 primitive impedance matrix | |
reduced_matrix: 3×3 reduced impedance matrix | |
distances: Dictionary of conductor distances | |
Returns: | |
Formatted string with results | |
""" | |
result = "=== 3-Phase Impedance Calculation Results ===\n\n" | |
# Display distances | |
result += "Conductor Distances:\n" | |
for pair, distance in distances.items(): | |
conductor1, conductor2 = pair[0].upper(), pair[1].upper() | |
result += f" {conductor1}-{conductor2}: {distance:.3f} ft\n" | |
result += "\nPrimitive Impedance Matrix (5×5) [Ω/mile]:\n" | |
result += " " | |
for label in self.conductor_labels: | |
result += f"{label.upper():>15s}" | |
result += "\n" | |
for i, label in enumerate(self.conductor_labels): | |
result += f"{label.upper():>3s}: " | |
for j in range(5): | |
z = primitive_matrix[i, j] | |
result += f"{z.real:>7.3f}{z.imag:>+7.3f}j " | |
result += "\n" | |
result += "\nReduced Phase Impedance Matrix (3×3) [Ω/mile]:\n" | |
result += " " | |
for label in self.phase_labels: | |
result += f"{label.upper():>15s}" | |
result += "\n" | |
for i, label in enumerate(self.phase_labels): | |
result += f"{label.upper():>3s}: " | |
for j in range(3): | |
z = reduced_matrix[i, j] | |
result += f"{z.real:>7.3f}{z.imag:>+7.3f}j " | |
result += "\n" | |
# Add matrix analysis | |
properties = self.analyze_matrix_properties(reduced_matrix) | |
result += f"\nMatrix Analysis:\n" | |
result += f" Condition Number: {properties['condition_number']:.2e}\n" | |
result += f" Is Symmetric: {properties['is_symmetric']}\n" | |
result += f" Average Self-Impedance Magnitude: {properties['avg_self_impedance_magnitude']:.5f} Ω/mile\n" | |
return result | |
def create_test_data(): | |
"""Create sample data for testing the calculator""" | |
# Sample conductor parameters (typical values) | |
conductor_params = { | |
'a': ConductorParams(resistance=0.055, gmr=0.038), # 4/0 ACSR | |
'b': ConductorParams(resistance=0.055, gmr=0.038), # 4/0 ACSR | |
'c': ConductorParams(resistance=0.055, gmr=0.038), # 4/0 ACSR | |
'n': ConductorParams(resistance=8.0, gmr=0.012), # #6 ACSR | |
'pe': ConductorParams(resistance=8.0, gmr=0.012) # #6 ACSR | |
} | |
# Sample coordinates (typical distribution line configuration) | |
coordinates = { | |
'a': Coordinate(x=0, y=42), | |
'b': Coordinate(x=23.5, y=42), | |
'c': Coordinate(x=47, y=42), | |
'n': Coordinate(x=10, y=74), | |
'pe': Coordinate(x=37, y=72) | |
} | |
return conductor_params, coordinates |