"""
Enhanced Diagram Generation Utilities for Power Systems
Generates professional SVG diagrams for power system concepts
"""
import json
import math
from typing import Dict, List, Tuple, Optional
from datetime import datetime
class DiagramGenerator:
"""
Generate professional SVG diagrams for power systems concepts
"""
def __init__(self):
self.svg_width = 1000
self.svg_height = 700
self.colors = {
'primary': '#2563eb',
'secondary': '#64748b',
'success': '#059669',
'danger': '#dc2626',
'warning': '#d97706',
'info': '#0891b2',
'light': '#f8fafc',
'dark': '#1e293b',
'bus': '#dc2626',
'line': '#2563eb',
'ground': '#374151',
'component': '#059669',
'fault': '#dc2626',
'protection': '#7c3aed'
}
def create_svg_header(self, width: int = None, height: int = None) -> str:
"""Create professional SVG header with advanced styling"""
w = width or self.svg_width
h = height or self.svg_height
return f""""
def add_title_block(self, title: str, subtitle: str = "", x: int = 500, y: int = 50) -> str:
"""Add professional title block"""
svg = f'\n'
svg += f' \n'
svg += f' {title}\n'
if subtitle:
svg += f' {subtitle}\n'
svg += f'\n'
return svg
def add_legend(self, items: List[Tuple[str, str, str]], x: int = 50, y: int = 550) -> str:
"""Add professional legend"""
svg = f'\n'
# Legend background
legend_height = len(items) * 25 + 40
svg += f' \n'
svg += f' Legend\n'
for i, (symbol, color, description) in enumerate(items):
y_pos = y + 35 + i * 25
svg += f' \n'
svg += f' {description}\n'
svg += f'\n'
return svg
def generate_single_line_diagram(self, system_config: Dict) -> str:
"""Generate professional single line diagram"""
svg = self.create_svg_header()
# Title block
svg += self.add_title_block("Single Line Diagram", "33kV Distribution System")
# Main bus
bus_y = 200
svg += f'\n'
svg += f'Main Bus - 33kV\n'
# Generator section
gen_x = 200
svg += f'\n'
svg += f'\n'
svg += f'Generator\n'
svg += f'100 MVA, 33kV\n'
svg += f'X"d = 15%\n'
# Circuit breaker 1
cb1_x = 280
svg += f'\n'
svg += f'CB-1\n'
svg += f'2000A, 40kA\n'
# Current transformer
ct1_x = 320
svg += f'\n'
svg += f'CT 2000/5\n'
svg += f'\n'
# Power transformer
trafo_x = 400
svg += f'\n'
svg += f'Power Transformer\n'
svg += f'50 MVA, 33/11kV\n'
svg += f'Z = 8%, Dyn11\n'
# HV/LV connection lines
svg += f'\n'
svg += f'\n'
# LV bus
lv_bus_y = bus_y + 150
svg += f'\n'
svg += f'Distribution Bus - 11kV\n'
svg += f'\n'
# Circuit breaker 2
cb2_x = 480
svg += f'\n'
svg += f'CB-2\n'
svg += f'3000A, 25kA\n'
# Distribution feeders
feeder_positions = [550, 650, 750]
feeder_names = ["Industrial Feeder", "Commercial Feeder", "Residential Feeder"]
feeder_loads = ["15 MW", "8 MW", "5 MW"]
for i, (x_pos, name, load) in enumerate(zip(feeder_positions, feeder_names, feeder_loads)):
# Feeder line
feeder_end_y = lv_bus_y + 100
svg += f'\n'
# Load symbol
svg += f'\n'
svg += f'{name}\n'
svg += f'{load}\n'
svg += f'cos φ = 0.85\n'
# Protection relay
relay_y = lv_bus_y + 30
svg += f'\n'
svg += f'R{i+1}\n'
# Add protection coordination indicators
svg += f'Protection Coordination: 0.3s intervals\n'
svg += f'Generator: Differential + Overcurrent\n'
svg += f'Transformer: Differential + REF\n'
# Add legend
legend_items = [
("line", self.colors['bus'], "Main Bus (33kV)"),
("line", self.colors['primary'], "Transmission Line"),
("rect", self.colors['component'], "Electrical Equipment"),
("rect", self.colors['protection'], "Protection Device")
]
svg += self.add_legend(legend_items)
# Grid reference
svg += self.add_grid_reference()
svg += self.create_svg_footer()
return svg
def generate_fault_analysis_diagram(self, fault_type: str = "line_to_ground") -> str:
"""Generate professional fault analysis diagram with sequence networks"""
svg = self.create_svg_header(1200, 800)
# Title block
fault_title = fault_type.replace("_", "-").title() + " Fault Analysis"
svg += self.add_title_block(fault_title, "Sequence Network Analysis")
if fault_type == "line_to_ground":
# Positive sequence network
svg += self._draw_sequence_network(200, 150, "Positive Sequence", "Z₁", self.colors['success'])
# Negative sequence network
svg += self._draw_sequence_network(200, 350, "Negative Sequence", "Z₂", self.colors['danger'])
# Zero sequence network
svg += self._draw_sequence_network(200, 550, "Zero Sequence", "Z₀", self.colors['warning'])
# Connection diagram for L-G fault
svg += self._draw_lg_fault_connection(650, 250)
# Fault current calculation
svg += f'\n'
svg += f' \n'
svg += f' Fault Current Calculation\n'
svg += f' For Line-to-Ground Fault:\n'
svg += f' I₁ = I₂ = I₀ = Ea / (Z₁ + Z₂ + Z₀)\n'
svg += f' If = 3 × I₁ = 3Ea / (Z₁ + Z₂ + Z₀)\n'
svg += f' Where: Ea = Phase voltage, Z = Sequence impedance\n'
svg += f'\n'
elif fault_type == "line_to_line":
# L-L fault diagram
svg += self._draw_ll_fault_diagram()
elif fault_type == "three_phase":
# 3-phase fault diagram
svg += self._draw_three_phase_fault_diagram()
# Add phasor diagram
svg += self._draw_fault_phasors(900, 150, fault_type)
svg += self.create_svg_footer()
return svg
def _draw_sequence_network(self, x: int, y: int, title: str, impedance: str, color: str) -> str:
"""Draw professional sequence network"""
svg = f'\n'
# Network title
svg += f' {title}\n'
# Voltage source
svg += f' \n'
svg += f' +\n'
svg += f' -\n'
svg += f' Ea\n'
# Impedance box
svg += f' \n'
svg += f' {impedance}\n'
# Connecting lines
svg += f' \n'
svg += f' \n'
# Ground symbol
svg += f' \n'
svg += f' \n'
svg += f' \n'
svg += f' \n'
# Current arrow
svg += f' \n'
svg += f' I{impedance[-1]}\n'
svg += f'\n'
return svg
def _draw_lg_fault_connection(self, x: int, y: int) -> str:
"""Draw L-G fault connection diagram"""
svg = f'\n'
# Connection title
svg += f' Series Connection for L-G Fault\n'
# Voltage source
svg += f' \n'
svg += f' Ea\n'
# Series impedances
impedances = [("Z₁", y+50), ("Z₂", y+100), ("Z₀", y+150)]
# Connecting lines and impedances
svg += f' \n'
for i, (z_label, y_pos) in enumerate(impedances):
# Impedance box
svg += f' \n'
svg += f' {z_label}\n'
if i < len(impedances) - 1:
svg += f' \n'
# Connection to ground
svg += f' \n'
svg += f' \n'
svg += f' \n'
# Fault point
svg += f' \n'
# Current flow indication
svg += f' \n'
svg += f' If\n'
svg += f'\n'
return svg
def _draw_fault_phasors(self, x: int, y: int, fault_type: str) -> str:
"""Draw fault condition phasor diagram"""
svg = f'\n'
# Phasor diagram background
svg += f' \n'
svg += f' Phasor Diagram\n'
center_x, center_y = x+50, y+50
radius = 60
# Reference circle
svg += f' \n'
if fault_type == "line_to_ground":
# Phase A (faulted) - reduced magnitude
svg += f' \n'
svg += f' Va\n'
# Phase B - normal
x_b = center_x + radius * math.cos(math.radians(120))
y_b = center_y - radius * math.sin(math.radians(120))
svg += f' \n'
svg += f' Vb\n'
# Phase C - normal
x_c = center_x + radius * math.cos(math.radians(240))
y_c = center_y - radius * math.sin(math.radians(240))
svg += f' \n'
svg += f' Vc\n'
elif fault_type == "line_to_line":
# Phase A - normal
svg += f' \n'
svg += f' Va\n'
# Phases B and C - faulted (closer together)
angle_b = 135 # Shifted due to fault
angle_c = 225 # Shifted due to fault
x_b = center_x + radius * 0.8 * math.cos(math.radians(angle_b))
y_b = center_y - radius * 0.8 * math.sin(math.radians(angle_b))
svg += f' \n'
svg += f' Vb\n'
x_c = center_x + radius * 0.8 * math.cos(math.radians(angle_c))
y_c = center_y - radius * 0.8 * math.sin(math.radians(angle_c))
svg += f' \n'
svg += f' Vc\n'
# Center point
svg += f' \n'
svg += f'\n'
return svg
def generate_protection_coordination_diagram(self) -> str:
"""Generate professional time-current coordination curves"""
svg = self.create_svg_header(1000, 700)
# Title block
svg += self.add_title_block("Protection Coordination Study", "Time-Current Characteristic Curves")
# Chart area
chart_x, chart_y = 150, 120
chart_width, chart_height = 700, 450
# Chart background
svg += f'\n'
# Grid lines
svg += self._draw_coordination_grid(chart_x, chart_y, chart_width, chart_height)
# Axes
svg += f'\n'
svg += f'\n'
# Axis labels
svg += f'Current (A)\n'
svg += f'Time (s)\n'
# Draw coordination curves
svg += self._draw_protection_curves(chart_x, chart_y, chart_width, chart_height)
# Add coordination analysis
svg += self._add_coordination_analysis(chart_x + chart_width + 20, chart_y)
svg += self.create_svg_footer()
return svg
def _draw_coordination_grid(self, x: int, y: int, width: int, height: int) -> str:
"""Draw logarithmic grid for coordination study"""
svg = ""
# Vertical grid lines (current)
current_values = [10, 100, 1000, 10000]
for i, current in enumerate(current_values):
x_pos = x + (i + 1) * (width // 5)
svg += f'\n'
svg += f'{current}\n'
# Horizontal grid lines (time)
time_values = [0.01, 0.1, 1.0, 10.0, 100.0]
for i, time_val in enumerate(time_values):
y_pos = y + height - (i + 1) * (height // 6)
svg += f'\n'
svg += f'{time_val}\n'
return svg
def _draw_protection_curves(self, x: int, y: int, width: int, height: int) -> str:
"""Draw protection device curves"""
svg = ""
# Fuse curve (fastest)
fuse_points = []
for i in range(50):
current_ratio = i / 10.0
current_pos = x + (current_ratio * width / 5)
# Inverse curve equation for fuse
time_val = max(0.01, 2.0 / (current_ratio + 1)**2) if current_ratio > 0 else 100
time_pos = y + height - (math.log10(max(0.01, time_val)) + 2) * height / 4
if y <= time_pos <= y + height:
fuse_points.append(f"{current_pos:.1f},{time_pos:.1f}")
if len(fuse_points) > 1:
svg += f'\n'
svg += f'Fuse 100A\n'
# Primary relay curve
relay1_points = []
for i in range(50):
current_ratio = i / 8.0
current_pos = x + (current_ratio * width / 5)
# Standard inverse curve
time_val = max(0.1, 0.14 / ((current_ratio/2) - 1)) if current_ratio > 2 else 100
time_pos = y + height - (math.log10(max(0.01, min(100, time_val))) + 2) * height / 4
if y <= time_pos <= y + height:
relay1_points.append(f"{current_pos:.1f},{time_pos:.1f}")
if len(relay1_points) > 1:
svg += f'\n'
svg += f'Primary Relay\n'
# Backup relay curve (shifted up by coordination interval)
relay2_points = []
for i in range(50):
current_ratio = i / 8.0
current_pos = x + (current_ratio * width / 5)
# Very inverse curve with time shift
time_val = max(0.3, 13.5 / ((current_ratio/2) - 1)) if current_ratio > 2 else 100
time_pos = y + height - (math.log10(max(0.01, min(100, time_val))) + 2) * height / 4
if y <= time_pos <= y + height:
relay2_points.append(f"{current_pos:.1f},{time_pos:.1f}")
if len(relay2_points) > 1:
svg += f'\n'
svg += f'Backup Relay\n'
return svg
def _add_coordination_analysis(self, x: int, y: int) -> str:
"""Add coordination analysis text box"""
svg = f'\n'
svg += f' \n'
svg += f' Coordination Analysis\n'
analysis_items = [
"✓ Fuse-Relay Coordination:",
" CTI = 0.2s minimum",
"",
"✓ Relay-Relay Coordination:",
" CTI = 0.3s standard",
"",
"✓ Settings Applied:",
" Primary: I> = 1.25 × FLA",
" Backup: I> = 1.1 × Primary",
"",
"✓ Fault Current Analysis:",
" 3φ fault: 8,500A",
" L-G fault: 6,200A",
"",
"✓ Coordination Verified:",
" All fault levels",
" Load conditions",
" Motor starting"
]
for i, item in enumerate(analysis_items):
svg += f' {item}\n'
svg += f'\n'
return svg
def add_grid_reference(self) -> str:
"""Add grid reference and scale"""
svg = f'\n'
svg += f' Scale: Not to scale - Schematic only\n'
svg += f' Date: {datetime.now().strftime("%Y-%m-%d")}\n'
svg += f' Power Systems Mini-Consultant\n'
svg += f'\n'
return svg
def generate_phasor_diagram(self, condition: str = "balanced") -> str:
"""Generate comprehensive phasor diagram"""
svg = self.create_svg_header(800, 600)
title = f"Phasor Diagram - {condition.title()} Conditions"
svg += self.add_title_block(title, "Voltage and Current Relationships")
center_x, center_y = 400, 300
voltage_radius = 100
current_radius = 60
# Voltage phasors
svg += f'\n'
svg += f' Voltage Phasors\n'
if condition == "balanced":
# Three balanced voltage phasors
angles = [0, 120, 240]
colors = [self.colors['danger'], self.colors['success'], self.colors['info']]
labels = ["Va", "Vb", "Vc"]
for angle, color, label in zip(angles, colors, labels):
x_end = center_x + voltage_radius * math.cos(math.radians(angle))
y_end = center_y - voltage_radius * math.sin(math.radians(angle))
svg += f' \n'
# Label positioning
label_x = center_x + (voltage_radius + 25) * math.cos(math.radians(angle))
label_y = center_y - (voltage_radius + 25) * math.sin(math.radians(angle))
svg += f' {label}\n'
# Current phasors (lagging by 30 degrees for inductive load)
svg += f' Current Phasors\n'
current_angles = [-30, 90, 210] # Lagging by 30 degrees
for i, (angle, color, phase) in enumerate(zip(current_angles, colors, ["a", "b", "c"])):
x_end = center_x + current_radius * math.cos(math.radians(angle))
y_end = center_y - current_radius * math.sin(math.radians(angle))
svg += f' \n'
# Current label
label_x = center_x + (current_radius + 20) * math.cos(math.radians(angle))
label_y = center_y - (current_radius + 20) * math.sin(math.radians(angle))
svg += f' I{phase}\n'
svg += f'\n'
# Reference circles
svg += f'\n'
svg += f'\n'
# Center point
svg += f'\n'
# Power triangle
svg += self._draw_power_triangle(center_x + 200, center_y + 150)
svg += self.create_svg_footer()
return svg
def _draw_power_triangle(self, x: int, y: int) -> str:
"""Draw power triangle diagram"""
svg = f'\n'
# Title
svg += f' Power Triangle\n'
# Triangle
base = 80
height = 60
# Real power (P)
svg += f' \n'
svg += f' P (kW)\n'
# Reactive power (Q)
svg += f' \n'
svg += f' Q (kVAr)\n'
# Apparent power (S)
svg += f' \n'
svg += f' S (kVA)\n'
# Power factor angle
svg += f' \n'
svg += f' φ\n'
svg += f'\n'
return svg
def generate_impedance_diagram(self) -> str:
"""Generate professional R-X impedance diagram for distance protection"""
svg = self.create_svg_header(900, 700)
# Title block
svg += self.add_title_block("Distance Protection", "R-X Impedance Diagram")
# Chart area
chart_x, chart_y = 150, 120
chart_width, chart_height = 500, 400
center_x = chart_x + chart_width // 2
center_y = chart_y + chart_height // 2
# Chart background
svg += f'\n'
# Axes
svg += f'\n'
svg += f'\n'
# Axis labels
svg += f'R (Ω)\n'
svg += f'X (Ω)\n'
# Grid markings
for i in range(1, 6):
# R axis markings
r_pos = center_x + i * 60
if r_pos <= chart_x + chart_width:
svg += f'\n'
svg += f'{i*2}\n'
# X axis markings
x_pos = center_y - i * 60
if x_pos >= chart_y:
svg += f'\n'
svg += f'{i*2}\n'
# Zone 1 - Mho circle (80% reach)
zone1_radius = 80
zone1_center_x = center_x + 40
svg += f'\n'
svg += f'Zone 1\n'
svg += f'80% Line\n'
svg += f'Instantaneous\n'
# Zone 2 - Larger Mho circle (120% reach)
zone2_radius = 120
zone2_center_x = center_x + 60
svg += f'\n'
svg += f'Zone 2\n'
svg += f'120% + 50% Next\n'
svg += f't = 0.3s\n'
# Zone 3 - Reverse reach
zone3_radius = 60
zone3_center_x = center_x - 30
svg += f'\n'
svg += f'Zone 3\n'
svg += f'Reverse Reach\n'
svg += f't = 1.0s\n'
# Load impedance trajectory
load_points = []
for i in range(15):
angle = i * 5 # 0 to 70 degrees (typical load range)
r_load = 8 + i * 0.5
x_load = r_load * math.tan(math.radians(angle))
x_pos = center_x + r_load * 6
y_pos = center_y - x_load * 6
if chart_x <= x_pos <= chart_x + chart_width and chart_y <= y_pos <= chart_y + chart_height:
load_points.append(f"{x_pos:.1f},{y_pos:.1f}")
if len(load_points) > 1:
svg += f'\n'
svg += f'Load Trajectory\n'
# Fault impedance points
fault_points = [
(center_x + 30, center_y - 100, "Close-in Fault"),
(center_x + 120, center_y - 80, "Remote Fault"),
(center_x + 200, center_y - 60, "External Fault")
]
for x_fault, y_fault, label in fault_points:
if chart_x <= x_fault <= chart_x + chart_width and chart_y <= y_fault <= chart_y + chart_height:
svg += f'\n'
svg += f'{label}\n'
# Settings table
svg += self._add_distance_settings_table(chart_x + chart_width + 30, chart_y)
svg += self.create_svg_footer()
return svg
def _add_distance_settings_table(self, x: int, y: int) -> str:
"""Add distance protection settings table"""
svg = f'\n'
# Table background
table_width = 220
table_height = 350
svg += f' \n'
# Table title
svg += f' Distance Protection Settings\n'
# Table content
settings_data = [
("Parameter", "Zone 1", "Zone 2", "Zone 3"),
("Reach (Ω)", "4.8", "7.2", "3.0"),
("Time (s)", "0.0", "0.3", "1.0"),
("Angle (°)", "75", "75", "75"),
("", "", "", ""),
("Line Parameters:", "", "", ""),
("Z1 = 0.4 + j1.2 Ω/km", "", "", ""),
("Line Length: 12 km", "", "", ""),
("", "", "", ""),
("Coordination:", "", "", ""),
("CTI = 300ms", "", "", ""),
("Load Encroachment:", "", "", ""),
("Avoided in all zones", "", "", ""),
("", "", "", ""),
("Protection Logic:", "", "", ""),
("Zone 1: Instantaneous", "", "", ""),
("Zone 2: Definite time", "", "", ""),
("Zone 3: Backup only", "", "", "")
]
row_height = 18
for i, row in enumerate(settings_data):
y_pos = y + 45 + i * row_height
if i == 0: # Header row
svg += f' \n'
for j, cell in enumerate(row):
x_pos = x + 15 + j * 50
svg += f' {cell}\n'
else:
for j, cell in enumerate(row):
x_pos = x + 15 + j * 50
font_size = "9px" if len(cell) > 15 else "10px"
color = self.colors["dark"] if j == 0 else self.colors["secondary"]
svg += f' {cell}\n'
svg += f'\n'
return svg
def _draw_ll_fault_diagram(self) -> str:
"""Draw line-to-line fault analysis diagram"""
svg = ""
# Connection diagram for L-L fault (parallel Z1 and Z2)
x, y = 300, 200
svg += f'L-L Fault: Z1 || Z2\n'
# Voltage source
svg += f'\n'
svg += f'Ea\n'
# Parallel branches
# Upper branch (Z1)
svg += f'\n'
svg += f'\n'
svg += f'Z₁\n'
svg += f'\n'
# Lower branch (Z2)
svg += f'\n'
svg += f'\n'
svg += f'Z₂\n'
svg += f'\n'
# Connect voltage source to parallel branches
svg += f'\n'
svg += f'\n'
svg += f'\n'
svg += f'\n'
# Connect parallel branches
svg += f'\n'
# Fault point
svg += f'\n'
# Current arrows
svg += f'\n'
svg += f'I₁\n'
svg += f'\n'
svg += f'I₂\n'
# Calculation box
svg += f'\n'
svg += f' \n'
svg += f' L-L Fault Current\n'
svg += f' If = √3 × Ea / (Z₁ + Z₂)\n'
svg += f' Z₀ does not appear in L-L fault\n'
svg += f'\n'
return svg
def _draw_three_phase_fault_diagram(self) -> str:
"""Draw three-phase fault analysis diagram"""
svg = ""
# Simple circuit for 3-phase fault
x, y = 300, 200
svg += f'3φ Fault: Positive Sequence Only\n'
# Voltage source
svg += f'\n'
svg += f'Ea\n'
# Single impedance (Z1 only)
svg += f'\n'
svg += f'\n'
svg += f'Z₁\n'
svg += f'\n'
# Fault point
svg += f'\n'
# Current arrow
svg += f'\n'
svg += f'If\n'
# Calculation box
svg += f'\n'
svg += f' \n'
svg += f' 3φ Fault Current\n'
svg += f' If = Ea / Z₁\n'
svg += f' Highest fault current\n'
svg += f' Only positive sequence present\n'
svg += f'\n'
return svg
# Example usage and testing functions
if __name__ == "__main__":
generator = DiagramGenerator()
# Test all diagram types
diagrams = {
"single_line": generator.generate_single_line_diagram({}),
"fault_lg": generator.generate_fault_analysis_diagram("line_to_ground"),
"fault_ll": generator.generate_fault_analysis_diagram("line_to_line"),
"fault_3ph": generator.generate_fault_analysis_diagram("three_phase"),
"protection_coordination": generator.generate_protection_coordination_diagram(),
"phasor": generator.generate_phasor_diagram("balanced"),
"impedance": generator.generate_impedance_diagram()
}
# Save diagrams to files for testing
for name, svg_content in diagrams.items():
with open(f"{name}_professional.svg", "w", encoding='utf-8') as f:
f.write(svg_content)
print(f"Generated professional {name} diagram")
print("All professional diagrams generated successfully!")