ashhal commited on
Commit
972e3d8
·
verified ·
1 Parent(s): cff6a7e

Update utils/diagram_generator.py

Browse files
Files changed (1) hide show
  1. utils/diagram_generator.py +865 -361
utils/diagram_generator.py CHANGED
@@ -1,184 +1,371 @@
1
  """
2
- Diagram Generation Utilities for Power Systems
3
- Generates SVG diagrams for power system concepts
4
  """
5
 
6
  import json
 
7
  from typing import Dict, List, Tuple, Optional
8
  from datetime import datetime
9
 
10
  class DiagramGenerator:
11
  """
12
- Generate SVG diagrams for power systems concepts
13
  """
14
 
15
  def __init__(self):
16
- self.svg_width = 800
17
- self.svg_height = 600
18
- self.grid_size = 20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  def create_svg_header(self, width: int = None, height: int = None) -> str:
21
- """Create SVG header with proper dimensions"""
22
  w = width or self.svg_width
23
  h = height or self.svg_height
24
 
25
  return f"""<svg width="{w}" height="{h}" viewBox="0 0 {w} {h}" xmlns="http://www.w3.org/2000/svg">
26
  <defs>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  <style>
28
- .line {{ stroke: #2563eb; stroke-width: 2; fill: none; }}
29
- .bus {{ stroke: #dc2626; stroke-width: 4; }}
30
- .text {{ font-family: Arial, sans-serif; font-size: 12px; fill: #374151; }}
31
- .title {{ font-family: Arial, sans-serif; font-size: 16px; font-weight: bold; fill: #1f2937; }}
32
- .component {{ stroke: #059669; stroke-width: 2; fill: #d1fae5; }}
33
- .fault {{ stroke: #dc2626; stroke-width: 3; fill: #fecaca; }}
34
- .protection {{ stroke: #7c3aed; stroke-width: 2; fill: #e9d5ff; }}
35
- .ground {{ stroke: #374151; stroke-width: 2; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  </style>
37
 
38
- <!-- Marker definitions for arrows -->
39
- <marker id="arrowhead" markerWidth="10" markerHeight="7"
40
- refX="9" refY="3.5" orient="auto">
41
- <polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
42
  </marker>
43
 
44
- <!-- Component symbols -->
45
- <g id="generator">
46
- <circle cx="0" cy="0" r="15" class="component"/>
47
- <text x="0" y="4" text-anchor="middle" class="text">G</text>
 
 
 
 
 
 
48
  </g>
49
 
50
- <g id="transformer">
51
- <circle cx="-10" cy="0" r="8" class="component"/>
52
- <circle cx="10" cy="0" r="8" class="component"/>
53
- <text x="0" y="25" text-anchor="middle" class="text">T</text>
 
54
  </g>
55
 
56
- <g id="load">
57
- <path d="M-10,-10 L10,-10 L5,10 L-5,10 Z" class="component"/>
58
- <text x="0" y="25" text-anchor="middle" class="text">Load</text>
 
 
 
 
 
 
59
  </g>
60
 
61
  <g id="fault-symbol">
62
- <circle cx="0" cy="0" r="8" class="fault"/>
63
- <line x1="-6" y1="-6" x2="6" y2="6" class="fault"/>
64
- <line x1="-6" y1="6" x2="6" y2="-6" class="fault"/>
65
  </g>
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  </defs>
67
  """
68
 
69
  def create_svg_footer(self) -> str:
70
  """Create SVG footer"""
71
  return "</svg>"
72
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  def generate_single_line_diagram(self, system_config: Dict) -> str:
74
- """Generate a single line diagram"""
75
  svg = self.create_svg_header()
76
 
77
- # Title
78
- svg += f'<text x="400" y="30" text-anchor="middle" class="title">Single Line Diagram</text>\n'
79
-
80
- # Main bus (horizontal line)
81
- svg += f'<line x1="100" y1="100" x2="700" y2="100" class="bus"/>\n'
82
- svg += f'<text x="400" y="85" text-anchor="middle" class="text">Main Bus (33kV)</text>\n'
83
-
84
- # Generator
85
- svg += f'<use href="#generator" transform="translate(150,100)"/>\n'
86
- svg += f'<line x1="135" y1="100" x2="100" y2="100" class="line"/>\n'
87
- svg += f'<text x="150" y="140" text-anchor="middle" class="text">Generator</text>\n'
88
- svg += f'<text x="150" y="155" text-anchor="middle" class="text">100MVA</text>\n'
89
-
90
- # Transformer
91
- svg += f'<use href="#transformer" transform="translate(300,100)"/>\n'
92
- svg += f'<line x1="290" y1="100" x2="250" y2="100" class="line"/>\n'
93
- svg += f'<line x1="310" y1="100" x2="350" y2="100" class="line"/>\n'
94
- svg += f'<text x="300" y="140" text-anchor="middle" class="text">Power Transformer</text>\n'
95
- svg += f'<text x="300" y="155" text-anchor="middle" class="text">33/11kV, 50MVA</text>\n'
96
-
97
- # Distribution lines
98
- svg += f'<line x1="400" y1="100" x2="400" y2="200" class="line"/>\n'
99
- svg += f'<line x1="350" y1="200" x2="450" y2="200" class="line"/>\n'
100
- svg += f'<text x="400" y="215" text-anchor="middle" class="text">Distribution Bus (11kV)</text>\n'
101
-
102
- # Loads
103
- positions = [375, 425]
104
- load_names = ["Industrial Load", "Commercial Load"]
105
- load_powers = ["15MW", "8MW"]
106
-
107
- for i, (pos, name, power) in enumerate(zip(positions, load_names, load_powers)):
108
- svg += f'<use href="#load" transform="translate({pos},250)"/>\n'
109
- svg += f'<line x1="{pos}" y1="200" x2="{pos}" y2="240" class="line"/>\n'
110
- svg += f'<text x="{pos}" y="285" text-anchor="middle" class="text">{name}</text>\n'
111
- svg += f'<text x="{pos}" y="300" text-anchor="middle" class="text">{power}</text>\n'
112
-
113
- # Protection devices
114
- svg += f'<rect x="195" y="95" width="10" height="10" class="protection"/>\n'
115
- svg += f'<text x="200" y="120" text-anchor="middle" class="text">CB1</text>\n'
116
-
117
- svg += f'<rect x="345" y="95" width="10" height="10" class="protection"/>\n'
118
- svg += f'<text x="350" y="120" text-anchor="middle" class="text">CB2</text>\n'
119
-
120
- # Legend
121
- svg += f'<text x="50" y="450" class="title">Legend:</text>\n'
122
- svg += f'<line x1="50" y1="470" x2="80" y2="470" class="bus"/>\n'
123
- svg += f'<text x="90" y="475" class="text">Bus</text>\n'
124
- svg += f'<line x1="50" y1="490" x2="80" y2="490" class="line"/>\n'
125
- svg += f'<text x="90" y="495" class="text">Transmission Line</text>\n'
126
- svg += f'<rect x="50" y="505" width="10" height="10" class="protection"/>\n'
127
- svg += f'<text x="70" y="515" class="text">Circuit Breaker</text>\n'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  svg += self.create_svg_footer()
130
  return svg
131
-
132
  def generate_fault_analysis_diagram(self, fault_type: str = "line_to_ground") -> str:
133
- """Generate fault analysis diagram with sequence networks"""
134
- svg = self.create_svg_header(900, 700)
135
 
136
- # Title
137
- svg += f'<text x="450" y="30" text-anchor="middle" class="title">Fault Analysis - {fault_type.replace("_", " ").title()}</text>\n'
 
138
 
139
  if fault_type == "line_to_ground":
140
  # Positive sequence network
141
- svg += f'<text x="150" y="80" text-anchor="middle" class="title">Positive Sequence</text>\n'
142
- svg += self._draw_sequence_network(150, 100, "Z1", "#059669")
143
 
144
  # Negative sequence network
145
- svg += f'<text x="450" y="80" text-anchor="middle" class="title">Negative Sequence</text>\n'
146
- svg += self._draw_sequence_network(450, 100, "Z2", "#dc2626")
147
 
148
  # Zero sequence network
149
- svg += f'<text x="750" y="80" text-anchor="middle" class="title">Zero Sequence</text>\n'
150
- svg += self._draw_sequence_network(750, 100, "Z0", "#7c3aed")
151
-
152
- # Connection diagram
153
- svg += f'<text x="450" y="320" text-anchor="middle" class="title">Network Connection</text>\n'
154
-
155
- # Series connection for L-G fault
156
- svg += f'<line x1="400" y1="350" x2="400" y2="450" class="line"/>\n'
157
- svg += f'<line x1="400" y1="370" x2="500" y2="370" class="line"/>\n'
158
- svg += f'<line x1="400" y1="410" x2="500" y2="410" class="line"/>\n'
159
- svg += f'<line x1="400" y1="450" x2="500" y2="450" class="line"/>\n'
160
 
161
- # Impedance boxes
162
- svg += f'<rect x="480" y="360" width="40" height="20" class="component"/>\n'
163
- svg += f'<text x="500" y="375" text-anchor="middle" class="text">Z1</text>\n'
164
 
165
- svg += f'<rect x="480" y="400" width="40" height="20" class="component"/>\n'
166
- svg += f'<text x="500" y="415" text-anchor="middle" class="text">Z2</text>\n'
167
-
168
- svg += f'<rect x="480" y="440" width="40" height="20" class="component"/>\n'
169
- svg += f'<text x="500" y="455" text-anchor="middle" class="text">Z0</text>\n'
170
-
171
- # Voltage source
172
- svg += f'<circle cx="370" cy="350" r="15" class="component"/>\n'
173
- svg += f'<text x="370" y="355" text-anchor="middle" class="text">Ea</text>\n'
174
-
175
- # Fault point
176
- svg += f'<use href="#fault-symbol" transform="translate(530,410)"/>\n'
177
- svg += f'<text x="530" y="440" text-anchor="middle" class="text">Fault</text>\n'
178
-
179
- # Current equation
180
- svg += f'<text x="450" y="520" text-anchor="middle" class="title">Fault Current Calculation</text>\n'
181
- svg += f'<text x="450" y="550" text-anchor="middle" class="text">I_fault = 3 × Ea / (Z1 + Z2 + Z0)</text>\n'
182
 
183
  elif fault_type == "line_to_line":
184
  # L-L fault diagram
@@ -188,325 +375,642 @@ class DiagramGenerator:
188
  # 3-phase fault diagram
189
  svg += self._draw_three_phase_fault_diagram()
190
 
 
 
 
191
  svg += self.create_svg_footer()
192
  return svg
193
-
194
- def _draw_sequence_network(self, x: int, y: int, impedance: str, color: str) -> str:
195
- """Draw a sequence network"""
196
- svg = ""
 
 
 
197
 
198
  # Voltage source
199
- svg += f'<circle cx="{x-50}" cy="{y+50}" r="15" stroke="{color}" stroke-width="2" fill="white"/>\n'
200
- svg += f'<text x="{x-50}" y="{y+55}" text-anchor="middle" class="text">E</text>\n'
 
 
201
 
202
- # Impedance
203
- svg += f'<rect x="{x-10}" y="{y+40}" width="40" height="20" stroke="{color}" stroke-width="2" fill="white"/>\n'
204
- svg += f'<text x="{x+10}" y="{y+55}" text-anchor="middle" class="text">{impedance}</text>\n'
205
 
206
  # Connecting lines
207
- svg += f'<line x1="{x-35}" y1="{y+50}" x2="{x-10}" y2="{y+50}" stroke="{color}" stroke-width="2"/>\n'
208
- svg += f'<line x1="{x+30}" y1="{y+50}" x2="{x+60}" y2="{y+50}" stroke="{color}" stroke-width="2"/>\n'
 
 
 
 
 
 
209
 
210
- # Ground/neutral
211
- svg += f'<line x1="{x+60}" y1="{y+50}" x2="{x+60}" y2="{y+80}" stroke="{color}" stroke-width="2"/>\n'
212
- svg += f'<line x1="{x+50}" y1="{y+80}" x2="{x+70}" y2="{y+80}" stroke="{color}" stroke-width="2"/>\n'
213
- svg += f'<line x1="{x+52}" y1="{y+85}" x2="{x+68}" y2="{y+85}" stroke="{color}" stroke-width="2"/>\n'
214
- svg += f'<line x1="{x+54}" y1="{y+90}" x2="{x+66}" y2="{y+90}" stroke="{color}" stroke-width="2"/>\n'
215
 
 
216
  return svg
217
-
218
- def _draw_ll_fault_diagram(self) -> str:
219
- """Draw line-to-line fault diagram"""
220
- svg = ""
221
 
222
- # Positive and negative sequence in parallel
223
- svg += f'<text x="300" y="80" text-anchor="middle" class="title">L-L Fault: Z1 and Z2 in Parallel</text>\n'
224
 
225
- # Parallel connection
226
- svg += f'<circle cx="200" cy="150" r="15" class="component"/>\n'
227
- svg += f'<text x="200" y="155" text-anchor="middle" class="text">Ea</text>\n'
228
 
229
- # Upper branch (Z1)
230
- svg += f'<line x1="215" y1="140" x2="300" y2="140" class="line"/>\n'
231
- svg += f'<rect x="300" y="130" width="40" height="20" class="component"/>\n'
232
- svg += f'<text x="320" y="145" text-anchor="middle" class="text">Z1</text>\n'
233
- svg += f'<line x1="340" y1="140" x2="400" y2="140" class="line"/>\n'
234
 
235
- # Lower branch (Z2)
236
- svg += f'<line x1="215" y1="160" x2="300" y2="160" class="line"/>\n'
237
- svg += f'<rect x="300" y="150" width="40" height="20" class="component"/>\n'
238
- svg += f'<text x="320" y="165" text-anchor="middle" class="text">Z2</text>\n'
239
- svg += f'<line x1="340" y1="160" x2="400" y2="160" class="line"/>\n'
240
 
241
- # Connection
242
- svg += f'<line x1="400" y1="140" x2="400" y2="160" class="line"/>\n'
 
 
 
 
 
 
 
 
 
 
243
 
244
- # Fault
245
- svg += f'<use href="#fault-symbol" transform="translate(420,150)"/>\n'
246
 
247
- # Current equation
248
- svg += f'<text x="300" y="250" text-anchor="middle" class="title">I_fault = √3 × Ea / (Z1 + Z2)</text>\n'
 
249
 
 
250
  return svg
251
-
252
- def _draw_three_phase_fault_diagram(self) -> str:
253
- """Draw three-phase fault diagram"""
254
- svg = ""
255
 
256
- svg += f'<text x="400" y="80" text-anchor="middle" class="title">Three-Phase Fault: Positive Sequence Only</text>\n'
 
 
257
 
258
- # Simple circuit
259
- svg += f'<circle cx="250" cy="150" r="15" class="component"/>\n'
260
- svg += f'<text x="250" y="155" text-anchor="middle" class="text">Ea</text>\n'
261
 
262
- svg += f'<line x1="265" y1="150" x2="350" y2="150" class="line"/>\n'
263
- svg += f'<rect x="350" y="140" width="40" height="20" class="component"/>\n'
264
- svg += f'<text x="370" y="155" text-anchor="middle" class="text">Z1</text>\n'
265
 
266
- svg += f'<line x1="390" y1="150" x2="450" y2="150" class="line"/>\n'
267
- svg += f'<use href="#fault-symbol" transform="translate(470,150)"/>\n'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
- # Current equation
270
- svg += f'<text x="400" y="220" text-anchor="middle" class="title">I_fault = Ea / Z1</text>\n'
271
 
 
272
  return svg
273
-
274
  def generate_protection_coordination_diagram(self) -> str:
275
- """Generate time-current coordination curves"""
276
- svg = self.create_svg_header(800, 600)
277
 
278
- # Title
279
- svg += f'<text x="400" y="30" text-anchor="middle" class="title">Protection Coordination Curves</text>\n'
280
 
281
- # Axes
282
- svg += f'<line x1="100" y1="500" x2="700" y2="500" class="line marker-end="url(#arrowhead)"/>\n'
283
- svg += f'<line x1="100" y1="500" x2="100" y2="100" class="line marker-end="url(#arrowhead)"/>\n'
284
 
285
- # Axis labels
286
- svg += f'<text x="400" y="530" text-anchor="middle" class="text">Current (A)</text>\n'
287
- svg += f'<text x="50" y="300" text-anchor="middle" class="text" transform="rotate(-90 50 300)">Time (s)</text>\n'
288
 
289
  # Grid lines
290
- for i in range(2, 7):
291
- x = 100 + i * 100
292
- svg += f'<line x1="{x}" y1="100" x2="{x}" y2="500" stroke="#e5e7eb" stroke-width="1"/>\n'
293
- svg += f'<text x="{x}" y="520" text-anchor="middle" class="text">{10**(i-1)}</text>\n'
294
-
295
- for i in range(1, 5):
296
- y = 500 - i * 80
297
- svg += f'<line x1="100" y1="{y}" x2="700" y2="{y}" stroke="#e5e7eb" stroke-width="1"/>\n'
298
- svg += f'<text x="85" y="{y+5}" text-anchor="end" class="text">{10**(i-1)}</text>\n'
299
 
300
- # Relay curves
301
- # Primary relay (closer to load)
302
- svg += self._draw_relay_curve(200, "Primary Relay", "#059669", "inverse")
303
-
304
- # Backup relay
305
- svg += self._draw_relay_curve(300, "Backup Relay", "#dc2626", "very_inverse")
306
 
307
- # Fuse curve
308
- svg += self._draw_relay_curve(150, "Fuse", "#7c3aed", "fuse")
 
309
 
310
- # Legend
311
- svg += f'<rect x="550" y="120" width="180" height="120" stroke="#374151" stroke-width="1" fill="white"/>\n'
312
- svg += f'<text x="640" y="140" text-anchor="middle" class="text">Legend</text>\n'
313
 
314
- svg += f'<line x1="560" y1="160" x2="590" y2="160" stroke="#7c3aed" stroke-width="3"/>\n'
315
- svg += f'<text x="600" y="165" class="text">Fuse</text>\n'
316
 
317
- svg += f'<line x1="560" y1="180" x2="590" y2="180" stroke="#059669" stroke-width="3"/>\n'
318
- svg += f'<text x="600" y="185" class="text">Primary Relay</text>\n'
 
 
 
 
319
 
320
- svg += f'<line x1="560" y1="200" x2="590" y2="200" stroke="#dc2626" stroke-width="3"/>\n'
321
- svg += f'<text x="600" y="205" class="text">Backup Relay</text>\n'
 
 
 
 
322
 
323
- svg += f'<text x="640" y="225" text-anchor="middle" class="text">Coordination Interval: 0.3s</text>\n'
 
 
 
 
 
324
 
325
- svg += self.create_svg_footer()
326
  return svg
327
-
328
- def _draw_relay_curve(self, x_offset: int, label: str, color: str, curve_type: str) -> str:
329
- """Draw a time-current curve for a relay"""
330
  svg = ""
331
 
332
- points = []
333
-
334
- if curve_type == "inverse":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  # Standard inverse curve
336
- for i in range(50):
337
- current = 10 ** (i / 10.0)
338
- time = 0.14 / ((current/100) ** 0.02 - 1)
339
- if time > 0 and time < 1000:
340
- x = 100 + (i * 12)
341
- y = 500 - (time * 40)
342
- if 100 <= x <= 700 and 100 <= y <= 500:
343
- points.append(f"{x},{y}")
344
-
345
- elif curve_type == "very_inverse":
346
- # Very inverse curve (higher up)
347
- for i in range(50):
348
- current = 10 ** (i / 10.0)
349
- time = 13.5 / ((current/100) ** 1.0 - 1)
350
- if time > 0 and time < 1000:
351
- x = 100 + (i * 12)
352
- y = 500 - (time * 20)
353
- if 100 <= x <= 700 and 100 <= y <= 500:
354
- points.append(f"{x},{y}")
355
-
356
- elif curve_type == "fuse":
357
- # Fuse curve (faster, lower)
358
- for i in range(40):
359
- current = 10 ** (i / 8.0)
360
- time = 0.01 / ((current/50) ** 2.0 - 1) if current > 50 else 1000
361
- if time > 0 and time < 1000:
362
- x = 100 + (i * 15)
363
- y = 500 - (time * 60)
364
- if 100 <= x <= 700 and 100 <= y <= 500:
365
- points.append(f"{x},{y}")
366
-
367
- if points:
368
- path = f'<polyline points="{" ".join(points)}" stroke="{color}" stroke-width="3" fill="none"/>\n'
369
- svg += path
370
-
371
- # Label
372
- if len(points) > 10:
373
- mid_point = points[len(points)//2].split(',')
374
- x, y = int(mid_point[0]), int(mid_point[1])
375
- svg += f'<text x="{x+10}" y="{y-5}" class="text" fill="{color}">{label}</text>\n'
376
 
377
  return svg
378
-
379
- def generate_phasor_diagram(self, fault_type: str = "balanced") -> str:
380
- """Generate phasor diagrams for different fault conditions"""
381
- svg = self.create_svg_header(600, 400)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
383
- # Title
384
- svg += f'<text x="300" y="30" text-anchor="middle" class="title">Phasor Diagram - {fault_type.title()} Conditions</text>\n'
 
 
 
 
385
 
386
- center_x, center_y = 300, 200
387
- radius = 80
 
388
 
389
- if fault_type == "balanced":
390
- # Three balanced phasors 120° apart
391
  angles = [0, 120, 240]
392
- colors = ["#dc2626", "#059669", "#2563eb"]
393
  labels = ["Va", "Vb", "Vc"]
394
 
395
- for i, (angle, color, label) in enumerate(zip(angles, colors, labels)):
396
- x_end = center_x + radius * cos(radians(angle))
397
- y_end = center_y - radius * sin(radians(angle))
398
 
399
- svg += f'<line x1="{center_x}" y1="{center_y}" x2="{x_end}" y2="{y_end}" '
400
- svg += f'stroke="{color}" stroke-width="3" marker-end="url(#arrowhead)"/>\n'
401
 
402
- # Label
403
- label_x = center_x + (radius + 20) * cos(radians(angle))
404
- label_y = center_y - (radius + 20) * sin(radians(angle))
405
- svg += f'<text x="{label_x}" y="{label_y}" text-anchor="middle" class="text" fill="{color}">{label}</text>\n'
406
-
407
- elif fault_type == "unbalanced":
408
- # Unbalanced phasors showing fault condition
409
- # Phase A (affected by fault) - reduced magnitude
410
- svg += f'<line x1="{center_x}" y1="{center_y}" x2="{center_x + 40}" y2="{center_y}" '
411
- svg += f'stroke="#dc2626" stroke-width="3" marker-end="url(#arrowhead)"/>\n'
412
- svg += f'<text x="{center_x + 60}" y="{center_y}" class="text" fill="#dc2626">Va (faulted)</text>\n'
 
413
 
414
- # Phase B - normal
415
- x_b = center_x + radius * cos(radians(120))
416
- y_b = center_y - radius * sin(radians(120))
417
- svg += f'<line x1="{center_x}" y1="{center_y}" x2="{x_b}" y2="{y_b}" '
418
- svg += f'stroke="#059669" stroke-width="3" marker-end="url(#arrowhead)"/>\n'
419
- svg += f'<text x="{x_b-20}" y="{y_b-10}" class="text" fill="#059669">Vb</text>\n'
420
 
421
- # Phase C - normal
422
- x_c = center_x + radius * cos(radians(240))
423
- y_c = center_y - radius * sin(radians(240))
424
- svg += f'<line x1="{center_x}" y1="{center_y}" x2="{x_c}" y2="{y_c}" '
425
- svg += f'stroke="#2563eb" stroke-width="3" marker-end="url(#arrowhead)"/>\n'
426
- svg += f'<text x="{x_c-20}" y="{y_c+20}" class="text" fill="#2563eb">Vc</text>\n'
 
 
 
 
427
 
428
  # Center point
429
- svg += f'<circle cx="{center_x}" cy="{center_y}" r="3" fill="#374151"/>\n'
430
 
431
- # Reference circle
432
- svg += f'<circle cx="{center_x}" cy="{center_y}" r="{radius}" stroke="#e5e7eb" stroke-width="1" fill="none" stroke-dasharray="5,5"/>\n'
433
 
434
  svg += self.create_svg_footer()
435
  return svg
436
-
437
- def generate_impedance_diagram(self) -> str:
438
- """Generate impedance diagram for distance protection"""
439
- svg = self.create_svg_header(600, 500)
440
 
441
  # Title
442
- svg += f'<text x="300" y="30" text-anchor="middle" class="title">Distance Protection - R-X Diagram</text>\n'
 
 
 
 
 
 
 
 
443
 
444
- center_x, center_y = 300, 250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
 
446
  # Axes
447
- svg += f'<line x1="100" y1="{center_y}" x2="500" y2="{center_y}" class="line" marker-end="url(#arrowhead)"/>\n'
448
- svg += f'<line x1="{center_x}" y1="400" x2="{center_x}" y2="100" class="line" marker-end="url(#arrowhead)"/>\n'
449
 
450
  # Axis labels
451
- svg += f'<text x="520" y="{center_y+5}" class="text">R (Ω)</text>\n'
452
- svg += f'<text x="{center_x-10}" y="90" class="text">X (Ω)</text>\n'
453
-
454
- # Mho circle (Zone 1)
455
- svg += f'<circle cx="{center_x+50}" cy="{center_y}" r="80" stroke="#059669" stroke-width="2" fill="none"/>\n'
456
- svg += f'<text x="{center_x+90}" y="{center_y-90}" class="text" fill="#059669">Zone 1 (Mho)</text>\n'
457
-
458
- # Zone 2 (larger circle)
459
- svg += f'<circle cx="{center_x+70}" cy="{center_y}" r="120" stroke="#dc2626" stroke-width="2" fill="none" stroke-dasharray="5,5"/>\n'
460
- svg += f'<text x="{center_x+140}" y="{center_y-130}" class="text" fill="#dc2626">Zone 2</text>\n'
461
-
462
- # Load impedance area
463
- svg += f'<path d="M{center_x+20},{center_y-20} Q{center_x+80},{center_y-40} {center_x+120},{center_y-10}" '
464
- svg += f'stroke="#7c3aed" stroke-width="2" fill="none"/>\n'
465
- svg += f'<text x="{center_x+70}" y="{center_y-50}" class="text" fill="#7c3aed">Load Region</text>\n'
466
-
467
- # Grid marks
468
- for i in range(1, 5):
469
- x = center_x + i * 40
470
- svg += f'<line x1="{x}" y1="{center_y-5}" x2="{x}" y2="{center_y+5}" stroke="#374151" stroke-width="1"/>\n'
471
- svg += f'<text x="{x}" y="{center_y+20}" text-anchor="middle" class="text">{i*5}</text>\n'
472
 
473
- y = center_y - i * 40
474
- svg += f'<line x1="{center_x-5}" y1="{y}" x2="{center_x+5}" y2="{y}" stroke="#374151" stroke-width="1"/>\n'
475
- svg += f'<text x="{center_x-20}" y="{y+5}" text-anchor="middle" class="text">{i*5}</text>\n'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
  svg += self.create_svg_footer()
478
  return svg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
 
480
- # Helper functions for calculations
481
- def cos(angle_deg):
482
- import math
483
- return math.cos(math.radians(angle_deg))
484
-
485
- def sin(angle_deg):
486
- import math
487
- return math.sin(math.radians(angle_deg))
488
-
489
- def radians(angle_deg):
490
- import math
491
- return math.radians(angle_deg)
492
-
493
- # Example usage
494
  if __name__ == "__main__":
495
  generator = DiagramGenerator()
496
 
497
- # Test diagram generation
498
  diagrams = {
499
  "single_line": generator.generate_single_line_diagram({}),
500
- "fault_analysis": generator.generate_fault_analysis_diagram("line_to_ground"),
 
 
501
  "protection_coordination": generator.generate_protection_coordination_diagram(),
502
  "phasor": generator.generate_phasor_diagram("balanced"),
503
  "impedance": generator.generate_impedance_diagram()
504
  }
505
 
506
- # Save diagrams
507
  for name, svg_content in diagrams.items():
508
- with open(f"{name}_diagram.svg", "w") as f:
509
  f.write(svg_content)
510
- print(f"Generated {name} diagram")
511
 
512
- print("All diagrams generated successfully!")
 
1
  """
2
+ Enhanced Diagram Generation Utilities for Power Systems
3
+ Generates professional SVG diagrams for power system concepts
4
  """
5
 
6
  import json
7
+ import math
8
  from typing import Dict, List, Tuple, Optional
9
  from datetime import datetime
10
 
11
  class DiagramGenerator:
12
  """
13
+ Generate professional SVG diagrams for power systems concepts
14
  """
15
 
16
  def __init__(self):
17
+ self.svg_width = 1000
18
+ self.svg_height = 700
19
+ self.colors = {
20
+ 'primary': '#2563eb',
21
+ 'secondary': '#64748b',
22
+ 'success': '#059669',
23
+ 'danger': '#dc2626',
24
+ 'warning': '#d97706',
25
+ 'info': '#0891b2',
26
+ 'light': '#f8fafc',
27
+ 'dark': '#1e293b',
28
+ 'bus': '#dc2626',
29
+ 'line': '#2563eb',
30
+ 'ground': '#374151',
31
+ 'component': '#059669',
32
+ 'fault': '#dc2626',
33
+ 'protection': '#7c3aed'
34
+ }
35
 
36
  def create_svg_header(self, width: int = None, height: int = None) -> str:
37
+ """Create professional SVG header with advanced styling"""
38
  w = width or self.svg_width
39
  h = height or self.svg_height
40
 
41
  return f"""<svg width="{w}" height="{h}" viewBox="0 0 {w} {h}" xmlns="http://www.w3.org/2000/svg">
42
  <defs>
43
+ <!-- Professional gradients -->
44
+ <linearGradient id="busGradient" x1="0%" y1="0%" x2="0%" y2="100%">
45
+ <stop offset="0%" style="stop-color:#ef4444;stop-opacity:1" />
46
+ <stop offset="100%" style="stop-color:#dc2626;stop-opacity:1" />
47
+ </linearGradient>
48
+
49
+ <linearGradient id="componentGradient" x1="0%" y1="0%" x2="0%" y2="100%">
50
+ <stop offset="0%" style="stop-color:#10b981;stop-opacity:1" />
51
+ <stop offset="100%" style="stop-color:#059669;stop-opacity:1" />
52
+ </linearGradient>
53
+
54
+ <linearGradient id="lineGradient" x1="0%" y1="0%" x2="100%" y2="0%">
55
+ <stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1" />
56
+ <stop offset="100%" style="stop-color:#2563eb;stop-opacity:1" />
57
+ </linearGradient>
58
+
59
+ <!-- Professional shadows -->
60
+ <filter id="dropShadow" x="-50%" y="-50%" width="200%" height="200%">
61
+ <feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
62
+ <feOffset dx="2" dy="2" result="offset"/>
63
+ <feComponentTransfer>
64
+ <feFuncA type="linear" slope="0.3"/>
65
+ </feComponentTransfer>
66
+ <feMerge>
67
+ <feMergeNode/>
68
+ <feMergeNode in="SourceGraphic"/>
69
+ </feMerge>
70
+ </filter>
71
+
72
+ <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
73
+ <feGaussianBlur stdDeviation="4" result="coloredBlur"/>
74
+ <feMerge>
75
+ <feMergeNode in="coloredBlur"/>
76
+ <feMergeNode in="SourceGraphic"/>
77
+ </feMerge>
78
+ </filter>
79
+
80
+ <!-- Professional styles -->
81
  <style>
82
+ .title-text {{
83
+ font-family: 'Arial', sans-serif;
84
+ font-size: 24px;
85
+ font-weight: bold;
86
+ fill: {self.colors['dark']};
87
+ text-anchor: middle;
88
+ filter: url(#dropShadow);
89
+ }}
90
+ .subtitle-text {{
91
+ font-family: 'Arial', sans-serif;
92
+ font-size: 16px;
93
+ font-weight: 500;
94
+ fill: {self.colors['secondary']};
95
+ text-anchor: middle;
96
+ }}
97
+ .label-text {{
98
+ font-family: 'Arial', sans-serif;
99
+ font-size: 12px;
100
+ fill: {self.colors['dark']};
101
+ text-anchor: middle;
102
+ }}
103
+ .value-text {{
104
+ font-family: 'Courier New', monospace;
105
+ font-size: 11px;
106
+ fill: {self.colors['info']};
107
+ text-anchor: middle;
108
+ }}
109
+ .bus-line {{
110
+ stroke: url(#busGradient);
111
+ stroke-width: 6;
112
+ fill: none;
113
+ filter: url(#dropShadow);
114
+ }}
115
+ .transmission-line {{
116
+ stroke: url(#lineGradient);
117
+ stroke-width: 3;
118
+ fill: none;
119
+ }}
120
+ .component-fill {{
121
+ fill: url(#componentGradient);
122
+ stroke: {self.colors['dark']};
123
+ stroke-width: 2;
124
+ filter: url(#dropShadow);
125
+ }}
126
+ .fault-symbol {{
127
+ fill: {self.colors['danger']};
128
+ stroke: {self.colors['dark']};
129
+ stroke-width: 2;
130
+ filter: url(#glow);
131
+ }}
132
+ .protection-device {{
133
+ fill: url(#componentGradient);
134
+ stroke: {self.colors['protection']};
135
+ stroke-width: 2;
136
+ filter: url(#dropShadow);
137
+ }}
138
+ .grid-line {{
139
+ stroke: #e2e8f0;
140
+ stroke-width: 1;
141
+ stroke-dasharray: 2,2;
142
+ }}
143
+ .axis-line {{
144
+ stroke: {self.colors['dark']};
145
+ stroke-width: 2;
146
+ fill: none;
147
+ }}
148
+ .curve-line {{
149
+ stroke-width: 3;
150
+ fill: none;
151
+ filter: url(#dropShadow);
152
+ }}
153
  </style>
154
 
155
+ <!-- Arrow markers -->
156
+ <marker id="arrowhead" markerWidth="12" markerHeight="8"
157
+ refX="10" refY="4" orient="auto" markerUnits="strokeWidth">
158
+ <path d="0,0 L0,8 L12,4 z" fill="{self.colors['primary']}" />
159
  </marker>
160
 
161
+ <marker id="arrowhead-red" markerWidth="12" markerHeight="8"
162
+ refX="10" refY="4" orient="auto" markerUnits="strokeWidth">
163
+ <path d="0,0 L0,8 L12,4 z" fill="{self.colors['danger']}" />
164
+ </marker>
165
+
166
+ <!-- Professional component symbols -->
167
+ <g id="generator-symbol">
168
+ <circle cx="0" cy="0" r="20" class="component-fill"/>
169
+ <text x="0" y="6" class="label-text">G</text>
170
+ <path d="M-15,-15 L15,-15 M-15,15 L15,15" stroke="{self.colors['dark']}" stroke-width="1"/>
171
  </g>
172
 
173
+ <g id="transformer-symbol">
174
+ <circle cx="-15" cy="0" r="12" class="component-fill"/>
175
+ <circle cx="15" cy="0" r="12" class="component-fill"/>
176
+ <path d="M-15,-12 L-15,-20 M15,-12 L15,-20" stroke="{self.colors['dark']}" stroke-width="2"/>
177
+ <path d="M-15,12 L-15,20 M15,12 L15,20" stroke="{self.colors['dark']}" stroke-width="2"/>
178
  </g>
179
 
180
+ <g id="load-symbol">
181
+ <path d="M-15,-12 L15,-12 L10,12 L-10,12 Z" class="component-fill"/>
182
+ <text x="0" y="3" class="label-text" style="font-size: 10px;">LOAD</text>
183
+ </g>
184
+
185
+ <g id="breaker-symbol">
186
+ <rect x="-8" y="-12" width="16" height="24" class="protection-device"/>
187
+ <path d="M-6,-8 L6,8 M-6,8 L6,-8" stroke="{self.colors['light']}" stroke-width="2"/>
188
+ <text x="0" y="3" class="label-text" style="font-size: 8px; fill: white;">CB</text>
189
  </g>
190
 
191
  <g id="fault-symbol">
192
+ <circle cx="0" cy="0" r="12" class="fault-symbol"/>
193
+ <path d="M-8,-8 L8,8 M-8,8 L8,-8" stroke="white" stroke-width="3"/>
194
+ <text x="0" y="-20" class="label-text" style="fill: {self.colors['danger']};">FAULT</text>
195
  </g>
196
+
197
+ <g id="ct-symbol">
198
+ <circle cx="0" cy="0" r="8" fill="none" stroke="{self.colors['dark']}" stroke-width="2"/>
199
+ <text x="0" y="4" class="label-text" style="font-size: 8px;">CT</text>
200
+ </g>
201
+
202
+ <g id="relay-symbol">
203
+ <rect x="-10" y="-8" width="20" height="16" class="protection-device"/>
204
+ <text x="0" y="3" class="label-text" style="font-size: 8px; fill: white;">R</text>
205
+ </g>
206
+
207
+ <!-- Background -->
208
+ <rect width="100%" height="100%" fill="linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%)"/>
209
  </defs>
210
  """
211
 
212
  def create_svg_footer(self) -> str:
213
  """Create SVG footer"""
214
  return "</svg>"
215
+
216
+ def add_title_block(self, title: str, subtitle: str = "", x: int = 500, y: int = 50) -> str:
217
+ """Add professional title block"""
218
+ svg = f'<g id="title-block">\n'
219
+ 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'
220
+ svg += f' <text x="{x}" y="{y-5}" class="title-text">{title}</text>\n'
221
+ if subtitle:
222
+ svg += f' <text x="{x}" y="{y+15}" class="subtitle-text">{subtitle}</text>\n'
223
+ svg += f'</g>\n'
224
+ return svg
225
+
226
+ def add_legend(self, items: List[Tuple[str, str, str]], x: int = 50, y: int = 550) -> str:
227
+ """Add professional legend"""
228
+ svg = f'<g id="legend">\n'
229
+
230
+ # Legend background
231
+ legend_height = len(items) * 25 + 40
232
+ 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'
233
+ svg += f' <text x="{x+100}" y="{y+20}" class="subtitle-text">Legend</text>\n'
234
+
235
+ for i, (symbol, color, description) in enumerate(items):
236
+ y_pos = y + 35 + i * 25
237
+ svg += f' <line x1="{x+10}" y1="{y_pos}" x2="{x+40}" y2="{y_pos}" stroke="{color}" stroke-width="4"/>\n'
238
+ svg += f' <text x="{x+50}" y="{y_pos+4}" class="label-text">{description}</text>\n'
239
+
240
+ svg += f'</g>\n'
241
+ return svg
242
+
243
  def generate_single_line_diagram(self, system_config: Dict) -> str:
244
+ """Generate professional single line diagram"""
245
  svg = self.create_svg_header()
246
 
247
+ # Title block
248
+ svg += self.add_title_block("Single Line Diagram", "33kV Distribution System")
249
+
250
+ # Main bus
251
+ bus_y = 200
252
+ svg += f'<line x1="150" y1="{bus_y}" x2="850" y2="{bus_y}" class="bus-line"/>\n'
253
+ svg += f'<text x="500" y="{bus_y-15}" class="label-text">Main Bus - 33kV</text>\n'
254
+
255
+ # Generator section
256
+ gen_x = 200
257
+ svg += f'<use href="#generator-symbol" transform="translate({gen_x},{bus_y})"/>\n'
258
+ svg += f'<line x1="{gen_x-35}" y1="{bus_y}" x2="150" y2="{bus_y}" class="transmission-line"/>\n'
259
+ svg += f'<text x="{gen_x}" y="{bus_y+45}" class="label-text">Generator</text>\n'
260
+ svg += f'<text x="{gen_x}" y="{bus_y+60}" class="value-text">100 MVA, 33kV</text>\n'
261
+ svg += f'<text x="{gen_x}" y="{bus_y+75}" class="value-text">X"d = 15%</text>\n'
262
+
263
+ # Circuit breaker 1
264
+ cb1_x = 280
265
+ svg += f'<use href="#breaker-symbol" transform="translate({cb1_x},{bus_y})"/>\n'
266
+ svg += f'<text x="{cb1_x}" y="{bus_y+45}" class="label-text">CB-1</text>\n'
267
+ svg += f'<text x="{cb1_x}" y="{bus_y+60}" class="value-text">2000A, 40kA</text>\n'
268
+
269
+ # Current transformer
270
+ ct1_x = 320
271
+ svg += f'<use href="#ct-symbol" transform="translate({ct1_x},{bus_y-30})"/>\n'
272
+ svg += f'<text x="{ct1_x}" y="{bus_y-50}" class="label-text">CT 2000/5</text>\n'
273
+ svg += f'<line x1="{ct1_x}" y1="{bus_y-22}" x2="{ct1_x}" y2="{bus_y-12}" class="transmission-line"/>\n'
274
+
275
+ # Power transformer
276
+ trafo_x = 400
277
+ svg += f'<use href="#transformer-symbol" transform="translate({trafo_x},{bus_y})"/>\n'
278
+ svg += f'<text x="{trafo_x}" y="{bus_y+45}" class="label-text">Power Transformer</text>\n'
279
+ svg += f'<text x="{trafo_x}" y="{bus_y+60}" class="value-text">50 MVA, 33/11kV</text>\n'
280
+ svg += f'<text x="{trafo_x}" y="{bus_y+75}" class="value-text">Z = 8%, Dyn11</text>\n'
281
+
282
+ # HV/LV connection lines
283
+ svg += f'<line x1="{trafo_x-30}" y1="{bus_y}" x2="{trafo_x-50}" y2="{bus_y}" class="transmission-line"/>\n'
284
+ svg += f'<line x1="{trafo_x+30}" y1="{bus_y}" x2="{trafo_x+50}" y2="{bus_y}" class="transmission-line"/>\n'
285
+
286
+ # LV bus
287
+ lv_bus_y = bus_y + 150
288
+ svg += f'<line x1="450" y1="{lv_bus_y}" x2="750" y2="{lv_bus_y}" class="bus-line"/>\n'
289
+ svg += f'<text x="600" y="{lv_bus_y-15}" class="label-text">Distribution Bus - 11kV</text>\n'
290
+ svg += f'<line x1="450" y1="{bus_y}" x2="450" y2="{lv_bus_y}" class="transmission-line"/>\n'
291
+
292
+ # Circuit breaker 2
293
+ cb2_x = 480
294
+ svg += f'<use href="#breaker-symbol" transform="translate({cb2_x},{lv_bus_y})"/>\n'
295
+ svg += f'<text x="{cb2_x}" y="{lv_bus_y+45}" class="label-text">CB-2</text>\n'
296
+ svg += f'<text x="{cb2_x}" y="{lv_bus_y+60}" class="value-text">3000A, 25kA</text>\n'
297
+
298
+ # Distribution feeders
299
+ feeder_positions = [550, 650, 750]
300
+ feeder_names = ["Industrial Feeder", "Commercial Feeder", "Residential Feeder"]
301
+ feeder_loads = ["15 MW", "8 MW", "5 MW"]
302
+
303
+ for i, (x_pos, name, load) in enumerate(zip(feeder_positions, feeder_names, feeder_loads)):
304
+ # Feeder line
305
+ feeder_end_y = lv_bus_y + 100
306
+ svg += f'<line x1="{x_pos}" y1="{lv_bus_y}" x2="{x_pos}" y2="{feeder_end_y}" class="transmission-line"/>\n'
307
+
308
+ # Load symbol
309
+ svg += f'<use href="#load-symbol" transform="translate({x_pos},{feeder_end_y+25})"/>\n'
310
+ svg += f'<text x="{x_pos}" y="{feeder_end_y+55}" class="label-text">{name}</text>\n'
311
+ svg += f'<text x="{x_pos}" y="{feeder_end_y+70}" class="value-text">{load}</text>\n'
312
+ svg += f'<text x="{x_pos}" y="{feeder_end_y+85}" class="value-text">cos φ = 0.85</text>\n'
313
+
314
+ # Protection relay
315
+ relay_y = lv_bus_y + 30
316
+ svg += f'<use href="#relay-symbol" transform="translate({x_pos+25},{relay_y})"/>\n'
317
+ svg += f'<text x="{x_pos+25}" y="{relay_y+25}" class="label-text">R{i+1}</text>\n'
318
+
319
+ # Add protection coordination indicators
320
+ svg += f'<text x="500" y="130" class="subtitle-text">Protection Coordination: 0.3s intervals</text>\n'
321
+ svg += f'<text x="500" y="145" class="label-text">Generator: Differential + Overcurrent</text>\n'
322
+ svg += f'<text x="500" y="160" class="label-text">Transformer: Differential + REF</text>\n'
323
+
324
+ # Add legend
325
+ legend_items = [
326
+ ("line", self.colors['bus'], "Main Bus (33kV)"),
327
+ ("line", self.colors['primary'], "Transmission Line"),
328
+ ("rect", self.colors['component'], "Electrical Equipment"),
329
+ ("rect", self.colors['protection'], "Protection Device")
330
+ ]
331
+ svg += self.add_legend(legend_items)
332
+
333
+ # Grid reference
334
+ svg += self.add_grid_reference()
335
 
336
  svg += self.create_svg_footer()
337
  return svg
338
+
339
  def generate_fault_analysis_diagram(self, fault_type: str = "line_to_ground") -> str:
340
+ """Generate professional fault analysis diagram with sequence networks"""
341
+ svg = self.create_svg_header(1200, 800)
342
 
343
+ # Title block
344
+ fault_title = fault_type.replace("_", "-").title() + " Fault Analysis"
345
+ svg += self.add_title_block(fault_title, "Sequence Network Analysis")
346
 
347
  if fault_type == "line_to_ground":
348
  # Positive sequence network
349
+ svg += self._draw_sequence_network(200, 150, "Positive Sequence", "Z₁", self.colors['success'])
 
350
 
351
  # Negative sequence network
352
+ svg += self._draw_sequence_network(200, 350, "Negative Sequence", "Z₂", self.colors['danger'])
 
353
 
354
  # Zero sequence network
355
+ svg += self._draw_sequence_network(200, 550, "Zero Sequence", "Z₀", self.colors['warning'])
 
 
 
 
 
 
 
 
 
 
356
 
357
+ # Connection diagram for L-G fault
358
+ svg += self._draw_lg_fault_connection(650, 250)
 
359
 
360
+ # Fault current calculation
361
+ svg += f'<g id="calculation-box">\n'
362
+ 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'
363
+ svg += f' <text x="875" y="525" class="subtitle-text">Fault Current Calculation</text>\n'
364
+ svg += f' <text x="675" y="555" class="label-text">For Line-to-Ground Fault:</text>\n'
365
+ svg += f' <text x="675" y="580" class="value-text">I₁ = I₂ = I₀ = Ea / (Z₁ + Z₂ + Z₀)</text>\n'
366
+ svg += f' <text x="675" y="605" class="value-text">If = 3 × I₁ = 3Ea / (Z₁ + Z₂ + Z₀)</text>\n'
367
+ svg += f' <text x="675" y="630" class="label-text">Where: Ea = Phase voltage, Z = Sequence impedance</text>\n'
368
+ svg += f'</g>\n'
 
 
 
 
 
 
 
 
369
 
370
  elif fault_type == "line_to_line":
371
  # L-L fault diagram
 
375
  # 3-phase fault diagram
376
  svg += self._draw_three_phase_fault_diagram()
377
 
378
+ # Add phasor diagram
379
+ svg += self._draw_fault_phasors(900, 150, fault_type)
380
+
381
  svg += self.create_svg_footer()
382
  return svg
383
+
384
+ def _draw_sequence_network(self, x: int, y: int, title: str, impedance: str, color: str) -> str:
385
+ """Draw professional sequence network"""
386
+ svg = f'<g id="sequence-network-{impedance.lower()}">\n'
387
+
388
+ # Network title
389
+ svg += f' <text x="{x+150}" y="{y-10}" class="subtitle-text">{title}</text>\n'
390
 
391
  # Voltage source
392
+ svg += f' <circle cx="{x+50}" cy="{y+50}" r="20" fill="white" stroke="{color}" stroke-width="3"/>\n'
393
+ svg += f' <text x="{x+50}" y="{y+35}" class="label-text" style="font-size: 10px;">+</text>\n'
394
+ svg += f' <text x="{x+50}" y="{y+65}" class="label-text" style="font-size: 10px;">-</text>\n'
395
+ svg += f' <text x="{x+50}" y="{y+55}" class="value-text">Ea</text>\n'
396
 
397
+ # Impedance box
398
+ svg += f' <rect x="{x+120}" y="{x+35}" width="60" height="30" fill="white" stroke="{color}" stroke-width="2" filter="url(#dropShadow)"/>\n'
399
+ svg += f' <text x="{x+150}" y="{y+55}" class="value-text">{impedance}</text>\n'
400
 
401
  # Connecting lines
402
+ svg += f' <line x1="{x+70}" y1="{y+50}" x2="{x+120}" y2="{y+50}" stroke="{color}" stroke-width="2"/>\n'
403
+ svg += f' <line x1="{x+180}" y1="{y+50}" x2="{x+250}" y2="{y+50}" stroke="{color}" stroke-width="2"/>\n'
404
+
405
+ # Ground symbol
406
+ svg += f' <line x1="{x+250}" y1="{y+50}" x2="{x+250}" y2="{y+80}" stroke="{color}" stroke-width="2"/>\n'
407
+ svg += f' <line x1="{x+235}" y1="{y+80}" x2="{x+265}" y2="{y+80}" stroke="{color}" stroke-width="3"/>\n'
408
+ svg += f' <line x1="{x+240}" y1="{y+85}" x2="{x+260}" y2="{y+85}" stroke="{color}" stroke-width="2"/>\n'
409
+ svg += f' <line x1="{x+245}" y1="{y+90}" x2="{x+255}" y2="{y+90}" stroke="{color}" stroke-width="1"/>\n'
410
 
411
+ # Current arrow
412
+ svg += f' <path d="M{x+200} {y+40} L{x+220} {y+40}" stroke="{color}" stroke-width="2" marker-end="url(#arrowhead)"/>\n'
413
+ svg += f' <text x="{x+210}" y="{y+30}" class="label-text" style="font-size: 10px;">I{impedance[-1]}</text>\n'
 
 
414
 
415
+ svg += f'</g>\n'
416
  return svg
417
+
418
+ def _draw_lg_fault_connection(self, x: int, y: int) -> str:
419
+ """Draw L-G fault connection diagram"""
420
+ svg = f'<g id="lg-fault-connection">\n'
421
 
422
+ # Connection title
423
+ svg += f' <text x="{x+150}" y="{y-20}" class="subtitle-text">Series Connection for L-G Fault</text>\n'
424
 
425
+ # Voltage source
426
+ svg += f' <circle cx="{x+50}" cy="{y+50}" r="20" fill="white" stroke="{self.colors["success"]}" stroke-width="3"/>\n'
427
+ svg += f' <text x="{x+50}" y="{y+55}" class="value-text">Ea</text>\n'
428
 
429
+ # Series impedances
430
+ impedances = [("Z₁", y+50), ("Z₂", y+100), ("Z₀", y+150)]
 
 
 
431
 
432
+ # Connecting lines and impedances
433
+ svg += f' <line x1="{x+70}" y1="{y+50}" x2="{x+120}" y2="{y+50}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
 
 
 
434
 
435
+ for i, (z_label, y_pos) in enumerate(impedances):
436
+ # Impedance box
437
+ 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'
438
+ svg += f' <text x="{x+145}" y="{y_pos+5}" class="value-text">{z_label}</text>\n'
439
+
440
+ if i < len(impedances) - 1:
441
+ 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'
442
+
443
+ # Connection to ground
444
+ svg += f' <line x1="{x+170}" y1="{y+50}" x2="{x+220}" y2="{y+50}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
445
+ svg += f' <line x1="{x+220}" y1="{y+50}" x2="{x+220}" y2="{y+150}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
446
+ svg += f' <line x1="{x+170}" y1="{y+150}" x2="{x+220}" y2="{y+150}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
447
 
448
+ # Fault point
449
+ svg += f' <use href="#fault-symbol" transform="translate({x+240},{y+100})"/>\n'
450
 
451
+ # Current flow indication
452
+ 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'
453
+ svg += f' <text x="{x+100}" y="{y+30}" class="label-text" style="font-size: 12px; fill: {self.colors["danger"]};">If</text>\n'
454
 
455
+ svg += f'</g>\n'
456
  return svg
457
+
458
+ def _draw_fault_phasors(self, x: int, y: int, fault_type: str) -> str:
459
+ """Draw fault condition phasor diagram"""
460
+ svg = f'<g id="phasor-diagram">\n'
461
 
462
+ # Phasor diagram background
463
+ 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'
464
+ svg += f' <text x="{x+50}" y="{y-25}" class="subtitle-text">Phasor Diagram</text>\n'
465
 
466
+ center_x, center_y = x+50, y+50
467
+ radius = 60
 
468
 
469
+ # Reference circle
470
+ 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'
 
471
 
472
+ if fault_type == "line_to_ground":
473
+ # Phase A (faulted) - reduced magnitude
474
+ 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'
475
+ svg += f' <text x="{center_x + 40}" y="{center_y + 5}" class="label-text" style="fill: {self.colors["danger"]};">Va</text>\n'
476
+
477
+ # Phase B - normal
478
+ x_b = center_x + radius * math.cos(math.radians(120))
479
+ y_b = center_y - radius * math.sin(math.radians(120))
480
+ 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'
481
+ svg += f' <text x="{x_b-20:.1f}" y="{y_b-5:.1f}" class="label-text" style="fill: {self.colors["success"]};">Vb</text>\n'
482
+
483
+ # Phase C - normal
484
+ x_c = center_x + radius * math.cos(math.radians(240))
485
+ y_c = center_y - radius * math.sin(math.radians(240))
486
+ 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'
487
+ svg += f' <text x="{x_c-20:.1f}" y="{y_c+15:.1f}" class="label-text" style="fill: {self.colors["info"]};">Vc</text>\n'
488
+
489
+ elif fault_type == "line_to_line":
490
+ # Phase A - normal
491
+ 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'
492
+ svg += f' <text x="{center_x + radius + 10}" y="{center_y + 5}" class="label-text" style="fill: {self.colors["success"]};">Va</text>\n'
493
+
494
+ # Phases B and C - faulted (closer together)
495
+ angle_b = 135 # Shifted due to fault
496
+ angle_c = 225 # Shifted due to fault
497
+
498
+ x_b = center_x + radius * 0.8 * math.cos(math.radians(angle_b))
499
+ y_b = center_y - radius * 0.8 * math.sin(math.radians(angle_b))
500
+ 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'
501
+ svg += f' <text x="{x_b-25:.1f}" y="{y_b-5:.1f}" class="label-text" style="fill: {self.colors["danger"]};">Vb</text>\n'
502
+
503
+ x_c = center_x + radius * 0.8 * math.cos(math.radians(angle_c))
504
+ y_c = center_y - radius * 0.8 * math.sin(math.radians(angle_c))
505
+ 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'
506
+ svg += f' <text x="{x_c-25:.1f}" y="{y_c+15:.1f}" class="label-text" style="fill: {self.colors["danger"]};">Vc</text>\n'
507
 
508
+ # Center point
509
+ svg += f' <circle cx="{center_x}" cy="{center_y}" r="2" fill="{self.colors["dark"]}"/>\n'
510
 
511
+ svg += f'</g>\n'
512
  return svg
513
+
514
  def generate_protection_coordination_diagram(self) -> str:
515
+ """Generate professional time-current coordination curves"""
516
+ svg = self.create_svg_header(1000, 700)
517
 
518
+ # Title block
519
+ svg += self.add_title_block("Protection Coordination Study", "Time-Current Characteristic Curves")
520
 
521
+ # Chart area
522
+ chart_x, chart_y = 150, 120
523
+ chart_width, chart_height = 700, 450
524
 
525
+ # Chart background
526
+ 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'
 
527
 
528
  # Grid lines
529
+ svg += self._draw_coordination_grid(chart_x, chart_y, chart_width, chart_height)
 
 
 
 
 
 
 
 
530
 
531
+ # Axes
532
+ 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'
533
+ 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'
 
 
 
534
 
535
+ # Axis labels
536
+ svg += f'<text x="{chart_x + chart_width//2}" y="{chart_y + chart_height + 40}" class="subtitle-text">Current (A)</text>\n'
537
+ 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'
538
 
539
+ # Draw coordination curves
540
+ svg += self._draw_protection_curves(chart_x, chart_y, chart_width, chart_height)
 
541
 
542
+ # Add coordination analysis
543
+ svg += self._add_coordination_analysis(chart_x + chart_width + 20, chart_y)
544
 
545
+ svg += self.create_svg_footer()
546
+ return svg
547
+
548
+ def _draw_coordination_grid(self, x: int, y: int, width: int, height: int) -> str:
549
+ """Draw logarithmic grid for coordination study"""
550
+ svg = ""
551
 
552
+ # Vertical grid lines (current)
553
+ current_values = [10, 100, 1000, 10000]
554
+ for i, current in enumerate(current_values):
555
+ x_pos = x + (i + 1) * (width // 5)
556
+ svg += f'<line x1="{x_pos}" y1="{y}" x2="{x_pos}" y2="{y + height}" class="grid-line"/>\n'
557
+ svg += f'<text x="{x_pos}" y="{y + height + 20}" class="label-text">{current}</text>\n'
558
 
559
+ # Horizontal grid lines (time)
560
+ time_values = [0.01, 0.1, 1.0, 10.0, 100.0]
561
+ for i, time_val in enumerate(time_values):
562
+ y_pos = y + height - (i + 1) * (height // 6)
563
+ svg += f'<line x1="{x}" y1="{y_pos}" x2="{x + width}" y2="{y_pos}" class="grid-line"/>\n'
564
+ svg += f'<text x="{x - 30}" y="{y_pos + 5}" class="label-text">{time_val}</text>\n'
565
 
 
566
  return svg
567
+
568
+ def _draw_protection_curves(self, x: int, y: int, width: int, height: int) -> str:
569
+ """Draw protection device curves"""
570
  svg = ""
571
 
572
+ # Fuse curve (fastest)
573
+ fuse_points = []
574
+ for i in range(50):
575
+ current_ratio = i / 10.0
576
+ current_pos = x + (current_ratio * width / 5)
577
+ # Inverse curve equation for fuse
578
+ time_val = max(0.01, 2.0 / (current_ratio + 1)**2) if current_ratio > 0 else 100
579
+ time_pos = y + height - (math.log10(max(0.01, time_val)) + 2) * height / 4
580
+ if y <= time_pos <= y + height:
581
+ fuse_points.append(f"{current_pos:.1f},{time_pos:.1f}")
582
+
583
+ if len(fuse_points) > 1:
584
+ svg += f'<polyline points="{" ".join(fuse_points)}" stroke="{self.colors["warning"]}" class="curve-line"/>\n'
585
+ svg += f'<text x="{x + width*0.3}" y="{y + height*0.2}" class="label-text" fill="{self.colors["warning"]}">Fuse 100A</text>\n'
586
+
587
+ # Primary relay curve
588
+ relay1_points = []
589
+ for i in range(50):
590
+ current_ratio = i / 8.0
591
+ current_pos = x + (current_ratio * width / 5)
592
  # Standard inverse curve
593
+ time_val = max(0.1, 0.14 / ((current_ratio/2) - 1)) if current_ratio > 2 else 100
594
+ time_pos = y + height - (math.log10(max(0.01, min(100, time_val))) + 2) * height / 4
595
+ if y <= time_pos <= y + height:
596
+ relay1_points.append(f"{current_pos:.1f},{time_pos:.1f}")
597
+
598
+ if len(relay1_points) > 1:
599
+ svg += f'<polyline points="{" ".join(relay1_points)}" stroke="{self.colors["success"]}" class="curve-line"/>\n'
600
+ svg += f'<text x="{x + width*0.5}" y="{y + height*0.4}" class="label-text" fill="{self.colors["success"]}">Primary Relay</text>\n'
601
+
602
+ # Backup relay curve (shifted up by coordination interval)
603
+ relay2_points = []
604
+ for i in range(50):
605
+ current_ratio = i / 8.0
606
+ current_pos = x + (current_ratio * width / 5)
607
+ # Very inverse curve with time shift
608
+ time_val = max(0.3, 13.5 / ((current_ratio/2) - 1)) if current_ratio > 2 else 100
609
+ time_pos = y + height - (math.log10(max(0.01, min(100, time_val))) + 2) * height / 4
610
+ if y <= time_pos <= y + height:
611
+ relay2_points.append(f"{current_pos:.1f},{time_pos:.1f}")
612
+
613
+ if len(relay2_points) > 1:
614
+ svg += f'<polyline points="{" ".join(relay2_points)}" stroke="{self.colors["danger"]}" class="curve-line"/>\n'
615
+ svg += f'<text x="{x + width*0.7}" y="{y + height*0.6}" class="label-text" fill="{self.colors["danger"]}">Backup Relay</text>\n'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
616
 
617
  return svg
618
+
619
+ def _add_coordination_analysis(self, x: int, y: int) -> str:
620
+ """Add coordination analysis text box"""
621
+ svg = f'<g id="coordination-analysis">\n'
622
+
623
+ 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'
624
+
625
+ svg += f' <text x="{x + 100}" y="{y + 25}" class="subtitle-text">Coordination Analysis</text>\n'
626
+
627
+ analysis_items = [
628
+ "✓ Fuse-Relay Coordination:",
629
+ " CTI = 0.2s minimum",
630
+ "",
631
+ "✓ Relay-Relay Coordination:",
632
+ " CTI = 0.3s standard",
633
+ "",
634
+ "✓ Settings Applied:",
635
+ " Primary: I> = 1.25 × FLA",
636
+ " Backup: I> = 1.1 × Primary",
637
+ "",
638
+ "✓ Fault Current Analysis:",
639
+ " 3φ fault: 8,500A",
640
+ " L-G fault: 6,200A",
641
+ "",
642
+ "✓ Coordination Verified:",
643
+ " All fault levels",
644
+ " Load conditions",
645
+ " Motor starting"
646
+ ]
647
+
648
+ for i, item in enumerate(analysis_items):
649
+ svg += f' <text x="{x + 10}" y="{y + 50 + i*15}" class="label-text" style="font-size: 10px;">{item}</text>\n'
650
+
651
+ svg += f'</g>\n'
652
+ return svg
653
+
654
+ def add_grid_reference(self) -> str:
655
+ """Add grid reference and scale"""
656
+ svg = f'<g id="grid-reference">\n'
657
+ svg += f' <text x="950" y="680" class="label-text">Scale: Not to scale - Schematic only</text>\n'
658
+ svg += f' <text x="50" y="680" class="label-text">Date: {datetime.now().strftime("%Y-%m-%d")}</text>\n'
659
+ svg += f' <text x="500" y="680" class="label-text">Power Systems Mini-Consultant</text>\n'
660
+ svg += f'</g>\n'
661
+ return svg
662
+
663
+ def generate_phasor_diagram(self, condition: str = "balanced") -> str:
664
+ """Generate comprehensive phasor diagram"""
665
+ svg = self.create_svg_header(800, 600)
666
 
667
+ title = f"Phasor Diagram - {condition.title()} Conditions"
668
+ svg += self.add_title_block(title, "Voltage and Current Relationships")
669
+
670
+ center_x, center_y = 400, 300
671
+ voltage_radius = 100
672
+ current_radius = 60
673
 
674
+ # Voltage phasors
675
+ svg += f'<g id="voltage-phasors">\n'
676
+ svg += f' <text x="{center_x - 150}" y="{center_y - 120}" class="subtitle-text">Voltage Phasors</text>\n'
677
 
678
+ if condition == "balanced":
679
+ # Three balanced voltage phasors
680
  angles = [0, 120, 240]
681
+ colors = [self.colors['danger'], self.colors['success'], self.colors['info']]
682
  labels = ["Va", "Vb", "Vc"]
683
 
684
+ for angle, color, label in zip(angles, colors, labels):
685
+ x_end = center_x + voltage_radius * math.cos(math.radians(angle))
686
+ y_end = center_y - voltage_radius * math.sin(math.radians(angle))
687
 
688
+ 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'
 
689
 
690
+ # Label positioning
691
+ label_x = center_x + (voltage_radius + 25) * math.cos(math.radians(angle))
692
+ label_y = center_y - (voltage_radius + 25) * math.sin(math.radians(angle))
693
+ svg += f' <text x="{label_x:.1f}" y="{label_y:.1f}" class="label-text" style="fill: {color}; font-weight: bold;">{label}</text>\n'
694
+
695
+ # Current phasors (lagging by 30 degrees for inductive load)
696
+ svg += f' <text x="{center_x + 150}" y="{center_y - 120}" class="subtitle-text">Current Phasors</text>\n'
697
+
698
+ current_angles = [-30, 90, 210] # Lagging by 30 degrees
699
+ for i, (angle, color, phase) in enumerate(zip(current_angles, colors, ["a", "b", "c"])):
700
+ x_end = center_x + current_radius * math.cos(math.radians(angle))
701
+ y_end = center_y - current_radius * math.sin(math.radians(angle))
702
 
703
+ 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'
 
 
 
 
 
704
 
705
+ # Current label
706
+ label_x = center_x + (current_radius + 20) * math.cos(math.radians(angle))
707
+ label_y = center_y - (current_radius + 20) * math.sin(math.radians(angle))
708
+ svg += f' <text x="{label_x:.1f}" y="{label_y:.1f}" class="label-text" style="fill: {color};">I{phase}</text>\n'
709
+
710
+ svg += f'</g>\n'
711
+
712
+ # Reference circles
713
+ 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'
714
+ 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'
715
 
716
  # Center point
717
+ svg += f'<circle cx="{center_x}" cy="{center_y}" r="3" fill="{self.colors["dark"]}"/>\n'
718
 
719
+ # Power triangle
720
+ svg += self._draw_power_triangle(center_x + 200, center_y + 150)
721
 
722
  svg += self.create_svg_footer()
723
  return svg
724
+
725
+ def _draw_power_triangle(self, x: int, y: int) -> str:
726
+ """Draw power triangle diagram"""
727
+ svg = f'<g id="power-triangle">\n'
728
 
729
  # Title
730
+ svg += f' <text x="{x}" y="{y - 20}" class="subtitle-text">Power Triangle</text>\n'
731
+
732
+ # Triangle
733
+ base = 80
734
+ height = 60
735
+
736
+ # Real power (P)
737
+ svg += f' <line x1="{x}" y1="{y}" x2="{x + base}" y2="{y}" stroke="{self.colors["success"]}" stroke-width="3"/>\n'
738
+ svg += f' <text x="{x + base//2}" y="{y + 15}" class="label-text" style="fill: {self.colors["success"]};">P (kW)</text>\n'
739
 
740
+ # Reactive power (Q)
741
+ svg += f' <line x1="{x + base}" y1="{y}" x2="{x + base}" y2="{y - height}" stroke="{self.colors["warning"]}" stroke-width="3"/>\n'
742
+ svg += f' <text x="{x + base + 15}" y="{y - height//2}" class="label-text" style="fill: {self.colors["warning"]};">Q (kVAr)</text>\n'
743
+
744
+ # Apparent power (S)
745
+ svg += f' <line x1="{x}" y1="{y}" x2="{x + base}" y2="{y - height}" stroke="{self.colors["primary"]}" stroke-width="3"/>\n'
746
+ 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'
747
+
748
+ # Power factor angle
749
+ 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'
750
+ svg += f' <text x="{x + 25}" y="{y - 5}" class="label-text" style="font-size: 10px;">φ</text>\n'
751
+
752
+ svg += f'</g>\n'
753
+ return svg
754
+
755
+ def generate_impedance_diagram(self) -> str:
756
+ """Generate professional R-X impedance diagram for distance protection"""
757
+ svg = self.create_svg_header(900, 700)
758
+
759
+ # Title block
760
+ svg += self.add_title_block("Distance Protection", "R-X Impedance Diagram")
761
+
762
+ # Chart area
763
+ chart_x, chart_y = 150, 120
764
+ chart_width, chart_height = 500, 400
765
+ center_x = chart_x + chart_width // 2
766
+ center_y = chart_y + chart_height // 2
767
+
768
+ # Chart background
769
+ 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'
770
 
771
  # Axes
772
+ 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'
773
+ 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'
774
 
775
  # Axis labels
776
+ svg += f'<text x="{chart_x + chart_width + 20}" y="{center_y + 5}" class="subtitle-text">R (Ω)</text>\n'
777
+ svg += f'<text x="{center_x - 10}" y="{chart_y - 10}" class="subtitle-text">X (Ω)</text>\n'
778
+
779
+ # Grid markings
780
+ for i in range(1, 6):
781
+ # R axis markings
782
+ r_pos = center_x + i * 60
783
+ if r_pos <= chart_x + chart_width:
784
+ 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'
785
+ svg += f'<text x="{r_pos}" y="{center_y + 20}" class="label-text">{i*2}</text>\n'
 
 
 
 
 
 
 
 
 
 
 
786
 
787
+ # X axis markings
788
+ x_pos = center_y - i * 60
789
+ if x_pos >= chart_y:
790
+ 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'
791
+ svg += f'<text x="{center_x - 25}" y="{x_pos + 5}" class="label-text">{i*2}</text>\n'
792
+
793
+ # Zone 1 - Mho circle (80% reach)
794
+ zone1_radius = 80
795
+ zone1_center_x = center_x + 40
796
+ 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'
797
+ 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'
798
+ svg += f'<text x="{zone1_center_x + 60}" y="{center_y - 75}" class="label-text" style="fill: {self.colors["success"]};">80% Line</text>\n'
799
+ svg += f'<text x="{zone1_center_x + 60}" y="{center_y - 60}" class="label-text" style="fill: {self.colors["success"]};">Instantaneous</text>\n'
800
+
801
+ # Zone 2 - Larger Mho circle (120% reach)
802
+ zone2_radius = 120
803
+ zone2_center_x = center_x + 60
804
+ 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'
805
+ 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'
806
+ 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'
807
+ 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'
808
+
809
+ # Zone 3 - Reverse reach
810
+ zone3_radius = 60
811
+ zone3_center_x = center_x - 30
812
+ 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'
813
+ 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'
814
+ svg += f'<text x="{zone3_center_x - 80}" y="{center_y - 55}" class="label-text" style="fill: {self.colors["danger"]};">Reverse Reach</text>\n'
815
+ 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'
816
+
817
+ # Load impedance trajectory
818
+ load_points = []
819
+ for i in range(15):
820
+ angle = i * 5 # 0 to 70 degrees (typical load range)
821
+ r_load = 8 + i * 0.5
822
+ x_load = r_load * math.tan(math.radians(angle))
823
+ x_pos = center_x + r_load * 6
824
+ y_pos = center_y - x_load * 6
825
+ if chart_x <= x_pos <= chart_x + chart_width and chart_y <= y_pos <= chart_y + chart_height:
826
+ load_points.append(f"{x_pos:.1f},{y_pos:.1f}")
827
+
828
+ if len(load_points) > 1:
829
+ svg += f'<polyline points="{" ".join(load_points)}" stroke="{self.colors["info"]}" stroke-width="3" fill="none"/>\n'
830
+ 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'
831
+
832
+ # Fault impedance points
833
+ fault_points = [
834
+ (center_x + 30, center_y - 100, "Close-in Fault"),
835
+ (center_x + 120, center_y - 80, "Remote Fault"),
836
+ (center_x + 200, center_y - 60, "External Fault")
837
+ ]
838
+
839
+ for x_fault, y_fault, label in fault_points:
840
+ if chart_x <= x_fault <= chart_x + chart_width and chart_y <= y_fault <= chart_y + chart_height:
841
+ svg += f'<circle cx="{x_fault}" cy="{y_fault}" r="4" fill="{self.colors["danger"]}" stroke="white" stroke-width="2"/>\n'
842
+ 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'
843
+
844
+ # Settings table
845
+ svg += self._add_distance_settings_table(chart_x + chart_width + 30, chart_y)
846
 
847
  svg += self.create_svg_footer()
848
  return svg
849
+
850
+ def _add_distance_settings_table(self, x: int, y: int) -> str:
851
+ """Add distance protection settings table"""
852
+ svg = f'<g id="settings-table">\n'
853
+
854
+ # Table background
855
+ table_width = 220
856
+ table_height = 350
857
+ 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'
858
+
859
+ # Table title
860
+ svg += f' <text x="{x + table_width//2}" y="{y + 25}" class="subtitle-text">Distance Protection Settings</text>\n'
861
+
862
+ # Table content
863
+ settings_data = [
864
+ ("Parameter", "Zone 1", "Zone 2", "Zone 3"),
865
+ ("Reach (Ω)", "4.8", "7.2", "3.0"),
866
+ ("Time (s)", "0.0", "0.3", "1.0"),
867
+ ("Angle (°)", "75", "75", "75"),
868
+ ("", "", "", ""),
869
+ ("Line Parameters:", "", "", ""),
870
+ ("Z1 = 0.4 + j1.2 Ω/km", "", "", ""),
871
+ ("Line Length: 12 km", "", "", ""),
872
+ ("", "", "", ""),
873
+ ("Coordination:", "", "", ""),
874
+ ("CTI = 300ms", "", "", ""),
875
+ ("Load Encroachment:", "", "", ""),
876
+ ("Avoided in all zones", "", "", ""),
877
+ ("", "", "", ""),
878
+ ("Protection Logic:", "", "", ""),
879
+ ("Zone 1: Instantaneous", "", "", ""),
880
+ ("Zone 2: Definite time", "", "", ""),
881
+ ("Zone 3: Backup only", "", "", "")
882
+ ]
883
+
884
+ row_height = 18
885
+ for i, row in enumerate(settings_data):
886
+ y_pos = y + 45 + i * row_height
887
+
888
+ if i == 0: # Header row
889
+ 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'
890
+ for j, cell in enumerate(row):
891
+ x_pos = x + 15 + j * 50
892
+ svg += f' <text x="{x_pos}" y="{y_pos}" class="label-text" style="fill: white; font-weight: bold; font-size: 10px;">{cell}</text>\n'
893
+ else:
894
+ for j, cell in enumerate(row):
895
+ x_pos = x + 15 + j * 50
896
+ font_size = "9px" if len(cell) > 15 else "10px"
897
+ color = self.colors["dark"] if j == 0 else self.colors["secondary"]
898
+ svg += f' <text x="{x_pos}" y="{y_pos}" class="label-text" style="fill: {color}; font-size: {font_size};">{cell}</text>\n'
899
+
900
+ svg += f'</g>\n'
901
+ return svg
902
+
903
+ def _draw_ll_fault_diagram(self) -> str:
904
+ """Draw line-to-line fault analysis diagram"""
905
+ svg = ""
906
+
907
+ # Connection diagram for L-L fault (parallel Z1 and Z2)
908
+ x, y = 300, 200
909
+
910
+ svg += f'<text x="{x+150}" y="{y-20}" class="subtitle-text">L-L Fault: Z1 || Z2</text>\n'
911
+
912
+ # Voltage source
913
+ svg += f'<circle cx="{x+50}" cy="{y+75}" r="20" fill="white" stroke="{self.colors["success"]}" stroke-width="3"/>\n'
914
+ svg += f'<text x="{x+50}" y="{y+80}" class="value-text">Ea</text>\n'
915
+
916
+ # Parallel branches
917
+ # Upper branch (Z1)
918
+ svg += f'<line x1="{x+70}" y1="{y+50}" x2="{x+150}" y2="{y+50}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
919
+ 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'
920
+ svg += f'<text x="{x+175}" y="{y+55}" class="value-text">Z₁</text>\n'
921
+ svg += f'<line x1="{x+200}" y1="{y+50}" x2="{x+250}" y2="{y+50}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
922
+
923
+ # Lower branch (Z2)
924
+ svg += f'<line x1="{x+70}" y1="{y+100}" x2="{x+150}" y2="{y+100}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
925
+ 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'
926
+ svg += f'<text x="{x+175}" y="{y+105}" class="value-text">Z₂</text>\n'
927
+ svg += f'<line x1="{x+200}" y1="{y+100}" x2="{x+250}" y2="{y+100}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
928
+
929
+ # Connect voltage source to parallel branches
930
+ svg += f'<line x1="{x+50}" y1="{y+55}" x2="{x+50}" y2="{y+50}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
931
+ svg += f'<line x1="{x+50}" y1="{y+50}" x2="{x+70}" y2="{y+50}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
932
+ svg += f'<line x1="{x+50}" y1="{y+95}" x2="{x+50}" y2="{y+100}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
933
+ svg += f'<line x1="{x+50}" y1="{y+100}" x2="{x+70}" y2="{y+100}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
934
+
935
+ # Connect parallel branches
936
+ svg += f'<line x1="{x+250}" y1="{y+50}" x2="{x+250}" y2="{y+100}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
937
+
938
+ # Fault point
939
+ svg += f'<use href="#fault-symbol" transform="translate({x+270},{y+75})"/>\n'
940
+
941
+ # Current arrows
942
+ 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'
943
+ svg += f'<text x="{x+135}" y="{y+30}" class="label-text" style="font-size: 10px; fill: {self.colors["danger"]};">I₁</text>\n'
944
+
945
+ 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'
946
+ svg += f'<text x="{x+135}" y="{y+115}" class="label-text" style="font-size: 10px; fill: {self.colors["danger"]};">I₂</text>\n'
947
+
948
+ # Calculation box
949
+ svg += f'<g id="ll-calculation">\n'
950
+ 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'
951
+ svg += f' <text x="{x+200}" y="{y+170}" class="subtitle-text">L-L Fault Current</text>\n'
952
+ svg += f' <text x="{x+65}" y="{y+195}" class="value-text">If = √3 × Ea / (Z₁ + Z₂)</text>\n'
953
+ svg += f' <text x="{x+65}" y="{y+215}" class="label-text">Z₀ does not appear in L-L fault</text>\n'
954
+ svg += f'</g>\n'
955
+
956
+ return svg
957
+
958
+ def _draw_three_phase_fault_diagram(self) -> str:
959
+ """Draw three-phase fault analysis diagram"""
960
+ svg = ""
961
+
962
+ # Simple circuit for 3-phase fault
963
+ x, y = 300, 200
964
+
965
+ svg += f'<text x="{x+150}" y="{y-20}" class="subtitle-text">3φ Fault: Positive Sequence Only</text>\n'
966
+
967
+ # Voltage source
968
+ svg += f'<circle cx="{x+50}" cy="{y+75}" r="20" fill="white" stroke="{self.colors["success"]}" stroke-width="3"/>\n'
969
+ svg += f'<text x="{x+50}" y="{y+80}" class="value-text">Ea</text>\n'
970
+
971
+ # Single impedance (Z1 only)
972
+ svg += f'<line x1="{x+70}" y1="{y+75}" x2="{x+150}" y2="{y+75}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
973
+ 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'
974
+ svg += f'<text x="{x+175}" y="{y+80}" class="value-text">Z₁</text>\n'
975
+ svg += f'<line x1="{x+200}" y1="{y+75}" x2="{x+250}" y2="{y+75}" stroke="{self.colors["dark"]}" stroke-width="2"/>\n'
976
+
977
+ # Fault point
978
+ svg += f'<use href="#fault-symbol" transform="translate({x+270},{y+75})"/>\n'
979
+
980
+ # Current arrow
981
+ 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'
982
+ svg += f'<text x="{x+130}" y="{y+55}" class="label-text" style="font-size: 12px; fill: {self.colors["danger"]};">If</text>\n'
983
+
984
+ # Calculation box
985
+ svg += f'<g id="3ph-calculation">\n'
986
+ 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'
987
+ svg += f' <text x="{x+175}" y="{y+150}" class="subtitle-text">3φ Fault Current</text>\n'
988
+ svg += f' <text x="{x+65}" y="{y+175}" class="value-text">If = Ea / Z₁</text>\n'
989
+ svg += f' <text x="{x+65}" y="{y+195}" class="label-text">Highest fault current</text>\n'
990
+ svg += f' <text x="{x+65}" y="{y+215}" class="label-text">Only positive sequence present</text>\n'
991
+ svg += f'</g>\n'
992
+
993
+ return svg
994
 
995
+ # Example usage and testing functions
 
 
 
 
 
 
 
 
 
 
 
 
 
996
  if __name__ == "__main__":
997
  generator = DiagramGenerator()
998
 
999
+ # Test all diagram types
1000
  diagrams = {
1001
  "single_line": generator.generate_single_line_diagram({}),
1002
+ "fault_lg": generator.generate_fault_analysis_diagram("line_to_ground"),
1003
+ "fault_ll": generator.generate_fault_analysis_diagram("line_to_line"),
1004
+ "fault_3ph": generator.generate_fault_analysis_diagram("three_phase"),
1005
  "protection_coordination": generator.generate_protection_coordination_diagram(),
1006
  "phasor": generator.generate_phasor_diagram("balanced"),
1007
  "impedance": generator.generate_impedance_diagram()
1008
  }
1009
 
1010
+ # Save diagrams to files for testing
1011
  for name, svg_content in diagrams.items():
1012
+ with open(f"{name}_professional.svg", "w", encoding='utf-8') as f:
1013
  f.write(svg_content)
1014
+ print(f"Generated professional {name} diagram")
1015
 
1016
+ print("All professional diagrams generated successfully!")