""" 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""" G LOAD CB FAULT CT R """ def create_svg_footer(self) -> str: """Create SVG footer""" return "" 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!")