Source code for pydynamicestimator.devices.inverter_voltage

# Created: 2026-06-05
# (c) Copyright 2025 ETH Zurich
#
# 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.

"""Inverter outer voltage-control strategies (the reactive / voltage side).

The voltage controller is the AVR-analogue of the converter: it turns the
reactive-power / voltage setpoints into the voltage-magnitude reference ``Vcd``
that the inner control ladder regulates the capacitor voltage to. It owns the
reactive-power measurement state ``Qc_tilde`` and the ``Qref`` / ``Vref``
setpoints. See ``docs/inverter_modernization_design.md`` ยง4.

NOTE (Phase 2 boundary, same as the angle source): the strategy owns the
``Qc_tilde`` *state* but the host writes its measurement-filter equation
``d Qc_tilde/dt = omega_f (Qc - Qc_tilde)`` because ``Qc`` comes from the shared
Park-transform loop in ``Inverter.fgcall``.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Dict, List

import casadi as ca
import numpy as np

if TYPE_CHECKING:
    from pydynamicestimator.system import Dae


[docs] class VoltageControl(ABC): """Abstract base class for inverter outer voltage-control strategies. Must expose the voltage-magnitude reference: :meth:`fgcall` returns the ``Vcd`` vector consumed by the inner control ladder (host-mediated via ``host.voltage_command``). Reads host params/states/setpoints by attribute. """
[docs] @abstractmethod def states(self) -> List[str]: ...
[docs] def algebs(self) -> List[str]: return []
[docs] def algebs_units(self) -> Dict[str, str]: return {}
[docs] def algebs_noise(self) -> Dict[str, float]: return {}
[docs] def algebs_x0(self) -> Dict[str, float]: return {}
[docs] @abstractmethod def units(self) -> List[str]: ...
[docs] @abstractmethod def params(self) -> Dict[str, float]: ...
[docs] @abstractmethod def states_noise(self) -> Dict[str, float]: ...
[docs] @abstractmethod def states_init_error(self) -> Dict[str, float]: ...
[docs] @abstractmethod def x0(self) -> Dict[str, float]: ...
[docs] @abstractmethod def descriptions(self) -> Dict[str, str]: ...
[docs] @abstractmethod def setpoints(self) -> Dict[str, float]: ...
[docs] @abstractmethod def fgcall(self, host, dae: Dae): """Publish the voltage-magnitude reference on ``host.Vcd`` (read by the inner controller via ``host.voltage_command(dae)``). The host writes the ``Qc_tilde`` measurement-filter equation (``Qc`` is host-computed).""" ...
[docs] def finit_sequential( self, host, dae: Dae, Qc: np.ndarray, Vcd: np.ndarray ) -> Dict[str, np.ndarray]: """Resolve the voltage controller's states/setpoints from the (frame-invariant) reactive power ``Qc`` and the inner controller's voltage command ``Vcd``. This is where the Q-V *gauge* lives. The base raises so a new voltage law declares its own resolution.""" raise NotImplementedError( f"{type(self).__name__} provides no sequential voltage init; implement " f"finit_sequential() or set _init_method='joint' on the device." )
[docs] class QVDroop(VoltageControl): """Reactive-power / voltage droop: ``Vcd = Vref + Kq (Qref - Qc_tilde)``. Owns the filtered reactive-power state ``Qc_tilde``, the droop gain ``Kq`` and the ``Qref`` / ``Vref`` setpoints. Byte-identical to the previously inlined voltage reference. """
[docs] def states(self) -> List[str]: return ["Qc_tilde"]
[docs] def units(self) -> List[str]: return ["p.u."]
[docs] def params(self) -> Dict[str, float]: return {"Kq": 0.1}
[docs] def states_noise(self) -> Dict[str, float]: return {"Qc_tilde": 1}
[docs] def states_init_error(self) -> Dict[str, float]: return {"Qc_tilde": 1e-1}
[docs] def x0(self) -> Dict[str, float]: return {"Qc_tilde": 0}
[docs] def setpoints(self) -> Dict[str, float]: return {"Qref": 0.01, "Vref": 1.05}
[docs] def descriptions(self) -> Dict[str, str]: return { "Kq": "Droop coefficient for Q-V", "Qref": "Reactive power set point", "Vref": "Voltage set point", "Qc_tilde": "Filtered internal reactive power", }
[docs] def fgcall(self, host, dae: Dae): # Publish the voltage-magnitude command on the host (read by the inner # controller via host.voltage_command). Vectorized; byte-identical. host.Vcd = host.Vref + host.Kq * (host.Qref - dae.x[host.Qc_tilde])
[docs] def finit_sequential( self, host, dae: Dae, Qc: np.ndarray, Vcd: np.ndarray ) -> Dict[str, np.ndarray]: # The Q-V droop Vcd = Vref + Kq(Qref - Qc_tilde) is static, so Qref/Vref are # a 1-parameter gauge; the convention Qref = Qc (the dispatched reactive # power, = Qc_tilde at steady state) zeroes the droop term, giving Vref = # Vcd. The power filter settles at Qc_tilde = Qc. return {"Qc_tilde": Qc, "Qref": Qc, "Vref": Vcd}
VOLTAGE_REGISTRY: Dict[str, type] = { "QVDroop": QVDroop, }