Pierwszy commit - konfiguracja automatyczna
This commit is contained in:
732
tester.py
Normal file
732
tester.py
Normal file
@@ -0,0 +1,732 @@
|
||||
import numpy as np
|
||||
import matplotlib
|
||||
matplotlib.use('Agg') # Non-interactive backend for saving plots
|
||||
import matplotlib.pyplot as plt
|
||||
from comtrade import Comtrade
|
||||
import math
|
||||
|
||||
|
||||
class DistanceRelay:
|
||||
"""
|
||||
Algorytm zabezpieczenia odległościowego
|
||||
Implementacja charakterystyki wielokątnej (quadrilateral)
|
||||
"""
|
||||
def __init__(self, Z_line_R=2.0, Z_line_X=8.0, line_angle=75.0):
|
||||
# Impedancja linii (obliczona z danych)
|
||||
self.Z_line_R = Z_line_R
|
||||
self.Z_line_X = Z_line_X
|
||||
self.Z_line_mag = np.sqrt(Z_line_R**2 + Z_line_X**2)
|
||||
self.line_angle = line_angle
|
||||
|
||||
# === Nastawy stref jako % impedancji linii ===
|
||||
# Strefa 1 - 80% linii (natychmiastowa)
|
||||
self.Z1_R = self.Z_line_R * 0.8
|
||||
self.Z1_X = self.Z_line_X * 0.8
|
||||
self.tZ1 = 0 # Brak opóźnienia
|
||||
|
||||
# Strefa 2 - 120% linii (koordynacja)
|
||||
self.Z2_R = self.Z_line_R * 1.2
|
||||
self.Z2_X = self.Z_line_X * 1.2
|
||||
self.tZ2 = 300 # 300ms
|
||||
|
||||
# Strefa 3 - 200% linii (rezerwowa)
|
||||
self.Z3_R = self.Z_line_R * 2.0
|
||||
self.Z3_X = self.Z_line_X * 2.0
|
||||
self.tZ3 = 600 # 600ms
|
||||
|
||||
# Kąt charakterystyki (na podstawie kąta linii)
|
||||
self.angle_r1 = line_angle
|
||||
|
||||
# Minimalny prąd i napięcie (zabezpieczenie przed szumem)
|
||||
self.I_min = 0.5 # A
|
||||
self.U_min = 1.0 # V
|
||||
|
||||
print(f"Nastawy stref (na podstawie Z linii = {self.Z_line_mag:.2f} Ohm, {line_angle:.1f} deg):")
|
||||
print(f" Strefa 1: R={self.Z1_R:.2f} Ohm, X={self.Z1_X:.2f} Ohm (natychmiast)")
|
||||
print(f" Strefa 2: R={self.Z2_R:.2f} Ohm, X={self.Z2_X:.2f} Ohm (300ms)")
|
||||
print(f" Strefa 3: R={self.Z3_R:.2f} Ohm, X={self.Z3_X:.2f} Ohm (600ms)")
|
||||
|
||||
# Stany wewnętrzne dla każdej fazy
|
||||
self.init_state()
|
||||
|
||||
def init_state(self):
|
||||
"""Inicjalizacja stanów dla każdej fazy"""
|
||||
# Timery dla każdej fazy i strefy
|
||||
self.timers = {
|
||||
'L1_Z1': 0, 'L1_Z2': 0, 'L1_Z3': 0,
|
||||
'L2_Z1': 0, 'L2_Z2': 0, 'L2_Z3': 0,
|
||||
'L3_Z1': 0, 'L3_Z2': 0, 'L3_Z3': 0,
|
||||
}
|
||||
# Flagi trip
|
||||
self.tripped = {'L1': False, 'L2': False, 'L3': False}
|
||||
|
||||
def init_relay(self):
|
||||
print("Zabezpieczenie odległościowe zainicjalizowane")
|
||||
self.init_state()
|
||||
|
||||
def in_polygon(self, R, X, reach_R, reach_X, angle_deg):
|
||||
"""
|
||||
Sprawdza czy punkt (R, X) jest wewnątrz wielokąta
|
||||
Charakterystyka czworokątna (quadrilateral)
|
||||
"""
|
||||
# Obrót punktu o -angle_deg aby wyprostować charakterystykę
|
||||
angle_rad = math.radians(angle_deg)
|
||||
cos_a = math.cos(-angle_rad)
|
||||
sin_a = math.sin(-angle_rad)
|
||||
|
||||
R_rot = R * cos_a - X * sin_a
|
||||
X_rot = R * sin_a + X * cos_a
|
||||
|
||||
# Sprawdź czy punkt jest wewnątrz prostokąta w układzie obróconym
|
||||
# R musi być dodatnie (kierunek forward)
|
||||
# X musi być w zakresie [-reach_X, reach_X]
|
||||
# R musi być mniejsze niż reach_R
|
||||
|
||||
# Dodatkowo: nachylone linie R
|
||||
R_max = reach_R
|
||||
X_max = reach_X
|
||||
R_min = 0.1 # Minimalna wartość R (strefa aktywna)
|
||||
|
||||
# Sprawdzenie podstawowych granic
|
||||
if R_rot < R_min or R_rot > R_max:
|
||||
return False
|
||||
if abs(X_rot) > X_max:
|
||||
return False
|
||||
|
||||
# Sprawdzenie linii nachylonych (opcjonalnie)
|
||||
# Górna granica X
|
||||
X_upper = X_max * (1 - (R_rot / R_max) * math.tan(math.radians(90 - angle_deg + 10)))
|
||||
# Dolna granica X
|
||||
X_lower = -X_max * (1 - (R_rot / R_max) * math.tan(math.radians(90 - angle_deg + 10)))
|
||||
|
||||
return True
|
||||
|
||||
def check_direction(self, U1_zg_re, U1_zg_im, I1_zg_re, I1_zg_im):
|
||||
"""
|
||||
Określenie kierunku na podstawie mocy
|
||||
P = Re(U * conj(I)) > 0 = forward
|
||||
"""
|
||||
power = U1_zg_re * I1_zg_re + U1_zg_im * I1_zg_im
|
||||
return power > 0 # True = forward
|
||||
|
||||
def step_relay(self, phase, u_re, u_im, i_re, i_im,
|
||||
u0_re, u0_im, i0_re, i0_im,
|
||||
u_zg_re, u_zg_im, i_zg_re, i_zg_im):
|
||||
"""
|
||||
Krok algorytmu dla jednej fazy
|
||||
phase: 'L1', 'L2' lub 'L3'
|
||||
"""
|
||||
# Oblicz moduł prądu
|
||||
i_mag = math.sqrt(i_re**2 + i_im**2)
|
||||
u_mag = math.sqrt(u_re**2 + u_im**2)
|
||||
|
||||
# Sprawdź progi minimalne
|
||||
if i_mag < self.I_min or u_mag < self.U_min:
|
||||
return 0
|
||||
|
||||
# Oblicz impedancję Z = U / I
|
||||
i_mag_sq = i_re**2 + i_im**2
|
||||
if i_mag_sq < 1e-9:
|
||||
return 0
|
||||
|
||||
z_re = (u_re * i_re + u_im * i_im) / i_mag_sq
|
||||
z_x = (u_im * i_re - u_re * i_im) / i_mag_sq
|
||||
|
||||
# Sprawdź kierunek (używamy składowej zgodnej)
|
||||
if i_zg_re is not None and i_zg_im is not None:
|
||||
forward = True # Uproszczone - zakładamy forward
|
||||
else:
|
||||
forward = True
|
||||
|
||||
# Jeśli już wyłączone, nie sprawdzaj dalej
|
||||
if self.tripped[phase]:
|
||||
return 1
|
||||
|
||||
trip = 0
|
||||
|
||||
if forward:
|
||||
# === Strefa 1 - natychmiastowa ===
|
||||
if self.in_polygon(z_re, z_x, self.Z1_R, self.Z1_X, self.angle_r1):
|
||||
self.tripped[phase] = True
|
||||
return 1
|
||||
|
||||
# === Strefa 2 - opóźniona ===
|
||||
key_Z2 = f'{phase}_Z2'
|
||||
if self.in_polygon(z_re, z_x, self.Z2_R, self.Z2_X, self.angle_r1):
|
||||
if self.timers[key_Z2] < self.tZ2:
|
||||
self.timers[key_Z2] += 1
|
||||
elif not self.tripped[phase]:
|
||||
self.tripped[phase] = True
|
||||
return 1
|
||||
else:
|
||||
self.timers[key_Z2] = 0
|
||||
|
||||
# === Strefa 3 - rezerwowa ===
|
||||
key_Z3 = f'{phase}_Z3'
|
||||
if self.in_polygon(z_re, z_x, self.Z3_R, self.Z3_X, self.angle_r1):
|
||||
if self.timers[key_Z3] < self.tZ3:
|
||||
self.timers[key_Z3] += 1
|
||||
elif not self.tripped[phase]:
|
||||
self.tripped[phase] = True
|
||||
return 1
|
||||
else:
|
||||
self.timers[key_Z3] = 0
|
||||
|
||||
return 0
|
||||
|
||||
def reset(self):
|
||||
"""Reset stanów dla nowego uruchomienia"""
|
||||
self.init_state()
|
||||
|
||||
|
||||
# Wczytanie pliku COMTRADE
|
||||
import sys
|
||||
|
||||
# Obsluga argumentow wiersza polecen
|
||||
if len(sys.argv) > 1:
|
||||
base_name = sys.argv[1] # Nazwa pliku bez rozszerzenia
|
||||
else:
|
||||
base_name = "zwarcie_testowe" # Domyslna nazwa
|
||||
|
||||
cfg_file = f"{base_name}.cfg"
|
||||
dat_file = f"{base_name}.dat"
|
||||
|
||||
print(f"Wczytywanie rejestracji: {cfg_file}")
|
||||
|
||||
rec = Comtrade()
|
||||
|
||||
# Probuj rozne kodowania dla plikow CFG
|
||||
encodings = ['utf-8', 'cp1250', 'cp1252', 'latin-1', 'iso-8859-1']
|
||||
loaded = False
|
||||
|
||||
for encoding in encodings:
|
||||
try:
|
||||
rec.load(cfg_file, dat_file, encoding=encoding)
|
||||
loaded = True
|
||||
break
|
||||
except (UnicodeDecodeError, Exception) as e:
|
||||
continue
|
||||
|
||||
if not loaded:
|
||||
print(f"BŁĄD: Nie można wczytać pliku {cfg_file}")
|
||||
sys.exit(1)
|
||||
|
||||
# Sprawdz czy mamy wystarczajaco kanalow analogowych
|
||||
num_analog = len(rec.analog)
|
||||
print(f"Liczba kanalow analogowych: {num_analog}")
|
||||
|
||||
if num_analog < 6:
|
||||
print(f"BŁĄD: Za mało kanałów analogowych (wymagane minimum 6)")
|
||||
sys.exit(1)
|
||||
|
||||
# Wyciagniecie danych (przyjmujemy indeksy kanałów analogowych)
|
||||
t = np.array(rec.time)
|
||||
Fs = rec.cfg.sample_rates[0][0] # Czestotliwosc probkowania
|
||||
f_nom = 50.0 # Czestotliwosc sieci
|
||||
N = int(Fs / f_nom) # Liczba probek na okres
|
||||
|
||||
if N < 1:
|
||||
N = 1 # Zabezpieczenie
|
||||
|
||||
print(f" Czestotliwosc probkowania: {Fs} Hz")
|
||||
print(f" Liczba probek na okres: {N}")
|
||||
|
||||
# Zabezpieczenie przed index error
|
||||
max_analog = min(6, num_analog)
|
||||
|
||||
# Pobierz tyle ile mamy
|
||||
i1_raw = np.array(rec.analog[0])
|
||||
i2_raw = np.array(rec.analog[1]) # Kanał 1: I_L2
|
||||
i3_raw = np.array(rec.analog[2]) # Kanał 2: I_L3
|
||||
u1_raw = np.array(rec.analog[3]) # Kanał 3: U_L1
|
||||
u2_raw = np.array(rec.analog[4]) # Kanał 4: U_L2
|
||||
u3_raw = np.array(rec.analog[5]) # Kanał 5: U_L3
|
||||
# (musisz dopasować indeksy analog[] do swojego pliku .cfg)
|
||||
|
||||
# === ANALIZA DANYCH - wyznaczenie impedancji przed zwarciem ===
|
||||
print(f" Czestotliwosc probkowania: {Fs} Hz")
|
||||
print(f" Liczba probek na okres: {N}")
|
||||
print(f" Liczba probek: {len(t)}")
|
||||
print(f" Czas trwania: {t[-1]:.3f} s")
|
||||
|
||||
# 3. Definicja filtru Full-Cycle DFT (FCDFT)
|
||||
def fcdft(samples):
|
||||
"""Oblicza ortogonalne (Re, Im) dla okna N probek z wykorzystaniem transformaty Fouriera"""
|
||||
if len(samples) < N: return 0.0, 0.0
|
||||
k = np.arange(N)
|
||||
cos_wave = np.cos(2 * np.pi * k / N)
|
||||
sin_wave = np.sin(2 * np.pi * k / N)
|
||||
|
||||
re = (2.0 / N) * np.sum(samples * cos_wave)
|
||||
im = -(2.0 / N) * np.sum(samples * sin_wave)
|
||||
return re, im
|
||||
|
||||
# Funkcja pomocnicza do obliczania impedancji
|
||||
def calculate_impedance_from_raw(u_raw, i_raw, idx):
|
||||
"""Oblicza impedancję dla próbki idx"""
|
||||
if idx < N:
|
||||
return 0, 0
|
||||
window_i = i_raw[idx-N:idx]
|
||||
window_u = u_raw[idx-N:idx]
|
||||
i_re, i_im = fcdft(window_i)
|
||||
u_re, u_im = fcdft(window_u)
|
||||
i_mag_sq = i_re**2 + i_im**2
|
||||
if i_mag_sq < 1e-9:
|
||||
return 0, 0
|
||||
z_re = (u_re * i_re + u_im * i_im) / i_mag_sq
|
||||
z_x = (u_im * i_re - u_re * i_im) / i_mag_sq
|
||||
return z_re, z_x
|
||||
|
||||
# Oblicz impedancję przed zwarciem (próbki 10-100)
|
||||
pre_fault_start = 10
|
||||
pre_fault_end = 100
|
||||
|
||||
# DFT dla przykładowych próbek
|
||||
def calculate_impedance_from_raw(u_raw, i_raw, idx):
|
||||
"""Oblicza impedancję dla próbki idx"""
|
||||
if idx < N:
|
||||
return 0, 0
|
||||
window_i = i_raw[idx-N:idx]
|
||||
window_u = u_raw[idx-N:idx]
|
||||
i_re, i_im = fcdft(window_i)
|
||||
u_re, u_im = fcdft(window_u)
|
||||
i_mag_sq = i_re**2 + i_im**2
|
||||
if i_mag_sq < 1e-9:
|
||||
return 0, 0
|
||||
z_re = (u_re * i_re + u_im * i_im) / i_mag_sq
|
||||
z_x = (u_im * i_re - u_re * i_im) / i_mag_sq
|
||||
return z_re, z_x
|
||||
|
||||
# Srednia impedancja przed zwarciem
|
||||
z_r_list, z_x_list = [], []
|
||||
|
||||
# Poprawka: krok = N, ale N nie moze byc 0
|
||||
step = max(N, 1)
|
||||
for idx in range(pre_fault_start, min(pre_fault_end, len(t)), step):
|
||||
z_r, z_x = calculate_impedance_from_raw(u1_raw, i1_raw, idx)
|
||||
if z_r > 0:
|
||||
z_r_list.append(z_r)
|
||||
z_x_list.append(z_x)
|
||||
|
||||
if z_r_list:
|
||||
Z_line_R = np.median(z_r_list)
|
||||
Z_line_X = np.median(z_x_list)
|
||||
Z_line_mag = np.sqrt(Z_line_R**2 + Z_line_X**2)
|
||||
print(f"\nImpedancja linii (przed zwarciem):")
|
||||
print(f" R = {Z_line_R:.2f} Ohm")
|
||||
print(f" X = {Z_line_X:.2f} Ohm")
|
||||
print(f" |Z| = {Z_line_mag:.2f} Ohm")
|
||||
|
||||
# Kat linii
|
||||
line_angle = np.degrees(np.arctan2(Z_line_X, Z_line_R))
|
||||
print(f" Kat = {line_angle:.1f} deg")
|
||||
else:
|
||||
# Wartosci domyslne jesli nie mozna obliczyc
|
||||
Z_line_R = 2.0
|
||||
Z_line_X = 8.0
|
||||
Z_line_mag = np.sqrt(Z_line_R**2 + Z_line_X**2)
|
||||
line_angle = 75.0
|
||||
print("Nie mozna wyznaczyc impedancji linii, uzywam wartosci domyslnych")
|
||||
|
||||
# Utworzenie relay z automatycznie dobranymi nastawami
|
||||
relay = DistanceRelay(Z_line_R=Z_line_R, Z_line_X=Z_line_X, line_angle=line_angle)
|
||||
|
||||
# Macierz operatora obrotu dla składowych symetrycznych
|
||||
a = complex(-0.5, np.sqrt(3)/2)
|
||||
a2 = complex(-0.5, -np.sqrt(3)/2)
|
||||
|
||||
# Symulacja "czasu rzeczywistego" próbka po próbce
|
||||
relay.reset()
|
||||
relay.init_relay()
|
||||
|
||||
trip_history_L1 = []
|
||||
trip_history_L2 = []
|
||||
trip_history_L3 = []
|
||||
|
||||
# Impedance phasors (R + jX)
|
||||
z1_r_history = []
|
||||
z1_x_history = []
|
||||
z2_r_history = []
|
||||
z2_x_history = []
|
||||
z3_r_history = []
|
||||
z3_x_history = []
|
||||
|
||||
def calculate_impedance(u_cpx, i_cpx):
|
||||
"""Oblicza impedancję Z = U/I jako liczbę zespoloną"""
|
||||
i_mag_sq = i_cpx.real**2 + i_cpx.imag**2
|
||||
if i_mag_sq < 1e-9: # Zabezpieczenie przed dzieleniem przez zero
|
||||
return 0.0, 0.0
|
||||
# Z = U / I = U * conj(I) / |I|^2
|
||||
z_cpx = u_cpx * complex(i_cpx.real, -i_cpx.imag) / i_mag_sq
|
||||
return z_cpx.real, z_cpx.imag
|
||||
|
||||
# Symulacja "czasu rzeczywistego" próbka po próbce
|
||||
for i in range(N, len(t)):
|
||||
# Pobranie okna danych (historyczne N próbek aż do obecnej chwili i)
|
||||
window_i1 = i1_raw[i-N:i]
|
||||
window_i2 = i2_raw[i-N:i]
|
||||
window_i3 = i3_raw[i-N:i]
|
||||
window_u1 = u1_raw[i-N:i]
|
||||
window_u2 = u2_raw[i-N:i]
|
||||
window_u3 = u3_raw[i-N:i]
|
||||
|
||||
# Filtracja DFT dla wszystkich faz
|
||||
i1_re, i1_im = fcdft(window_i1)
|
||||
i2_re, i2_im = fcdft(window_i2)
|
||||
i3_re, i3_im = fcdft(window_i3)
|
||||
u1_re, u1_im = fcdft(window_u1)
|
||||
u2_re, u2_im = fcdft(window_u2)
|
||||
u3_re, u3_im = fcdft(window_u3)
|
||||
|
||||
# Tworzenie liczb zespolonych dla łatwiejszej matematyki symetrycznej
|
||||
I1_cpx = complex(i1_re, i1_im)
|
||||
I2_cpx = complex(i2_re, i2_im)
|
||||
I3_cpx = complex(i3_re, i3_im)
|
||||
U1_cpx = complex(u1_re, u1_im)
|
||||
U2_cpx = complex(u2_re, u2_im)
|
||||
U3_cpx = complex(u3_re, u3_im)
|
||||
|
||||
# Obliczanie składowych symetrycznych
|
||||
I0_cpx = (I1_cpx + I2_cpx + I3_cpx) / 3.0
|
||||
I1zg_cpx = (I1_cpx + a * I2_cpx + a2 * I3_cpx) / 3.0
|
||||
I2pr_cpx = (I1_cpx + a2 * I2_cpx + a * I3_cpx) / 3.0
|
||||
U0_cpx = (U1_cpx + U2_cpx + U3_cpx) / 3.0
|
||||
U1zg_cpx = (U1_cpx + a * U2_cpx + a2 * U3_cpx) / 3.0
|
||||
U2pr_cpx = (U1_cpx + a2 * U2_cpx + a * U3_cpx) / 3.0
|
||||
|
||||
# Przekazanie danych do algorytmu zabezpieczeniowego
|
||||
trip_l1 = relay.step_relay('L1',
|
||||
U1_cpx.real, U1_cpx.imag, I1_cpx.real, I1_cpx.imag,
|
||||
I0_cpx.real, I0_cpx.imag, U0_cpx.real, U0_cpx.imag,
|
||||
U1zg_cpx.real, U1zg_cpx.imag, I1zg_cpx.real, I1zg_cpx.imag
|
||||
)
|
||||
|
||||
trip_l2 = relay.step_relay('L2',
|
||||
U2_cpx.real, U2_cpx.imag, I2_cpx.real, I2_cpx.imag,
|
||||
I0_cpx.real, I0_cpx.imag, U0_cpx.real, U0_cpx.imag,
|
||||
U1zg_cpx.real, U1zg_cpx.imag, I1zg_cpx.real, I1zg_cpx.imag
|
||||
)
|
||||
|
||||
trip_l3 = relay.step_relay('L3',
|
||||
U3_cpx.real, U3_cpx.imag, I3_cpx.real, I3_cpx.imag,
|
||||
I0_cpx.real, I0_cpx.imag, U0_cpx.real, U0_cpx.imag,
|
||||
U1zg_cpx.real, U1zg_cpx.imag, I1zg_cpx.real, I1zg_cpx.imag
|
||||
)
|
||||
|
||||
# Rejestracja wyniku
|
||||
trip_history_L1.append(trip_l1)
|
||||
trip_history_L2.append(trip_l2)
|
||||
trip_history_L3.append(trip_l3)
|
||||
|
||||
# Obliczanie impedancji Z = U / I
|
||||
z1_r, z1_x = calculate_impedance(U1_cpx, I1_cpx)
|
||||
z2_r, z2_x = calculate_impedance(U2_cpx, I2_cpx)
|
||||
z3_r, z3_x = calculate_impedance(U3_cpx, I3_cpx)
|
||||
|
||||
z1_r_history.append(z1_r)
|
||||
z1_x_history.append(z1_x)
|
||||
z2_r_history.append(z2_r)
|
||||
z2_x_history.append(z2_x)
|
||||
z3_r_history.append(z3_r)
|
||||
z3_x_history.append(z3_x)
|
||||
|
||||
# 5. Rysowanie wyników
|
||||
plt.figure(figsize=(16, 12))
|
||||
|
||||
# Prądy faz L1, L2, L3
|
||||
plt.subplot(3, 2, 1)
|
||||
plt.plot(t[N:], i1_raw[N:], label='I_L1', color='blue')
|
||||
plt.plot(t[N:], i2_raw[N:], label='I_L2', color='green')
|
||||
plt.plot(t[N:], i3_raw[N:], label='I_L3', color='orange')
|
||||
plt.title('Prady faz L1, L2, L3')
|
||||
plt.xlabel('Czas [s]')
|
||||
plt.ylabel('Prad [A]')
|
||||
plt.legend()
|
||||
plt.grid(True)
|
||||
|
||||
# Charakterystyka R-X
|
||||
plt.subplot(3, 2, 2)
|
||||
# Rysuj strefy
|
||||
angle_rad = math.radians(relay.angle_r1)
|
||||
|
||||
# Strefa 1
|
||||
r1_vals = np.linspace(0, relay.Z1_R, 50)
|
||||
x1_upper = relay.Z1_X * (1 - r1_vals / relay.Z1_R * math.tan(math.radians(15)))
|
||||
x1_lower = -relay.Z1_X * (1 - r1_vals / relay.Z1_R * math.tan(math.radians(15)))
|
||||
plt.fill_between(r1_vals, x1_lower, x1_upper, alpha=0.2, color='green', label='Strefa 1')
|
||||
|
||||
# Strefa 2
|
||||
r2_vals = np.linspace(0, relay.Z2_R, 50)
|
||||
x2_upper = relay.Z2_X * (1 - r2_vals / relay.Z2_R * math.tan(math.radians(15)))
|
||||
x2_lower = -relay.Z2_X * (1 - r2_vals / relay.Z2_R * math.tan(math.radians(15)))
|
||||
plt.fill_between(r2_vals, x2_lower, x2_upper, alpha=0.15, color='yellow', label='Strefa 2')
|
||||
|
||||
# Strefa 3
|
||||
r3_vals = np.linspace(0, relay.Z3_R, 50)
|
||||
x3_upper = relay.Z3_X * (1 - r3_vals / relay.Z3_R * math.tan(math.radians(15)))
|
||||
x3_lower = -relay.Z3_X * (1 - r3_vals / relay.Z3_R * math.tan(math.radians(15)))
|
||||
plt.fill_between(r3_vals, x3_lower, x3_upper, alpha=0.1, color='red', label='Strefa 3')
|
||||
|
||||
# Linia impedancji linii
|
||||
z_line = np.linspace(0, relay.Z_line_mag * 1.5, 50)
|
||||
x_line = z_line * math.tan(angle_rad)
|
||||
plt.plot(z_line, x_line, 'k--', linewidth=1, label='Linia Z')
|
||||
|
||||
# Trajektorie impedancji
|
||||
# Filtruj tylko próbki podczas zwarcia (użyj indeksu gdzie napięcie spada)
|
||||
fault_indices = np.where(np.array(z1_x_history) < 1.0)[0]
|
||||
if len(fault_indices) > 0:
|
||||
plt.plot(np.array(z1_r_history)[fault_indices], np.array(z1_x_history)[fault_indices],
|
||||
'b-', linewidth=2, label='Z_L1 (zwarcie)')
|
||||
plt.plot(z1_r_history, z1_x_history, 'b.', markersize=1, alpha=0.5, label='Trajektoria L1')
|
||||
plt.plot(z2_r_history, z2_x_history, 'g.', markersize=1, alpha=0.5, label='Trajektoria L2')
|
||||
plt.plot(z3_r_history, z3_x_history, 'o', color='orange', markersize=1, alpha=0.5, label='Trajektoria L3')
|
||||
|
||||
plt.xlim(-1, relay.Z3_R * 1.2)
|
||||
plt.ylim(-relay.Z3_X * 1.2, relay.Z3_X * 1.2)
|
||||
plt.xlabel('R [Ohm]')
|
||||
plt.ylabel('X [Ohm]')
|
||||
plt.title('Charakterystyka R-X zabezpieczenia odleglosciowego')
|
||||
plt.legend(loc='upper right', fontsize=8)
|
||||
plt.grid(True)
|
||||
plt.axhline(y=0, color='k', linewidth=0.5)
|
||||
plt.axvline(x=0, color='k', linewidth=0.5)
|
||||
|
||||
# Napięcia faz L1, L2, L3
|
||||
plt.subplot(3, 2, 3)
|
||||
plt.plot(t[N:], u1_raw[N:], label='U_L1', color='blue')
|
||||
plt.plot(t[N:], u2_raw[N:], label='U_L2', color='green')
|
||||
plt.plot(t[N:], u3_raw[N:], label='U_L3', color='orange')
|
||||
plt.title('Napięcia faz L1, L2, L3')
|
||||
plt.xlabel('Czas [s]')
|
||||
plt.ylabel('Napięcie [V]')
|
||||
plt.legend()
|
||||
plt.grid(True)
|
||||
|
||||
# Impedancje faz L1, L2, L3
|
||||
plt.subplot(3, 2, 4)
|
||||
plt.plot(t[N:], z1_r_history, label='R_L1', color='blue', linestyle='-')
|
||||
plt.plot(t[N:], z1_x_history, label='X_L1', color='blue', linestyle='--')
|
||||
plt.plot(t[N:], z2_r_history, label='R_L2', color='green', linestyle='-')
|
||||
plt.plot(t[N:], z2_x_history, label='X_L2', color='green', linestyle='--')
|
||||
plt.plot(t[N:], z3_r_history, label='R_L3', color='orange', linestyle='-')
|
||||
plt.plot(t[N:], z3_x_history, label='X_L3', color='orange', linestyle='--')
|
||||
plt.title('Impedancje faz (R + jX)')
|
||||
plt.xlabel('Czas [s]')
|
||||
plt.ylabel('Impedancja [Ohm]')
|
||||
plt.legend()
|
||||
plt.grid(True)
|
||||
|
||||
# Wyjscia zabezpieczenia
|
||||
plt.subplot(3, 2, 5)
|
||||
max_val = max(max(i1_raw), max(i2_raw), max(i3_raw))
|
||||
plt.plot(t[N:], i1_raw[N:], label='I_L1', color='blue', alpha=0.5)
|
||||
plt.plot(t[N:], i2_raw[N:], label='I_L2', color='green', alpha=0.5)
|
||||
plt.plot(t[N:], i3_raw[N:], label='I_L3', color='orange', alpha=0.5)
|
||||
plt.plot(t[N:], np.array(trip_history_L1) * max_val, label='Trip L1-E', color='red', linewidth=2)
|
||||
plt.plot(t[N:], np.array(trip_history_L2) * max_val, label='Trip L2-E', color='darkred', linewidth=2, linestyle='--')
|
||||
plt.plot(t[N:], np.array(trip_history_L3) * max_val, label='Trip L3-E', color='darkorange', linewidth=2, linestyle=':')
|
||||
plt.title('Wynik testu algorytmu ZDistA')
|
||||
plt.xlabel('Czas [s]')
|
||||
plt.ylabel('Wartosc')
|
||||
plt.legend()
|
||||
plt.grid(True)
|
||||
|
||||
# Modul impedancji
|
||||
plt.subplot(3, 2, 6)
|
||||
z1_mag = np.sqrt(np.array(z1_r_history)**2 + np.array(z1_x_history)**2)
|
||||
z2_mag = np.sqrt(np.array(z2_r_history)**2 + np.array(z2_x_history)**2)
|
||||
z3_mag = np.sqrt(np.array(z3_r_history)**2 + np.array(z3_x_history)**2)
|
||||
plt.plot(t[N:], z1_mag, label='|Z_L1|', color='blue')
|
||||
plt.plot(t[N:], z2_mag, label='|Z_L2|', color='green')
|
||||
plt.plot(t[N:], z3_mag, label='|Z_L3|', color='orange')
|
||||
z1_reach = np.sqrt(relay.Z1_R**2 + relay.Z1_X**2)
|
||||
z2_reach = np.sqrt(relay.Z2_R**2 + relay.Z2_X**2)
|
||||
z3_reach = np.sqrt(relay.Z3_R**2 + relay.Z3_X**2)
|
||||
plt.axhline(y=z1_reach, color='green', linestyle='--', label='Z1 reach')
|
||||
plt.axhline(y=z2_reach, color='orange', linestyle='--', label='Z2 reach')
|
||||
plt.axhline(y=z3_reach, color='red', linestyle='--', label='Z3 reach')
|
||||
plt.title('Modul impedancji |Z|')
|
||||
plt.xlabel('Czas [s]')
|
||||
plt.ylabel('|Z| [Ohm]')
|
||||
plt.legend()
|
||||
plt.grid(True)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig('test_result.png')
|
||||
print("Wynik zapisany do test_result.png")
|
||||
|
||||
# Generowanie pliku rezultat.md
|
||||
def detect_fault_and_generate_report(t, trip_history, z_r_history, z_x_history,
|
||||
i1_raw, i2_raw, i3_raw,
|
||||
u1_raw, u2_raw, u3_raw,
|
||||
relay, base_name):
|
||||
"""Wykrywa zwarcie i generuje raport"""
|
||||
|
||||
# Znajdz indeks pierwszego trip
|
||||
fault_idx = None
|
||||
for i, trip in enumerate(trip_history):
|
||||
if trip > 0:
|
||||
fault_idx = i
|
||||
break
|
||||
|
||||
# Przygotuj dane do raportu
|
||||
report = []
|
||||
report.append(f"# Wynik analizy zabezpieczenia odleglosciowego")
|
||||
report.append(f"")
|
||||
report.append(f"## Rejestracja: {base_name}")
|
||||
report.append(f"")
|
||||
report.append(f"## Parametry zabezpieczenia")
|
||||
report.append(f"- Impedancja linii: {relay.Z_line_mag:.2f} Ohm")
|
||||
report.append(f"- Kat linii: {relay.line_angle:.1f} deg")
|
||||
report.append(f"- Strefa 1: R={relay.Z1_R:.2f} Ohm, X={relay.Z1_X:.2f} Ohm (natychmiast)")
|
||||
report.append(f"- Strefa 2: R={relay.Z2_R:.2f} Ohm, X={relay.Z2_X:.2f} Ohm (300ms)")
|
||||
report.append(f"- Strefa 3: R={relay.Z3_R:.2f} Ohm, X={relay.Z3_X:.2f} Ohm (600ms)")
|
||||
report.append(f"")
|
||||
|
||||
if fault_idx is not None:
|
||||
# Zwarcie wykryte
|
||||
fault_time = t[N + fault_idx]
|
||||
report.append(f"## Zwarcie wykryte")
|
||||
report.append(f"- Czas zwarcia: {fault_time*1000:.2f} ms")
|
||||
report.append(f"")
|
||||
|
||||
# Ktora faza
|
||||
if trip_history[fault_idx] > 0:
|
||||
report.append(f"- Faza: L1 (trip)")
|
||||
report.append(f"")
|
||||
|
||||
# Wartosci podczas zwarcia
|
||||
z_r_fault = z_r_history[fault_idx]
|
||||
z_x_fault = z_x_history[fault_idx]
|
||||
z_mag_fault = np.sqrt(z_r_fault**2 + z_x_fault**2)
|
||||
|
||||
report.append(f"## Wartosci podczas zwarcia")
|
||||
report.append(f"- Impedancja Z: {z_mag_fault:.2f} Ohm")
|
||||
report.append(f"- R: {z_r_fault:.2f} Ohm")
|
||||
report.append(f"- X: {z_x_fault:.2f} Ohm")
|
||||
report.append(f"")
|
||||
|
||||
# Prad i napiecie
|
||||
i1_fault = i1_raw[N + fault_idx]
|
||||
u1_fault = u1_raw[N + fault_idx]
|
||||
report.append(f"- Prad L1: {i1_fault:.2f} A")
|
||||
report.append(f"- Napiecie L1: {u1_fault:.2f} V")
|
||||
else:
|
||||
report.append(f"## Brak zwarcia")
|
||||
report.append(f"- Nie wykryto zadzialania zabezpieczenia")
|
||||
report.append(f"")
|
||||
|
||||
# Podsumowanie
|
||||
any_trip = any(t > 0 for t in trip_history)
|
||||
report.append(f"## Podsumowanie")
|
||||
report.append(f"- Trip L1: {'TAK' if any(trip_history) else 'NIE'}")
|
||||
report.append(f"")
|
||||
|
||||
# Zapisz raport
|
||||
report_content = "\n".join(report)
|
||||
with open('rezultat.md', 'w', encoding='utf-8') as f:
|
||||
f.write(report_content)
|
||||
|
||||
print("Raport zapisany do rezultat.md")
|
||||
return fault_idx is not None
|
||||
|
||||
# Generuj raport
|
||||
has_fault = detect_fault_and_generate_report(
|
||||
t,
|
||||
[trip_history_L1[i] + trip_history_L2[i] + trip_history_L3[i] for i in range(len(trip_history_L1))],
|
||||
z1_r_history, z1_x_history,
|
||||
i1_raw, i2_raw, i3_raw,
|
||||
u1_raw, u2_raw, u3_raw,
|
||||
relay, base_name
|
||||
)
|
||||
|
||||
# Generowanie pliku rezultat.md
|
||||
def generate_result_md(t, trip_history_L1, trip_history_L2, trip_history_L3,
|
||||
z1_r_history, z1_x_history, z2_r_history, z2_x_history, z3_r_history, z3_x_history,
|
||||
Z_line_R, Z_line_X, relay):
|
||||
"""Generuje plik rezultat.md z informacjami o wykryciu zwarcia"""
|
||||
|
||||
md_content = []
|
||||
md_content.append("# Wynik analizy zabezpieczenia odleglosciowego")
|
||||
md_content.append("")
|
||||
md_content.append(f"## Parametry zabezpieczenia")
|
||||
md_content.append(f"- Impedancja linii: R={Z_line_R:.2f} Ohm, X={Z_line_X:.2f} Ohm")
|
||||
md_content.append(f"- Kat linii: {relay.angle_r1:.1f} st.")
|
||||
md_content.append(f"- Strefa 1: R={relay.Z1_R:.2f} Ohm, X={relay.Z1_X:.2f} Ohm (natychmiast)")
|
||||
md_content.append(f"- Strefa 2: R={relay.Z2_R:.2f} Ohm, X={relay.Z2_X:.2f} Ohm (300ms)")
|
||||
md_content.append(f"- Strefa 3: R={relay.Z3_R:.2f} Ohm, X={relay.Z3_X:.2f} Ohm (600ms)")
|
||||
md_content.append("")
|
||||
|
||||
# Sprawdz czy bylo zwarcie
|
||||
trip_any = any(trip_history_L1) or any(trip_history_L2) or any(trip_history_L3)
|
||||
|
||||
if trip_any:
|
||||
md_content.append("## Wykrycie zwarcia")
|
||||
md_content.append("")
|
||||
md_content.append("**Zwarcie wykryte - zadzialanie zabezpieczenia**")
|
||||
md_content.append("")
|
||||
|
||||
# Znajdz czas pierwszego zadzialania
|
||||
fault_time = None
|
||||
fault_phase = None
|
||||
|
||||
for i, (t1, t2, t3) in enumerate(zip(trip_history_L1, trip_history_L2, trip_history_L3)):
|
||||
if t1 or t2 or t3:
|
||||
fault_time = t[N + i]
|
||||
if t1:
|
||||
fault_phase = "L1-E"
|
||||
elif t2:
|
||||
fault_phase = "L2-E"
|
||||
elif t3:
|
||||
fault_phase = "L3-E"
|
||||
break
|
||||
|
||||
if fault_time is not None:
|
||||
md_content.append(f"- Czas wykrycia: {fault_time:.4f} s")
|
||||
md_content.append(f"- Faza: {fault_phase}")
|
||||
md_content.append("")
|
||||
|
||||
# Wartosci impedancji w momencie zwarcia
|
||||
idx = int((fault_time - t[N]) * Fs) if Fs > 0 else 0
|
||||
idx = max(0, min(idx, len(z1_r_history) - 1))
|
||||
|
||||
if fault_phase == "L1-E":
|
||||
z_r, z_x = z1_r_history[idx], z1_x_history[idx]
|
||||
elif fault_phase == "L2-E":
|
||||
z_r, z_x = z2_r_history[idx], z2_x_history[idx]
|
||||
else:
|
||||
z_r, z_x = z3_r_history[idx], z3_x_history[idx]
|
||||
|
||||
z_mag = math.sqrt(z_r**2 + z_x**2) if z_r or z_x else 0
|
||||
z_angle = math.degrees(math.atan2(z_x, z_r)) if z_r else 0
|
||||
|
||||
md_content.append("## Wartosci w momencie wykrycia zwarcia")
|
||||
md_content.append(f"- R = {z_r:.4f} Ohm")
|
||||
md_content.append(f"- X = {z_x:.4f} Ohm")
|
||||
md_content.append(f"- |Z| = {z_mag:.4f} Ohm")
|
||||
md_content.append(f"- Kat Z = {z_angle:.2f} st.")
|
||||
else:
|
||||
md_content.append("## Wykrycie zwarcia")
|
||||
md_content.append("")
|
||||
md_content.append("**Brak wykrycia zwarcia**")
|
||||
md_content.append("")
|
||||
md_content.append("Zabezpieczenie nie zadzialalo w okresie rejestracji.")
|
||||
|
||||
md_content.append("")
|
||||
md_content.append("---")
|
||||
md_content.append("*Wygenerowano automatycznie przez tester zabezpieczenia odleglosciowego*")
|
||||
|
||||
return "\n".join(md_content)
|
||||
|
||||
# Generuj i zapisz rezultat.md
|
||||
result_md = generate_result_md(
|
||||
t, trip_history_L1, trip_history_L2, trip_history_L3,
|
||||
z1_r_history, z1_x_history, z2_r_history, z2_x_history, z3_r_history, z3_x_history,
|
||||
Z_line_R, Z_line_X, relay
|
||||
)
|
||||
|
||||
with open('rezultat.md', 'w', encoding='utf-8') as f:
|
||||
f.write(result_md)
|
||||
|
||||
print("Zapisano rezultat.md")
|
||||
Reference in New Issue
Block a user