Power_Systems_Mini-Consultant / utils /diagram_generator.py
ashhal's picture
Update utils/diagram_generator.py
972e3d8 verified
"""
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"""<svg width="{w}" height="{h}" viewBox="0 0 {w} {h}" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Professional gradients -->
<linearGradient id="busGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#ef4444;stop-opacity:1" />
<stop offset="100%" style="stop-color:#dc2626;stop-opacity:1" />
</linearGradient>
<linearGradient id="componentGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#10b981;stop-opacity:1" />
<stop offset="100%" style="stop-color:#059669;stop-opacity:1" />
</linearGradient>
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#2563eb;stop-opacity:1" />
</linearGradient>
<!-- Professional shadows -->
<filter id="dropShadow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="2" dy="2" result="offset"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.3"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<!-- Professional styles -->
<style>
.title-text {{
font-family: 'Arial', sans-serif;
font-size: 24px;
font-weight: bold;
fill: {self.colors['dark']};
text-anchor: middle;
filter: url(#dropShadow);
}}
.subtitle-text {{
font-family: 'Arial', sans-serif;
font-size: 16px;
font-weight: 500;
fill: {self.colors['secondary']};
text-anchor: middle;
}}
.label-text {{
font-family: 'Arial', sans-serif;
font-size: 12px;
fill: {self.colors['dark']};
text-anchor: middle;
}}
.value-text {{
font-family: 'Courier New', monospace;
font-size: 11px;
fill: {self.colors['info']};
text-anchor: middle;
}}
.bus-line {{
stroke: url(#busGradient);
stroke-width: 6;
fill: none;
filter: url(#dropShadow);
}}
.transmission-line {{
stroke: url(#lineGradient);
stroke-width: 3;
fill: none;
}}
.component-fill {{
fill: url(#componentGradient);
stroke: {self.colors['dark']};
stroke-width: 2;
filter: url(#dropShadow);
}}
.fault-symbol {{
fill: {self.colors['danger']};
stroke: {self.colors['dark']};
stroke-width: 2;
filter: url(#glow);
}}
.protection-device {{
fill: url(#componentGradient);
stroke: {self.colors['protection']};
stroke-width: 2;
filter: url(#dropShadow);
}}
.grid-line {{
stroke: #e2e8f0;
stroke-width: 1;
stroke-dasharray: 2,2;
}}
.axis-line {{
stroke: {self.colors['dark']};
stroke-width: 2;
fill: none;
}}
.curve-line {{
stroke-width: 3;
fill: none;
filter: url(#dropShadow);
}}
</style>
<!-- Arrow markers -->
<marker id="arrowhead" markerWidth="12" markerHeight="8"
refX="10" refY="4" orient="auto" markerUnits="strokeWidth">
<path d="0,0 L0,8 L12,4 z" fill="{self.colors['primary']}" />
</marker>
<marker id="arrowhead-red" markerWidth="12" markerHeight="8"
refX="10" refY="4" orient="auto" markerUnits="strokeWidth">
<path d="0,0 L0,8 L12,4 z" fill="{self.colors['danger']}" />
</marker>
<!-- Professional component symbols -->
<g id="generator-symbol">
<circle cx="0" cy="0" r="20" class="component-fill"/>
<text x="0" y="6" class="label-text">G</text>
<path d="M-15,-15 L15,-15 M-15,15 L15,15" stroke="{self.colors['dark']}" stroke-width="1"/>
</g>
<g id="transformer-symbol">
<circle cx="-15" cy="0" r="12" class="component-fill"/>
<circle cx="15" cy="0" r="12" class="component-fill"/>
<path d="M-15,-12 L-15,-20 M15,-12 L15,-20" stroke="{self.colors['dark']}" stroke-width="2"/>
<path d="M-15,12 L-15,20 M15,12 L15,20" stroke="{self.colors['dark']}" stroke-width="2"/>
</g>
<g id="load-symbol">
<path d="M-15,-12 L15,-12 L10,12 L-10,12 Z" class="component-fill"/>
<text x="0" y="3" class="label-text" style="font-size: 10px;">LOAD</text>
</g>
<g id="breaker-symbol">
<rect x="-8" y="-12" width="16" height="24" class="protection-device"/>
<path d="M-6,-8 L6,8 M-6,8 L6,-8" stroke="{self.colors['light']}" stroke-width="2"/>
<text x="0" y="3" class="label-text" style="font-size: 8px; fill: white;">CB</text>
</g>
<g id="fault-symbol">
<circle cx="0" cy="0" r="12" class="fault-symbol"/>
<path d="M-8,-8 L8,8 M-8,8 L8,-8" stroke="white" stroke-width="3"/>
<text x="0" y="-20" class="label-text" style="fill: {self.colors['danger']};">FAULT</text>
</g>
<g id="ct-symbol">
<circle cx="0" cy="0" r="8" fill="none" stroke="{self.colors['dark']}" stroke-width="2"/>
<text x="0" y="4" class="label-text" style="font-size: 8px;">CT</text>
</g>
<g id="relay-symbol">
<rect x="-10" y="-8" width="20" height="16" class="protection-device"/>
<text x="0" y="3" class="label-text" style="font-size: 8px; fill: white;">R</text>
</g>
<!-- Background -->
<rect width="100%" height="100%" fill="linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%)"/>
</defs>
"""
def create_svg_footer(self) -> str:
"""Create SVG footer"""
return "</svg>"
def add_title_block(self, title: str, subtitle: str = "", x: int = 500, y: int = 50) -> str:
"""Add professional title block"""
svg = f'<g id="title-block">\n'
svg += f' <rect x="{x-200}" y="{y-30}" width="400" height="60" rx="8" fill="white" stroke="{self.colors["primary"]}" stroke-width="2" filter="url(#dropShadow)"/>\n'
svg += f' <text x="{x}" y="{y-5}" class="title-text">{title}</text>\n'
if subtitle:
svg += f' <text x="{x}" y="{y+15}" class="subtitle-text">{subtitle}</text>\n'
svg += f'</g>\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'<g id="legend">\n'
# Legend background
legend_height = len(items) * 25 + 40
svg += f' <rect x="{x}" y="{y}" width="200" height="{legend_height}" rx="8" fill="white" stroke="{self.colors["secondary"]}" stroke-width="1" filter="url(#dropShadow)"/>\n'
svg += f' <text x="{x+100}" y="{y+20}" class="subtitle-text">Legend</text>\n'
for i, (symbol, color, description) in enumerate(items):
y_pos = y + 35 + i * 25
svg += f' <line x1="{x+10}" y1="{y_pos}" x2="{x+40}" y2="{y_pos}" stroke="{color}" stroke-width="4"/>\n'
svg += f' <text x="{x+50}" y="{y_pos+4}" class="label-text">{description}</text>\n'
svg += f'</g>\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'<line x1="150" y1="{bus_y}" x2="850" y2="{bus_y}" class="bus-line"/>\n'
svg += f'<text x="500" y="{bus_y-15}" class="label-text">Main Bus - 33kV</text>\n'
# Generator section
gen_x = 200
svg += f'<use href="#generator-symbol" transform="translate({gen_x},{bus_y})"/>\n'
svg += f'<line x1="{gen_x-35}" y1="{bus_y}" x2="150" y2="{bus_y}" class="transmission-line"/>\n'
svg += f'<text x="{gen_x}" y="{bus_y+45}" class="label-text">Generator</text>\n'
svg += f'<text x="{gen_x}" y="{bus_y+60}" class="value-text">100 MVA, 33kV</text>\n'
svg += f'<text x="{gen_x}" y="{bus_y+75}" class="value-text">X"d = 15%</text>\n'
# Circuit breaker 1
cb1_x = 280
svg += f'<use href="#breaker-symbol" transform="translate({cb1_x},{bus_y})"/>\n'
svg += f'<text x="{cb1_x}" y="{bus_y+45}" class="label-text">CB-1</text>\n'
svg += f'<text x="{cb1_x}" y="{bus_y+60}" class="value-text">2000A, 40kA</text>\n'
# Current transformer
ct1_x = 320
svg += f'<use href="#ct-symbol" transform="translate({ct1_x},{bus_y-30})"/>\n'
svg += f'<text x="{ct1_x}" y="{bus_y-50}" class="label-text">CT 2000/5</text>\n'
svg += f'<line x1="{ct1_x}" y1="{bus_y-22}" x2="{ct1_x}" y2="{bus_y-12}" class="transmission-line"/>\n'
# Power transformer
trafo_x = 400
svg += f'<use href="#transformer-symbol" transform="translate({trafo_x},{bus_y})"/>\n'
svg += f'<text x="{trafo_x}" y="{bus_y+45}" class="label-text">Power Transformer</text>\n'
svg += f'<text x="{trafo_x}" y="{bus_y+60}" class="value-text">50 MVA, 33/11kV</text>\n'
svg += f'<text x="{trafo_x}" y="{bus_y+75}" class="value-text">Z = 8%, Dyn11</text>\n'
# HV/LV connection lines
svg += f'<line x1="{trafo_x-30}" y1="{bus_y}" x2="{trafo_x-50}" y2="{bus_y}" class="transmission-line"/>\n'
svg += f'<line x1="{trafo_x+30}" y1="{bus_y}" x2="{trafo_x+50}" y2="{bus_y}" class="transmission-line"/>\n'
# LV bus
lv_bus_y = bus_y + 150
svg += f'<line x1="450" y1="{lv_bus_y}" x2="750" y2="{lv_bus_y}" class="bus-line"/>\n'
svg += f'<text x="600" y="{lv_bus_y-15}" class="label-text">Distribution Bus - 11kV</text>\n'
svg += f'<line x1="450" y1="{bus_y}" x2="450" y2="{lv_bus_y}" class="transmission-line"/>\n'
# Circuit breaker 2
cb2_x = 480
svg += f'<use href="#breaker-symbol" transform="translate({cb2_x},{lv_bus_y})"/>\n'
svg += f'<text x="{cb2_x}" y="{lv_bus_y+45}" class="label-text">CB-2</text>\n'
svg += f'<text x="{cb2_x}" y="{lv_bus_y+60}" class="value-text">3000A, 25kA</text>\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'<line x1="{x_pos}" y1="{lv_bus_y}" x2="{x_pos}" y2="{feeder_end_y}" class="transmission-line"/>\n'
# Load symbol
svg += f'<use href="#load-symbol" transform="translate({x_pos},{feeder_end_y+25})"/>\n'
svg += f'<text x="{x_pos}" y="{feeder_end_y+55}" class="label-text">{name}</text>\n'
svg += f'<text x="{x_pos}" y="{feeder_end_y+70}" class="value-text">{load}</text>\n'
svg += f'<text x="{x_pos}" y="{feeder_end_y+85}" class="value-text">cos φ = 0.85</text>\n'
# Protection relay
relay_y = lv_bus_y + 30
svg += f'<use href="#relay-symbol" transform="translate({x_pos+25},{relay_y})"/>\n'
svg += f'<text x="{x_pos+25}" y="{relay_y+25}" class="label-text">R{i+1}</text>\n'
# Add protection coordination indicators
svg += f'<text x="500" y="130" class="subtitle-text">Protection Coordination: 0.3s intervals</text>\n'
svg += f'<text x="500" y="145" class="label-text">Generator: Differential + Overcurrent</text>\n'
svg += f'<text x="500" y="160" class="label-text">Transformer: Differential + REF</text>\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'<g id="calculation-box">\n'
svg += f' <rect x="650" y="500" width="450" height="150" rx="10" fill="white" stroke="{self.colors["primary"]}" stroke-width="2" filter="url(#dropShadow)"/>\n'
svg += f' <text x="875" y="525" class="subtitle-text">Fault Current Calculation</text>\n'
svg += f' <text x="675" y="555" class="label-text">For Line-to-Ground Fault:</text>\n'
svg += f' <text x="675" y="580" class="value-text">I₁ = I₂ = I₀ = Ea / (Z₁ + Z₂ + Z₀)</text>\n'
svg += f' <text x="675" y="605" class="value-text">If = 3 × I₁ = 3Ea / (Z₁ + Z₂ + Z₀)</text>\n'
svg += f' <text x="675" y="630" class="label-text">Where: Ea = Phase voltage, Z = Sequence impedance</text>\n'
svg += f'</g>\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'<g id="sequence-network-{impedance.lower()}">\n'
# Network title
svg += f' <text x="{x+150}" y="{y-10}" class="subtitle-text">{title}</text>\n'
# Voltage source
svg += f' <circle cx="{x+50}" cy="{y+50}" r="20" fill="white" stroke="{color}" stroke-width="3"/>\n'
svg += f' <text x="{x+50}" y="{y+35}" class="label-text" style="font-size: 10px;">+</text>\n'
svg += f' <text x="{x+50}" y="{y+65}" class="label-text" style="font-size: 10px;">-</text>\n'
svg += f' <text x="{x+50}" y="{y+55}" class="value-text">Ea</text>\n'
# Impedance box
svg += f' <rect x="{x+120}" y="{x+35}" width="60" height="30" fill="white" stroke="{color}" stroke-width="2" filter="url(#dropShadow)"/>\n'
svg += f' <text x="{x+150}" y="{y+55}" class="value-text">{impedance}</text>\n'
# Connecting lines
svg += f' <line x1="{x+70}" y1="{y+50}" x2="{x+120}" y2="{y+50}" stroke="{color}" stroke-width="2"/>\n'
svg += f' <line x1="{x+180}" y1="{y+50}" x2="{x+250}" y2="{y+50}" stroke="{color}" stroke-width="2"/>\n'
# Ground symbol
svg += f' <line x1="{x+250}" y1="{y+50}" x2="{x+250}" y2="{y+80}" stroke="{color}" stroke-width="2"/>\n'
svg += f' <line x1="{x+235}" y1="{y+80}" x2="{x+265}" y2="{y+80}" stroke="{color}" stroke-width="3"/>\n'
svg += f' <line x1="{x+240}" y1="{y+85}" x2="{x+260}" y2="{y+85}" stroke="{color}" stroke-width="2"/>\n'
svg += f' <line x1="{x+245}" y1="{y+90}" x2="{x+255}" y2="{y+90}" stroke="{color}" stroke-width="1"/>\n'
# Current arrow
svg += f' <path d="M{x+200} {y+40} L{x+220} {y+40}" stroke="{color}" stroke-width="2" marker-end="url(#arrowhead)"/>\n'
svg += f' <text x="{x+210}" y="{y+30}" class="label-text" style="font-size: 10px;">I{impedance[-1]}</text>\n'
svg += f'</g>\n'
return svg
def _draw_lg_fault_connection(self, x: int, y: int) -> str:
"""Draw L-G fault connection diagram"""
svg = f'<g id="lg-fault-connection">\n'
# Connection title
svg += f' <text x="{x+150}" y="{y-20}" class="subtitle-text">Series Connection for L-G Fault</text>\n'
# Voltage source
svg += f' <circle cx="{x+50}" cy="{y+50}" r="20" fill="white" stroke="{self.colors["success"]}" stroke-width="3"/>\n'
svg += f' <text x="{x+50}" y="{y+55}" class="value-text">Ea</text>\n'
# Series impedances
impedances = [("Z₁", y+50), ("Z₂", y+100), ("Z₀", y+150)]
# Connecting lines and impedances
svg += f' <line x1="{x+70}" y1="{y+50}" x2="{x+120}" y2="{y+50}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
for i, (z_label, y_pos) in enumerate(impedances):
# Impedance box
svg += f' <rect x="{x+120}" y="{y_pos-15}" width="50" height="30" fill="white" stroke="{self.colors["primary"]}" stroke-width="2" filter="url(#dropShadow)"/>\n'
svg += f' <text x="{x+145}" y="{y_pos+5}" class="value-text">{z_label}</text>\n'
if i < len(impedances) - 1:
svg += f' <line x1="{x+145}" y1="{y_pos+15}" x2="{x+145}" y2="{y_pos+35}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
# Connection to ground
svg += f' <line x1="{x+170}" y1="{y+50}" x2="{x+220}" y2="{y+50}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
svg += f' <line x1="{x+220}" y1="{y+50}" x2="{x+220}" y2="{y+150}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
svg += f' <line x1="{x+170}" y1="{y+150}" x2="{x+220}" y2="{y+150}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
# Fault point
svg += f' <use href="#fault-symbol" transform="translate({x+240},{y+100})"/>\n'
# Current flow indication
svg += f' <path d="M{x+90} {y+40} L{x+110} {y+40}" stroke="{self.colors["danger"]}" stroke-width="3" marker-end="url(#arrowhead-red)"/>\n'
svg += f' <text x="{x+100}" y="{y+30}" class="label-text" style="font-size: 12px; fill: {self.colors["danger"]};">If</text>\n'
svg += f'</g>\n'
return svg
def _draw_fault_phasors(self, x: int, y: int, fault_type: str) -> str:
"""Draw fault condition phasor diagram"""
svg = f'<g id="phasor-diagram">\n'
# Phasor diagram background
svg += f' <rect x="{x-50}" y="{y-50}" width="200" height="200" rx="10" fill="white" stroke="{self.colors["secondary"]}" stroke-width="1" filter="url(#dropShadow)"/>\n'
svg += f' <text x="{x+50}" y="{y-25}" class="subtitle-text">Phasor Diagram</text>\n'
center_x, center_y = x+50, y+50
radius = 60
# Reference circle
svg += f' <circle cx="{center_x}" cy="{center_y}" r="{radius}" stroke="{self.colors["secondary"]}" stroke-width="1" fill="none" stroke-dasharray="3,3"/>\n'
if fault_type == "line_to_ground":
# Phase A (faulted) - reduced magnitude
svg += f' <line x1="{center_x}" y1="{center_y}" x2="{center_x + 30}" y2="{center_y}" stroke="{self.colors["danger"]}" stroke-width="3" marker-end="url(#arrowhead-red)"/>\n'
svg += f' <text x="{center_x + 40}" y="{center_y + 5}" class="label-text" style="fill: {self.colors["danger"]};">Va</text>\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' <line x1="{center_x}" y1="{center_y}" x2="{x_b:.1f}" y2="{y_b:.1f}" stroke="{self.colors["success"]}" stroke-width="3" marker-end="url(#arrowhead)"/>\n'
svg += f' <text x="{x_b-20:.1f}" y="{y_b-5:.1f}" class="label-text" style="fill: {self.colors["success"]};">Vb</text>\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' <line x1="{center_x}" y1="{center_y}" x2="{x_c:.1f}" y2="{y_c:.1f}" stroke="{self.colors["info"]}" stroke-width="3" marker-end="url(#arrowhead)"/>\n'
svg += f' <text x="{x_c-20:.1f}" y="{y_c+15:.1f}" class="label-text" style="fill: {self.colors["info"]};">Vc</text>\n'
elif fault_type == "line_to_line":
# Phase A - normal
svg += f' <line x1="{center_x}" y1="{center_y}" x2="{center_x + radius}" y2="{center_y}" stroke="{self.colors["success"]}" stroke-width="3" marker-end="url(#arrowhead)"/>\n'
svg += f' <text x="{center_x + radius + 10}" y="{center_y + 5}" class="label-text" style="fill: {self.colors["success"]};">Va</text>\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' <line x1="{center_x}" y1="{center_y}" x2="{x_b:.1f}" y2="{y_b:.1f}" stroke="{self.colors["danger"]}" stroke-width="3" marker-end="url(#arrowhead-red)"/>\n'
svg += f' <text x="{x_b-25:.1f}" y="{y_b-5:.1f}" class="label-text" style="fill: {self.colors["danger"]};">Vb</text>\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' <line x1="{center_x}" y1="{center_y}" x2="{x_c:.1f}" y2="{y_c:.1f}" stroke="{self.colors["danger"]}" stroke-width="3" marker-end="url(#arrowhead-red)"/>\n'
svg += f' <text x="{x_c-25:.1f}" y="{y_c+15:.1f}" class="label-text" style="fill: {self.colors["danger"]};">Vc</text>\n'
# Center point
svg += f' <circle cx="{center_x}" cy="{center_y}" r="2" fill="{self.colors["dark"]}"/>\n'
svg += f'</g>\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'<rect x="{chart_x}" y="{chart_y}" width="{chart_width}" height="{chart_height}" fill="white" stroke="{self.colors["dark"]}" stroke-width="2" filter="url(#dropShadow)"/>\n'
# Grid lines
svg += self._draw_coordination_grid(chart_x, chart_y, chart_width, chart_height)
# Axes
svg += f'<line x1="{chart_x}" y1="{chart_y + chart_height}" x2="{chart_x + chart_width}" y2="{chart_y + chart_height}" class="axis-line" marker-end="url(#arrowhead)"/>\n'
svg += f'<line x1="{chart_x}" y1="{chart_y + chart_height}" x2="{chart_x}" y2="{chart_y}" class="axis-line" marker-end="url(#arrowhead)"/>\n'
# Axis labels
svg += f'<text x="{chart_x + chart_width//2}" y="{chart_y + chart_height + 40}" class="subtitle-text">Current (A)</text>\n'
svg += f'<text x="{chart_x - 60}" y="{chart_y + chart_height//2}" class="subtitle-text" transform="rotate(-90 {chart_x - 60} {chart_y + chart_height//2})">Time (s)</text>\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'<line x1="{x_pos}" y1="{y}" x2="{x_pos}" y2="{y + height}" class="grid-line"/>\n'
svg += f'<text x="{x_pos}" y="{y + height + 20}" class="label-text">{current}</text>\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'<line x1="{x}" y1="{y_pos}" x2="{x + width}" y2="{y_pos}" class="grid-line"/>\n'
svg += f'<text x="{x - 30}" y="{y_pos + 5}" class="label-text">{time_val}</text>\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'<polyline points="{" ".join(fuse_points)}" stroke="{self.colors["warning"]}" class="curve-line"/>\n'
svg += f'<text x="{x + width*0.3}" y="{y + height*0.2}" class="label-text" fill="{self.colors["warning"]}">Fuse 100A</text>\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'<polyline points="{" ".join(relay1_points)}" stroke="{self.colors["success"]}" class="curve-line"/>\n'
svg += f'<text x="{x + width*0.5}" y="{y + height*0.4}" class="label-text" fill="{self.colors["success"]}">Primary Relay</text>\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'<polyline points="{" ".join(relay2_points)}" stroke="{self.colors["danger"]}" class="curve-line"/>\n'
svg += f'<text x="{x + width*0.7}" y="{y + height*0.6}" class="label-text" fill="{self.colors["danger"]}">Backup Relay</text>\n'
return svg
def _add_coordination_analysis(self, x: int, y: int) -> str:
"""Add coordination analysis text box"""
svg = f'<g id="coordination-analysis">\n'
svg += f' <rect x="{x}" y="{y}" width="200" height="300" rx="10" fill="white" stroke="{self.colors["primary"]}" stroke-width="2" filter="url(#dropShadow)"/>\n'
svg += f' <text x="{x + 100}" y="{y + 25}" class="subtitle-text">Coordination Analysis</text>\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' <text x="{x + 10}" y="{y + 50 + i*15}" class="label-text" style="font-size: 10px;">{item}</text>\n'
svg += f'</g>\n'
return svg
def add_grid_reference(self) -> str:
"""Add grid reference and scale"""
svg = f'<g id="grid-reference">\n'
svg += f' <text x="950" y="680" class="label-text">Scale: Not to scale - Schematic only</text>\n'
svg += f' <text x="50" y="680" class="label-text">Date: {datetime.now().strftime("%Y-%m-%d")}</text>\n'
svg += f' <text x="500" y="680" class="label-text">Power Systems Mini-Consultant</text>\n'
svg += f'</g>\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'<g id="voltage-phasors">\n'
svg += f' <text x="{center_x - 150}" y="{center_y - 120}" class="subtitle-text">Voltage Phasors</text>\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' <line x1="{center_x}" y1="{center_y}" x2="{x_end:.1f}" y2="{y_end:.1f}" stroke="{color}" stroke-width="4" marker-end="url(#arrowhead)"/>\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' <text x="{label_x:.1f}" y="{label_y:.1f}" class="label-text" style="fill: {color}; font-weight: bold;">{label}</text>\n'
# Current phasors (lagging by 30 degrees for inductive load)
svg += f' <text x="{center_x + 150}" y="{center_y - 120}" class="subtitle-text">Current Phasors</text>\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' <line x1="{center_x}" y1="{center_y}" x2="{x_end:.1f}" y2="{y_end:.1f}" stroke="{color}" stroke-width="3" stroke-dasharray="5,5" marker-end="url(#arrowhead)"/>\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' <text x="{label_x:.1f}" y="{label_y:.1f}" class="label-text" style="fill: {color};">I{phase}</text>\n'
svg += f'</g>\n'
# Reference circles
svg += f'<circle cx="{center_x}" cy="{center_y}" r="{voltage_radius}" stroke="{self.colors["secondary"]}" stroke-width="1" fill="none" stroke-dasharray="2,2"/>\n'
svg += f'<circle cx="{center_x}" cy="{center_y}" r="{current_radius}" stroke="{self.colors["secondary"]}" stroke-width="1" fill="none" stroke-dasharray="2,2"/>\n'
# Center point
svg += f'<circle cx="{center_x}" cy="{center_y}" r="3" fill="{self.colors["dark"]}"/>\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'<g id="power-triangle">\n'
# Title
svg += f' <text x="{x}" y="{y - 20}" class="subtitle-text">Power Triangle</text>\n'
# Triangle
base = 80
height = 60
# Real power (P)
svg += f' <line x1="{x}" y1="{y}" x2="{x + base}" y2="{y}" stroke="{self.colors["success"]}" stroke-width="3"/>\n'
svg += f' <text x="{x + base//2}" y="{y + 15}" class="label-text" style="fill: {self.colors["success"]};">P (kW)</text>\n'
# Reactive power (Q)
svg += f' <line x1="{x + base}" y1="{y}" x2="{x + base}" y2="{y - height}" stroke="{self.colors["warning"]}" stroke-width="3"/>\n'
svg += f' <text x="{x + base + 15}" y="{y - height//2}" class="label-text" style="fill: {self.colors["warning"]};">Q (kVAr)</text>\n'
# Apparent power (S)
svg += f' <line x1="{x}" y1="{y}" x2="{x + base}" y2="{y - height}" stroke="{self.colors["primary"]}" stroke-width="3"/>\n'
svg += f' <text x="{x + base//2 - 10}" y="{y - height//2 - 10}" class="label-text" style="fill: {self.colors["primary"]};">S (kVA)</text>\n'
# Power factor angle
svg += f' <path d="M{x+20} {y} A20,20 0 0,0 {x + 20*math.cos(math.radians(37)):.1f} {y - 20*math.sin(math.radians(37)):.1f}" stroke="{self.colors["dark"]}" stroke-width="1" fill="none"/>\n'
svg += f' <text x="{x + 25}" y="{y - 5}" class="label-text" style="font-size: 10px;">φ</text>\n'
svg += f'</g>\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'<rect x="{chart_x}" y="{chart_y}" width="{chart_width}" height="{chart_height}" fill="white" stroke="{self.colors["dark"]}" stroke-width="2" filter="url(#dropShadow)"/>\n'
# Axes
svg += f'<line x1="{chart_x}" y1="{center_y}" x2="{chart_x + chart_width}" y2="{center_y}" class="axis-line" marker-end="url(#arrowhead)"/>\n'
svg += f'<line x1="{center_x}" y1="{chart_y + chart_height}" x2="{center_x}" y2="{chart_y}" class="axis-line" marker-end="url(#arrowhead)"/>\n'
# Axis labels
svg += f'<text x="{chart_x + chart_width + 20}" y="{center_y + 5}" class="subtitle-text">R (Ω)</text>\n'
svg += f'<text x="{center_x - 10}" y="{chart_y - 10}" class="subtitle-text">X (Ω)</text>\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'<line x1="{r_pos}" y1="{center_y - 5}" x2="{r_pos}" y2="{center_y + 5}" stroke="{self.colors["dark"]}" stroke-width="1"/>\n'
svg += f'<text x="{r_pos}" y="{center_y + 20}" class="label-text">{i*2}</text>\n'
# X axis markings
x_pos = center_y - i * 60
if x_pos >= chart_y:
svg += f'<line x1="{center_x - 5}" y1="{x_pos}" x2="{center_x + 5}" y2="{x_pos}" stroke="{self.colors["dark"]}" stroke-width="1"/>\n'
svg += f'<text x="{center_x - 25}" y="{x_pos + 5}" class="label-text">{i*2}</text>\n'
# Zone 1 - Mho circle (80% reach)
zone1_radius = 80
zone1_center_x = center_x + 40
svg += f'<circle cx="{zone1_center_x}" cy="{center_y}" r="{zone1_radius}" stroke="{self.colors["success"]}" stroke-width="3" fill="rgba(16, 185, 129, 0.1)"/>\n'
svg += f'<text x="{zone1_center_x + 60}" y="{center_y - 90}" class="label-text" style="fill: {self.colors["success"]}; font-weight: bold;">Zone 1</text>\n'
svg += f'<text x="{zone1_center_x + 60}" y="{center_y - 75}" class="label-text" style="fill: {self.colors["success"]};">80% Line</text>\n'
svg += f'<text x="{zone1_center_x + 60}" y="{center_y - 60}" class="label-text" style="fill: {self.colors["success"]};">Instantaneous</text>\n'
# Zone 2 - Larger Mho circle (120% reach)
zone2_radius = 120
zone2_center_x = center_x + 60
svg += f'<circle cx="{zone2_center_x}" cy="{center_y}" r="{zone2_radius}" stroke="{self.colors["warning"]}" stroke-width="2" fill="none" stroke-dasharray="8,4"/>\n'
svg += f'<text x="{zone2_center_x + 90}" y="{center_y - 130}" class="label-text" style="fill: {self.colors["warning"]}; font-weight: bold;">Zone 2</text>\n'
svg += f'<text x="{zone2_center_x + 90}" y="{center_y - 115}" class="label-text" style="fill: {self.colors["warning"]};">120% + 50% Next</text>\n'
svg += f'<text x="{zone2_center_x + 90}" y="{center_y - 100}" class="label-text" style="fill: {self.colors["warning"]};">t = 0.3s</text>\n'
# Zone 3 - Reverse reach
zone3_radius = 60
zone3_center_x = center_x - 30
svg += f'<circle cx="{zone3_center_x}" cy="{center_y}" r="{zone3_radius}" stroke="{self.colors["danger"]}" stroke-width="2" fill="none" stroke-dasharray="4,8"/>\n'
svg += f'<text x="{zone3_center_x - 80}" y="{center_y - 70}" class="label-text" style="fill: {self.colors["danger"]}; font-weight: bold;">Zone 3</text>\n'
svg += f'<text x="{zone3_center_x - 80}" y="{center_y - 55}" class="label-text" style="fill: {self.colors["danger"]};">Reverse Reach</text>\n'
svg += f'<text x="{zone3_center_x - 80}" y="{center_y - 40}" class="label-text" style="fill: {self.colors["danger"]};">t = 1.0s</text>\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'<polyline points="{" ".join(load_points)}" stroke="{self.colors["info"]}" stroke-width="3" fill="none"/>\n'
svg += f'<text x="{center_x + 150}" y="{center_y - 20}" class="label-text" style="fill: {self.colors["info"]}; font-weight: bold;">Load Trajectory</text>\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'<circle cx="{x_fault}" cy="{y_fault}" r="4" fill="{self.colors["danger"]}" stroke="white" stroke-width="2"/>\n'
svg += f'<text x="{x_fault + 10}" y="{y_fault - 5}" class="label-text" style="fill: {self.colors["danger"]}; font-size: 10px;">{label}</text>\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'<g id="settings-table">\n'
# Table background
table_width = 220
table_height = 350
svg += f' <rect x="{x}" y="{y}" width="{table_width}" height="{table_height}" rx="10" fill="white" stroke="{self.colors["primary"]}" stroke-width="2" filter="url(#dropShadow)"/>\n'
# Table title
svg += f' <text x="{x + table_width//2}" y="{y + 25}" class="subtitle-text">Distance Protection Settings</text>\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' <rect x="{x + 5}" y="{y_pos - 12}" width="{table_width - 10}" height="{row_height - 2}" fill="{self.colors["primary"]}" rx="3"/>\n'
for j, cell in enumerate(row):
x_pos = x + 15 + j * 50
svg += f' <text x="{x_pos}" y="{y_pos}" class="label-text" style="fill: white; font-weight: bold; font-size: 10px;">{cell}</text>\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' <text x="{x_pos}" y="{y_pos}" class="label-text" style="fill: {color}; font-size: {font_size};">{cell}</text>\n'
svg += f'</g>\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'<text x="{x+150}" y="{y-20}" class="subtitle-text">L-L Fault: Z1 || Z2</text>\n'
# Voltage source
svg += f'<circle cx="{x+50}" cy="{y+75}" r="20" fill="white" stroke="{self.colors["success"]}" stroke-width="3"/>\n'
svg += f'<text x="{x+50}" y="{y+80}" class="value-text">Ea</text>\n'
# Parallel branches
# Upper branch (Z1)
svg += f'<line x1="{x+70}" y1="{y+50}" x2="{x+150}" y2="{y+50}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
svg += f'<rect x="{x+150}" y="{y+35}" width="50" height="30" fill="white" stroke="{self.colors["primary"]}" stroke-width="2" filter="url(#dropShadow)"/>\n'
svg += f'<text x="{x+175}" y="{y+55}" class="value-text">Z₁</text>\n'
svg += f'<line x1="{x+200}" y1="{y+50}" x2="{x+250}" y2="{y+50}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
# Lower branch (Z2)
svg += f'<line x1="{x+70}" y1="{y+100}" x2="{x+150}" y2="{y+100}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
svg += f'<rect x="{x+150}" y="{y+85}" width="50" height="30" fill="white" stroke="{self.colors["primary"]}" stroke-width="2" filter="url(#dropShadow)"/>\n'
svg += f'<text x="{x+175}" y="{y+105}" class="value-text">Z₂</text>\n'
svg += f'<line x1="{x+200}" y1="{y+100}" x2="{x+250}" y2="{y+100}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
# Connect voltage source to parallel branches
svg += f'<line x1="{x+50}" y1="{y+55}" x2="{x+50}" y2="{y+50}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
svg += f'<line x1="{x+50}" y1="{y+50}" x2="{x+70}" y2="{y+50}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
svg += f'<line x1="{x+50}" y1="{y+95}" x2="{x+50}" y2="{y+100}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
svg += f'<line x1="{x+50}" y1="{y+100}" x2="{x+70}" y2="{y+100}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
# Connect parallel branches
svg += f'<line x1="{x+250}" y1="{y+50}" x2="{x+250}" y2="{y+100}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
# Fault point
svg += f'<use href="#fault-symbol" transform="translate({x+270},{y+75})"/>\n'
# Current arrows
svg += f'<path d="M{x+130} {y+40} L{x+140} {y+40}" stroke="{self.colors["danger"]}" stroke-width="3" marker-end="url(#arrowhead-red)"/>\n'
svg += f'<text x="{x+135}" y="{y+30}" class="label-text" style="font-size: 10px; fill: {self.colors["danger"]};">I₁</text>\n'
svg += f'<path d="M{x+130} {y+90} L{x+140} {y+90}" stroke="{self.colors["danger"]}" stroke-width="3" marker-end="url(#arrowhead-red)"/>\n'
svg += f'<text x="{x+135}" y="{y+115}" class="label-text" style="font-size: 10px; fill: {self.colors["danger"]};">I₂</text>\n'
# Calculation box
svg += f'<g id="ll-calculation">\n'
svg += f' <rect x="{x+50}" y="{y+150}" width="300" height="80" rx="8" fill="white" stroke="{self.colors["primary"]}" stroke-width="2" filter="url(#dropShadow)"/>\n'
svg += f' <text x="{x+200}" y="{y+170}" class="subtitle-text">L-L Fault Current</text>\n'
svg += f' <text x="{x+65}" y="{y+195}" class="value-text">If = √3 × Ea / (Z₁ + Z₂)</text>\n'
svg += f' <text x="{x+65}" y="{y+215}" class="label-text">Z₀ does not appear in L-L fault</text>\n'
svg += f'</g>\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'<text x="{x+150}" y="{y-20}" class="subtitle-text">3φ Fault: Positive Sequence Only</text>\n'
# Voltage source
svg += f'<circle cx="{x+50}" cy="{y+75}" r="20" fill="white" stroke="{self.colors["success"]}" stroke-width="3"/>\n'
svg += f'<text x="{x+50}" y="{y+80}" class="value-text">Ea</text>\n'
# Single impedance (Z1 only)
svg += f'<line x1="{x+70}" y1="{y+75}" x2="{x+150}" y2="{y+75}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
svg += f'<rect x="{x+150}" y="{y+60}" width="50" height="30" fill="white" stroke="{self.colors["primary"]}" stroke-width="2" filter="url(#dropShadow)"/>\n'
svg += f'<text x="{x+175}" y="{y+80}" class="value-text">Z₁</text>\n'
svg += f'<line x1="{x+200}" y1="{y+75}" x2="{x+250}" y2="{y+75}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
# Fault point
svg += f'<use href="#fault-symbol" transform="translate({x+270},{y+75})"/>\n'
# Current arrow
svg += f'<path d="M{x+120} {y+65} L{x+140} {y+65}" stroke="{self.colors["danger"]}" stroke-width="3" marker-end="url(#arrowhead-red)"/>\n'
svg += f'<text x="{x+130}" y="{y+55}" class="label-text" style="font-size: 12px; fill: {self.colors["danger"]};">If</text>\n'
# Calculation box
svg += f'<g id="3ph-calculation">\n'
svg += f' <rect x="{x+50}" y="{y+130}" width="250" height="100" rx="8" fill="white" stroke="{self.colors["primary"]}" stroke-width="2" filter="url(#dropShadow)"/>\n'
svg += f' <text x="{x+175}" y="{y+150}" class="subtitle-text">3φ Fault Current</text>\n'
svg += f' <text x="{x+65}" y="{y+175}" class="value-text">If = Ea / Z₁</text>\n'
svg += f' <text x="{x+65}" y="{y+195}" class="label-text">Highest fault current</text>\n'
svg += f' <text x="{x+65}" y="{y+215}" class="label-text">Only positive sequence present</text>\n'
svg += f'</g>\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!")