Spaces:
Sleeping
Sleeping
File size: 4,083 Bytes
81e328b 20bc5e4 81e328b 20bc5e4 81e328b 20bc5e4 81e328b 20bc5e4 81e328b 20bc5e4 81e328b b8a96e0 81e328b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | """
SRF-PLL Discrete-Time Simulation.
Implements the Synchronous Reference Frame Phase-Locked Loop used in
grid-connected inverters. Discrete time step Δt = 1 ms.
Steps:
1. Generate true 3-phase grid voltages (50 Hz, 1.0 pu)
2. Apply attack injection on va
3. Clarke transform (αβ)
4. Park transform (dq) using estimated angle θ̂
5. PI controller to update ω̂ and θ̂
6. Compute phase error
"""
import numpy as np
import math
# Constants
V_NOM = 1.0 # Nominal voltage (pu)
F0 = 50.0 # Grid frequency (Hz)
OMEGA0 = 2.0 * math.pi * F0 # Nominal angular freq (rad/s)
DT = 1e-3 # Time step (1 ms)
KP = 50.0 # PI proportional gain
KI = 1500.0 # PI integral gain
def wrap_angle(angle: float) -> float:
"""Wrap angle to [-π, π]."""
return (angle + math.pi) % (2.0 * math.pi) - math.pi
class SRFPLLSimulator:
"""Discrete-time SRF-PLL simulator."""
def __init__(self, rng: np.random.Generator = None,
kp: float = None, ki: float = None):
self.rng = rng or np.random.default_rng()
self.kp = kp if kp is not None else KP
self.ki = ki if ki is not None else KI
self.reset()
def reset(self):
"""Reset PLL state to initial conditions."""
self.t = 0.0 # Simulation time (s)
self.theta_true = 0.0 # True grid angle (rad)
self.theta_hat = 0.0 # Estimated angle (rad)
self.omega_hat = OMEGA0 # Estimated angular freq (rad/s)
self.vq_integral = 0.0 # Integral of vq for PI controller
# Current signal values
self.vd = 0.0
self.vq = 0.0
self.va_m = 0.0
self.vb_m = 0.0
self.vc_m = 0.0
self.theta_err = 0.0
def step(self, attack_signal: float = 0.0):
"""
Advance the PLL by one time step.
Args:
attack_signal: Attack injection added to va (pu).
Returns:
dict with vd, vq, omega_hat, theta_err, va_m, vb_m, vc_m, theta_true, theta_hat
"""
# Step 1 — True three-phase grid voltages
va = V_NOM * math.sin(self.theta_true)
vb = V_NOM * math.sin(self.theta_true - 2.0 * math.pi / 3.0)
vc = V_NOM * math.sin(self.theta_true + 2.0 * math.pi / 3.0)
# Step 2 — Apply attack injection on va
va_m = va + attack_signal
vb_m = vb
vc_m = vc
# Measurement noise (σ = 0.01 pu, realistic for grid sensors)
NOISE_STD = 0.01
va_m += self.rng.normal(0, NOISE_STD)
vb_m += self.rng.normal(0, NOISE_STD)
vc_m += self.rng.normal(0, NOISE_STD)
# Step 3 — Clarke Transform (αβ)
v_alpha = va_m
v_beta = -(va_m + 2.0 * vb_m) / math.sqrt(3.0)
# Step 4 — Park Transform (dq) using estimated angle θ̂
cos_th = math.cos(self.theta_hat)
sin_th = math.sin(self.theta_hat)
vd = v_alpha * cos_th + v_beta * sin_th
vq = -v_alpha * sin_th + v_beta * cos_th
# Step 5 — PI Controller
self.vq_integral = np.clip(self.vq_integral + vq * DT, -0.3, 0.3)
omega_hat = OMEGA0 + self.kp * vq + self.ki * self.vq_integral
self.theta_hat = wrap_angle(self.theta_hat + omega_hat * DT)
# Advance true angle
self.theta_true = wrap_angle(self.theta_true + OMEGA0 * DT)
# Step 6 — Phase error wrapped to [-π, π]
theta_err = wrap_angle(self.theta_hat - self.theta_true)
# Update time
self.t += DT
# Storing current values
self.vd = vd
self.vq = vq
self.omega_hat = omega_hat
self.va_m = va_m
self.vb_m = vb_m
self.vc_m = vc_m
self.theta_err = theta_err
return {
"vd": vd,
"vq": vq,
"omega_hat": omega_hat,
"theta_err": theta_err,
"va_m": va_m,
"vb_m": vb_m,
"vc_m": vc_m,
"theta_true": self.theta_true,
"theta_hat": self.theta_hat,
}
|