Power_Systems_Mini-Consultant / utils /diagram_generator.py
ashhal's picture
Update utils/diagram_generator.py
5b68bfb verified
raw
history blame
24.4 kB
"""
Diagram Generation Utilities for Power Systems
Generates SVG diagrams for power system concepts
"""
import json
from typing import Dict, List, Tuple, Optional
from datetime import datetime
class DiagramGenerator:
"""
Generate SVG diagrams for power systems concepts
"""
def __init__(self):
self.svg_width = 800
self.svg_height = 600
self.grid_size = 20
def create_svg_header(self, width: int = None, height: int = None) -> str:
"""Create SVG header with proper dimensions"""
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>
<style>
.line {{ stroke: #2563eb; stroke-width: 2; fill: none; }}
.bus {{ stroke: #dc2626; stroke-width: 4; }}
.text {{ font-family: Arial, sans-serif; font-size: 12px; fill: #374151; }}
.title {{ font-family: Arial, sans-serif; font-size: 16px; font-weight: bold; fill: #1f2937; }}
.component {{ stroke: #059669; stroke-width: 2; fill: #d1fae5; }}
.fault {{ stroke: #dc2626; stroke-width: 3; fill: #fecaca; }}
.protection {{ stroke: #7c3aed; stroke-width: 2; fill: #e9d5ff; }}
.ground {{ stroke: #374151; stroke-width: 2; }}
</style>
<!-- Marker definitions for arrows -->
<marker id="arrowhead" markerWidth="10" markerHeight="7"
refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
</marker>
<!-- Component symbols -->
<g id="generator">
<circle cx="0" cy="0" r="15" class="component"/>
<text x="0" y="4" text-anchor="middle" class="text">G</text>
</g>
<g id="transformer">
<circle cx="-10" cy="0" r="8" class="component"/>
<circle cx="10" cy="0" r="8" class="component"/>
<text x="0" y="25" text-anchor="middle" class="text">T</text>
</g>
<g id="load">
<path d="M-10,-10 L10,-10 L5,10 L-5,10 Z" class="component"/>
<text x="0" y="25" text-anchor="middle" class="text">Load</text>
</g>
<g id="fault-symbol">
<circle cx="0" cy="0" r="8" class="fault"/>
<line x1="-6" y1="-6" x2="6" y2="6" class="fault"/>
<line x1="-6" y1="6" x2="6" y2="-6" class="fault"/>
</g>
</defs>
"""
def create_svg_footer(self) -> str:
"""Create SVG footer"""
return "</svg>"
def generate_single_line_diagram(self, system_config: Dict) -> str:
"""Generate a single line diagram"""
svg = self.create_svg_header()
# Title
svg += f'<text x="400" y="30" text-anchor="middle" class="title">Single Line Diagram</text>\n'
# Main bus (horizontal line)
svg += f'<line x1="100" y1="100" x2="700" y2="100" class="bus"/>\n'
svg += f'<text x="400" y="85" text-anchor="middle" class="text">Main Bus (33kV)</text>\n'
# Generator
svg += f'<use href="#generator" transform="translate(150,100)"/>\n'
svg += f'<line x1="135" y1="100" x2="100" y2="100" class="line"/>\n'
svg += f'<text x="150" y="140" text-anchor="middle" class="text">Generator</text>\n'
svg += f'<text x="150" y="155" text-anchor="middle" class="text">100MVA</text>\n'
# Transformer
svg += f'<use href="#transformer" transform="translate(300,100)"/>\n'
svg += f'<line x1="290" y1="100" x2="250" y2="100" class="line"/>\n'
svg += f'<line x1="310" y1="100" x2="350" y2="100" class="line"/>\n'
svg += f'<text x="300" y="140" text-anchor="middle" class="text">Power Transformer</text>\n'
svg += f'<text x="300" y="155" text-anchor="middle" class="text">33/11kV, 50MVA</text>\n'
# Distribution lines
svg += f'<line x1="400" y1="100" x2="400" y2="200" class="line"/>\n'
svg += f'<line x1="350" y1="200" x2="450" y2="200" class="line"/>\n'
svg += f'<text x="400" y="215" text-anchor="middle" class="text">Distribution Bus (11kV)</text>\n'
# Loads
positions = [375, 425]
load_names = ["Industrial Load", "Commercial Load"]
load_powers = ["15MW", "8MW"]
for i, (pos, name, power) in enumerate(zip(positions, load_names, load_powers)):
svg += f'<use href="#load" transform="translate({pos},250)"/>\n'
svg += f'<line x1="{pos}" y1="200" x2="{pos}" y2="240" class="line"/>\n'
svg += f'<text x="{pos}" y="285" text-anchor="middle" class="text">{name}</text>\n'
svg += f'<text x="{pos}" y="300" text-anchor="middle" class="text">{power}</text>\n'
# Protection devices
svg += f'<rect x="195" y="95" width="10" height="10" class="protection"/>\n'
svg += f'<text x="200" y="120" text-anchor="middle" class="text">CB1</text>\n'
svg += f'<rect x="345" y="95" width="10" height="10" class="protection"/>\n'
svg += f'<text x="350" y="120" text-anchor="middle" class="text">CB2</text>\n'
# Legend
svg += f'<text x="50" y="450" class="title">Legend:</text>\n'
svg += f'<line x1="50" y1="470" x2="80" y2="470" class="bus"/>\n'
svg += f'<text x="90" y="475" class="text">Bus</text>\n'
svg += f'<line x1="50" y1="490" x2="80" y2="490" class="line"/>\n'
svg += f'<text x="90" y="495" class="text">Transmission Line</text>\n'
svg += f'<rect x="50" y="505" width="10" height="10" class="protection"/>\n'
svg += f'<text x="70" y="515" class="text">Circuit Breaker</text>\n'
svg += self.create_svg_footer()
return svg
def generate_fault_analysis_diagram(self, fault_type: str = "line_to_ground") -> str:
"""Generate fault analysis diagram with sequence networks"""
svg = self.create_svg_header(900, 700)
# Title
svg += f'<text x="450" y="30" text-anchor="middle" class="title">Fault Analysis - {fault_type.replace("_", " ").title()}</text>\n'
if fault_type == "line_to_ground":
# Positive sequence network
svg += f'<text x="150" y="80" text-anchor="middle" class="title">Positive Sequence</text>\n'
svg += self._draw_sequence_network(150, 100, "Z1", "#059669")
# Negative sequence network
svg += f'<text x="450" y="80" text-anchor="middle" class="title">Negative Sequence</text>\n'
svg += self._draw_sequence_network(450, 100, "Z2", "#dc2626")
# Zero sequence network
svg += f'<text x="750" y="80" text-anchor="middle" class="title">Zero Sequence</text>\n'
svg += self._draw_sequence_network(750, 100, "Z0", "#7c3aed")
# Connection diagram
svg += f'<text x="450" y="320" text-anchor="middle" class="title">Network Connection</text>\n'
# Series connection for L-G fault
svg += f'<line x1="400" y1="350" x2="400" y2="450" class="line"/>\n'
svg += f'<line x1="400" y1="370" x2="500" y2="370" class="line"/>\n'
svg += f'<line x1="400" y1="410" x2="500" y2="410" class="line"/>\n'
svg += f'<line x1="400" y1="450" x2="500" y2="450" class="line"/>\n'
# Impedance boxes
svg += f'<rect x="480" y="360" width="40" height="20" class="component"/>\n'
svg += f'<text x="500" y="375" text-anchor="middle" class="text">Z1</text>\n'
svg += f'<rect x="480" y="400" width="40" height="20" class="component"/>\n'
svg += f'<text x="500" y="415" text-anchor="middle" class="text">Z2</text>\n'
svg += f'<rect x="480" y="440" width="40" height="20" class="component"/>\n'
svg += f'<text x="500" y="455" text-anchor="middle" class="text">Z0</text>\n'
# Voltage source
svg += f'<circle cx="370" cy="350" r="15" class="component"/>\n'
svg += f'<text x="370" y="355" text-anchor="middle" class="text">Ea</text>\n'
# Fault point
svg += f'<use href="#fault-symbol" transform="translate(530,410)"/>\n'
svg += f'<text x="530" y="440" text-anchor="middle" class="text">Fault</text>\n'
# Current equation
svg += f'<text x="450" y="520" text-anchor="middle" class="title">Fault Current Calculation</text>\n'
svg += f'<text x="450" y="550" text-anchor="middle" class="text">I_fault = 3 × Ea / (Z1 + Z2 + Z0)</text>\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()
svg += self.create_svg_footer()
return svg
def _draw_sequence_network(self, x: int, y: int, impedance: str, color: str) -> str:
"""Draw a sequence network"""
svg = ""
# Voltage source
svg += f'<circle cx="{x-50}" cy="{y+50}" r="15" stroke="{color}" stroke-width="2" fill="white"/>\n'
svg += f'<text x="{x-50}" y="{y+55}" text-anchor="middle" class="text">E</text>\n'
# Impedance
svg += f'<rect x="{x-10}" y="{y+40}" width="40" height="20" stroke="{color}" stroke-width="2" fill="white"/>\n'
svg += f'<text x="{x+10}" y="{y+55}" text-anchor="middle" class="text">{impedance}</text>\n'
# Connecting lines
svg += f'<line x1="{x-35}" y1="{y+50}" x2="{x-10}" y2="{y+50}" stroke="{color}" stroke-width="2"/>\n'
svg += f'<line x1="{x+30}" y1="{y+50}" x2="{x+60}" y2="{y+50}" stroke="{color}" stroke-width="2"/>\n'
# Ground/neutral
svg += f'<line x1="{x+60}" y1="{y+50}" x2="{x+60}" y2="{y+80}" stroke="{color}" stroke-width="2"/>\n'
svg += f'<line x1="{x+50}" y1="{y+80}" x2="{x+70}" y2="{y+80}" stroke="{color}" stroke-width="2"/>\n'
svg += f'<line x1="{x+52}" y1="{y+85}" x2="{x+68}" y2="{y+85}" stroke="{color}" stroke-width="2"/>\n'
svg += f'<line x1="{x+54}" y1="{y+90}" x2="{x+66}" y2="{y+90}" stroke="{color}" stroke-width="2"/>\n'
return svg
def _draw_ll_fault_diagram(self) -> str:
"""Draw line-to-line fault diagram"""
svg = ""
# Positive and negative sequence in parallel
svg += f'<text x="300" y="80" text-anchor="middle" class="title">L-L Fault: Z1 and Z2 in Parallel</text>\n'
# Parallel connection
svg += f'<circle cx="200" cy="150" r="15" class="component"/>\n'
svg += f'<text x="200" y="155" text-anchor="middle" class="text">Ea</text>\n'
# Upper branch (Z1)
svg += f'<line x1="215" y1="140" x2="300" y2="140" class="line"/>\n'
svg += f'<rect x="300" y="130" width="40" height="20" class="component"/>\n'
svg += f'<text x="320" y="145" text-anchor="middle" class="text">Z1</text>\n'
svg += f'<line x1="340" y1="140" x2="400" y2="140" class="line"/>\n'
# Lower branch (Z2)
svg += f'<line x1="215" y1="160" x2="300" y2="160" class="line"/>\n'
svg += f'<rect x="300" y="150" width="40" height="20" class="component"/>\n'
svg += f'<text x="320" y="165" text-anchor="middle" class="text">Z2</text>\n'
svg += f'<line x1="340" y1="160" x2="400" y2="160" class="line"/>\n'
# Connection
svg += f'<line x1="400" y1="140" x2="400" y2="160" class="line"/>\n'
# Fault
svg += f'<use href="#fault-symbol" transform="translate(420,150)"/>\n'
# Current equation
svg += f'<text x="300" y="250" text-anchor="middle" class="title">I_fault = √3 × Ea / (Z1 + Z2)</text>\n'
return svg
def _draw_three_phase_fault_diagram(self) -> str:
"""Draw three-phase fault diagram"""
svg = ""
svg += f'<text x="400" y="80" text-anchor="middle" class="title">Three-Phase Fault: Positive Sequence Only</text>\n'
# Simple circuit
svg += f'<circle cx="250" cy="150" r="15" class="component"/>\n'
svg += f'<text x="250" y="155" text-anchor="middle" class="text">Ea</text>\n'
svg += f'<line x1="265" y1="150" x2="350" y2="150" class="line"/>\n'
svg += f'<rect x="350" y="140" width="40" height="20" class="component"/>\n'
svg += f'<text x="370" y="155" text-anchor="middle" class="text">Z1</text>\n'
svg += f'<line x1="390" y1="150" x2="450" y2="150" class="line"/>\n'
svg += f'<use href="#fault-symbol" transform="translate(470,150)"/>\n'
# Current equation
svg += f'<text x="400" y="220" text-anchor="middle" class="title">I_fault = Ea / Z1</text>\n'
return svg
def generate_protection_coordination_diagram(self) -> str:
"""Generate time-current coordination curves"""
svg = self.create_svg_header(800, 600)
# Title
svg += f'<text x="400" y="30" text-anchor="middle" class="title">Protection Coordination Curves</text>\n'
# Axes
svg += f'<line x1="100" y1="500" x2="700" y2="500" class="line marker-end="url(#arrowhead)"/>\n'
svg += f'<line x1="100" y1="500" x2="100" y2="100" class="line marker-end="url(#arrowhead)"/>\n'
# Axis labels
svg += f'<text x="400" y="530" text-anchor="middle" class="text">Current (A)</text>\n'
svg += f'<text x="50" y="300" text-anchor="middle" class="text" transform="rotate(-90 50 300)">Time (s)</text>\n'
# Grid lines
for i in range(2, 7):
x = 100 + i * 100
svg += f'<line x1="{x}" y1="100" x2="{x}" y2="500" stroke="#e5e7eb" stroke-width="1"/>\n'
svg += f'<text x="{x}" y="520" text-anchor="middle" class="text">{10**(i-1)}</text>\n'
for i in range(1, 5):
y = 500 - i * 80
svg += f'<line x1="100" y1="{y}" x2="700" y2="{y}" stroke="#e5e7eb" stroke-width="1"/>\n'
svg += f'<text x="85" y="{y+5}" text-anchor="end" class="text">{10**(i-1)}</text>\n'
# Relay curves
# Primary relay (closer to load)
svg += self._draw_relay_curve(200, "Primary Relay", "#059669", "inverse")
# Backup relay
svg += self._draw_relay_curve(300, "Backup Relay", "#dc2626", "very_inverse")
# Fuse curve
svg += self._draw_relay_curve(150, "Fuse", "#7c3aed", "fuse")
# Legend
svg += f'<rect x="550" y="120" width="180" height="120" stroke="#374151" stroke-width="1" fill="white"/>\n'
svg += f'<text x="640" y="140" text-anchor="middle" class="text">Legend</text>\n'
svg += f'<line x1="560" y1="160" x2="590" y2="160" stroke="#7c3aed" stroke-width="3"/>\n'
svg += f'<text x="600" y="165" class="text">Fuse</text>\n'
svg += f'<line x1="560" y1="180" x2="590" y2="180" stroke="#059669" stroke-width="3"/>\n'
svg += f'<text x="600" y="185" class="text">Primary Relay</text>\n'
svg += f'<line x1="560" y1="200" x2="590" y2="200" stroke="#dc2626" stroke-width="3"/>\n'
svg += f'<text x="600" y="205" class="text">Backup Relay</text>\n'
svg += f'<text x="640" y="225" text-anchor="middle" class="text">Coordination Interval: 0.3s</text>\n'
svg += self.create_svg_footer()
return svg
def _draw_relay_curve(self, x_offset: int, label: str, color: str, curve_type: str) -> str:
"""Draw a time-current curve for a relay"""
svg = ""
points = []
if curve_type == "inverse":
# Standard inverse curve
for i in range(50):
current = 10 ** (i / 10.0)
time = 0.14 / ((current/100) ** 0.02 - 1)
if time > 0 and time < 1000:
x = 100 + (i * 12)
y = 500 - (time * 40)
if 100 <= x <= 700 and 100 <= y <= 500:
points.append(f"{x},{y}")
elif curve_type == "very_inverse":
# Very inverse curve (higher up)
for i in range(50):
current = 10 ** (i / 10.0)
time = 13.5 / ((current/100) ** 1.0 - 1)
if time > 0 and time < 1000:
x = 100 + (i * 12)
y = 500 - (time * 20)
if 100 <= x <= 700 and 100 <= y <= 500:
points.append(f"{x},{y}")
elif curve_type == "fuse":
# Fuse curve (faster, lower)
for i in range(40):
current = 10 ** (i / 8.0)
time = 0.01 / ((current/50) ** 2.0 - 1) if current > 50 else 1000
if time > 0 and time < 1000:
x = 100 + (i * 15)
y = 500 - (time * 60)
if 100 <= x <= 700 and 100 <= y <= 500:
points.append(f"{x},{y}")
if points:
path = f'<polyline points="{" ".join(points)}" stroke="{color}" stroke-width="3" fill="none"/>\n'
svg += path
# Label
if len(points) > 10:
mid_point = points[len(points)//2].split(',')
x, y = int(mid_point[0]), int(mid_point[1])
svg += f'<text x="{x+10}" y="{y-5}" class="text" fill="{color}">{label}</text>\n'
return svg
def generate_phasor_diagram(self, fault_type: str = "balanced") -> str:
"""Generate phasor diagrams for different fault conditions"""
svg = self.create_svg_header(600, 400)
# Title
svg += f'<text x="300" y="30" text-anchor="middle" class="title">Phasor Diagram - {fault_type.title()} Conditions</text>\n'
center_x, center_y = 300, 200
radius = 80
if fault_type == "balanced":
# Three balanced phasors 120° apart
angles = [0, 120, 240]
colors = ["#dc2626", "#059669", "#2563eb"]
labels = ["Va", "Vb", "Vc"]
for i, (angle, color, label) in enumerate(zip(angles, colors, labels)):
x_end = center_x + radius * cos(radians(angle))
y_end = center_y - radius * sin(radians(angle))
svg += f'<line x1="{center_x}" y1="{center_y}" x2="{x_end}" y2="{y_end}" '
svg += f'stroke="{color}" stroke-width="3" marker-end="url(#arrowhead)"/>\n'
# Label
label_x = center_x + (radius + 20) * cos(radians(angle))
label_y = center_y - (radius + 20) * sin(radians(angle))
svg += f'<text x="{label_x}" y="{label_y}" text-anchor="middle" class="text" fill="{color}">{label}</text>\n'
elif fault_type == "unbalanced":
# Unbalanced phasors showing fault condition
# Phase A (affected by fault) - reduced magnitude
svg += f'<line x1="{center_x}" y1="{center_y}" x2="{center_x + 40}" y2="{center_y}" '
svg += f'stroke="#dc2626" stroke-width="3" marker-end="url(#arrowhead)"/>\n'
svg += f'<text x="{center_x + 60}" y="{center_y}" class="text" fill="#dc2626">Va (faulted)</text>\n'
# Phase B - normal
x_b = center_x + radius * cos(radians(120))
y_b = center_y - radius * sin(radians(120))
svg += f'<line x1="{center_x}" y1="{center_y}" x2="{x_b}" y2="{y_b}" '
svg += f'stroke="#059669" stroke-width="3" marker-end="url(#arrowhead)"/>\n'
svg += f'<text x="{x_b-20}" y="{y_b-10}" class="text" fill="#059669">Vb</text>\n'
# Phase C - normal
x_c = center_x + radius * cos(radians(240))
y_c = center_y - radius * sin(radians(240))
svg += f'<line x1="{center_x}" y1="{center_y}" x2="{x_c}" y2="{y_c}" '
svg += f'stroke="#2563eb" stroke-width="3" marker-end="url(#arrowhead)"/>\n'
svg += f'<text x="{x_c-20}" y="{y_c+20}" class="text" fill="#2563eb">Vc</text>\n'
# Center point
svg += f'<circle cx="{center_x}" cy="{center_y}" r="3" fill="#374151"/>\n'
# Reference circle
svg += f'<circle cx="{center_x}" cy="{center_y}" r="{radius}" stroke="#e5e7eb" stroke-width="1" fill="none" stroke-dasharray="5,5"/>\n'
svg += self.create_svg_footer()
return svg
def generate_impedance_diagram(self) -> str:
"""Generate impedance diagram for distance protection"""
svg = self.create_svg_header(600, 500)
# Title
svg += f'<text x="300" y="30" text-anchor="middle" class="title">Distance Protection - R-X Diagram</text>\n'
center_x, center_y = 300, 250
# Axes
svg += f'<line x1="100" y1="{center_y}" x2="500" y2="{center_y}" class="line" marker-end="url(#arrowhead)"/>\n'
svg += f'<line x1="{center_x}" y1="400" x2="{center_x}" y2="100" class="line" marker-end="url(#arrowhead)"/>\n'
# Axis labels
svg += f'<text x="520" y="{center_y+5}" class="text">R (Ω)</text>\n'
svg += f'<text x="{center_x-10}" y="90" class="text">X (Ω)</text>\n'
# Mho circle (Zone 1)
svg += f'<circle cx="{center_x+50}" cy="{center_y}" r="80" stroke="#059669" stroke-width="2" fill="none"/>\n'
svg += f'<text x="{center_x+90}" y="{center_y-90}" class="text" fill="#059669">Zone 1 (Mho)</text>\n'
# Zone 2 (larger circle)
svg += f'<circle cx="{center_x+70}" cy="{center_y}" r="120" stroke="#dc2626" stroke-width="2" fill="none" stroke-dasharray="5,5"/>\n'
svg += f'<text x="{center_x+140}" y="{center_y-130}" class="text" fill="#dc2626">Zone 2</text>\n'
# Load impedance area
svg += f'<path d="M{center_x+20},{center_y-20} Q{center_x+80},{center_y-40} {center_x+120},{center_y-10}" '
svg += f'stroke="#7c3aed" stroke-width="2" fill="none"/>\n'
svg += f'<text x="{center_x+70}" y="{center_y-50}" class="text" fill="#7c3aed">Load Region</text>\n'
# Grid marks
for i in range(1, 5):
x = center_x + i * 40
svg += f'<line x1="{x}" y1="{center_y-5}" x2="{x}" y2="{center_y+5}" stroke="#374151" stroke-width="1"/>\n'
svg += f'<text x="{x}" y="{center_y+20}" text-anchor="middle" class="text">{i*5}</text>\n'
y = center_y - i * 40
svg += f'<line x1="{center_x-5}" y1="{y}" x2="{center_x+5}" y2="{y}" stroke="#374151" stroke-width="1"/>\n'
svg += f'<text x="{center_x-20}" y="{y+5}" text-anchor="middle" class="text">{i*5}</text>\n'
svg += self.create_svg_footer()
return svg
# Helper functions for calculations
def cos(angle_deg):
import math
return math.cos(math.radians(angle_deg))
def sin(angle_deg):
import math
return math.sin(math.radians(angle_deg))
def radians(angle_deg):
import math
return math.radians(angle_deg)
# Example usage
if __name__ == "__main__":
generator = DiagramGenerator()
# Test diagram generation
diagrams = {
"single_line": generator.generate_single_line_diagram({}),
"fault_analysis": generator.generate_fault_analysis_diagram("line_to_ground"),
"protection_coordination": generator.generate_protection_coordination_diagram(),
"phasor": generator.generate_phasor_diagram("balanced"),
"impedance": generator.generate_impedance_diagram()
}
# Save diagrams
for name, svg_content in diagrams.items():
with open(f"{name}_diagram.svg", "w") as f:
f.write(svg_content)
print(f"Generated {name} diagram")
print("All diagrams generated successfully!")