Source code for hermess.devices.svc

# © 2024-2026 ETH Zurich
# Original author: Milos Katanic
# Simulation-only fork & maintainer: Maitraya Avadhut Desai
#
# Licensed under the GNU General Public License v3.0;
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
#     https://www.gnu.org/licenses/gpl-3.0.en.html
#
# This software is distributed "AS IS", WITHOUT WARRANTY OF ANY KIND,
# express or implied. See the License for specific language governing
# permissions and limitations under the License.
#
# Simulation-only fork of PowerDynamicEstimator
# (https://doi.org/10.5905/ethz-1007-842); dynamic state estimation removed.
# For inquiries, contact: mdesai@ethz.ch

# Created: 2026-06-10
# Static Var Compensator device model.
#
# Implements the SVC of the simplified 14-generator South East Australian
# benchmark (Gibbard & Vowles, Univ. of Adelaide, Rev 4, 2014), Appendix I.2.3
# Fig. 22: a continuously-controlled shunt susceptance regulated by an
# integrator voltage controller with current (Q/Vt) droop.
#
# The published model is written in per-unit on the SVC Mvar base (MBASE) with
# an explicit base-conversion factor Ks = MBASE/SBASE. Multiplying the loop
# through by Ks gives the equivalent system-base form implemented here:
#
#     e   = Vref − Vt − Kd·(B·Vt − Bref)          (droop on Q/Vt = B·Vt)
#     dxB/dt = KA · e                              (integrator)
#     Td · dB/dt = 2.5 · xB − B                    (thyristor lag)
#     I_inj  = jB·V  →  load-convention currents
#         g[vre] += −B·vim ,   g[vim] += +B·vre
#
# with B in pu on the system base (100 MVA); B > 0 injects reactive power
# Q = B·|V|². Bref is a finit-solved setpoint so that the droop term measures
# the *deviation* of Q/Vt from the initial operating point, exactly like the
# Δ[Q/Vt] signal of the small-signal model; Vref then equals the loadflow
# voltage at the regulated bus. B carries limits (B_min/B_max from the SVC
# Mvar range) enforced by the simulator's limiter loop when incl_lim=True.

from __future__ import annotations
from typing import TYPE_CHECKING

import numpy as np

from hermess.devices.device import DeviceRect

if TYPE_CHECKING:
    from hermess.system import Dae, Grid

sqrt = np.sqrt


[docs] class SVC(DeviceRect): r"""Static Var Compensator: voltage-regulated shunt susceptance. Parameters ---------- KA : float Integrator gain of the voltage regulator [pu B (system base) / s / pu V]. Kd : float Q/Vt droop gain [pu V / pu Q (system base)]. Td : float Thyristor-control lag time constant [s]. B_min, B_max : float Susceptance limits in pu on the system base (from the SVC Mvar range at 1.0 pu voltage). Setpoints (closed-form finit) ----------------------------- Vref : voltage reference of the regulator [pu]. Bref : initial Q/Vt operating point anchoring the droop [pu]. Initialization is sequential (closed-form): SVC buses typically also carry loads, and a joint Newton solve would attribute the whole bus current to this device. The initial reactive output ``q`` [Mvar] is a device input (the loadflow result at the SVC bus, e.g. Table 9 of the SEA benchmark), from which B₀ = (q/Sb)/V₀², xB₀ = B₀/2.5, Vref = V₀ and Bref = B₀·V₀ follow directly. Consistency with the actual power flow is verified by the standard zero-residual initialization check. """ _init_method = "sequential" def __init__(self) -> None: super().__init__() self._type = "SVC" self._name = "Static_var_compensator" self.states.extend(["xB", "B"]) self.units.extend(["pu", "pu"]) self.ns = 2 self._params.update( { "KA": 500.0, "Kd": 0.01, "Td": 0.005, "B_min": -10.0, "B_max": 10.0, "q": 0.0, # initial reactive output [Mvar] from the loadflow } ) self._setpoints.update({"Vref": 1.0, "Bref": 0.0}) self._x0.update({"xB": 0.0, "B": 0.0}) self._descr.update( { "KA": "voltage regulator integrator gain", "Kd": "Q/Vt droop gain", "Td": "thyristor control lag", "B_min": "minimum susceptance (system base)", "B_max": "maximum susceptance (system base)", "Vref": "voltage reference", "Bref": "initial Q/Vt anchoring the droop", } ) # DAE index arrays (filled by xy_index) self.xB = np.array([], dtype=int) self.B = np.array([], dtype=int) # parameters / setpoints as arrays (filled by add()) self.KA = np.array([], dtype=float) self.Kd = np.array([], dtype=float) self.Td = np.array([], dtype=float) self.B_min = np.array([], dtype=float) self.B_max = np.array([], dtype=float) self.q = np.array([], dtype=float) self.Vref = np.array([], dtype=float) self.Bref = np.array([], dtype=float) self.properties.update( { "fgcall": True, "finit": True, "init_data": True, "xy_index": True, "save_data": True, } ) self._init_data()
[docs] def _finit_sequential(self, dae: Dae) -> None: v0 = np.sqrt(dae.yinit[self.vre] ** 2 + dae.yinit[self.vim] ** 2) b0 = (self.q / dae.Sb) / v0**2 self.xinit["B"] = list(b0) self.xinit["xB"] = list(b0 / 2.5) dae.xinit[self.B] = b0 dae.xinit[self.xB] = b0 / 2.5 # Regulator equilibrium: droop deviation zero, voltage error zero. self.Vref = v0 self.Bref = b0 * v0 # Remove this device's share of the bus init current from dae.iinit: # co-located devices that calibrate themselves from the *remaining* # bus current in their own finit (e.g. StaticZIP loads sharing the # SVC bus) then see exactly the non-SVC injection. Requires SVC # entries to precede those devices in the system file (finit order # is file order). i_re0 = -b0 * dae.yinit[self.vim] i_im0 = b0 * dae.yinit[self.vre] dae.iinit[self.vre] += i_re0 dae.iinit[self.vim] += i_im0
[docs] def input_current(self, dae: Dae): """Load-convention terminal currents of the shunt susceptance.""" i_re = -dae.x[self.B] * dae.y[self.vim] i_im = dae.x[self.B] * dae.y[self.vre] return i_re, i_im
[docs] def fgcall(self, dae: Dae) -> None: v_t = sqrt(dae.y[self.vre] ** 2 + dae.y[self.vim] ** 2) b = dae.x[self.B] # Droop on the Q/Vt deviation from the initial operating point. err = self.Vref - v_t - self.Kd * (b * v_t - self.Bref) dae.f[self.xB] = self.KA * err dae.f[self.B] = (2.5 * dae.x[self.xB] - b) / self.Td i_re, i_im = self.input_current(dae) dae.g[self.vre] += i_re dae.g[self.vim] += i_im