""" 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, }