Spaces:
Runtime error
Runtime error
""" | |
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!") |