ashhal commited on
Commit
5b68bfb
·
verified ·
1 Parent(s): f742cf9

Update utils/diagram_generator.py

Browse files
Files changed (1) hide show
  1. utils/diagram_generator.py +368 -1
utils/diagram_generator.py CHANGED
@@ -142,4 +142,371 @@ class DiagramGenerator:
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="
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
185
+ svg += self._draw_ll_fault_diagram()
186
+
187
+ elif fault_type == "three_phase":
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!")