From 87c3f0f93bcd371c5e9756f0629adb36afe6fde1 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Tue, 14 Jul 2026 20:42:29 -0500 Subject: [PATCH 01/19] Docs and Implementation for REPCA [skip ci] --- CHANGELOG.md | 1 + .../Model/PhasorDynamics/ComponentLibrary.hpp | 1 + .../PhasorDynamics/Converter/CMakeLists.txt | 1 + .../Model/PhasorDynamics/Converter/README.md | 1 + .../Converter/REPCA/CMakeLists.txt | 54 ++ .../PhasorDynamics/Converter/REPCA/README.md | 355 +++++++ .../PhasorDynamics/Converter/REPCA/Repca.cpp | 27 + .../PhasorDynamics/Converter/REPCA/Repca.hpp | 205 +++++ .../Converter/REPCA/RepcaData.hpp | 112 +++ .../REPCA/RepcaDependencyTracking.cpp | 27 + .../Converter/REPCA/RepcaEnzyme.cpp | 109 +++ .../Converter/REPCA/RepcaImpl.hpp | 719 +++++++++++++++ GridKit/Model/PhasorDynamics/INPUT_FORMAT.md | 1 + .../Model/PhasorDynamics/SystemModelData.hpp | 3 + .../SystemModelDataJSONParser.hpp | 6 + .../Model/PhasorDynamics/SystemModelImpl.hpp | 81 ++ docs/Figures/PhasorDynamics/REPCA/diagram.png | Bin 0 -> 91838 bytes .../Model/PhasorDynamics/Converter/README.md | 1 + .../PhasorDynamics/Converter/REPCA/README.md | 6 + tests/UnitTests/PhasorDynamics/CMakeLists.txt | 10 + .../PhasorDynamics/ConverterRepcaTests.hpp | 866 ++++++++++++++++++ .../PhasorDynamics/runConverterRepcaTests.cpp | 19 + 22 files changed, 2605 insertions(+) create mode 100644 GridKit/Model/PhasorDynamics/Converter/REPCA/CMakeLists.txt create mode 100644 GridKit/Model/PhasorDynamics/Converter/REPCA/README.md create mode 100644 GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp create mode 100644 GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp create mode 100644 GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp create mode 100644 GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaDependencyTracking.cpp create mode 100644 GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp create mode 100644 GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp create mode 100644 docs/Figures/PhasorDynamics/REPCA/diagram.png create mode 100644 docs/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md create mode 100644 tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp create mode 100644 tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index ec2882196..e99716231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ - Added `REGCA` converter model implementation for PhasorDynamics. - Remove unnecessary data copying while evaluating `PowerElectronics` models, speeding up large simulations by up to 3x - Added `HYGOV` governor model implementation for PhasorDynamics. +- Added `REPCA` converter model implementation for PhasorDynamics. ## v0.1 diff --git a/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp b/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp index 51c2d78e5..4942f364a 100644 --- a/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp +++ b/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include diff --git a/GridKit/Model/PhasorDynamics/Converter/CMakeLists.txt b/GridKit/Model/PhasorDynamics/Converter/CMakeLists.txt index cafb2cc36..74372e2d6 100644 --- a/GridKit/Model/PhasorDynamics/Converter/CMakeLists.txt +++ b/GridKit/Model/PhasorDynamics/Converter/CMakeLists.txt @@ -4,3 +4,4 @@ # ]] add_subdirectory(REGCA) +add_subdirectory(REPCA) diff --git a/GridKit/Model/PhasorDynamics/Converter/README.md b/GridKit/Model/PhasorDynamics/Converter/README.md index ad38ba19a..f14960a80 100644 --- a/GridKit/Model/PhasorDynamics/Converter/README.md +++ b/GridKit/Model/PhasorDynamics/Converter/README.md @@ -12,3 +12,4 @@ The GridKit converter documentation includes: - Renewable Energy Generator/Converter Model REGCA (See [REGCA](REGCA/README.md)) - Renewable Energy Generator/Converter Model REGCB (See [REGCB](REGCB/README.md)) - Renewable Energy Electrical Control Model REECA (See [REECA](REECA/README.md)) +- Renewable Energy Plant Control Model REPCA (See [REPCA](REPCA/README.md)) diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/CMakeLists.txt b/GridKit/Model/PhasorDynamics/Converter/REPCA/CMakeLists.txt new file mode 100644 index 000000000..c315b9c8d --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/CMakeLists.txt @@ -0,0 +1,54 @@ +# [[ +# Author(s): +# - Luke Lowery +# ]] + +set(_install_headers Repca.hpp RepcaData.hpp) + +if(GRIDKIT_ENABLE_ENZYME) + gridkit_add_library( + phasor_dynamics_converter_repca + SOURCES RepcaEnzyme.cpp + HEADERS ${_install_headers} + INCLUDE_DIRECTORIES PRIVATE ${GRIDKIT_THIRD_PARTY_DIR}/magic-enum/include + LINK_LIBRARIES + PUBLIC + GridKit::phasor_dynamics_core + PUBLIC + GridKit::phasor_dynamics_signal + PRIVATE + ClangEnzymeFlags + COMPILE_OPTIONS + PRIVATE + -mllvm + -enzyme-auto-sparsity=1 + -fno-math-errno) +else() + gridkit_add_library( + phasor_dynamics_converter_repca + SOURCES Repca.cpp + HEADERS ${_install_headers} + INCLUDE_DIRECTORIES PRIVATE ${GRIDKIT_THIRD_PARTY_DIR}/magic-enum/include + LINK_LIBRARIES + PUBLIC + GridKit::phasor_dynamics_core + PUBLIC + GridKit::phasor_dynamics_signal) +endif() + +gridkit_add_library( + phasor_dynamics_converter_repca_dependency_tracking + SOURCES RepcaDependencyTracking.cpp + INCLUDE_DIRECTORIES PRIVATE ${GRIDKIT_THIRD_PARTY_DIR}/magic-enum/include + LINK_LIBRARIES + PUBLIC + GridKit::phasor_dynamics_core + PUBLIC + GridKit::phasor_dynamics_signal_dependency_tracking) + +target_link_libraries( + phasor_dynamics_components + INTERFACE GridKit::phasor_dynamics_converter_repca) +target_link_libraries( + phasor_dynamics_components_dependency_tracking + INTERFACE GridKit::phasor_dynamics_converter_repca_dependency_tracking) diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md new file mode 100644 index 000000000..2587e2ecf --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md @@ -0,0 +1,355 @@ +# **Renewable Energy Plant Control Model (REPCA)** + +REPCA is a WECC renewable energy plant control model for inverter-coupled +resources. + +## Block Diagram + +Standard REPCA block diagram. + +![](../../../../../docs/Figures/PhasorDynamics/REPCA/diagram.png) + +Figure 1: REPCA block diagram. Figure courtesy of [PowerWorld](https://www.powerworld.com/WebHelp/) + +## Model Parameters + +Symbol | Units | JSON | Description | Typical Value | Note +------------------------------------|----------|-------------|---------------------------------------------------------|---------------|------ +$S^\mathrm{base}$ | [MVA] | `mva` | REPCA component power base | 100.0 | +$s_{\mathrm{comp}}$ | [binary] | `VcompFlag` | Voltage-compensation mode flag | 1.0 | +$s_{\mathrm{ref}}$ | [binary] | `RefFlag` | Reactive-loop reference flag | 1.0 | +$s_{\mathrm{freq}}$ | [binary] | `Freqflag` | Active-power control output flag | 0.0 | +$T_{\mathrm{fltr}}$ | [sec] | `Tfltr` | Voltage and reactive-power measurement filter time constant | 0.05 | +$V^\mathrm{frz}$ | [p.u.] | `Vfrz` | Regulated-voltage threshold below which the reactive PI state freezes | 0.7 | +$R_c$ | [p.u.] | `Rc` | Line-drop compensation resistance | 0.0 | +$X_c$ | [p.u.] | `Xc` | Line-drop compensation reactance | 0.0 | +$K_c$ | [p.u.] | `Kc` | Reactive-current compensation gain | 1.0 | +$D_{\mathrm{bd1}}$ | [p.u.] | `dbdlow` | Lower reactive-loop deadband threshold | 0.0 | +$D_{\mathrm{bd2}}$ | [p.u.] | `dbdupper` | Upper reactive-loop deadband threshold | 0.0 | +$e^{\max}$ | [p.u.] | `emax` | Maximum reactive-loop error limit | 1.0 | +$e^{\min}$ | [p.u.] | `emin` | Minimum reactive-loop error limit | -1.0 | +$K_{\mathrm{p}}$ | [p.u.] | `Kp` | Reactive-power controller proportional gain | 10.0 | +$K_{\mathrm{i}}$ | [p.u./s] | `Ki` | Reactive-power controller integral gain | 10.0 | +$Q^{\max}$ | [p.u.] | `Qmax` | Maximum reactive-power command | 1.0 | +$Q^{\min}$ | [p.u.] | `Qmin` | Minimum reactive-power command | -1.0 | +$T_{\mathrm{ft}}$ | [sec] | `Tft` | Reactive-command lead time constant | 0.0 | +$T_{\mathrm{fv}}$ | [sec] | `Tfv` | Reactive-command lag time constant | 3.0 | +$T_{\mathrm{p}}$ | [sec] | `Tp` | Active-power measurement filter time constant | 0.0 | +$D_{\mathrm{bd1}}^f$ | [p.u.] | `fdbd1` | Lower frequency-error deadband threshold | 0.0 | +$D_{\mathrm{bd2}}^f$ | [p.u.] | `fdbd2` | Upper frequency-error deadband threshold | 0.0 | +$D_{\mathrm{dn}}$ | [p.u.] | `Ddn` | Down-frequency droop response gain | 20.0 | +$D_{\mathrm{up}}$ | [p.u.] | `Dup` | Up-frequency droop response gain | 0.0 | +$e_P^{\max}$ | [p.u.] | `femax` | Maximum active-power error limit | 1.0 | +$e_P^{\min}$ | [p.u.] | `femin` | Minimum active-power error limit | -1.0 | +$K_{\mathrm{pg}}$ | [p.u.] | `Kpg` | Active-power controller proportional gain | 10.0 | +$K_{\mathrm{ig}}$ | [p.u./s] | `Kig` | Active-power controller integral gain | 10.0 | +$P^{\max}$ | [p.u.] | `Pmax` | Maximum active-power command | 2.0 | +$P^{\min}$ | [p.u.] | `Pmin` | Minimum active-power command | 0.0 | +$T_{\mathrm{lag}}$ | [sec] | `Tlag` | Active-power command lag time constant | 3.0 | + +### Parameter Validation + +Invalid REPCA parameter sets are rejected by the following checks. Let $\epsilon_T=10^{-3}$. + +```math +\begin{aligned} + T &\leftarrow \max\!\left(T, \epsilon_T\right) + \quad T\in\{T_{\mathrm{fltr}},T_{\mathrm{p}},T_{\mathrm{lag}}\} \\ + S^\mathrm{base} + &> 0 \\ + s_{\mathrm{comp}}, s_{\mathrm{ref}}, s_{\mathrm{freq}} + &\in \{0,1\} \\ + T_{\mathrm{fv}} + &> 0 \\ + D_{\mathrm{bd1}} + &\le 0 \le D_{\mathrm{bd2}} \\ + e^{\min} + &\le 0 \le e^{\max} \\ + Q^{\min} + &\le Q^{\max} \\ + D_{\mathrm{bd1}}^f + &\le 0 \le D_{\mathrm{bd2}}^f \\ + e_P^{\min} + &\le 0 \le e_P^{\max} \\ + P^{\min} + &\le P^{\max} +\end{aligned} +``` + +### Model Derived Parameters + +```math +\begin{aligned} + s_{\mathrm{comp}}^\mathrm{off} &= 1 - s_{\mathrm{comp}} \\ + s_{\mathrm{ref}}^\mathrm{off} &= 1 - s_{\mathrm{ref}} \\ + k_{\mathrm{base}} &= \dfrac{S^\mathrm{sys}}{S^\mathrm{base}} +\end{aligned} +``` + +## Model Ports + +Name | Port | Init | Description +------------|--------|---------|------ +`bus` | Bus | Known | Regulated bus voltage +`ibranchr` | Input | Known | Branch current real component +`ibranchi` | Input | Known | Branch current imaginary component +`pbranch` | Input | Known | Branch active power +`qbranch` | Input | Known | Branch reactive power +`freq` | Input | Known | Frequency input +`freqref` | Input | Unknown | Frequency reference +`vref` | Input | Unknown | Voltage-control reference +`qref` | Input | Unknown | Reactive-power reference +`pplantref` | Input | Unknown | Plant active-power reference +`qext` | Output | Known | Reactive-power command output +`pext` | Output | Known | Active-power command output + +## Model Variables + +### Internal Variables + +#### Differential + +Symbol | Units | Description | Note +------------------------|--------|-------------------------------------|------ +$V^\mathrm{meas}$ | [p.u.] | Filtered regulated voltage | State 1 in Fig. 1; source label: `Vmeas` +$Q^\mathrm{meas}$ | [p.u.] | Filtered reactive-power signal | State 2 in Fig. 1; source label: `Qmeas` +$x_Q^\mathrm{PI}$ | [p.u.] | Reactive PI controller state | State 3 in Fig. 1; source label: `Reactive PI` +$x_Q^\mathrm{lag}$ | [p.u.] | Reactive-command lead-lag state | State 4 in Fig. 1; source label: `Qext` +$P^\mathrm{meas}$ | [p.u.] | Filtered active-power signal | State 5 in Fig. 1; source label: `Pmeas` +$x_P^\mathrm{PI}$ | [p.u.] | Active-power PI controller state | State 6 in Fig. 1; source label: `Power PI` +$P^\mathrm{ref}$ | [p.u.] | Active-power command lag state | State 7 in Fig. 1; source label: `Pref` + +#### Algebraic + +Symbol | Units | Description | Note +--------------------------------|--------|-------------------------------------|------ +$V$ | [p.u.] | Regulated-bus voltage magnitude | +$V^\mathrm{ldc}$ | [p.u.] | Line-drop compensated voltage magnitude | +$V^\mathrm{droop}$ | [p.u.] | Reactive-droop-compensated voltage | +$V^\mathrm{ctrl}$ | [p.u.] | Selected voltage-measurement input | +$s_{\mathrm{frz}}$ | [binary] | Reactive-PI voltage-enable indicator | +$e_{\mathrm{RQ}}$ | [p.u.] | Selected reactive-loop error | +$e_{\mathrm{RQ}}^\mathrm{db}$ | [p.u.] | Deadbanded reactive-loop error | +$e_{\mathrm{RQ}}^\mathrm{lim}$ | [p.u.] | Limited reactive-loop error | +$Q^\mathrm{PI}$ | [p.u.] | Reactive PI output | +$Q^\mathrm{ext}$ | [p.u.] | Reactive-power command output | System base +$e_f$ | [p.u.] | Frequency error after deadband | +$e_P$ | [p.u.] | Active-power control error | +$e_P^\mathrm{lim}$ | [p.u.] | Limited active-power control error | +$P^\mathrm{PI}$ | [p.u.] | Active-power PI output | +$P^\mathrm{ext}$ | [p.u.] | Active-power command output | System base + +### External Variables + +#### Differential +None. + +#### Algebraic + +Symbol | Units | Type | Description | Note +-------------------------------------|--------|--------|-----------------------------------|------ +$V_{\mathrm{r}}$ | [p.u.] | Known | Regulated-bus voltage, real component | Source label: `Vreg` +$V_{\mathrm{i}}$ | [p.u.] | Known | Regulated-bus voltage, imaginary component | Source label: `Vreg` +$I_{\mathrm{r}}$ | [p.u.] | Known | Branch current real component | Source label: `Ibranch` +$I_{\mathrm{i}}$ | [p.u.] | Known | Branch current imaginary component | Source label: `Ibranch` +$P^\mathrm{br}$ | [p.u.] | Known | Branch active power | +$Q^\mathrm{br}$ | [p.u.] | Known | Branch reactive power | +$f$ | [p.u.] | Known | Frequency input | Source label: `Freq` +$f^\mathrm{ref}$ | [p.u.] | Unknown | Frequency reference | Optional signal port `freqref`; source label: `Freq_ref` +$V^\mathrm{ref}$ | [p.u.] | Unknown | Voltage-control reference | Optional signal port `vref` +$Q^\mathrm{ref}$ | [p.u.] | Unknown | Reactive-power reference | Optional signal port `qref`; system base +$P_\mathrm{plant}^\mathrm{ref}$ | [p.u.] | Unknown | Plant active-power reference | Optional signal port `pplantref`; system base + +## Model Equations + +### Differential Equations + +```math +\begin{aligned} + 0 &= + -\dot{V}^\mathrm{meas} + + \dfrac{1}{T_{\mathrm{fltr}}} + \left(V^\mathrm{ctrl} - V^\mathrm{meas}\right) \\ + 0 &= + -\dot{Q}^\mathrm{meas} + + \dfrac{1}{T_{\mathrm{fltr}}} + \left(k_{\mathrm{base}}Q^\mathrm{br} - Q^\mathrm{meas}\right) \\ + 0 &= + -\dot{x}_Q^\mathrm{PI} + + s_{\mathrm{frz}} + \text{antiwindup}\!\left( + Q^\mathrm{PI}, + K_{\mathrm{i}}e_{\mathrm{RQ}}^\mathrm{lim}, + Q^{\min}, + Q^{\max} + \right) \\ + 0 &= + -\dot{x}_Q^\mathrm{lag} + + \dfrac{1}{T_{\mathrm{fv}}} + \left(Q^\mathrm{PI} - x_Q^\mathrm{lag}\right) \\ + 0 &= + -\dot{P}^\mathrm{meas} + + \dfrac{1}{T_{\mathrm{p}}} + \left(k_{\mathrm{base}}P^\mathrm{br} - P^\mathrm{meas}\right) \\ + 0 &= + -\dot{x}_P^\mathrm{PI} + + \text{antiwindup}\!\left( + P^\mathrm{PI}, + K_{\mathrm{ig}}e_P^\mathrm{lim}, + P^{\min}, + P^{\max} + \right) \\ + 0 &= + -\dot{P}^\mathrm{ref} + + \dfrac{1}{T_{\mathrm{lag}}} + \left(P^\mathrm{PI} - P^\mathrm{ref}\right) +\end{aligned} +``` + +CommonMath defines the [Anti-Windup](../../../../CommonMath.md#anti-windup-indicator) target and smooth approximation. + +### Algebraic Equations + +```math +\begin{aligned} + 0 &= -V^2 + + V_{\mathrm{r}}^2 + + V_{\mathrm{i}}^2 \\ + 0 &= + -\left(V^\mathrm{ldc}\right)^2 + + \left(V_{\mathrm{r}} - R_c I_{\mathrm{r}} + X_c I_{\mathrm{i}}\right)^2 + + \left(V_{\mathrm{i}} - R_c I_{\mathrm{i}} - X_c I_{\mathrm{r}}\right)^2 \\ + 0 &= -V^\mathrm{droop} + V + K_c k_{\mathrm{base}}Q^\mathrm{br} \\ + 0 &= -V^\mathrm{ctrl} + s_{\mathrm{comp}}V^\mathrm{ldc} + s_{\mathrm{comp}}^\mathrm{off}V^\mathrm{droop} \\ + 0 &= -s_{\mathrm{frz}} + \text{above}(V, V^\mathrm{frz}) \\[0.5ex] + 0 &= -e_{\mathrm{RQ}} + + s_{\mathrm{ref}}\left(V^\mathrm{ref} - V^\mathrm{meas}\right) + + s_{\mathrm{ref}}^\mathrm{off}\left(k_{\mathrm{base}}Q^\mathrm{ref} - Q^\mathrm{meas}\right) \\ + 0 &= -e_{\mathrm{RQ}}^\mathrm{db} + + \text{deadband2}(e_{\mathrm{RQ}}, D_{\mathrm{bd1}}, D_{\mathrm{bd2}}) \\ + 0 &= -e_{\mathrm{RQ}}^\mathrm{lim} + + \text{clamp}(e_{\mathrm{RQ}}^\mathrm{db}, e^{\min}, e^{\max}) \\ + 0 &= -Q^\mathrm{PI} + \text{clamp}(K_{\mathrm{p}} e_{\mathrm{RQ}}^\mathrm{lim} + x_Q^\mathrm{PI}, Q^{\min}, Q^{\max}) \\ + 0 &= -T_{\mathrm{fv}}(k_{\mathrm{base}}Q^\mathrm{ext} - x_Q^\mathrm{lag}) + + T_{\mathrm{ft}}(Q^\mathrm{PI} - x_Q^\mathrm{lag}) \\[0.5ex] + 0 &= -e_f + \text{deadband2}(f^\mathrm{ref} - f, D_{\mathrm{bd1}}^f, D_{\mathrm{bd2}}^f) \\ + 0 &= -e_P + + k_{\mathrm{base}}P_\mathrm{plant}^\mathrm{ref} + - P^\mathrm{meas} + + D_{\mathrm{dn}}\rho(e_f) + - D_{\mathrm{up}}\rho(-e_f) \\ + 0 &= -e_P^\mathrm{lim} + \text{clamp}(e_P, e_P^{\min}, e_P^{\max}) \\ + 0 &= -P^\mathrm{PI} + \text{clamp}(K_{\mathrm{pg}} e_P^\mathrm{lim} + x_P^\mathrm{PI}, P^{\min}, P^{\max}) \\ + 0 &= -k_{\mathrm{base}}P^\mathrm{ext} + s_{\mathrm{freq}}P^\mathrm{ref} +\end{aligned} +``` + +CommonMath defines [above](../../../../CommonMath.md#derived-functions), +[clamp](../../../../CommonMath.md#derived-functions), +[deadband2](../../../../CommonMath.md#derived-functions), and +[ramp](../../../../CommonMath.md#primitives) $\rho$. + +## Initialization + +### Input Initialization + +```math +\begin{aligned} + V_{\mathrm{r}}, V_{\mathrm{i}} + &\leftarrow \text{regulated-bus voltage} \\ + I_{\mathrm{r}}, I_{\mathrm{i}} + &\leftarrow \text{branch current} \\ + P^\mathrm{br}, Q^\mathrm{br} + &\leftarrow \text{branch power} \\ + f + &\leftarrow \text{frequency input} \\ + Q^\mathrm{ext} + &\leftarrow \text{reactive-power command start} \\ + P^\mathrm{ext} + &\leftarrow \text{active-power command start} +\end{aligned} +``` + +### Internal Initialization + +```math +\begin{aligned} + V_0 + &= \sqrt{ + V_{\mathrm{r},0}^2 + + V_{\mathrm{i},0}^2 + } \\ + V_0^\mathrm{ldc} + &= \sqrt{ + \left(V_{\mathrm{r},0} - R_c I_{\mathrm{r},0} + X_c I_{\mathrm{i},0}\right)^2 + + \left(V_{\mathrm{i},0} - R_c I_{\mathrm{i},0} - X_c I_{\mathrm{r},0}\right)^2 + } \\ + V_0^\mathrm{droop} + &= V_0 + + K_c k_{\mathrm{base}}Q_0^\mathrm{br} \\ + V_0^\mathrm{ctrl} + &= s_{\mathrm{comp}}V_0^\mathrm{ldc} + + s_{\mathrm{comp}}^\mathrm{off}V_0^\mathrm{droop} \\ + e_{f,0} &= 0 \\ + P_0^\mathrm{freq} + &= D_{\mathrm{dn}}\rho(e_{f,0}) + - D_{\mathrm{up}}\rho(-e_{f,0}) \\ + V_0^\mathrm{meas} &= V_0^\mathrm{ctrl} \\ + Q_0^\mathrm{meas} &= k_{\mathrm{base}}Q_0^\mathrm{br} \\ + P_0^\mathrm{meas} &= k_{\mathrm{base}}P_0^\mathrm{br} \\ + s_{\mathrm{frz},0} + &= \text{above}\left(V_0, V^\mathrm{frz}\right) \\ + e_{\mathrm{RQ},0} &= 0 \\ + e_{\mathrm{RQ},0}^\mathrm{db} + &= \text{deadband2}\left(e_{\mathrm{RQ},0}, D_{\mathrm{bd1}}, D_{\mathrm{bd2}}\right) \\ + e_{\mathrm{RQ},0}^\mathrm{lim} + &= \text{clamp}\left(e_{\mathrm{RQ},0}^\mathrm{db}, e^{\min}, e^{\max}\right) \\ + Q_0^\mathrm{PI} &= k_{\mathrm{base}}Q_0^\mathrm{ext} \\ + x_{Q,0}^\mathrm{lag} &= k_{\mathrm{base}}Q_0^\mathrm{ext} \\ + x_{Q,0}^\mathrm{PI} + &= k_{\mathrm{base}}Q_0^\mathrm{ext} - K_{\mathrm{p}}e_{\mathrm{RQ},0}^\mathrm{lim} \\ + e_{P,0} &= 0 \\ + e_{P,0}^\mathrm{lim} + &= \text{clamp}\left(e_{P,0}, e_P^{\min}, e_P^{\max}\right) \\ + P_0^\mathrm{ref} + &= + \begin{cases} + k_{\mathrm{base}}P_0^\mathrm{ext} & s_{\mathrm{freq}} = 1 \\ + \text{clamp} + \left(k_{\mathrm{base}}P_0^\mathrm{br}, P^{\min}, P^{\max}\right) + & s_{\mathrm{freq}} = 0 + \end{cases} \\ + P_0^\mathrm{PI} &= P_0^\mathrm{ref} \\ + x_{P,0}^\mathrm{PI} + &= P_0^\mathrm{ref} - K_{\mathrm{pg}}e_{P,0}^\mathrm{lim} +\end{aligned} +``` + +### Output Initialization + +```math +\begin{aligned} + f^\mathrm{ref} &\leftarrow f_0 \\ + V^\mathrm{ref} &\leftarrow V_0^\mathrm{meas} \\ + Q^\mathrm{ref} &\leftarrow \dfrac{Q_0^\mathrm{meas}}{k_{\mathrm{base}}} \\ + P_{\mathrm{plant}}^\mathrm{ref} + &\leftarrow \dfrac{P_0^\mathrm{meas} - P_0^\mathrm{freq}}{k_{\mathrm{base}}} +\end{aligned} +``` + +Initialization rejects nonzero reactive-power or active-power PI antiwindup +rates. + +REPCA writes the resolved references to attached `freqref`, `vref`, `qref`, +and `pplantref` signal inputs. If no controller is connected, those values are +used as constant reference inputs. + +## Monitorable Outputs + +Output | Units | Description | Note +----------------|--------|-------------------------------------|------ +`qext` | [p.u.] | Reactive-power command output | $Q^\mathrm{ext}$ (system base) +`pext` | [p.u.] | Active-power command output | $P^\mathrm{ext}$ (system base) +`vmeas` | [p.u.] | Filtered regulated voltage | $V^\mathrm{meas}$ +`qmeas` | [p.u.] | Filtered reactive-power signal | $Q^\mathrm{meas}$ (component base) +`pmeas` | [p.u.] | Filtered active-power signal | $P^\mathrm{meas}$ (component base) diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp new file mode 100644 index 000000000..56a711282 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp @@ -0,0 +1,27 @@ +/** + * @file Repca.cpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Template instantiations for the REPCA plant-control model. + */ + +#include "RepcaImpl.hpp" + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Converter + { + template + int Repca::evaluateJacobian() + { + Log::misc() << "Evaluate Jacobian for Repca..." << std::endl; + Log::misc() << "Jacobian evaluation is not implemented!" << std::endl; + return 0; + } + + template class Repca; + template class Repca; + } // namespace Converter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp new file mode 100644 index 000000000..6a3cdc359 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp @@ -0,0 +1,205 @@ +/** + * @file Repca.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Declaration of the REPCA phasor-dynamics plant-control model. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + template + class BusBase; + + template + class SignalNode; + + namespace Converter + { + /// Internal variables of a `Repca`. + enum class RepcaInternalVariables : size_t + { + VMEAS, ///< Filtered regulated voltage + QMEAS, ///< Filtered reactive-power signal + XQPI, ///< Reactive PI controller state + XQLAG, ///< Reactive-command lead-lag state + PMEAS, ///< Filtered active-power signal + XPPI, ///< Active-power PI controller state + PREF, ///< Active-power command lag state + V, ///< Regulated-bus voltage magnitude + VLDC, ///< Line-drop compensated voltage magnitude + VDROOP, ///< Reactive-droop-compensated voltage + VCTRL, ///< Selected voltage-measurement input + SFRZ, ///< Reactive-PI voltage-enable indicator + ERQ, ///< Selected reactive-loop error + ERQDB, ///< Deadbanded reactive-loop error + ERQLIM, ///< Limited reactive-loop error + QPI, ///< Reactive PI output + QEXT, ///< Reactive-power command output + EF, ///< Frequency error after deadband + EP, ///< Active-power control error + EPLIM, ///< Limited active-power control error + PPI, ///< Active-power PI output + PEXT, ///< Active-power command output + MAXIMUM, + }; + + /// External variables of a `Repca`. + enum class RepcaExternalVariables : size_t + { + IBRANCHR, ///< Branch current real component + IBRANCHI, ///< Branch current imaginary component + PBRANCH, ///< Branch active power + QBRANCH, ///< Branch reactive power + FREQ, ///< Frequency input + FREQREF, ///< Frequency reference + VREF, ///< Voltage-control reference + QREF, ///< Reactive-power reference + PPLANTREF, ///< Plant active-power reference + MAXIMUM, + }; + + template + class Repca : public Component + { + using Component::abs_tol_; + using Component::allocated_; + using Component::alpha_; + using Component::f_; + using Component::gridkit_component_id_; + using Component::J_cols_buffer_; + using Component::J_rows_buffer_; + using Component::J_vals_buffer_; + using Component::nnz_; + using Component::residual_indices_; + using Component::size_; + using Component::tag_; + using Component::va_system_base_; + using Component::variable_indices_; + using Component::wb_; + using Component::y_; + using Component::yp_; + + public: + using ScalarT = scalar_type; + using IdxT = index_type; + using RealT = typename Component::RealT; + using BusT = BusBase; + using SignalT = SignalNode; + using ModelDataT = RepcaData; + using MonitorT = Model::VariableMonitor; + + Repca(BusT* bus); + Repca(BusT* bus, const ModelDataT& data); + ~Repca(); + + int setGridKitComponentID(IdxT) override final; + int allocate() override final; + int verify() const override final; + int initialize() override final; + int tagDifferentiable() override final; + int setAbsoluteTolerance(RealT) override final; + int evaluateResidual() override final; + int evaluateJacobian() override final; + + auto getSignals() + -> ComponentSignals& + { + return signals_; + } + + const Model::VariableMonitorBase* getMonitor() const override; + + __attribute__((always_inline)) inline int evaluateInternalResidual( + const ScalarT*, const ScalarT*, const ScalarT*, const ScalarT*, ScalarT*); + + private: + void initializeParameters(const ModelDataT& data); + void initializeMonitor(); + void setDerivedParameters(); + + ScalarT toComponentBase(ScalarT value) const; + ScalarT toSystemBase(ScalarT value) const; + + ScalarT& Vr() + { + return bus_->Vr(); + } + + ScalarT& Vi() + { + return bus_->Vi(); + } + + static constexpr RealT TIME_CONSTANT_MINIMUM = static_cast(1.0e-3); + + BusT* bus_{nullptr}; + + RealT mva_{static_cast(100.0)}; + bool VcompFlag_{true}; + bool RefFlag_{true}; + bool Freqflag_{false}; + RealT Tfltr_{static_cast(0.05)}; + RealT Vfrz_{static_cast(0.7)}; + RealT Rc_{ZERO}; + RealT Xc_{ZERO}; + RealT Kc_{ONE}; + RealT dbdlow_{ZERO}; + RealT dbdupper_{ZERO}; + RealT emax_{ONE}; + RealT emin_{-ONE}; + RealT Kp_{static_cast(10.0)}; + RealT Ki_{static_cast(10.0)}; + RealT Qmax_{ONE}; + RealT Qmin_{-ONE}; + RealT Tft_{ZERO}; + RealT Tfv_{static_cast(3.0)}; + RealT Tp_{ZERO}; + RealT fdbd1_{ZERO}; + RealT fdbd2_{ZERO}; + RealT Ddn_{static_cast(20.0)}; + RealT Dup_{ZERO}; + RealT femax_{ONE}; + RealT femin_{-ONE}; + RealT Kpg_{static_cast(10.0)}; + RealT Kig_{static_cast(10.0)}; + RealT Pmax_{static_cast(2.0)}; + RealT Pmin_{ZERO}; + RealT Tlag_{static_cast(3.0)}; + + IdxT parameter_error_count_{0}; + RealT va_component_base_{ZERO}; + RealT vcomp_on_{ONE}; + RealT vcomp_off_{ZERO}; + RealT ref_on_{ONE}; + RealT ref_off_{ZERO}; + RealT freq_on_{ZERO}; + + ScalarT freqref_set_{ONE}; + ScalarT vref_set_{ONE}; + ScalarT qref_set_{ZERO}; + ScalarT pplantref_set_{ZERO}; + + ComponentSignals signals_; + std::unique_ptr monitor_; + + std::vector ws_; + std::vector ws_indices_; + }; + } // namespace Converter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp new file mode 100644 index 000000000..b507d88ac --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp @@ -0,0 +1,112 @@ +/** + * @file RepcaData.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Modeling data for the REPCA converter plant-control model. + */ + +#pragma once + +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Converter + { + /// Parameter keys for the REPCA plant-control model. + enum class RepcaParameters + { + mva, ///< REPCA component power base + VcompFlag, ///< Voltage-compensation mode flag + RefFlag, ///< Reactive-loop reference flag + Freqflag, ///< Active-power control output flag + Tfltr, ///< Voltage and reactive-power measurement filter time constant + Vfrz, ///< Reactive-PI freeze voltage threshold + Rc, ///< Line-drop compensation resistance + Xc, ///< Line-drop compensation reactance + Kc, ///< Reactive-current compensation gain + dbdlow, ///< Lower reactive-loop deadband threshold + dbdupper, ///< Upper reactive-loop deadband threshold + emax, ///< Maximum reactive-loop error limit + emin, ///< Minimum reactive-loop error limit + Kp, ///< Reactive-power controller proportional gain + Ki, ///< Reactive-power controller integral gain + Qmax, ///< Maximum reactive-power command + Qmin, ///< Minimum reactive-power command + Tft, ///< Reactive-command lead time constant + Tfv, ///< Reactive-command lag time constant + Tp, ///< Active-power measurement filter time constant + fdbd1, ///< Lower frequency-error deadband threshold + fdbd2, ///< Upper frequency-error deadband threshold + Ddn, ///< Down-frequency droop response gain + Dup, ///< Up-frequency droop response gain + femax, ///< Maximum active-power error limit + femin, ///< Minimum active-power error limit + Kpg, ///< Active-power controller proportional gain + Kig, ///< Active-power controller integral gain + Pmax, ///< Maximum active-power command + Pmin, ///< Minimum active-power command + Tlag ///< Active-power command lag time constant + }; + + /// Buses for the REPCA plant-control model. + enum class RepcaBuses : size_t + { + bus, ///< Regulated bus ID + SIZE + }; + + /// Signal inputs for the REPCA plant-control model. + enum class RepcaSignalInputs : size_t + { + ibranchr, ///< Branch current real-component signal ID + ibranchi, ///< Branch current imaginary-component signal ID + pbranch, ///< Branch active-power signal ID + qbranch, ///< Branch reactive-power signal ID + freq, ///< Frequency input signal ID + freqref, ///< Frequency-reference signal ID + vref, ///< Voltage-reference signal ID + qref, ///< Reactive-power reference signal ID + pplantref, ///< Plant active-power reference signal ID + SIZE + }; + + /// Signal outputs for the REPCA plant-control model. + enum class RepcaSignalOutputs : size_t + { + qext, ///< Reactive-power command output signal ID + pext, ///< Active-power command output signal ID + SIZE + }; + + /// Variables available through the monitor interface. + enum class RepcaMonitorableVariables + { + qext, ///< Reactive-power command output + pext, ///< Active-power command output + vmeas, ///< Filtered regulated voltage + qmeas, ///< Filtered reactive-power signal + pmeas ///< Filtered active-power signal + }; + + template + struct RepcaData : public ComponentData + { + RepcaData() = default; + + using Parameters = RepcaParameters; + using Buses = RepcaBuses; + using SignalInputs = RepcaSignalInputs; + using SignalOutputs = RepcaSignalOutputs; + using MonitorableVariables = RepcaMonitorableVariables; + }; + } // namespace Converter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaDependencyTracking.cpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaDependencyTracking.cpp new file mode 100644 index 000000000..7b8365122 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaDependencyTracking.cpp @@ -0,0 +1,27 @@ +/** + * @file RepcaDependencyTracking.cpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Dependency-tracking instantiations for the REPCA plant-control model. + */ + +#include "RepcaImpl.hpp" + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Converter + { + template + int Repca::evaluateJacobian() + { + Log::misc() << "Evaluate Jacobian for Repca..." << std::endl; + Log::misc() << "Jacobian evaluation is not implemented!" << std::endl; + return 0; + } + + template class Repca; + template class Repca; + } // namespace Converter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp new file mode 100644 index 000000000..e484ac001 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp @@ -0,0 +1,109 @@ +/** + * @file RepcaEnzyme.cpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Enzyme sparse Jacobian for the REPCA plant-control model. + */ + +#include + +#include "RepcaImpl.hpp" + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Converter + { + template + int Repca::evaluateJacobian() + { + Log::misc() << "Evaluate Jacobian for Repca..." << std::endl; + Log::misc() << "Jacobian evaluation is experimental!" << std::endl; + + if (J_rows_buffer_ == nullptr) + { + auto size = static_cast(size_); + auto bus_size = static_cast(bus_->size()); + auto signal_size = static_cast(ws_.size()); + auto buffer_size = 2 * size * size + size * bus_size + size * signal_size; + J_rows_buffer_ = new IdxT[buffer_size]; + J_cols_buffer_ = new IdxT[buffer_size]; + J_vals_buffer_ = new RealT[buffer_size]; + } + + using RepcaT = GridKit::PhasorDynamics::Converter::Repca; + using Fn = GridKit::Enzyme::Sparse::MemberFunctions; + + nnz_ = 0; + + GridKit::Enzyme::Sparse::DfDy::eval( + this, + static_cast(f_.getSize()), + static_cast(y_.getSize()), + (this->getResidualIndices()).data(), + (this->getVariableIndices()).data(), + y_.getData(), + yp_.getData(), + wb_.data(), + ws_.data(), + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + nnz_); + + GridKit::Enzyme::Sparse::DfDyp::eval( + this, + static_cast(f_.getSize()), + static_cast(y_.getSize()), + (this->getResidualIndices()).data(), + (this->getVariableIndices()).data(), + y_.getData(), + yp_.getData(), + wb_.data(), + ws_.data(), + alpha_, + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + nnz_); + + GridKit::Enzyme::Sparse::DfDwb::eval( + this, + static_cast(f_.getSize()), + static_cast(bus_->size()), + (this->getResidualIndices()).data(), + (bus_->getVariableIndices()).data(), + y_.getData(), + yp_.getData(), + wb_.data(), + ws_.data(), + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + nnz_); + + GridKit::Enzyme::Sparse::DfDws::eval( + this, + static_cast(f_.getSize()), + ws_.size(), + (this->getResidualIndices()).data(), + ws_indices_.data(), + y_.getData(), + yp_.getData(), + wb_.data(), + ws_.data(), + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + nnz_); + + this->constructCoo(); + + return 0; + } + + template class Repca; + template class Repca; + } // namespace Converter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp new file mode 100644 index 000000000..b5b888d54 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp @@ -0,0 +1,719 @@ +/** + * @file RepcaImpl.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Definition of the REPCA phasor-dynamics plant-control model. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Converter + { + using Log = ::GridKit::Utilities::Logger; + + template + Repca::Repca(BusT* bus) + : bus_(bus) + { + size_ = static_cast(RepcaInternalVariables::MAXIMUM); + setDerivedParameters(); + } + + template + Repca::Repca(BusT* bus, const ModelDataT& data) + : bus_(bus), + monitor_(std::make_unique(data)) + { + initializeParameters(data); + initializeMonitor(); + size_ = static_cast(RepcaInternalVariables::MAXIMUM); + setDerivedParameters(); + } + + template + Repca::~Repca() + { + } + + template + void Repca::initializeParameters(const ModelDataT& data) + { + using Params = typename ModelDataT::Parameters; + + parameter_error_count_ = 0; + + auto load_real = [&](auto key, RealT& target, const char* name) + { + if (!data.parameters.contains(key)) + { + return; + } + + const auto& value = data.parameters.at(key); + if (const auto* real_value = std::get_if(&value)) + { + target = *real_value; + } + else if (const auto* index_value = std::get_if(&value)) + { + target = static_cast(*index_value); + } + else + { + Log::error() << "Repca: parameter '" << name << "' must be numeric\n"; + ++parameter_error_count_; + } + }; + + auto load_switch = [&](auto key, bool& target, const char* name) + { + if (!data.parameters.contains(key)) + { + return; + } + + const auto& value = data.parameters.at(key); + if (const auto* bool_value = std::get_if(&value)) + { + target = *bool_value; + } + else if (const auto* index_value = std::get_if(&value); + index_value && (*index_value == 0 || *index_value == 1)) + { + target = (*index_value == 1); + } + else if (const auto* real_value = std::get_if(&value); + real_value && (*real_value == ZERO || *real_value == ONE) ) + { + target = (*real_value == ONE); + } + else + { + Log::error() << "Repca: parameter '" << name << "' must be bool or 0/1\n"; + ++parameter_error_count_; + } + }; + + load_real(Params::mva, mva_, "mva"); + load_switch(Params::VcompFlag, VcompFlag_, "VcompFlag"); + load_switch(Params::RefFlag, RefFlag_, "RefFlag"); + load_switch(Params::Freqflag, Freqflag_, "Freqflag"); + load_real(Params::Tfltr, Tfltr_, "Tfltr"); + load_real(Params::Vfrz, Vfrz_, "Vfrz"); + load_real(Params::Rc, Rc_, "Rc"); + load_real(Params::Xc, Xc_, "Xc"); + load_real(Params::Kc, Kc_, "Kc"); + load_real(Params::dbdlow, dbdlow_, "dbdlow"); + load_real(Params::dbdupper, dbdupper_, "dbdupper"); + load_real(Params::emax, emax_, "emax"); + load_real(Params::emin, emin_, "emin"); + load_real(Params::Kp, Kp_, "Kp"); + load_real(Params::Ki, Ki_, "Ki"); + load_real(Params::Qmax, Qmax_, "Qmax"); + load_real(Params::Qmin, Qmin_, "Qmin"); + load_real(Params::Tft, Tft_, "Tft"); + load_real(Params::Tfv, Tfv_, "Tfv"); + load_real(Params::Tp, Tp_, "Tp"); + load_real(Params::fdbd1, fdbd1_, "fdbd1"); + load_real(Params::fdbd2, fdbd2_, "fdbd2"); + load_real(Params::Ddn, Ddn_, "Ddn"); + load_real(Params::Dup, Dup_, "Dup"); + load_real(Params::femax, femax_, "femax"); + load_real(Params::femin, femin_, "femin"); + load_real(Params::Kpg, Kpg_, "Kpg"); + load_real(Params::Kig, Kig_, "Kig"); + load_real(Params::Pmax, Pmax_, "Pmax"); + load_real(Params::Pmin, Pmin_, "Pmin"); + load_real(Params::Tlag, Tlag_, "Tlag"); + } + + template + void Repca::setDerivedParameters() + { + Tfltr_ = std::max(Tfltr_, TIME_CONSTANT_MINIMUM); + Tp_ = std::max(Tp_, TIME_CONSTANT_MINIMUM); + Tlag_ = std::max(Tlag_, TIME_CONSTANT_MINIMUM); + + va_component_base_ = mva_ * static_cast(1.0e6); + + vcomp_on_ = VcompFlag_ ? ONE : ZERO; + vcomp_off_ = ONE - vcomp_on_; + ref_on_ = RefFlag_ ? ONE : ZERO; + ref_off_ = ONE - ref_on_; + freq_on_ = Freqflag_ ? ONE : ZERO; + } + + template + scalar_type Repca::toComponentBase(scalar_type value) const + { + return value * va_system_base_ / va_component_base_; + } + + template + scalar_type Repca::toSystemBase(scalar_type value) const + { + return value / toComponentBase(static_cast(ONE)); + } + + template + const Model::VariableMonitorBase* Repca::getMonitor() const + { + return monitor_.get(); + } + + template + void Repca::initializeMonitor() + { + using Variable = typename ModelDataT::MonitorableVariables; + auto index = [](RepcaInternalVariables variable) + { + return static_cast(variable); + }; + + monitor_->set(Variable::qext, [this, index] + { return y_.getData()[index(RepcaInternalVariables::QEXT)]; }); + monitor_->set(Variable::pext, [this, index] + { return y_.getData()[index(RepcaInternalVariables::PEXT)]; }); + monitor_->set(Variable::vmeas, [this, index] + { return y_.getData()[index(RepcaInternalVariables::VMEAS)]; }); + monitor_->set(Variable::qmeas, [this, index] + { return y_.getData()[index(RepcaInternalVariables::QMEAS)]; }); + monitor_->set(Variable::pmeas, [this, index] + { return y_.getData()[index(RepcaInternalVariables::PMEAS)]; }); + } + + template + int Repca::setGridKitComponentID(IdxT component_id) + { + gridkit_component_id_ = component_id; + return 0; + } + + template + int Repca::allocate() + { + if (!allocated_) + { + this->allocateVectors(size_); + } + auto size = static_cast(size_); + + tag_.assign(size, false); + variable_indices_.resize(size); + residual_indices_.resize(size); + + wb_.assign(2, ScalarT{0}); + + auto signal_size = static_cast(RepcaExternalVariables::MAXIMUM); + ws_.assign(signal_size, ScalarT{0}); + ws_indices_.assign(signal_size, INVALID_INDEX); + + for (IdxT j = 0; j < size_; ++j) + { + this->setVariableIndex(j, j); + this->setResidualIndex(j, j); + } + + if (signals_.template isAssigned()) + { + auto* y = y_.getData(); + signals_.template getSignalNode()->set( + &y[static_cast(RepcaInternalVariables::QEXT)], + &(this->getVariableIndex(static_cast(RepcaInternalVariables::QEXT)))); + } + + if (signals_.template isAssigned()) + { + auto* y = y_.getData(); + signals_.template getSignalNode()->set( + &y[static_cast(RepcaInternalVariables::PEXT)], + &(this->getVariableIndex(static_cast(RepcaInternalVariables::PEXT)))); + } + + allocated_ = true; + return 0; + } + + template + int Repca::verify() const + { + int ret = static_cast(parameter_error_count_); + + auto check = [&](bool condition, const char* message) + { + if (!condition) + { + Log::error() << "Repca: " << message << '\n'; + ret += 1; + } + }; + + if (bus_ == nullptr) + { + Log::error() << "Repca: bus pointer is null\n"; + ret += 1; + } + + check(mva_ > ZERO, "mva must be positive"); + check(Tfv_ > ZERO, "Tfv must be positive"); + check(dbdlow_ <= ZERO && ZERO <= dbdupper_, + "dbdlow <= 0 <= dbdupper is required"); + check(emin_ <= ZERO && ZERO <= emax_, + "emin <= 0 <= emax is required"); + check(Qmin_ <= Qmax_, "Qmin must be less than or equal to Qmax"); + check(fdbd1_ <= ZERO && ZERO <= fdbd2_, + "fdbd1 <= 0 <= fdbd2 is required"); + check(femin_ <= ZERO && ZERO <= femax_, + "femin <= 0 <= femax is required"); + check(Pmin_ <= Pmax_, "Pmin must be less than or equal to Pmax"); + + auto check_required_signal = [&](bool attached, bool linked, const char* name) + { + if (!attached) + { + Log::error() << "Repca: " << name << " signal is required\n"; + ret += 1; + } + else if (!linked) + { + Log::error() << "Repca: " << name << " signal attached with no linked source\n"; + ret += 1; + } + }; + + check_required_signal( + signals_.template isAttached(), + signals_.template isAttached() + && signals_.template isLinked(), + "ibranchr"); + check_required_signal( + signals_.template isAttached(), + signals_.template isAttached() + && signals_.template isLinked(), + "ibranchi"); + check_required_signal( + signals_.template isAttached(), + signals_.template isAttached() + && signals_.template isLinked(), + "pbranch"); + check_required_signal( + signals_.template isAttached(), + signals_.template isAttached() + && signals_.template isLinked(), + "qbranch"); + check_required_signal( + signals_.template isAttached(), + signals_.template isAttached() + && signals_.template isLinked(), + "freq"); + + auto check_optional_signal = [&](bool attached, bool linked, const char* name) + { + if (attached && !linked) + { + Log::error() << "Repca: " << name << " signal attached with no linked source\n"; + ret += 1; + } + }; + + check_optional_signal( + signals_.template isAttached(), + signals_.template isAttached() + && signals_.template isLinked(), + "freqref"); + check_optional_signal( + signals_.template isAttached(), + signals_.template isAttached() + && signals_.template isLinked(), + "vref"); + check_optional_signal( + signals_.template isAttached(), + signals_.template isAttached() + && signals_.template isLinked(), + "qref"); + check_optional_signal( + signals_.template isAttached(), + signals_.template isAttached() + && signals_.template isLinked(), + "pplantref"); + + return ret; + } + + template + int Repca::initialize() + { + if (verify() > 0) + { + Log::error() << "Repca: cannot initialize with invalid configuration\n"; + return 1; + } + + auto* y = y_.getData(); + auto* yp = yp_.getData(); + + const auto VMEAS = static_cast(RepcaInternalVariables::VMEAS); + const auto QMEAS = static_cast(RepcaInternalVariables::QMEAS); + const auto XQPI = static_cast(RepcaInternalVariables::XQPI); + const auto XQLAG = static_cast(RepcaInternalVariables::XQLAG); + const auto PMEAS = static_cast(RepcaInternalVariables::PMEAS); + const auto XPPI = static_cast(RepcaInternalVariables::XPPI); + const auto PREF = static_cast(RepcaInternalVariables::PREF); + const auto V = static_cast(RepcaInternalVariables::V); + const auto VLDC = static_cast(RepcaInternalVariables::VLDC); + const auto VDROOP = static_cast(RepcaInternalVariables::VDROOP); + const auto VCTRL = static_cast(RepcaInternalVariables::VCTRL); + const auto SFRZ = static_cast(RepcaInternalVariables::SFRZ); + const auto ERQ = static_cast(RepcaInternalVariables::ERQ); + const auto ERQDB = static_cast(RepcaInternalVariables::ERQDB); + const auto ERQLIM = static_cast(RepcaInternalVariables::ERQLIM); + const auto QPI = static_cast(RepcaInternalVariables::QPI); + const auto QEXT = static_cast(RepcaInternalVariables::QEXT); + const auto EF = static_cast(RepcaInternalVariables::EF); + const auto EP = static_cast(RepcaInternalVariables::EP); + const auto EPLIM = static_cast(RepcaInternalVariables::EPLIM); + const auto PPI = static_cast(RepcaInternalVariables::PPI); + const auto PEXT = static_cast(RepcaInternalVariables::PEXT); + + const ScalarT vr = Vr(); + const ScalarT vi = Vi(); + const ScalarT ibranchr = + signals_.template readExternalVariable(); + const ScalarT ibranchi = + signals_.template readExternalVariable(); + const ScalarT pbranch = + signals_.template readExternalVariable(); + const ScalarT qbranch = + signals_.template readExternalVariable(); + const ScalarT freq = + signals_.template readExternalVariable(); + + const ScalarT qext0 = toComponentBase(y[QEXT]); + const ScalarT pext0 = toComponentBase(y[PEXT]); + + const ScalarT vldc_r = vr - Rc_ * ibranchr + Xc_ * ibranchi; + const ScalarT vldc_i = vi - Rc_ * ibranchi - Xc_ * ibranchr; + + y[V] = std::sqrt(vr * vr + vi * vi); + y[VLDC] = std::sqrt(vldc_r * vldc_r + vldc_i * vldc_i); + y[VDROOP] = y[V] + Kc_ * toComponentBase(qbranch); + y[VCTRL] = vcomp_on_ * y[VLDC] + vcomp_off_ * y[VDROOP]; + + y[EF] = ZERO; + const ScalarT pfreq0 = Ddn_ * Math::ramp(y[EF]) - Dup_ * Math::ramp(-y[EF]); + + y[VMEAS] = y[VCTRL]; + y[QMEAS] = toComponentBase(qbranch); + y[PMEAS] = toComponentBase(pbranch); + y[SFRZ] = Math::above(y[V], Vfrz_); + + y[ERQ] = ZERO; + y[ERQDB] = Math::deadband2(y[ERQ], dbdlow_, dbdupper_); + y[ERQLIM] = Math::clamp(y[ERQDB], emin_, emax_); + y[QPI] = qext0; + y[XQLAG] = qext0; + y[QEXT] = toSystemBase(qext0); + y[XQPI] = qext0 - Kp_ * y[ERQLIM]; + + y[EP] = ZERO; + y[EPLIM] = Math::clamp(y[EP], femin_, femax_); + y[PREF] = Freqflag_ ? pext0 : Math::clamp(y[PMEAS], Pmin_, Pmax_); + y[PPI] = y[PREF]; + y[XPPI] = y[PREF] - Kpg_ * y[EPLIM]; + y[PEXT] = toSystemBase(freq_on_ * y[PREF]); + + const ScalarT q_aw = Math::antiwindup(y[QPI], Ki_ * y[ERQLIM], Qmin_, Qmax_); + const ScalarT p_aw = Math::antiwindup(y[PPI], Kig_ * y[EPLIM], Pmin_, Pmax_); + if (std::abs(static_cast(q_aw)) > static_cast(1.0e-10)) + { + Log::error() << "Repca: reactive PI antiwindup rate is nonzero at initialization\n"; + return 1; + } + if (std::abs(static_cast(p_aw)) > static_cast(1.0e-10)) + { + Log::error() << "Repca: active PI antiwindup rate is nonzero at initialization\n"; + return 1; + } + + freqref_set_ = freq; + vref_set_ = y[VMEAS]; + qref_set_ = toSystemBase(y[QMEAS]); + pplantref_set_ = toSystemBase(y[PMEAS] - pfreq0); + + if (signals_.template isAttached()) + { + signals_.template writeExternalVariable( + freqref_set_); + } + if (signals_.template isAttached()) + { + signals_.template writeExternalVariable(vref_set_); + } + if (signals_.template isAttached()) + { + signals_.template writeExternalVariable(qref_set_); + } + if (signals_.template isAttached()) + { + signals_.template writeExternalVariable( + pplantref_set_); + } + + for (IdxT i = 0; i < yp_.getSize(); ++i) + { + yp[i] = ZERO; + } + + y_.setDataUpdated(); + yp_.setDataUpdated(); + return 0; + } + + template + int Repca::tagDifferentiable() + { + std::fill(tag_.begin(), tag_.end(), false); + tag_[static_cast(RepcaInternalVariables::VMEAS)] = true; + tag_[static_cast(RepcaInternalVariables::QMEAS)] = true; + tag_[static_cast(RepcaInternalVariables::XQPI)] = true; + tag_[static_cast(RepcaInternalVariables::XQLAG)] = true; + tag_[static_cast(RepcaInternalVariables::PMEAS)] = true; + tag_[static_cast(RepcaInternalVariables::XPPI)] = true; + tag_[static_cast(RepcaInternalVariables::PREF)] = true; + return 0; + } + + template + int Repca::setAbsoluteTolerance(RealT rel_tol) + { + abs_tol_.setToConst(static_cast(rel_tol)); + return 0; + } + + template + __attribute__((always_inline)) inline int Repca::evaluateInternalResidual( + const ScalarT* y, + const ScalarT* yp, + const ScalarT* wb, + const ScalarT* ws, + ScalarT* f) + { + const auto VMEAS = static_cast(RepcaInternalVariables::VMEAS); + const auto QMEAS = static_cast(RepcaInternalVariables::QMEAS); + const auto XQPI = static_cast(RepcaInternalVariables::XQPI); + const auto XQLAG = static_cast(RepcaInternalVariables::XQLAG); + const auto PMEAS = static_cast(RepcaInternalVariables::PMEAS); + const auto XPPI = static_cast(RepcaInternalVariables::XPPI); + const auto PREF = static_cast(RepcaInternalVariables::PREF); + const auto V = static_cast(RepcaInternalVariables::V); + const auto VLDC = static_cast(RepcaInternalVariables::VLDC); + const auto VDROOP = static_cast(RepcaInternalVariables::VDROOP); + const auto VCTRL = static_cast(RepcaInternalVariables::VCTRL); + const auto SFRZ = static_cast(RepcaInternalVariables::SFRZ); + const auto ERQ = static_cast(RepcaInternalVariables::ERQ); + const auto ERQDB = static_cast(RepcaInternalVariables::ERQDB); + const auto ERQLIM = static_cast(RepcaInternalVariables::ERQLIM); + const auto QPI = static_cast(RepcaInternalVariables::QPI); + const auto QEXT = static_cast(RepcaInternalVariables::QEXT); + const auto EF = static_cast(RepcaInternalVariables::EF); + const auto EP = static_cast(RepcaInternalVariables::EP); + const auto EPLIM = static_cast(RepcaInternalVariables::EPLIM); + const auto PPI = static_cast(RepcaInternalVariables::PPI); + const auto PEXT = static_cast(RepcaInternalVariables::PEXT); + + const auto IBRANCHR = static_cast(RepcaExternalVariables::IBRANCHR); + const auto IBRANCHI = static_cast(RepcaExternalVariables::IBRANCHI); + const auto PBRANCH = static_cast(RepcaExternalVariables::PBRANCH); + const auto QBRANCH = static_cast(RepcaExternalVariables::QBRANCH); + const auto FREQ = static_cast(RepcaExternalVariables::FREQ); + const auto FREQREF = static_cast(RepcaExternalVariables::FREQREF); + const auto VREF = static_cast(RepcaExternalVariables::VREF); + const auto QREF = static_cast(RepcaExternalVariables::QREF); + const auto PPLANTREF = static_cast(RepcaExternalVariables::PPLANTREF); + + const ScalarT vmeas = y[VMEAS]; + const ScalarT qmeas = y[QMEAS]; + const ScalarT xqpi = y[XQPI]; + const ScalarT xqlag = y[XQLAG]; + const ScalarT pmeas = y[PMEAS]; + const ScalarT xppi = y[XPPI]; + const ScalarT pref = y[PREF]; + const ScalarT v = y[V]; + const ScalarT vldc = y[VLDC]; + const ScalarT vdroop = y[VDROOP]; + const ScalarT vctrl = y[VCTRL]; + const ScalarT sfrz = y[SFRZ]; + const ScalarT erq = y[ERQ]; + const ScalarT erqdb = y[ERQDB]; + const ScalarT erqlim = y[ERQLIM]; + const ScalarT qpi = y[QPI]; + const ScalarT qext = toComponentBase(y[QEXT]); + const ScalarT ef = y[EF]; + const ScalarT ep = y[EP]; + const ScalarT eplim = y[EPLIM]; + const ScalarT ppi = y[PPI]; + const ScalarT pext = toComponentBase(y[PEXT]); + + const ScalarT vmeas_dot = yp[VMEAS]; + const ScalarT qmeas_dot = yp[QMEAS]; + const ScalarT xqpi_dot = yp[XQPI]; + const ScalarT xqlag_dot = yp[XQLAG]; + const ScalarT pmeas_dot = yp[PMEAS]; + const ScalarT xppi_dot = yp[XPPI]; + const ScalarT pref_dot = yp[PREF]; + + const ScalarT vr = wb[0]; + const ScalarT vi = wb[1]; + + const ScalarT ibranchr = ws[IBRANCHR]; + const ScalarT ibranchi = ws[IBRANCHI]; + const ScalarT pbranch = ws[PBRANCH]; + const ScalarT qbranch = ws[QBRANCH]; + const ScalarT freq = ws[FREQ]; + const ScalarT freqref = ws[FREQREF]; + const ScalarT vref = ws[VREF]; + const ScalarT qref = toComponentBase(ws[QREF]); + const ScalarT pplantref = toComponentBase(ws[PPLANTREF]); + + const ScalarT vldc_r = vr - Rc_ * ibranchr + Xc_ * ibranchi; + const ScalarT vldc_i = vi - Rc_ * ibranchi - Xc_ * ibranchr; + const ScalarT pfreq = Ddn_ * Math::ramp(ef) - Dup_ * Math::ramp(-ef); + + f[VMEAS] = -vmeas_dot + (vctrl - vmeas) / Tfltr_; + f[QMEAS] = -qmeas_dot + (toComponentBase(qbranch) - qmeas) / Tfltr_; + f[XQPI] = -xqpi_dot + sfrz * Math::antiwindup(qpi, Ki_ * erqlim, Qmin_, Qmax_); + f[XQLAG] = -xqlag_dot + (qpi - xqlag) / Tfv_; + f[PMEAS] = -pmeas_dot + (toComponentBase(pbranch) - pmeas) / Tp_; + f[XPPI] = -xppi_dot + Math::antiwindup(ppi, Kig_ * eplim, Pmin_, Pmax_); + f[PREF] = -pref_dot + (ppi - pref) / Tlag_; + + f[V] = -v * v + vr * vr + vi * vi; + f[VLDC] = -vldc * vldc + vldc_r * vldc_r + vldc_i * vldc_i; + f[VDROOP] = -vdroop + v + Kc_ * toComponentBase(qbranch); + f[VCTRL] = -vctrl + vcomp_on_ * vldc + vcomp_off_ * vdroop; + f[SFRZ] = -sfrz + Math::above(v, Vfrz_); + f[ERQ] = -erq + ref_on_ * (vref - vmeas) + ref_off_ * (qref - qmeas); + f[ERQDB] = -erqdb + Math::deadband2(erq, dbdlow_, dbdupper_); + f[ERQLIM] = -erqlim + Math::clamp(erqdb, emin_, emax_); + f[QPI] = -qpi + Math::clamp(Kp_ * erqlim + xqpi, Qmin_, Qmax_); + f[QEXT] = -Tfv_ * (qext - xqlag) + Tft_ * (qpi - xqlag); + + f[EF] = -ef + Math::deadband2(freqref - freq, fdbd1_, fdbd2_); + f[EP] = -ep + pplantref - pmeas + pfreq; + f[EPLIM] = -eplim + Math::clamp(ep, femin_, femax_); + f[PPI] = -ppi + Math::clamp(Kpg_ * eplim + xppi, Pmin_, Pmax_); + f[PEXT] = -pext + freq_on_ * pref; + + return 0; + } + + template + int Repca::evaluateResidual() + { + const auto IBRANCHR = static_cast(RepcaExternalVariables::IBRANCHR); + const auto IBRANCHI = static_cast(RepcaExternalVariables::IBRANCHI); + const auto PBRANCH = static_cast(RepcaExternalVariables::PBRANCH); + const auto QBRANCH = static_cast(RepcaExternalVariables::QBRANCH); + const auto FREQ = static_cast(RepcaExternalVariables::FREQ); + const auto FREQREF = static_cast(RepcaExternalVariables::FREQREF); + const auto VREF = static_cast(RepcaExternalVariables::VREF); + const auto QREF = static_cast(RepcaExternalVariables::QREF); + const auto PPLANTREF = static_cast(RepcaExternalVariables::PPLANTREF); + + std::fill(ws_.begin(), ws_.end(), ZERO); + std::fill(ws_indices_.begin(), ws_indices_.end(), INVALID_INDEX); + + ws_[FREQREF] = freqref_set_; + ws_[VREF] = vref_set_; + ws_[QREF] = qref_set_; + ws_[PPLANTREF] = pplantref_set_; + + if (signals_.template isAttached()) + { + ws_[IBRANCHR] = + signals_.template readExternalVariable(); + ws_indices_[IBRANCHR] = + signals_.template readExternalVariableIndex(); + } + if (signals_.template isAttached()) + { + ws_[IBRANCHI] = + signals_.template readExternalVariable(); + ws_indices_[IBRANCHI] = + signals_.template readExternalVariableIndex(); + } + if (signals_.template isAttached()) + { + ws_[PBRANCH] = + signals_.template readExternalVariable(); + ws_indices_[PBRANCH] = + signals_.template readExternalVariableIndex(); + } + if (signals_.template isAttached()) + { + ws_[QBRANCH] = + signals_.template readExternalVariable(); + ws_indices_[QBRANCH] = + signals_.template readExternalVariableIndex(); + } + if (signals_.template isAttached()) + { + ws_[FREQ] = signals_.template readExternalVariable(); + ws_indices_[FREQ] = + signals_.template readExternalVariableIndex(); + } + if (signals_.template isAttached()) + { + ws_[FREQREF] = + signals_.template readExternalVariable(); + ws_indices_[FREQREF] = + signals_.template readExternalVariableIndex(); + } + if (signals_.template isAttached()) + { + ws_[VREF] = signals_.template readExternalVariable(); + ws_indices_[VREF] = + signals_.template readExternalVariableIndex(); + } + if (signals_.template isAttached()) + { + ws_[QREF] = signals_.template readExternalVariable(); + ws_indices_[QREF] = + signals_.template readExternalVariableIndex(); + } + if (signals_.template isAttached()) + { + ws_[PPLANTREF] = + signals_.template readExternalVariable(); + ws_indices_[PPLANTREF] = + signals_.template readExternalVariableIndex(); + } + + wb_[0] = Vr(); + wb_[1] = Vi(); + + const auto* y = y_.getData(); + const auto* yp = yp_.getData(); + auto* f = f_.getData(); + + evaluateInternalResidual(y, yp, wb_.data(), ws_.data(), f); + f_.setDataUpdated(); + return 0; + } + } // namespace Converter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md b/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md index 7a8fa915f..8ae69fcb1 100644 --- a/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md +++ b/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md @@ -152,6 +152,7 @@ are specified: [Gensal](SynchronousMachine/GENSAL/README.md) | 5th order salient-pole machine model [GenClassical](SynchronousMachine/GenClassical/README.md) | the classical machine model [Regca](Converter/REGCA/README.md) | WECC REGCA renewable generator/converter model + [Repca](Controller/REPCA/README.md) | the REPCA renewable plant-control model [Tgov1](Governor/Tgov1/README.md) | the TGOV1 governor model [Hygov](Governor/HYGOV/README.md) | the HYGOV hydro turbine-governor model [Ieeet1](Exciter/IEEET1/README.md) | the IEEET1 exciter model diff --git a/GridKit/Model/PhasorDynamics/SystemModelData.hpp b/GridKit/Model/PhasorDynamics/SystemModelData.hpp index e82fc965e..e50f8c1c5 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelData.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelData.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -45,6 +46,7 @@ namespace GridKit using BusToSignalAdapterDataT = BusToSignalAdapterData; using BusFaultDataT = BusFaultData; using RegcaDataT = Converter::RegcaData; + using RepcaDataT = Converter::RepcaData; using Tgov1DataT = Governor::Tgov1Data; using Esdc1aDataT = Exciter::Esdc1aData; using HygovDataT = Governor::HygovData; @@ -102,6 +104,7 @@ namespace GridKit std::vector branch; ///< Branches within the model std::vector bus_fault; ///< Bus faults within the model std::vector regca; ///< REGCA converter instances within the model + std::vector repca; ///< REPCA plant controllers within the model std::vector genrou; ///< GENROU instances within the model std::vector gensal; ///< GENSAL instances within the model std::vector genclassical; ///< Classical generator instances within the model diff --git a/GridKit/Model/PhasorDynamics/SystemModelDataJSONParser.hpp b/GridKit/Model/PhasorDynamics/SystemModelDataJSONParser.hpp index 328053288..c1701687a 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelDataJSONParser.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelDataJSONParser.hpp @@ -141,6 +141,12 @@ namespace GridKit raw_component.get_to(regca); sm.regca.push_back(regca); } + else if (kind == "Repca") + { + typename SystemModelData::RepcaDataT repca; + raw_component.get_to(repca); + sm.repca.push_back(repca); + } else if (kind == "Tgov1") { typename SystemModelData::Tgov1DataT gov; diff --git a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp index ceaa8ed6d..b09281aaf 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp @@ -162,6 +162,87 @@ namespace GridKit addComponent(regca); } + // Add REPCA plant controllers + for (const auto& repcadata : data.repca) + { + IdxT bus_index = 0; + if (repcadata.buses.contains(RepcaBuses::bus)) + { + bus_index = repcadata.buses.at(RepcaBuses::bus); + } + + auto* repca = new Repca(getBus(bus_index), repcadata); + + if (repcadata.signal_inputs.contains(RepcaSignalInputs::ibranchr)) + { + const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::ibranchr); + constexpr auto IBRANCHR = RepcaExternalVariables::IBRANCHR; + repca->getSignals().template attachSignalNode(getSignal(signal)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::ibranchi)) + { + const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::ibranchi); + constexpr auto IBRANCHI = RepcaExternalVariables::IBRANCHI; + repca->getSignals().template attachSignalNode(getSignal(signal)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::pbranch)) + { + const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::pbranch); + constexpr auto PBRANCH = RepcaExternalVariables::PBRANCH; + repca->getSignals().template attachSignalNode(getSignal(signal)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::qbranch)) + { + const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::qbranch); + constexpr auto QBRANCH = RepcaExternalVariables::QBRANCH; + repca->getSignals().template attachSignalNode(getSignal(signal)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::freq)) + { + const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::freq); + constexpr auto FREQ = RepcaExternalVariables::FREQ; + repca->getSignals().template attachSignalNode(getSignal(signal)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::freqref)) + { + const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::freqref); + constexpr auto FREQREF = RepcaExternalVariables::FREQREF; + repca->getSignals().template attachSignalNode(getSignal(signal)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::vref)) + { + const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::vref); + constexpr auto VREF = RepcaExternalVariables::VREF; + repca->getSignals().template attachSignalNode(getSignal(signal)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::qref)) + { + const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::qref); + constexpr auto QREF = RepcaExternalVariables::QREF; + repca->getSignals().template attachSignalNode(getSignal(signal)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::pplantref)) + { + const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::pplantref); + constexpr auto PPLANTREF = RepcaExternalVariables::PPLANTREF; + repca->getSignals().template attachSignalNode(getSignal(signal)); + } + if (repcadata.signal_outputs.contains(RepcaSignalOutputs::qext)) + { + const IdxT signal = repcadata.signal_outputs.at(RepcaSignalOutputs::qext); + constexpr auto QEXT = RepcaInternalVariables::QEXT; + repca->getSignals().template assignSignalNode(getSignal(signal)); + } + if (repcadata.signal_outputs.contains(RepcaSignalOutputs::pext)) + { + const IdxT signal = repcadata.signal_outputs.at(RepcaSignalOutputs::pext); + constexpr auto PEXT = RepcaInternalVariables::PEXT; + repca->getSignals().template assignSignalNode(getSignal(signal)); + } + + addComponent(repca); + } + // Add branches for (const auto& branchdata : data.branch) { diff --git a/docs/Figures/PhasorDynamics/REPCA/diagram.png b/docs/Figures/PhasorDynamics/REPCA/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..08fef3c155f65641aa9c581283f3322fc208d367 GIT binary patch literal 91838 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 REGCB REECA +REPCA ``` ```{include} ../../../../../GridKit/Model/PhasorDynamics/Converter/README.md diff --git a/docs/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md b/docs/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md new file mode 100644 index 000000000..855a3ea10 --- /dev/null +++ b/docs/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md @@ -0,0 +1,6 @@ +# REPCA + +```{include} ../../../../../../GridKit/Model/PhasorDynamics/Converter/REPCA/README.md +:start-line: 1 +:relative-images: +``` diff --git a/tests/UnitTests/PhasorDynamics/CMakeLists.txt b/tests/UnitTests/PhasorDynamics/CMakeLists.txt index 122e925ee..d8945dd55 100644 --- a/tests/UnitTests/PhasorDynamics/CMakeLists.txt +++ b/tests/UnitTests/PhasorDynamics/CMakeLists.txt @@ -132,6 +132,14 @@ target_link_libraries( GridKit::phasor_dynamics_bus_dependency_tracking GridKit::testing) +add_executable(test_phasor_converter_repca runConverterRepcaTests.cpp) +target_link_libraries( + test_phasor_converter_repca + GridKit::definitions + GridKit::phasor_dynamics_systemmodel + GridKit::phasor_dynamics_systemmodel_dependency_tracking + GridKit::testing) + add_executable(test_phasor_stabilizer_ieeest runStabilizerIeeestTests.cpp) target_link_libraries( test_phasor_stabilizer_ieeest @@ -187,6 +195,7 @@ add_test(NAME PhasorDynamicsExciterEsdc1aTest COMMAND test_phasor_exciter_esdc1a add_test(NAME PhasorDynamicsGensalTest COMMAND test_phasor_gensal) add_test(NAME PhasorDynamicsExciterSexsPtiTest COMMAND test_phasor_exciter_sexspti) add_test(NAME PhasorDynamicsConverterRegcaTest COMMAND test_phasor_converter_regca) +add_test(NAME PhasorDynamicsConverterRepcaTest COMMAND test_phasor_converter_repca) add_test(NAME PhasorDynamicsStabilizerIeeestTest COMMAND test_phasor_stabilizer_ieeest) add_test(NAME PhasorDynamicsGenClassicalTest COMMAND test_phasor_gen_classical) add_test(NAME PhasorDynamicsLoadZTest COMMAND test_phasor_loadz) @@ -213,6 +222,7 @@ install( test_phasor_gensal test_phasor_exciter_sexspti test_phasor_converter_regca + test_phasor_converter_repca test_phasor_stabilizer_ieeest test_phasor_gen_classical test_phasor_system diff --git a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp new file mode 100644 index 000000000..01c3a23b9 --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp @@ -0,0 +1,866 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + namespace Testing + { + template + class ConverterRepcaTests + { + public: + using ScalarT = scalar_type; + using IdxT = index_type; + using RealT = typename PhasorDynamics::Component::RealT; + + static constexpr ScalarT kTol = static_cast(1.0e-8); + + TestOutcome constructionAndValidation() + { + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + bus.allocate(); + bus.initialize(); + + ScalarT ir{0.0}; + ScalarT ii{0.0}; + ScalarT p{0.4}; + ScalarT q{0.1}; + ScalarT freq{1.0}; + IdxT ir_i = 10; + IdxT ii_i = 11; + IdxT p_i = 12; + IdxT q_i = 13; + IdxT f_i = 14; + + PhasorDynamics::SignalNode ir_node; + PhasorDynamics::SignalNode ii_node; + PhasorDynamics::SignalNode p_node; + PhasorDynamics::SignalNode q_node; + PhasorDynamics::SignalNode freq_node; + ir_node.set(&ir, &ir_i); + ii_node.set(&ii, &ii_i); + p_node.set(&p, &p_i); + q_node.set(&q, &q_i); + freq_node.set(&freq, &f_i); + + auto data = makeData(); + PhasorDynamics::Converter::Repca repca(&bus, data); + auto& signals = repca.getSignals(); + signals.template attachSignalNode(&ir_node); + signals.template attachSignalNode(&ii_node); + signals.template attachSignalNode(&p_node); + signals.template attachSignalNode(&q_node); + signals.template attachSignalNode(&freq_node); + + success *= (repca.size() == static_cast(Vars::MAXIMUM)); + success *= (repca.getMonitor() != nullptr); + success *= (repca.verify() == 0); + + auto bad = data; + bad.parameters[Params::mva] = static_cast(0.0); + PhasorDynamics::Converter::Repca bad_mva(&bus, bad); + bad_mva.getSignals().template attachSignalNode(&ir_node); + bad_mva.getSignals().template attachSignalNode(&ii_node); + bad_mva.getSignals().template attachSignalNode(&p_node); + bad_mva.getSignals().template attachSignalNode(&q_node); + bad_mva.getSignals().template attachSignalNode(&freq_node); + success *= (bad_mva.verify() > 0); + + bad = data; + bad.parameters[Params::Tfv] = static_cast(0.0); + PhasorDynamics::Converter::Repca bad_tfv(&bus, bad); + bad_tfv.getSignals().template attachSignalNode(&ir_node); + bad_tfv.getSignals().template attachSignalNode(&ii_node); + bad_tfv.getSignals().template attachSignalNode(&p_node); + bad_tfv.getSignals().template attachSignalNode(&q_node); + bad_tfv.getSignals().template attachSignalNode(&freq_node); + success *= (bad_tfv.verify() > 0); + + bad = data; + bad.parameters[Params::Qmin] = static_cast(2.0); + PhasorDynamics::Converter::Repca bad_q_limits(&bus, bad); + bad_q_limits.getSignals().template attachSignalNode(&ir_node); + bad_q_limits.getSignals().template attachSignalNode(&ii_node); + bad_q_limits.getSignals().template attachSignalNode(&p_node); + bad_q_limits.getSignals().template attachSignalNode(&q_node); + bad_q_limits.getSignals().template attachSignalNode(&freq_node); + success *= (bad_q_limits.verify() > 0); + + bad = data; + bad.parameters[Params::fdbd1] = static_cast(0.1); + PhasorDynamics::Converter::Repca bad_deadband(&bus, bad); + bad_deadband.getSignals().template attachSignalNode(&ir_node); + bad_deadband.getSignals().template attachSignalNode(&ii_node); + bad_deadband.getSignals().template attachSignalNode(&p_node); + bad_deadband.getSignals().template attachSignalNode(&q_node); + bad_deadband.getSignals().template attachSignalNode(&freq_node); + success *= (bad_deadband.verify() > 0); + + return success.report(__func__); + } + + TestOutcome signalVerification() + { + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + PhasorDynamics::Converter::Repca repca(&bus, makeData()); + + PhasorDynamics::SignalNode ir_node; + repca.getSignals().template attachSignalNode(&ir_node); + success *= (repca.verify() > 0); + + ScalarT ir{0.0}; + ScalarT ii{0.0}; + ScalarT p{0.4}; + ScalarT q{0.1}; + ScalarT freq{1.0}; + ScalarT vref{1.0}; + IdxT ir_i = 20; + IdxT ii_i = 21; + IdxT p_i = 22; + IdxT q_i = 23; + IdxT f_i = 24; + IdxT v_i = 25; + + PhasorDynamics::SignalNode ii_node; + PhasorDynamics::SignalNode p_node; + PhasorDynamics::SignalNode q_node; + PhasorDynamics::SignalNode freq_node; + PhasorDynamics::SignalNode vref_node; + ir_node.set(&ir, &ir_i); + ii_node.set(&ii, &ii_i); + p_node.set(&p, &p_i); + q_node.set(&q, &q_i); + freq_node.set(&freq, &f_i); + + repca.getSignals().template attachSignalNode(&ii_node); + repca.getSignals().template attachSignalNode(&p_node); + repca.getSignals().template attachSignalNode(&q_node); + repca.getSignals().template attachSignalNode(&freq_node); + success *= (repca.verify() == 0); + + repca.getSignals().template attachSignalNode(&vref_node); + success *= (repca.verify() > 0); + vref_node.set(&vref, &v_i); + success *= (repca.verify() == 0); + + return success.report(__func__); + } + + TestOutcome initializationAndResidual() + { + TestStatus success = true; + + auto data = makeData(); + data.parameters[Params::mva] = static_cast(50.0); + data.parameters[Params::Freqflag] = true; + + PhasorDynamics::Bus bus(0.8, 0.6); + bus.allocate(); + bus.initialize(); + + ScalarT ir{0.2}; + ScalarT ii{-0.1}; + ScalarT p{0.4}; + ScalarT q{0.1}; + ScalarT freq{1.0}; + ScalarT freqref{-4.0}; + ScalarT vref{-3.0}; + ScalarT qref{-2.0}; + ScalarT pplantref{-1.0}; + IdxT ir_i = 30; + IdxT ii_i = 31; + IdxT p_i = 32; + IdxT q_i = 33; + IdxT f_i = 34; + IdxT freqref_i = 35; + IdxT vref_i = 36; + IdxT qref_i = 37; + IdxT plantref_i = 38; + + PhasorDynamics::SignalNode ir_node; + PhasorDynamics::SignalNode ii_node; + PhasorDynamics::SignalNode p_node; + PhasorDynamics::SignalNode q_node; + PhasorDynamics::SignalNode freq_node; + PhasorDynamics::SignalNode freqref_node; + PhasorDynamics::SignalNode vref_node; + PhasorDynamics::SignalNode qref_node; + PhasorDynamics::SignalNode plantref_node; + PhasorDynamics::SignalNode qext_node; + PhasorDynamics::SignalNode pext_node; + + ir_node.set(&ir, &ir_i); + ii_node.set(&ii, &ii_i); + p_node.set(&p, &p_i); + q_node.set(&q, &q_i); + freq_node.set(&freq, &f_i); + freqref_node.set(&freqref, &freqref_i); + vref_node.set(&vref, &vref_i); + qref_node.set(&qref, &qref_i); + plantref_node.set(&pplantref, &plantref_i); + + PhasorDynamics::Converter::Repca repca(&bus, data); + auto& signals = repca.getSignals(); + signals.template attachSignalNode(&ir_node); + signals.template attachSignalNode(&ii_node); + signals.template attachSignalNode(&p_node); + signals.template attachSignalNode(&q_node); + signals.template attachSignalNode(&freq_node); + signals.template attachSignalNode(&freqref_node); + signals.template attachSignalNode(&vref_node); + signals.template attachSignalNode(&qref_node); + signals.template attachSignalNode(&plantref_node); + signals.template assignSignalNode(&qext_node); + signals.template assignSignalNode(&pext_node); + + success *= (repca.allocate() == 0); + repca.y().getData()[index(Vars::QEXT)] = static_cast(0.25); + repca.y().getData()[index(Vars::PEXT)] = static_cast(0.70); + repca.y().setDataUpdated(); + success *= (repca.initialize() == 0); + success *= (repca.tagDifferentiable() == 0); + success *= (repca.evaluateResidual() == 0); + + const ScalarT base_ratio = static_cast(2.0); + const ScalarT qmeas = base_ratio * q; + const ScalarT pmeas = base_ratio * p; + const ScalarT pfreq0 = + static_cast(20.0) * Math::ramp(static_cast(0.0)); + + success *= isEqual(repca.y().getData()[index(Vars::QEXT)], static_cast(0.25), kTol); + success *= isEqual(repca.y().getData()[index(Vars::PEXT)], static_cast(0.70), kTol); + success *= isEqual(qext_node.read(), static_cast(0.25), kTol); + success *= isEqual(pext_node.read(), static_cast(0.70), kTol); + success *= isEqual(freqref, freq, kTol); + success *= isEqual(vref, repca.y().getData()[index(Vars::VMEAS)], kTol); + success *= isEqual(qref, qmeas / base_ratio, kTol); + success *= isEqual(pplantref, (pmeas - pfreq0) / base_ratio, kTol); + + for (size_t i = 0; i < repca.getResidual().getSize(); ++i) + { + if (!isEqual(repca.getResidual().getData()[i], static_cast(0.0), kTol)) + { + std::cout << "REPCA residual row " << i << " is " + << repca.getResidual().getData()[i] << "\n"; + success = false; + } + } + + return success.report(__func__); + } + + TestOutcome residualEquations() + { + TestStatus success = true; + + auto data = makeResidualData(); + PhasorDynamics::Bus bus(0.9, 0.4); + bus.allocate(); + bus.initialize(); + + ScalarT ir{0.08}; + ScalarT ii{-0.02}; + ScalarT p{0.41}; + ScalarT q{0.13}; + ScalarT freq{0.99}; + ScalarT freqref{1.0}; + ScalarT vref{1.01}; + ScalarT qref{0.12}; + ScalarT pplantref{0.55}; + IdxT ir_i = 40; + IdxT ii_i = 41; + IdxT p_i = 42; + IdxT q_i = 43; + IdxT f_i = 44; + IdxT freqref_i = 45; + IdxT vref_i = 46; + IdxT qref_i = 47; + IdxT plantref_i = 48; + + PhasorDynamics::SignalNode ir_node; + PhasorDynamics::SignalNode ii_node; + PhasorDynamics::SignalNode p_node; + PhasorDynamics::SignalNode q_node; + PhasorDynamics::SignalNode freq_node; + PhasorDynamics::SignalNode freqref_node; + PhasorDynamics::SignalNode vref_node; + PhasorDynamics::SignalNode qref_node; + PhasorDynamics::SignalNode plantref_node; + + ir_node.set(&ir, &ir_i); + ii_node.set(&ii, &ii_i); + p_node.set(&p, &p_i); + q_node.set(&q, &q_i); + freq_node.set(&freq, &f_i); + freqref_node.set(&freqref, &freqref_i); + vref_node.set(&vref, &vref_i); + qref_node.set(&qref, &qref_i); + plantref_node.set(&pplantref, &plantref_i); + + PhasorDynamics::Converter::Repca repca(&bus, data); + repca.getSignals().template attachSignalNode(&ir_node); + repca.getSignals().template attachSignalNode(&ii_node); + repca.getSignals().template attachSignalNode(&p_node); + repca.getSignals().template attachSignalNode(&q_node); + repca.getSignals().template attachSignalNode(&freq_node); + repca.getSignals().template attachSignalNode(&freqref_node); + repca.getSignals().template attachSignalNode(&vref_node); + repca.getSignals().template attachSignalNode(&qref_node); + repca.getSignals().template attachSignalNode(&plantref_node); + + success *= (repca.allocate() == 0); + + repca.y().getData()[index(Vars::VMEAS)] = static_cast(0.98); + repca.y().getData()[index(Vars::QMEAS)] = static_cast(0.11); + repca.y().getData()[index(Vars::XQPI)] = static_cast(0.07); + repca.y().getData()[index(Vars::XQLAG)] = static_cast(0.14); + repca.y().getData()[index(Vars::PMEAS)] = static_cast(0.44); + repca.y().getData()[index(Vars::XPPI)] = static_cast(0.21); + repca.y().getData()[index(Vars::PREF)] = static_cast(0.60); + repca.y().getData()[index(Vars::V)] = static_cast(1.0); + repca.y().getData()[index(Vars::VLDC)] = static_cast(0.99); + repca.y().getData()[index(Vars::VDROOP)] = static_cast(1.05); + repca.y().getData()[index(Vars::VCTRL)] = static_cast(1.02); + repca.y().getData()[index(Vars::SFRZ)] = static_cast(0.8); + repca.y().getData()[index(Vars::ERQ)] = static_cast(0.03); + repca.y().getData()[index(Vars::ERQDB)] = static_cast(0.02); + repca.y().getData()[index(Vars::ERQLIM)] = static_cast(0.02); + repca.y().getData()[index(Vars::QPI)] = static_cast(0.27); + repca.y().getData()[index(Vars::QEXT)] = static_cast(0.20); + repca.y().getData()[index(Vars::EF)] = static_cast(0.01); + repca.y().getData()[index(Vars::EP)] = static_cast(0.04); + repca.y().getData()[index(Vars::EPLIM)] = static_cast(0.04); + repca.y().getData()[index(Vars::PPI)] = static_cast(0.66); + repca.y().getData()[index(Vars::PEXT)] = static_cast(0.61); + + repca.yp().getData()[index(Vars::VMEAS)] = static_cast(0.01); + repca.yp().getData()[index(Vars::QMEAS)] = static_cast(-0.02); + repca.yp().getData()[index(Vars::XQPI)] = static_cast(0.03); + repca.yp().getData()[index(Vars::XQLAG)] = static_cast(-0.04); + repca.yp().getData()[index(Vars::PMEAS)] = static_cast(0.02); + repca.yp().getData()[index(Vars::XPPI)] = static_cast(-0.01); + repca.yp().getData()[index(Vars::PREF)] = static_cast(0.05); + + repca.y().setDataUpdated(); + repca.yp().setDataUpdated(); + + success *= (repca.evaluateResidual() == 0); + + const ScalarT vldc_r = bus.Vr() - static_cast(0.02) * ir + + static_cast(0.03) * ii; + const ScalarT vldc_i = bus.Vi() - static_cast(0.02) * ii + - static_cast(0.03) * ir; + const ScalarT pfreq = static_cast(2.0) * Math::ramp(repca.y().getData()[index(Vars::EF)]) + - Math::ramp(-repca.y().getData()[index(Vars::EF)]); + + std::vector expected(static_cast(Vars::MAXIMUM), + static_cast(0.0)); + expected[index(Vars::VMEAS)] = + -repca.yp().getData()[index(Vars::VMEAS)] + + (repca.y().getData()[index(Vars::VCTRL)] - repca.y().getData()[index(Vars::VMEAS)]) + / static_cast(0.2); + expected[index(Vars::QMEAS)] = + -repca.yp().getData()[index(Vars::QMEAS)] + + (q - repca.y().getData()[index(Vars::QMEAS)]) / static_cast(0.2); + expected[index(Vars::XQPI)] = + -repca.yp().getData()[index(Vars::XQPI)] + + repca.y().getData()[index(Vars::SFRZ)] + * Math::antiwindup(repca.y().getData()[index(Vars::QPI)], + static_cast(3.0) + * repca.y().getData()[index(Vars::ERQLIM)], + static_cast(-0.8), + static_cast(0.9)); + expected[index(Vars::XQLAG)] = + -repca.yp().getData()[index(Vars::XQLAG)] + + (repca.y().getData()[index(Vars::QPI)] - repca.y().getData()[index(Vars::XQLAG)]) + / static_cast(1.5); + expected[index(Vars::PMEAS)] = + -repca.yp().getData()[index(Vars::PMEAS)] + + (p - repca.y().getData()[index(Vars::PMEAS)]) / static_cast(0.4); + expected[index(Vars::XPPI)] = + -repca.yp().getData()[index(Vars::XPPI)] + + Math::antiwindup(repca.y().getData()[index(Vars::PPI)], + static_cast(1.8) * repca.y().getData()[index(Vars::EPLIM)], + static_cast(0.0), + static_cast(1.2)); + expected[index(Vars::PREF)] = + -repca.yp().getData()[index(Vars::PREF)] + + (repca.y().getData()[index(Vars::PPI)] - repca.y().getData()[index(Vars::PREF)]) + / static_cast(0.5); + + expected[index(Vars::V)] = + -repca.y().getData()[index(Vars::V)] * repca.y().getData()[index(Vars::V)] + + bus.Vr() * bus.Vr() + bus.Vi() * bus.Vi(); + expected[index(Vars::VLDC)] = + -repca.y().getData()[index(Vars::VLDC)] * repca.y().getData()[index(Vars::VLDC)] + + vldc_r * vldc_r + vldc_i * vldc_i; + expected[index(Vars::VDROOP)] = + -repca.y().getData()[index(Vars::VDROOP)] + repca.y().getData()[index(Vars::V)] + + static_cast(0.4) * q; + expected[index(Vars::VCTRL)] = + -repca.y().getData()[index(Vars::VCTRL)] + repca.y().getData()[index(Vars::VLDC)]; + expected[index(Vars::SFRZ)] = + -repca.y().getData()[index(Vars::SFRZ)] + + Math::above(repca.y().getData()[index(Vars::V)], static_cast(0.7)); + expected[index(Vars::ERQ)] = + -repca.y().getData()[index(Vars::ERQ)] + vref - repca.y().getData()[index(Vars::VMEAS)]; + expected[index(Vars::ERQDB)] = + -repca.y().getData()[index(Vars::ERQDB)] + + Math::deadband2(repca.y().getData()[index(Vars::ERQ)], + static_cast(-0.02), + static_cast(0.03)); + expected[index(Vars::ERQLIM)] = + -repca.y().getData()[index(Vars::ERQLIM)] + + Math::clamp(repca.y().getData()[index(Vars::ERQDB)], + static_cast(-0.7), + static_cast(0.8)); + expected[index(Vars::QPI)] = + -repca.y().getData()[index(Vars::QPI)] + + Math::clamp(static_cast(2.0) * repca.y().getData()[index(Vars::ERQLIM)] + + repca.y().getData()[index(Vars::XQPI)], + static_cast(-0.8), + static_cast(0.9)); + expected[index(Vars::QEXT)] = + -static_cast(1.5) + * (repca.y().getData()[index(Vars::QEXT)] - repca.y().getData()[index(Vars::XQLAG)]) + + static_cast(0.2) + * (repca.y().getData()[index(Vars::QPI)] - repca.y().getData()[index(Vars::XQLAG)]); + + expected[index(Vars::EF)] = + -repca.y().getData()[index(Vars::EF)] + + Math::deadband2(freqref - freq, + static_cast(-0.01), + static_cast(0.015)); + expected[index(Vars::EP)] = + -repca.y().getData()[index(Vars::EP)] + pplantref - repca.y().getData()[index(Vars::PMEAS)] + + pfreq; + expected[index(Vars::EPLIM)] = + -repca.y().getData()[index(Vars::EPLIM)] + + Math::clamp(repca.y().getData()[index(Vars::EP)], + static_cast(-0.5), + static_cast(0.6)); + expected[index(Vars::PPI)] = + -repca.y().getData()[index(Vars::PPI)] + + Math::clamp(static_cast(1.7) * repca.y().getData()[index(Vars::EPLIM)] + + repca.y().getData()[index(Vars::XPPI)], + static_cast(0.0), + static_cast(1.2)); + expected[index(Vars::PEXT)] = + -repca.y().getData()[index(Vars::PEXT)] + repca.y().getData()[index(Vars::PREF)]; + + for (size_t i = 0; i < expected.size(); ++i) + { + if (!isEqual(repca.getResidual().getData()[i], expected[i], kTol)) + { + std::cout << "REPCA residual mismatch at row " << i << ": " + << repca.getResidual().getData()[i] << " != " << expected[i] << "\n"; + success = false; + } + } + + return success.report(__func__); + } + + TestOutcome jsonParseAndSystemAssembly() + { + TestStatus success = true; + + std::istringstream input(R"json( +{ + "header": { + "format_version": 0, + "format_revision": 1, + "case_name": "REPCA parser", + "case_description": "REPCA parser behavior test", + "case_comments": "", + "freq_base": 60.0, + "va_base": 100000000.0 + }, + "buses": [ + { + "number": 1, + "class": "bus", + "name": "Bus 1", + "init": { "Vr": 1.0, "Vi": 0.0 }, + "params": { "kv": 1.0 } + } + ], + "signals": [ + { "signal_id": 10, "name": "Ir" }, + { "signal_id": 11, "name": "Ii" }, + { "signal_id": 12, "name": "Pbranch" }, + { "signal_id": 13, "name": "Qbranch" }, + { "signal_id": 14, "name": "Freq" }, + { "signal_id": 15, "name": "Qext" }, + { "signal_id": 16, "name": "Pext" } + ], + "devices": [ + { + "class": "Repca", + "ports": { + "bus": 1, + "ibranchr": 10, + "ibranchi": 11, + "pbranch": 12, + "qbranch": 13, + "freq": 14, + "qext": 15, + "pext": 16 + }, + "id": "RP1", + "params": { "mva": 100.0 }, + "mon": ["qext", "pext", "vmeas", "qmeas", "pmeas"] + } + ] +} +)json"); + + auto data = PhasorDynamics::parseSystemModelData(input); + success *= (data.repca.size() == 1); + success *= (data.repca[0].device_class == "Repca"); + success *= (data.repca[0].buses.at(PhasorDynamics::Converter::RepcaBuses::bus) == 1); + success *= (data.repca[0].signal_inputs.at(PhasorDynamics::Converter::RepcaSignalInputs::ibranchr) == 10); + success *= (data.repca[0].signal_inputs.at(PhasorDynamics::Converter::RepcaSignalInputs::ibranchi) == 11); + success *= (data.repca[0].signal_inputs.at(PhasorDynamics::Converter::RepcaSignalInputs::pbranch) == 12); + success *= (data.repca[0].signal_inputs.at(PhasorDynamics::Converter::RepcaSignalInputs::qbranch) == 13); + success *= (data.repca[0].signal_inputs.at(PhasorDynamics::Converter::RepcaSignalInputs::freq) == 14); + success *= (data.repca[0].signal_outputs.at(PhasorDynamics::Converter::RepcaSignalOutputs::qext) == 15); + success *= (data.repca[0].signal_outputs.at(PhasorDynamics::Converter::RepcaSignalOutputs::pext) == 16); + success *= (std::get_if( + &data.repca[0].parameters.at(Params::mva)) + != nullptr); + + PhasorDynamics::SystemModel system(data); + success *= (system.size() == 0); + + return success.report(__func__); + } + +#ifdef GRIDKIT_ENABLE_ENZYME + TestOutcome jacobian() + { + TestStatus success = true; + + auto dependency_tracking_jacobian = dependencyTrackingJacobian(); + auto enzyme_jacobian = enzymeJacobian(); + + success *= (dependency_tracking_jacobian.size() == enzyme_jacobian.size()); + const auto nrows = + std::min(dependency_tracking_jacobian.size(), enzyme_jacobian.size()); + for (size_t i = 0; i < nrows; ++i) + { + success *= isEqual(dependency_tracking_jacobian[i], + enzyme_jacobian[i], + static_cast(1.0e-8)); + } + + return success.report(__func__); + } +#endif + + private: + using Params = PhasorDynamics::Converter::RepcaParameters; + using Vars = PhasorDynamics::Converter::RepcaInternalVariables; + using Ext = PhasorDynamics::Converter::RepcaExternalVariables; + + static size_t index(Vars variable) + { + return static_cast(variable); + } + + auto makeData() -> PhasorDynamics::Converter::RepcaData + { + using Mon = PhasorDynamics::Converter::RepcaMonitorableVariables; + + PhasorDynamics::Converter::RepcaData data; + data.device_class = "Repca"; + data.disambiguation_string = "repca_test"; + data.monitored_variables.insert(Mon::qext); + data.monitored_variables.insert(Mon::pext); + data.monitored_variables.insert(Mon::vmeas); + data.monitored_variables.insert(Mon::qmeas); + data.monitored_variables.insert(Mon::pmeas); + return data; + } + + auto makeResidualData() -> PhasorDynamics::Converter::RepcaData + { + auto data = makeData(); + + data.parameters[Params::mva] = static_cast(100.0); + data.parameters[Params::VcompFlag] = true; + data.parameters[Params::RefFlag] = true; + data.parameters[Params::Freqflag] = true; + data.parameters[Params::Tfltr] = static_cast(0.2); + data.parameters[Params::Vfrz] = static_cast(0.7); + data.parameters[Params::Rc] = static_cast(0.02); + data.parameters[Params::Xc] = static_cast(0.03); + data.parameters[Params::Kc] = static_cast(0.4); + data.parameters[Params::dbdlow] = static_cast(-0.02); + data.parameters[Params::dbdupper] = static_cast(0.03); + data.parameters[Params::emax] = static_cast(0.8); + data.parameters[Params::emin] = static_cast(-0.7); + data.parameters[Params::Kp] = static_cast(2.0); + data.parameters[Params::Ki] = static_cast(3.0); + data.parameters[Params::Qmax] = static_cast(0.9); + data.parameters[Params::Qmin] = static_cast(-0.8); + data.parameters[Params::Tft] = static_cast(0.2); + data.parameters[Params::Tfv] = static_cast(1.5); + data.parameters[Params::Tp] = static_cast(0.4); + data.parameters[Params::fdbd1] = static_cast(-0.01); + data.parameters[Params::fdbd2] = static_cast(0.015); + data.parameters[Params::Ddn] = static_cast(2.0); + data.parameters[Params::Dup] = static_cast(1.0); + data.parameters[Params::femax] = static_cast(0.6); + data.parameters[Params::femin] = static_cast(-0.5); + data.parameters[Params::Kpg] = static_cast(1.7); + data.parameters[Params::Kig] = static_cast(1.8); + data.parameters[Params::Pmax] = static_cast(1.2); + data.parameters[Params::Pmin] = static_cast(0.0); + data.parameters[Params::Tlag] = static_cast(0.5); + return data; + } + +#ifdef GRIDKIT_ENABLE_ENZYME + void setValue(double& target, double value) + { + target = value; + } + + void setValue(DependencyTracking::Variable& target, double value) + { + target.setValue(value); + } + + template + void setJacobianState(model_type& repca, bus_type& bus) + { + setValue(bus.Vr(), 0.9); + setValue(bus.Vi(), 0.4); + + setValue(repca.y().getData()[index(Vars::VMEAS)], 0.98); + setValue(repca.y().getData()[index(Vars::QMEAS)], 0.11); + setValue(repca.y().getData()[index(Vars::XQPI)], 0.07); + setValue(repca.y().getData()[index(Vars::XQLAG)], 0.14); + setValue(repca.y().getData()[index(Vars::PMEAS)], 0.44); + setValue(repca.y().getData()[index(Vars::XPPI)], 0.21); + setValue(repca.y().getData()[index(Vars::PREF)], 0.60); + setValue(repca.y().getData()[index(Vars::V)], 1.0); + setValue(repca.y().getData()[index(Vars::VLDC)], 0.99); + setValue(repca.y().getData()[index(Vars::VDROOP)], 1.05); + setValue(repca.y().getData()[index(Vars::VCTRL)], 1.02); + setValue(repca.y().getData()[index(Vars::SFRZ)], 0.8); + setValue(repca.y().getData()[index(Vars::ERQ)], 0.03); + setValue(repca.y().getData()[index(Vars::ERQDB)], 0.02); + setValue(repca.y().getData()[index(Vars::ERQLIM)], 0.02); + setValue(repca.y().getData()[index(Vars::QPI)], 0.27); + setValue(repca.y().getData()[index(Vars::QEXT)], 0.20); + setValue(repca.y().getData()[index(Vars::EF)], 0.01); + setValue(repca.y().getData()[index(Vars::EP)], 0.04); + setValue(repca.y().getData()[index(Vars::EPLIM)], 0.04); + setValue(repca.y().getData()[index(Vars::PPI)], 0.66); + setValue(repca.y().getData()[index(Vars::PEXT)], 0.61); + + setValue(repca.yp().getData()[index(Vars::VMEAS)], 0.01); + setValue(repca.yp().getData()[index(Vars::QMEAS)], -0.02); + setValue(repca.yp().getData()[index(Vars::XQPI)], 0.03); + setValue(repca.yp().getData()[index(Vars::XQLAG)], -0.04); + setValue(repca.yp().getData()[index(Vars::PMEAS)], 0.02); + setValue(repca.yp().getData()[index(Vars::XPPI)], -0.01); + setValue(repca.yp().getData()[index(Vars::PREF)], 0.05); + + bus.y().setDataUpdated(); + repca.y().setDataUpdated(); + repca.yp().setDataUpdated(); + } + + std::vector dependencyTrackingJacobian() + { + using DepVar = DependencyTracking::Variable; + + auto data = makeResidualData(); + + PhasorDynamics::Bus bus(DepVar{0.9}, DepVar{0.4}); + PhasorDynamics::Converter::Repca repca(&bus, data); + + DepVar ir{0.08}; + DepVar ii{-0.02}; + DepVar p{0.41}; + DepVar q{0.13}; + DepVar freq{0.99}; + DepVar freqref{1.0}; + DepVar vref{1.01}; + DepVar qref{0.12}; + DepVar pplantref{0.55}; + IdxT ir_i = 24; + IdxT ii_i = 25; + IdxT p_i = 26; + IdxT q_i = 27; + IdxT f_i = 28; + IdxT freqref_i = 29; + IdxT vref_i = 30; + IdxT qref_i = 31; + IdxT plantref_i = 32; + + ir.setVariableNumber(ir_i); + ii.setVariableNumber(ii_i); + p.setVariableNumber(p_i); + q.setVariableNumber(q_i); + freq.setVariableNumber(f_i); + freqref.setVariableNumber(freqref_i); + vref.setVariableNumber(vref_i); + qref.setVariableNumber(qref_i); + pplantref.setVariableNumber(plantref_i); + + PhasorDynamics::SignalNode ir_node; + PhasorDynamics::SignalNode ii_node; + PhasorDynamics::SignalNode p_node; + PhasorDynamics::SignalNode q_node; + PhasorDynamics::SignalNode freq_node; + PhasorDynamics::SignalNode freqref_node; + PhasorDynamics::SignalNode vref_node; + PhasorDynamics::SignalNode qref_node; + PhasorDynamics::SignalNode plantref_node; + ir_node.set(&ir, &ir_i); + ii_node.set(&ii, &ii_i); + p_node.set(&p, &p_i); + q_node.set(&q, &q_i); + freq_node.set(&freq, &f_i); + freqref_node.set(&freqref, &freqref_i); + vref_node.set(&vref, &vref_i); + qref_node.set(&qref, &qref_i); + plantref_node.set(&pplantref, &plantref_i); + + repca.getSignals().template attachSignalNode(&ir_node); + repca.getSignals().template attachSignalNode(&ii_node); + repca.getSignals().template attachSignalNode(&p_node); + repca.getSignals().template attachSignalNode(&q_node); + repca.getSignals().template attachSignalNode(&freq_node); + repca.getSignals().template attachSignalNode(&freqref_node); + repca.getSignals().template attachSignalNode(&vref_node); + repca.getSignals().template attachSignalNode(&qref_node); + repca.getSignals().template attachSignalNode(&plantref_node); + + bus.allocate(); + repca.allocate(); + for (IdxT i = 0; i < repca.size(); ++i) + { + repca.y().getData()[static_cast(i)].setVariableNumber(i); + repca.yp().getData()[static_cast(i)].setVariableNumber(i); + } + for (IdxT i = 0; i < bus.size(); ++i) + { + bus.y().getData()[static_cast(i)].setVariableNumber(i + repca.size()); + } + + repca.y().setDataUpdated(); + repca.yp().setDataUpdated(); + bus.y().setDataUpdated(); + + setJacobianState(repca, bus); + repca.evaluateResidual(); + + std::vector dependencies( + static_cast(repca.size())); + for (IdxT i = 0; i < repca.size(); ++i) + { + dependencies[static_cast(i)] = + repca.getResidual().getData()[static_cast(i)].getDependencies(); + } + return dependencies; + } + + std::vector enzymeJacobian() + { + auto data = makeResidualData(); + + PhasorDynamics::Bus bus(0.9, 0.4); + PhasorDynamics::Converter::Repca repca(&bus, data); + + ScalarT ir{0.08}; + ScalarT ii{-0.02}; + ScalarT p{0.41}; + ScalarT q{0.13}; + ScalarT freq{0.99}; + ScalarT freqref{1.0}; + ScalarT vref{1.01}; + ScalarT qref{0.12}; + ScalarT pplantref{0.55}; + IdxT ir_i = 24; + IdxT ii_i = 25; + IdxT p_i = 26; + IdxT q_i = 27; + IdxT f_i = 28; + IdxT freqref_i = 29; + IdxT vref_i = 30; + IdxT qref_i = 31; + IdxT plantref_i = 32; + + PhasorDynamics::SignalNode ir_node; + PhasorDynamics::SignalNode ii_node; + PhasorDynamics::SignalNode p_node; + PhasorDynamics::SignalNode q_node; + PhasorDynamics::SignalNode freq_node; + PhasorDynamics::SignalNode freqref_node; + PhasorDynamics::SignalNode vref_node; + PhasorDynamics::SignalNode qref_node; + PhasorDynamics::SignalNode plantref_node; + ir_node.set(&ir, &ir_i); + ii_node.set(&ii, &ii_i); + p_node.set(&p, &p_i); + q_node.set(&q, &q_i); + freq_node.set(&freq, &f_i); + freqref_node.set(&freqref, &freqref_i); + vref_node.set(&vref, &vref_i); + qref_node.set(&qref, &qref_i); + plantref_node.set(&pplantref, &plantref_i); + + repca.getSignals().template attachSignalNode(&ir_node); + repca.getSignals().template attachSignalNode(&ii_node); + repca.getSignals().template attachSignalNode(&p_node); + repca.getSignals().template attachSignalNode(&q_node); + repca.getSignals().template attachSignalNode(&freq_node); + repca.getSignals().template attachSignalNode(&freqref_node); + repca.getSignals().template attachSignalNode(&vref_node); + repca.getSignals().template attachSignalNode(&qref_node); + repca.getSignals().template attachSignalNode(&plantref_node); + + bus.allocate(); + repca.allocate(); + for (IdxT i = 0; i < bus.size(); ++i) + { + bus.setVariableIndex(i, i + repca.size()); + bus.setResidualIndex(i, i + repca.size()); + } + + setJacobianState(repca, bus); + repca.updateTime(0.0, 1.0); + repca.evaluateResidual(); + repca.evaluateJacobian(); + repca.constructCsr(); + return MapFromCsr(repca.getCsrJacobian()); + } +#endif + }; + } // namespace Testing +} // namespace GridKit diff --git a/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp b/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp new file mode 100644 index 000000000..3103f8106 --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp @@ -0,0 +1,19 @@ +#include "ConverterRepcaTests.hpp" + +int main() +{ + GridKit::Testing::TestingResults result; + + GridKit::Testing::ConverterRepcaTests test; + + result += test.constructionAndValidation(); + result += test.signalVerification(); + result += test.initializationAndResidual(); + result += test.residualEquations(); + result += test.jsonParseAndSystemAssembly(); +#ifdef GRIDKIT_ENABLE_ENZYME + result += test.jacobian(); +#endif + + return result.summary(); +} From 4f21ee87e2fd66f5d2163b6f18a5d9cffb409962 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Mon, 27 Jul 2026 02:54:27 -0500 Subject: [PATCH 02/19] improved quality and readability of REPCA --- .../PhasorDynamics/Converter/REPCA/README.md | 479 ++-- .../PhasorDynamics/Converter/REPCA/Repca.cpp | 6 +- .../PhasorDynamics/Converter/REPCA/Repca.hpp | 97 +- .../Converter/REPCA/RepcaData.hpp | 26 +- .../REPCA/RepcaDependencyTracking.cpp | 4 +- .../Converter/REPCA/RepcaEnzyme.cpp | 44 +- .../Converter/REPCA/RepcaImpl.hpp | 736 +++--- tests/UnitTests/PhasorDynamics/CMakeLists.txt | 6 +- .../PhasorDynamics/ConverterRepcaTests.hpp | 2324 +++++++++++------ .../PhasorDynamics/runConverterRepcaTests.cpp | 12 +- 10 files changed, 2385 insertions(+), 1349 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md index 2587e2ecf..e1df1cec0 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md @@ -3,72 +3,78 @@ REPCA is a WECC renewable energy plant control model for inverter-coupled resources. -## Block Diagram +## Notes + +- REPCA is a control model only. It measures the regulated bus and branch + signals, publishes plant power commands, and injects no current into the + network. +- Branch-current, branch-power, power-reference, and command signals are on + system base. Internal power states and controller limits are on component + base. -Standard REPCA block diagram. +## Block Diagram -![](../../../../../docs/Figures/PhasorDynamics/REPCA/diagram.png) +![REPCA plant-control block diagram](../../../../../docs/Figures/PhasorDynamics/REPCA/diagram.png) -Figure 1: REPCA block diagram. Figure courtesy of [PowerWorld](https://www.powerworld.com/WebHelp/) +Figure 1: REPCA plant-control model. Figure courtesy of +[PowerWorld](https://www.powerworld.com/WebHelp/). ## Model Parameters Symbol | Units | JSON | Description | Typical Value | Note ------------------------------------|----------|-------------|---------------------------------------------------------|---------------|------ -$S^\mathrm{base}$ | [MVA] | `mva` | REPCA component power base | 100.0 | -$s_{\mathrm{comp}}$ | [binary] | `VcompFlag` | Voltage-compensation mode flag | 1.0 | -$s_{\mathrm{ref}}$ | [binary] | `RefFlag` | Reactive-loop reference flag | 1.0 | -$s_{\mathrm{freq}}$ | [binary] | `Freqflag` | Active-power control output flag | 0.0 | -$T_{\mathrm{fltr}}$ | [sec] | `Tfltr` | Voltage and reactive-power measurement filter time constant | 0.05 | -$V^\mathrm{frz}$ | [p.u.] | `Vfrz` | Regulated-voltage threshold below which the reactive PI state freezes | 0.7 | +$S^\text{base}$ | [MVA] | `mva` | REPCA component power base | 100.0 | +$s_\text{comp}$ | [binary] | `VcompFlag` | Voltage-compensation mode flag | 1 | 1 = line-drop compensation, 0 = reactive droop +$s_\text{ref}$ | [binary] | `RefFlag` | Reactive-loop reference flag | 1 | 1 = voltage control, 0 = reactive-power control +$s_\text{freq}$ | [binary] | `Freqflag` | Active-power control output flag | 0 | 1 = command enabled, 0 = zero output +$T_\text{fltr}$ | [sec] | `Tfltr` | Voltage and reactive-power measurement filter time constant | 0.05 | +$V^\text{frz}$ | [p.u.] | `Vfrz` | Regulated-voltage threshold below which the reactive-power PI state freezes | 0.7 | $R_c$ | [p.u.] | `Rc` | Line-drop compensation resistance | 0.0 | $X_c$ | [p.u.] | `Xc` | Line-drop compensation reactance | 0.0 | $K_c$ | [p.u.] | `Kc` | Reactive-current compensation gain | 1.0 | -$D_{\mathrm{bd1}}$ | [p.u.] | `dbdlow` | Lower reactive-loop deadband threshold | 0.0 | -$D_{\mathrm{bd2}}$ | [p.u.] | `dbdupper` | Upper reactive-loop deadband threshold | 0.0 | +$D_\text{bd1}$ | [p.u.] | `dbdlow` | Lower reactive-loop deadband threshold | 0.0 | +$D_\text{bd2}$ | [p.u.] | `dbdupper` | Upper reactive-loop deadband threshold | 0.0 | $e^{\max}$ | [p.u.] | `emax` | Maximum reactive-loop error limit | 1.0 | $e^{\min}$ | [p.u.] | `emin` | Minimum reactive-loop error limit | -1.0 | -$K_{\mathrm{p}}$ | [p.u.] | `Kp` | Reactive-power controller proportional gain | 10.0 | -$K_{\mathrm{i}}$ | [p.u./s] | `Ki` | Reactive-power controller integral gain | 10.0 | +$K_\text{p}$ | [p.u.] | `Kp` | Reactive-power controller proportional gain | 10.0 | +$K_\text{i}$ | [p.u./s] | `Ki` | Reactive-power controller integral gain | 10.0 | $Q^{\max}$ | [p.u.] | `Qmax` | Maximum reactive-power command | 1.0 | $Q^{\min}$ | [p.u.] | `Qmin` | Minimum reactive-power command | -1.0 | -$T_{\mathrm{ft}}$ | [sec] | `Tft` | Reactive-command lead time constant | 0.0 | -$T_{\mathrm{fv}}$ | [sec] | `Tfv` | Reactive-command lag time constant | 3.0 | -$T_{\mathrm{p}}$ | [sec] | `Tp` | Active-power measurement filter time constant | 0.0 | -$D_{\mathrm{bd1}}^f$ | [p.u.] | `fdbd1` | Lower frequency-error deadband threshold | 0.0 | -$D_{\mathrm{bd2}}^f$ | [p.u.] | `fdbd2` | Upper frequency-error deadband threshold | 0.0 | -$D_{\mathrm{dn}}$ | [p.u.] | `Ddn` | Down-frequency droop response gain | 20.0 | -$D_{\mathrm{up}}$ | [p.u.] | `Dup` | Up-frequency droop response gain | 0.0 | +$T_\text{ft}$ | [sec] | `Tft` | Reactive-command lead time constant | 0.0 | +$T_\text{fv}$ | [sec] | `Tfv` | Reactive-command lag time constant | 3.0 | +$T_\text{p}$ | [sec] | `Tp` | Active-power measurement filter time constant | 0.0 | +$D_\text{bd1}^f$ | [p.u.] | `fdbd1` | Lower frequency-error deadband threshold | 0.0 | +$D_\text{bd2}^f$ | [p.u.] | `fdbd2` | Upper frequency-error deadband threshold | 0.0 | +$D_\text{dn}$ | [p.u.] | `Ddn` | Down-frequency droop response gain | 20.0 | +$D_\text{up}$ | [p.u.] | `Dup` | Up-frequency droop response gain | 0.0 | $e_P^{\max}$ | [p.u.] | `femax` | Maximum active-power error limit | 1.0 | $e_P^{\min}$ | [p.u.] | `femin` | Minimum active-power error limit | -1.0 | -$K_{\mathrm{pg}}$ | [p.u.] | `Kpg` | Active-power controller proportional gain | 10.0 | -$K_{\mathrm{ig}}$ | [p.u./s] | `Kig` | Active-power controller integral gain | 10.0 | +$K_\text{pg}$ | [p.u.] | `Kpg` | Active-power controller proportional gain | 10.0 | +$K_\text{ig}$ | [p.u./s] | `Kig` | Active-power controller integral gain | 10.0 | $P^{\max}$ | [p.u.] | `Pmax` | Maximum active-power command | 2.0 | $P^{\min}$ | [p.u.] | `Pmin` | Minimum active-power command | 0.0 | -$T_{\mathrm{lag}}$ | [sec] | `Tlag` | Active-power command lag time constant | 3.0 | +$T_\text{lag}$ | [sec] | `Tlag` | Active-power command lag time constant | 3.0 | ### Parameter Validation -Invalid REPCA parameter sets are rejected by the following checks. Let $\epsilon_T=10^{-3}$. +Invalid REPCA parameter sets are rejected by the following checks: ```math \begin{aligned} - T &\leftarrow \max\!\left(T, \epsilon_T\right) - \quad T\in\{T_{\mathrm{fltr}},T_{\mathrm{p}},T_{\mathrm{lag}}\} \\ - S^\mathrm{base} + S^\text{base} &> 0 \\ - s_{\mathrm{comp}}, s_{\mathrm{ref}}, s_{\mathrm{freq}} + s_\text{comp}, s_\text{ref}, s_\text{freq} &\in \{0,1\} \\ - T_{\mathrm{fv}} + T_\text{fv} &> 0 \\ - D_{\mathrm{bd1}} - &\le 0 \le D_{\mathrm{bd2}} \\ + D_\text{bd1} + &\le 0 \le D_\text{bd2} \\ e^{\min} &\le 0 \le e^{\max} \\ Q^{\min} &\le Q^{\max} \\ - D_{\mathrm{bd1}}^f - &\le 0 \le D_{\mathrm{bd2}}^f \\ + D_\text{bd1}^f + &\le 0 \le D_\text{bd2}^f \\ e_P^{\min} &\le 0 \le e_P^{\max} \\ P^{\min} @@ -76,32 +82,49 @@ Invalid REPCA parameter sets are rejected by the following checks. Let $\epsilon \end{aligned} ``` +`VcompFlag`, `RefFlag`, and `Freqflag` accept Boolean values or integer/real +values equal to 0 or 1. + ### Model Derived Parameters +Let $\epsilon_T=10^{-3}\ \text{s}$. Each time constant below $\epsilon_T$ is +raised to that floor and logged as a warning: + ```math \begin{aligned} - s_{\mathrm{comp}}^\mathrm{off} &= 1 - s_{\mathrm{comp}} \\ - s_{\mathrm{ref}}^\mathrm{off} &= 1 - s_{\mathrm{ref}} \\ - k_{\mathrm{base}} &= \dfrac{S^\mathrm{sys}}{S^\mathrm{base}} + T_x + &\leftarrow \max\!\left(T_x,\epsilon_T\right), + \quad x\in\{\text{fltr},\text{p},\text{lag}\} \\ + s_\text{comp}^\text{off} &= 1 - s_\text{comp} \\ + s_\text{ref}^\text{off} &= 1 - s_\text{ref} \\ + k_\text{base} &= \dfrac{S^\text{sys}}{S^\text{base}} \end{aligned} ``` +Multiplying by $k_\text{base}$ converts system base to component base. + ## Model Ports -Name | Port | Init | Description -------------|--------|---------|------ -`bus` | Bus | Known | Regulated bus voltage -`ibranchr` | Input | Known | Branch current real component -`ibranchi` | Input | Known | Branch current imaginary component -`pbranch` | Input | Known | Branch active power -`qbranch` | Input | Known | Branch reactive power -`freq` | Input | Known | Frequency input -`freqref` | Input | Unknown | Frequency reference -`vref` | Input | Unknown | Voltage-control reference -`qref` | Input | Unknown | Reactive-power reference -`pplantref` | Input | Unknown | Plant active-power reference -`qext` | Output | Known | Reactive-power command output -`pext` | Output | Known | Active-power command output +Name | Port | Init | Base | Description +------------|--------|---------|--------|------ +`bus` | Bus | Known | - | Regulated-bus voltage +`ibranchr` | Input | Known | System | Branch-current real component input +`ibranchi` | Input | Known | System | Branch-current imaginary component input +`pbranch` | Input | Known | System | Branch active-power input +`qbranch` | Input | Known | System | Branch reactive-power input +`freq` | Input | Known | - | Frequency input +`freqref` | Input | Unknown | - | Frequency reference +`vref` | Input | Unknown | - | Voltage-control reference +`qref` | Input | Unknown | System | Reactive-power reference +`pplantref` | Input | Unknown | System | Plant active-power reference +`qext` | Output | Known | System | Reactive-power command output +`pext` | Output | Known | System | Active-power command output + +The known bus and input ports are seeded before `initialize()` and preserved by +it. `qext` is also seeded and preserved. `pext` is preserved when +$s_\text{freq}=1$ and set to zero when $s_\text{freq}=0$. Unknown reference +inputs are resolved during initialization and written to attached signal +storage, or retained as constant inputs when the port is unattached. ## Model Variables @@ -111,33 +134,33 @@ Name | Port | Init | Description Symbol | Units | Description | Note ------------------------|--------|-------------------------------------|------ -$V^\mathrm{meas}$ | [p.u.] | Filtered regulated voltage | State 1 in Fig. 1; source label: `Vmeas` -$Q^\mathrm{meas}$ | [p.u.] | Filtered reactive-power signal | State 2 in Fig. 1; source label: `Qmeas` -$x_Q^\mathrm{PI}$ | [p.u.] | Reactive PI controller state | State 3 in Fig. 1; source label: `Reactive PI` -$x_Q^\mathrm{lag}$ | [p.u.] | Reactive-command lead-lag state | State 4 in Fig. 1; source label: `Qext` -$P^\mathrm{meas}$ | [p.u.] | Filtered active-power signal | State 5 in Fig. 1; source label: `Pmeas` -$x_P^\mathrm{PI}$ | [p.u.] | Active-power PI controller state | State 6 in Fig. 1; source label: `Power PI` -$P^\mathrm{ref}$ | [p.u.] | Active-power command lag state | State 7 in Fig. 1; source label: `Pref` +$V^\text{meas}$ | [p.u.] | Filtered regulated voltage | State 1 in Fig. 1 +$Q^\text{meas}$ | [p.u.] | Filtered reactive-power signal | State 2 in Fig. 1; component base +$x_Q^\text{PI}$ | [p.u.] | Reactive-power PI controller state | State 3 in Fig. 1; component base +$x_Q^\text{lag}$ | [p.u.] | Reactive-command lead-lag state | State 4 in Fig. 1; component base +$P^\text{meas}$ | [p.u.] | Filtered active-power signal | State 5 in Fig. 1; component base +$x_P^\text{PI}$ | [p.u.] | Active-power PI controller state | State 6 in Fig. 1; component base +$P^\text{ref}$ | [p.u.] | Active-power command lag state | State 7 in Fig. 1; component base #### Algebraic -Symbol | Units | Description | Note ---------------------------------|--------|-------------------------------------|------ -$V$ | [p.u.] | Regulated-bus voltage magnitude | -$V^\mathrm{ldc}$ | [p.u.] | Line-drop compensated voltage magnitude | -$V^\mathrm{droop}$ | [p.u.] | Reactive-droop-compensated voltage | -$V^\mathrm{ctrl}$ | [p.u.] | Selected voltage-measurement input | -$s_{\mathrm{frz}}$ | [binary] | Reactive-PI voltage-enable indicator | -$e_{\mathrm{RQ}}$ | [p.u.] | Selected reactive-loop error | -$e_{\mathrm{RQ}}^\mathrm{db}$ | [p.u.] | Deadbanded reactive-loop error | -$e_{\mathrm{RQ}}^\mathrm{lim}$ | [p.u.] | Limited reactive-loop error | -$Q^\mathrm{PI}$ | [p.u.] | Reactive PI output | -$Q^\mathrm{ext}$ | [p.u.] | Reactive-power command output | System base -$e_f$ | [p.u.] | Frequency error after deadband | -$e_P$ | [p.u.] | Active-power control error | -$e_P^\mathrm{lim}$ | [p.u.] | Limited active-power control error | -$P^\mathrm{PI}$ | [p.u.] | Active-power PI output | -$P^\mathrm{ext}$ | [p.u.] | Active-power command output | System base +Symbol | Units | Description | Note +--------------------------|--------|-------------------------------------|------ +$V$ | [p.u.] | Regulated-bus voltage magnitude | +$V^\text{ldc}$ | [p.u.] | Line-drop compensated voltage magnitude | +$V^\text{droop}$ | [p.u.] | Reactive-droop-compensated voltage | +$V^\text{ctrl}$ | [p.u.] | Selected voltage-measurement input | +$s_\text{frz}$ | [-] | Smooth reactive-power PI voltage-enable gate | +$e_\text{RQ}$ | [p.u.] | Selected reactive-loop error | +$e_\text{RQ}^\text{db}$ | [p.u.] | Deadbanded reactive-loop error | +$e_\text{RQ}^\text{lim}$ | [p.u.] | Limited reactive-loop error | +$Q^\text{PI}$ | [p.u.] | Reactive-power PI output | Component base +$Q^\text{ext}$ | [p.u.] | Reactive-power command output | System base +$e_f$ | [p.u.] | Frequency error after deadband | +$e_P$ | [p.u.] | Active-power control error | Component base +$e_P^\text{lim}$ | [p.u.] | Limited active-power control error | Component base +$P^\text{PI}$ | [p.u.] | Active-power PI output | Component base +$P^\text{ext}$ | [p.u.] | Active-power command output | System base ### External Variables @@ -146,19 +169,19 @@ None. #### Algebraic -Symbol | Units | Type | Description | Note --------------------------------------|--------|--------|-----------------------------------|------ -$V_{\mathrm{r}}$ | [p.u.] | Known | Regulated-bus voltage, real component | Source label: `Vreg` -$V_{\mathrm{i}}$ | [p.u.] | Known | Regulated-bus voltage, imaginary component | Source label: `Vreg` -$I_{\mathrm{r}}$ | [p.u.] | Known | Branch current real component | Source label: `Ibranch` -$I_{\mathrm{i}}$ | [p.u.] | Known | Branch current imaginary component | Source label: `Ibranch` -$P^\mathrm{br}$ | [p.u.] | Known | Branch active power | -$Q^\mathrm{br}$ | [p.u.] | Known | Branch reactive power | -$f$ | [p.u.] | Known | Frequency input | Source label: `Freq` -$f^\mathrm{ref}$ | [p.u.] | Unknown | Frequency reference | Optional signal port `freqref`; source label: `Freq_ref` -$V^\mathrm{ref}$ | [p.u.] | Unknown | Voltage-control reference | Optional signal port `vref` -$Q^\mathrm{ref}$ | [p.u.] | Unknown | Reactive-power reference | Optional signal port `qref`; system base -$P_\mathrm{plant}^\mathrm{ref}$ | [p.u.] | Unknown | Plant active-power reference | Optional signal port `pplantref`; system base +Symbol | Units | Init | Description | Note +-------------------------------|--------|---------|-----------------------------------|------ +$V_\text{r}$ | [p.u.] | Known | Regulated-bus voltage, real component | Bus input +$V_\text{i}$ | [p.u.] | Known | Regulated-bus voltage, imaginary component | Bus input +$I_\text{r}$ | [p.u.] | Known | Branch-current real component | Signal port `ibranchr`; system base +$I_\text{i}$ | [p.u.] | Known | Branch-current imaginary component | Signal port `ibranchi`; system base +$P^\text{br}$ | [p.u.] | Known | Branch active power | Signal port `pbranch`; system base +$Q^\text{br}$ | [p.u.] | Known | Branch reactive power | Signal port `qbranch`; system base +$f$ | [p.u.] | Known | Frequency input | Signal port `freq` +$f^\text{ref}$ | [p.u.] | Unknown | Frequency reference | Optional signal port `freqref` +$V^\text{ref}$ | [p.u.] | Unknown | Voltage-control reference | Optional signal port `vref` +$Q^\text{ref}$ | [p.u.] | Unknown | Reactive-power reference | Optional signal port `qref`; system base +$P_\text{plant}^\text{ref}$ | [p.u.] | Unknown | Plant active-power reference | Optional signal port `pplantref`; system base ## Model Equations @@ -167,87 +190,102 @@ $P_\mathrm{plant}^\mathrm{ref}$ | [p.u.] | Unknown | Plant active-power refe ```math \begin{aligned} 0 &= - -\dot{V}^\mathrm{meas} - + \dfrac{1}{T_{\mathrm{fltr}}} - \left(V^\mathrm{ctrl} - V^\mathrm{meas}\right) \\ + -\dot{V}^\text{meas} + + \dfrac{1}{T_\text{fltr}} + \left(V^\text{ctrl} - V^\text{meas}\right) \\ 0 &= - -\dot{Q}^\mathrm{meas} - + \dfrac{1}{T_{\mathrm{fltr}}} - \left(k_{\mathrm{base}}Q^\mathrm{br} - Q^\mathrm{meas}\right) \\ + -\dot{Q}^\text{meas} + + \dfrac{1}{T_\text{fltr}} + \left(k_\text{base}Q^\text{br} - Q^\text{meas}\right) \\ 0 &= - -\dot{x}_Q^\mathrm{PI} - + s_{\mathrm{frz}} - \text{antiwindup}\!\left( - Q^\mathrm{PI}, - K_{\mathrm{i}}e_{\mathrm{RQ}}^\mathrm{lim}, - Q^{\min}, - Q^{\max} - \right) \\ + -\dot{x}_Q^\text{PI} + + s_\text{frz}\, + \text{antiwindup}\!\left( + Q^\text{PI}, + K_\text{i}e_\text{RQ}^\text{lim};\, + Q^{\min}, + Q^{\max} + \right) \\ 0 &= - -\dot{x}_Q^\mathrm{lag} - + \dfrac{1}{T_{\mathrm{fv}}} - \left(Q^\mathrm{PI} - x_Q^\mathrm{lag}\right) \\ + -\dot{x}_Q^\text{lag} + + \dfrac{1}{T_\text{fv}} + \left(Q^\text{PI} - x_Q^\text{lag}\right) \\ 0 &= - -\dot{P}^\mathrm{meas} - + \dfrac{1}{T_{\mathrm{p}}} - \left(k_{\mathrm{base}}P^\mathrm{br} - P^\mathrm{meas}\right) \\ + -\dot{P}^\text{meas} + + \dfrac{1}{T_\text{p}} + \left(k_\text{base}P^\text{br} - P^\text{meas}\right) \\ 0 &= - -\dot{x}_P^\mathrm{PI} + -\dot{x}_P^\text{PI} + \text{antiwindup}\!\left( - P^\mathrm{PI}, - K_{\mathrm{ig}}e_P^\mathrm{lim}, + P^\text{PI}, + K_\text{ig}e_P^\text{lim};\, P^{\min}, P^{\max} \right) \\ 0 &= - -\dot{P}^\mathrm{ref} - + \dfrac{1}{T_{\mathrm{lag}}} - \left(P^\mathrm{PI} - P^\mathrm{ref}\right) + -\dot{P}^\text{ref} + + \dfrac{1}{T_\text{lag}} + \left(P^\text{PI} - P^\text{ref}\right) \end{aligned} ``` -CommonMath defines the [Anti-Windup](../../../../CommonMath.md#anti-windup-indicator) target and smooth approximation. +CommonMath defines the [`antiwindup`](../../../../CommonMath.md#antiwindup) +target and smooth approximation. ### Algebraic Equations ```math \begin{aligned} 0 &= -V^2 - + V_{\mathrm{r}}^2 - + V_{\mathrm{i}}^2 \\ + + V_\text{r}^2 + + V_\text{i}^2 \\ 0 &= - -\left(V^\mathrm{ldc}\right)^2 - + \left(V_{\mathrm{r}} - R_c I_{\mathrm{r}} + X_c I_{\mathrm{i}}\right)^2 - + \left(V_{\mathrm{i}} - R_c I_{\mathrm{i}} - X_c I_{\mathrm{r}}\right)^2 \\ - 0 &= -V^\mathrm{droop} + V + K_c k_{\mathrm{base}}Q^\mathrm{br} \\ - 0 &= -V^\mathrm{ctrl} + s_{\mathrm{comp}}V^\mathrm{ldc} + s_{\mathrm{comp}}^\mathrm{off}V^\mathrm{droop} \\ - 0 &= -s_{\mathrm{frz}} + \text{above}(V, V^\mathrm{frz}) \\[0.5ex] - 0 &= -e_{\mathrm{RQ}} - + s_{\mathrm{ref}}\left(V^\mathrm{ref} - V^\mathrm{meas}\right) - + s_{\mathrm{ref}}^\mathrm{off}\left(k_{\mathrm{base}}Q^\mathrm{ref} - Q^\mathrm{meas}\right) \\ - 0 &= -e_{\mathrm{RQ}}^\mathrm{db} - + \text{deadband2}(e_{\mathrm{RQ}}, D_{\mathrm{bd1}}, D_{\mathrm{bd2}}) \\ - 0 &= -e_{\mathrm{RQ}}^\mathrm{lim} - + \text{clamp}(e_{\mathrm{RQ}}^\mathrm{db}, e^{\min}, e^{\max}) \\ - 0 &= -Q^\mathrm{PI} + \text{clamp}(K_{\mathrm{p}} e_{\mathrm{RQ}}^\mathrm{lim} + x_Q^\mathrm{PI}, Q^{\min}, Q^{\max}) \\ - 0 &= -T_{\mathrm{fv}}(k_{\mathrm{base}}Q^\mathrm{ext} - x_Q^\mathrm{lag}) - + T_{\mathrm{ft}}(Q^\mathrm{PI} - x_Q^\mathrm{lag}) \\[0.5ex] - 0 &= -e_f + \text{deadband2}(f^\mathrm{ref} - f, D_{\mathrm{bd1}}^f, D_{\mathrm{bd2}}^f) \\ + -\left(V^\text{ldc}\right)^2 + + \left(V_\text{r} - R_c I_\text{r} + X_c I_\text{i}\right)^2 + + \left(V_\text{i} - R_c I_\text{i} - X_c I_\text{r}\right)^2 \\ + 0 &= -V^\text{droop} + V + K_c k_\text{base}Q^\text{br} \\ + 0 &= -V^\text{ctrl} + + s_\text{comp}V^\text{ldc} + + s_\text{comp}^\text{off}V^\text{droop} \\ + 0 &= -s_\text{frz} + \text{above}(V;\,V^\text{frz}) \\[0.5ex] + 0 &= -e_\text{RQ} + + s_\text{ref}\left(V^\text{ref} - V^\text{meas}\right) + + s_\text{ref}^\text{off} + \left(k_\text{base}Q^\text{ref} - Q^\text{meas}\right) \\ + 0 &= -e_\text{RQ}^\text{db} + + \text{deadband2} + \left(e_\text{RQ};\,D_\text{bd1},D_\text{bd2}\right) \\ + 0 &= -e_\text{RQ}^\text{lim} + + \text{clamp} + \left(e_\text{RQ}^\text{db};\,e^{\min},e^{\max}\right) \\ + 0 &= -Q^\text{PI} + + \text{clamp} + \left(K_\text{p}e_\text{RQ}^\text{lim}+x_Q^\text{PI};\, + Q^{\min},Q^{\max}\right) \\ + 0 &= -T_\text{fv} + \left(k_\text{base}Q^\text{ext}-x_Q^\text{lag}\right) + + T_\text{ft} + \left(Q^\text{PI}-x_Q^\text{lag}\right) \\[0.5ex] + 0 &= -e_f + + \text{deadband2} + \left(f^\text{ref}-f;\,D_\text{bd1}^f,D_\text{bd2}^f\right) \\ 0 &= -e_P - + k_{\mathrm{base}}P_\mathrm{plant}^\mathrm{ref} - - P^\mathrm{meas} - + D_{\mathrm{dn}}\rho(e_f) - - D_{\mathrm{up}}\rho(-e_f) \\ - 0 &= -e_P^\mathrm{lim} + \text{clamp}(e_P, e_P^{\min}, e_P^{\max}) \\ - 0 &= -P^\mathrm{PI} + \text{clamp}(K_{\mathrm{pg}} e_P^\mathrm{lim} + x_P^\mathrm{PI}, P^{\min}, P^{\max}) \\ - 0 &= -k_{\mathrm{base}}P^\mathrm{ext} + s_{\mathrm{freq}}P^\mathrm{ref} + + k_\text{base}P_\text{plant}^\text{ref} + - P^\text{meas} + + D_\text{dn}\text{ramp}(e_f) + - D_\text{up}\text{ramp}(-e_f) \\ + 0 &= -e_P^\text{lim} + + \text{clamp}\left(e_P;\,e_P^{\min},e_P^{\max}\right) \\ + 0 &= -P^\text{PI} + + \text{clamp} + \left(K_\text{pg}e_P^\text{lim}+x_P^\text{PI};\, + P^{\min},P^{\max}\right) \\ + 0 &= -k_\text{base}P^\text{ext} + s_\text{freq}P^\text{ref} \end{aligned} ``` -CommonMath defines [above](../../../../CommonMath.md#derived-functions), -[clamp](../../../../CommonMath.md#derived-functions), -[deadband2](../../../../CommonMath.md#derived-functions), and -[ramp](../../../../CommonMath.md#primitives) $\rho$. +CommonMath defines the [derived limiter functions](../../../../CommonMath.md#derived-functions) +and [ramp primitive](../../../../CommonMath.md#primitives) used above. ## Initialization @@ -255,101 +293,130 @@ CommonMath defines [above](../../../../CommonMath.md#derived-functions), ```math \begin{aligned} - V_{\mathrm{r}}, V_{\mathrm{i}} + V_\text{r}, V_\text{i} &\leftarrow \text{regulated-bus voltage} \\ - I_{\mathrm{r}}, I_{\mathrm{i}} + I_\text{r}, I_\text{i} &\leftarrow \text{branch current} \\ - P^\mathrm{br}, Q^\mathrm{br} + P^\text{br}, Q^\text{br} &\leftarrow \text{branch power} \\ f &\leftarrow \text{frequency input} \\ - Q^\mathrm{ext} - &\leftarrow \text{reactive-power command start} \\ - P^\mathrm{ext} - &\leftarrow \text{active-power command start} + Q_0^\text{seed} + &\leftarrow k_\text{base}Q_0^\text{ext} \\ + P_0^\text{seed} + &\leftarrow k_\text{base}P_0^\text{ext} \end{aligned} ``` ### Internal Initialization +With initialization tolerance $\epsilon_0=10^{-10}$, +$\text{clamp}^{-1}(z;\ell,u)$ denotes a finite smooth-limiter input whose output +matches $z$ within $\epsilon_0$. It is available for $z\in[\ell,u]$, +including at both exact limits. + +Subscript $0$ denotes initial values; all internal derivatives start at zero: + ```math \begin{aligned} V_0 &= \sqrt{ - V_{\mathrm{r},0}^2 - + V_{\mathrm{i},0}^2 + V_{\text{r},0}^2 + + V_{\text{i},0}^2 } \\ - V_0^\mathrm{ldc} + V_0^\text{ldc} &= \sqrt{ - \left(V_{\mathrm{r},0} - R_c I_{\mathrm{r},0} + X_c I_{\mathrm{i},0}\right)^2 - + \left(V_{\mathrm{i},0} - R_c I_{\mathrm{i},0} - X_c I_{\mathrm{r},0}\right)^2 + \left(V_{\text{r},0}-R_c I_{\text{r},0}+X_c I_{\text{i},0}\right)^2 + + \left(V_{\text{i},0}-R_c I_{\text{i},0}-X_c I_{\text{r},0}\right)^2 } \\ - V_0^\mathrm{droop} + V_0^\text{droop} &= V_0 - + K_c k_{\mathrm{base}}Q_0^\mathrm{br} \\ - V_0^\mathrm{ctrl} - &= s_{\mathrm{comp}}V_0^\mathrm{ldc} - + s_{\mathrm{comp}}^\mathrm{off}V_0^\mathrm{droop} \\ - e_{f,0} &= 0 \\ - P_0^\mathrm{freq} - &= D_{\mathrm{dn}}\rho(e_{f,0}) - - D_{\mathrm{up}}\rho(-e_{f,0}) \\ - V_0^\mathrm{meas} &= V_0^\mathrm{ctrl} \\ - Q_0^\mathrm{meas} &= k_{\mathrm{base}}Q_0^\mathrm{br} \\ - P_0^\mathrm{meas} &= k_{\mathrm{base}}P_0^\mathrm{br} \\ - s_{\mathrm{frz},0} - &= \text{above}\left(V_0, V^\mathrm{frz}\right) \\ - e_{\mathrm{RQ},0} &= 0 \\ - e_{\mathrm{RQ},0}^\mathrm{db} - &= \text{deadband2}\left(e_{\mathrm{RQ},0}, D_{\mathrm{bd1}}, D_{\mathrm{bd2}}\right) \\ - e_{\mathrm{RQ},0}^\mathrm{lim} - &= \text{clamp}\left(e_{\mathrm{RQ},0}^\mathrm{db}, e^{\min}, e^{\max}\right) \\ - Q_0^\mathrm{PI} &= k_{\mathrm{base}}Q_0^\mathrm{ext} \\ - x_{Q,0}^\mathrm{lag} &= k_{\mathrm{base}}Q_0^\mathrm{ext} \\ - x_{Q,0}^\mathrm{PI} - &= k_{\mathrm{base}}Q_0^\mathrm{ext} - K_{\mathrm{p}}e_{\mathrm{RQ},0}^\mathrm{lim} \\ + + K_c k_\text{base}Q_0^\text{br} \\ + V_0^\text{ctrl} + &= s_\text{comp}V_0^\text{ldc} + + s_\text{comp}^\text{off}V_0^\text{droop} \\ + V_0^\text{meas} &= V_0^\text{ctrl} \\ + Q_0^\text{meas} &= k_\text{base}Q_0^\text{br} \\ + P_0^\text{meas} &= k_\text{base}P_0^\text{br} \\ + s_{\text{frz},0} + &= \text{above}\left(V_0;\,V^\text{frz}\right) \\ + e_{\text{RQ},0} &= 0 \\ + e_{\text{RQ},0}^\text{db} + &= \text{deadband2} + \left(e_{\text{RQ},0};\,D_\text{bd1},D_\text{bd2}\right) \\ + e_{\text{RQ},0}^\text{lim} + &= \text{clamp} + \left(e_{\text{RQ},0}^\text{db};\,e^{\min},e^{\max}\right) \\ + Q_0^\text{PI} &= Q_0^\text{seed} \\ + x_{Q,0}^\text{lag} &= Q_0^\text{PI} \\ + u_{Q,0}^\text{PI} + &= \text{clamp}^{-1} + \left(Q_0^\text{PI};\,Q^{\min},Q^{\max}\right) \\ + x_{Q,0}^\text{PI} + &= u_{Q,0}^\text{PI}-K_\text{p}e_{\text{RQ},0}^\text{lim} \\ + e_{f,0} + &= \text{deadband2}\left(0;\,D_\text{bd1}^f,D_\text{bd2}^f\right) \\ + P_0^\text{freq} + &= D_\text{dn}\text{ramp}(e_{f,0}) + - D_\text{up}\text{ramp}(-e_{f,0}) \\ e_{P,0} &= 0 \\ - e_{P,0}^\mathrm{lim} - &= \text{clamp}\left(e_{P,0}, e_P^{\min}, e_P^{\max}\right) \\ - P_0^\mathrm{ref} + e_{P,0}^\text{lim} + &= \text{clamp}\left(e_{P,0};\,e_P^{\min},e_P^{\max}\right) \\ + P_0^\text{ref} &= \begin{cases} - k_{\mathrm{base}}P_0^\mathrm{ext} & s_{\mathrm{freq}} = 1 \\ + P_0^\text{seed} & s_\text{freq}=1 \\ \text{clamp} - \left(k_{\mathrm{base}}P_0^\mathrm{br}, P^{\min}, P^{\max}\right) - & s_{\mathrm{freq}} = 0 + \left(P_0^\text{meas};\,P^{\min},P^{\max}\right) + & s_\text{freq}=0 \end{cases} \\ - P_0^\mathrm{PI} &= P_0^\mathrm{ref} \\ - x_{P,0}^\mathrm{PI} - &= P_0^\mathrm{ref} - K_{\mathrm{pg}}e_{P,0}^\mathrm{lim} + P_0^\text{PI} &= P_0^\text{ref} \\ + u_{P,0}^\text{PI} + &= + \begin{cases} + \text{clamp}^{-1} + \left(P_0^\text{PI};\,P^{\min},P^{\max}\right) + & s_\text{freq}=1 \\ + P_0^\text{meas} & s_\text{freq}=0 + \end{cases} \\ + x_{P,0}^\text{PI} + &= u_{P,0}^\text{PI}-K_\text{pg}e_{P,0}^\text{lim} \\ + P_0^\text{ext} + &= \dfrac{s_\text{freq}}{k_\text{base}}P_0^\text{ref} \end{aligned} ``` +Initialization rejects an operating point when any of the following holds: + +- a required bus or signal value, $Q_0^\text{seed}$, or a frequency-enabled + $P_0^\text{seed}$ is not finite; +- $Q_0^\text{seed}\notin[Q^{\min},Q^{\max}]$, or + $s_\text{freq}=1$ and + $P_0^\text{seed}\notin[P^{\min},P^{\max}]$; or +- either PI antiwindup rate is nonfinite or has magnitude greater than + $\epsilon_0$. + +Every check resolves before any storage is written, so rejected initialization +leaves model and signal storage unchanged. + ### Output Initialization ```math \begin{aligned} - f^\mathrm{ref} &\leftarrow f_0 \\ - V^\mathrm{ref} &\leftarrow V_0^\mathrm{meas} \\ - Q^\mathrm{ref} &\leftarrow \dfrac{Q_0^\mathrm{meas}}{k_{\mathrm{base}}} \\ - P_{\mathrm{plant}}^\mathrm{ref} - &\leftarrow \dfrac{P_0^\mathrm{meas} - P_0^\mathrm{freq}}{k_{\mathrm{base}}} + f^\text{ref} &\leftarrow f_0 \\ + V^\text{ref} &\leftarrow V_0^\text{meas} \\ + Q^\text{ref} &\leftarrow Q_0^\text{br} \\ + P_\text{plant}^\text{ref} + &\leftarrow \dfrac{P_0^\text{meas}-P_0^\text{freq}}{k_\text{base}} \end{aligned} ``` -Initialization rejects nonzero reactive-power or active-power PI antiwindup -rates. - -REPCA writes the resolved references to attached `freqref`, `vref`, `qref`, -and `pplantref` signal inputs. If no controller is connected, those values are -used as constant reference inputs. - ## Monitorable Outputs Output | Units | Description | Note ----------------|--------|-------------------------------------|------ -`qext` | [p.u.] | Reactive-power command output | $Q^\mathrm{ext}$ (system base) -`pext` | [p.u.] | Active-power command output | $P^\mathrm{ext}$ (system base) -`vmeas` | [p.u.] | Filtered regulated voltage | $V^\mathrm{meas}$ -`qmeas` | [p.u.] | Filtered reactive-power signal | $Q^\mathrm{meas}$ (component base) -`pmeas` | [p.u.] | Filtered active-power signal | $P^\mathrm{meas}$ (component base) +`qext` | [p.u.] | Reactive-power command output | $Q^\text{ext}$ (system base) +`pext` | [p.u.] | Active-power command output | $P^\text{ext}$ (system base) +`vmeas` | [p.u.] | Filtered regulated voltage | $V^\text{meas}$ +`qmeas` | [p.u.] | Filtered reactive-power signal | $Q^\text{meas}$ (component base) +`pmeas` | [p.u.] | Filtered active-power signal | $P^\text{meas}$ (component base) diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp index 56a711282..9d7911e1f 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp @@ -1,7 +1,7 @@ /** * @file Repca.cpp * @author Luke Lowery (lukel@tamu.edu) - * @brief Template instantiations for the REPCA plant-control model. + * @brief Non-Enzyme instantiation for the REPCA plant-control model. */ #include "RepcaImpl.hpp" @@ -15,8 +15,8 @@ namespace GridKit template int Repca::evaluateJacobian() { - Log::misc() << "Evaluate Jacobian for Repca..." << std::endl; - Log::misc() << "Jacobian evaluation is not implemented!" << std::endl; + Log::misc() << "Evaluate Jacobian for Repca...\n"; + Log::misc() << "Jacobian evaluation is not implemented!\n"; return 0; } diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp index 6a3cdc359..a5fcc2461 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp @@ -32,7 +32,7 @@ namespace GridKit { VMEAS, ///< Filtered regulated voltage QMEAS, ///< Filtered reactive-power signal - XQPI, ///< Reactive PI controller state + XQPI, ///< Reactive-power PI controller state XQLAG, ///< Reactive-command lead-lag state PMEAS, ///< Filtered active-power signal XPPI, ///< Active-power PI controller state @@ -41,11 +41,11 @@ namespace GridKit VLDC, ///< Line-drop compensated voltage magnitude VDROOP, ///< Reactive-droop-compensated voltage VCTRL, ///< Selected voltage-measurement input - SFRZ, ///< Reactive-PI voltage-enable indicator + SFRZ, ///< Reactive-power PI voltage-enable gate ERQ, ///< Selected reactive-loop error ERQDB, ///< Deadbanded reactive-loop error ERQLIM, ///< Limited reactive-loop error - QPI, ///< Reactive PI output + QPI, ///< Reactive-power PI output QEXT, ///< Reactive-power command output EF, ///< Frequency error after deadband EP, ///< Active-power control error @@ -58,24 +58,67 @@ namespace GridKit /// External variables of a `Repca`. enum class RepcaExternalVariables : size_t { - IBRANCHR, ///< Branch current real component - IBRANCHI, ///< Branch current imaginary component - PBRANCH, ///< Branch active power - QBRANCH, ///< Branch reactive power - FREQ, ///< Frequency input - FREQREF, ///< Frequency reference - VREF, ///< Voltage-control reference - QREF, ///< Reactive-power reference - PPLANTREF, ///< Plant active-power reference + IBRANCHR, ///< Required branch-current real component + IBRANCHI, ///< Required branch-current imaginary component + PBRANCH, ///< Required branch active power + QBRANCH, ///< Required branch reactive power + FREQ, ///< Required frequency input + FREQREF, ///< Optional frequency reference + VREF, ///< Optional voltage-control reference + QREF, ///< Optional reactive-power reference + PPLANTREF, ///< Optional plant active-power reference MAXIMUM, }; + /// Indices into the REPCA state, derivative, and residual vectors. + struct RepcaIdx + { + static constexpr size_t VMEAS = static_cast(RepcaInternalVariables::VMEAS); + static constexpr size_t QMEAS = static_cast(RepcaInternalVariables::QMEAS); + static constexpr size_t XQPI = static_cast(RepcaInternalVariables::XQPI); + static constexpr size_t XQLAG = static_cast(RepcaInternalVariables::XQLAG); + static constexpr size_t PMEAS = static_cast(RepcaInternalVariables::PMEAS); + static constexpr size_t XPPI = static_cast(RepcaInternalVariables::XPPI); + static constexpr size_t PREF = static_cast(RepcaInternalVariables::PREF); + static constexpr size_t V = static_cast(RepcaInternalVariables::V); + static constexpr size_t VLDC = static_cast(RepcaInternalVariables::VLDC); + static constexpr size_t VDROOP = static_cast(RepcaInternalVariables::VDROOP); + static constexpr size_t VCTRL = static_cast(RepcaInternalVariables::VCTRL); + static constexpr size_t SFRZ = static_cast(RepcaInternalVariables::SFRZ); + static constexpr size_t ERQ = static_cast(RepcaInternalVariables::ERQ); + static constexpr size_t ERQDB = static_cast(RepcaInternalVariables::ERQDB); + static constexpr size_t ERQLIM = static_cast(RepcaInternalVariables::ERQLIM); + static constexpr size_t QPI = static_cast(RepcaInternalVariables::QPI); + static constexpr size_t QEXT = static_cast(RepcaInternalVariables::QEXT); + static constexpr size_t EF = static_cast(RepcaInternalVariables::EF); + static constexpr size_t EP = static_cast(RepcaInternalVariables::EP); + static constexpr size_t EPLIM = static_cast(RepcaInternalVariables::EPLIM); + static constexpr size_t PPI = static_cast(RepcaInternalVariables::PPI); + static constexpr size_t PEXT = static_cast(RepcaInternalVariables::PEXT); + static constexpr size_t MAXIMUM = static_cast(RepcaInternalVariables::MAXIMUM); + }; + + /// Indices into the REPCA external-signal buffers. + struct RepcaExt + { + static constexpr size_t IBRANCHR = static_cast(RepcaExternalVariables::IBRANCHR); + static constexpr size_t IBRANCHI = static_cast(RepcaExternalVariables::IBRANCHI); + static constexpr size_t PBRANCH = static_cast(RepcaExternalVariables::PBRANCH); + static constexpr size_t QBRANCH = static_cast(RepcaExternalVariables::QBRANCH); + static constexpr size_t FREQ = static_cast(RepcaExternalVariables::FREQ); + static constexpr size_t FREQREF = static_cast(RepcaExternalVariables::FREQREF); + static constexpr size_t VREF = static_cast(RepcaExternalVariables::VREF); + static constexpr size_t QREF = static_cast(RepcaExternalVariables::QREF); + static constexpr size_t PPLANTREF = static_cast(RepcaExternalVariables::PPLANTREF); + static constexpr size_t MAXIMUM = static_cast(RepcaExternalVariables::MAXIMUM); + }; + template class Repca : public Component { + using Component::alpha_; using Component::abs_tol_; using Component::allocated_; - using Component::alpha_; using Component::f_; using Component::gridkit_component_id_; using Component::J_cols_buffer_; @@ -132,24 +175,30 @@ namespace GridKit void initializeMonitor(); void setDerivedParameters(); + bool solveLimiterInput(ScalarT requested_output, + RealT lower_limit, + RealT upper_limit, + ScalarT& limiter_input) const; + + /// Evaluate log(1 - exp(-x)) without cancellation for positive x. + RealT logOneMinusExp(RealT x) const; + ScalarT toComponentBase(ScalarT value) const; ScalarT toSystemBase(ScalarT value) const; - ScalarT& Vr() - { - return bus_->Vr(); - } + ScalarT& Vr(); + ScalarT& Vi(); - ScalarT& Vi() - { - return bus_->Vi(); - } + static constexpr RealT TIME_CONSTANT_MINIMUM = static_cast(1.0e-3); + static constexpr RealT INITIALIZATION_TOLERANCE = static_cast(1.0e-10); - static constexpr RealT TIME_CONSTANT_MINIMUM = static_cast(1.0e-3); + /// Distance past a smooth-clamp bound; with CommonMath MU = 240, + /// this keeps exact-limit initialization residuals below 2e-13. + static constexpr RealT INITIALIZATION_LIMIT_OFFSET = static_cast(0.1); BusT* bus_{nullptr}; - RealT mva_{static_cast(100.0)}; + RealT mva_base_{static_cast(100.0)}; bool VcompFlag_{true}; bool RefFlag_{true}; bool Freqflag_{false}; @@ -182,7 +231,7 @@ namespace GridKit RealT Tlag_{static_cast(3.0)}; IdxT parameter_error_count_{0}; - RealT va_component_base_{ZERO}; + RealT va_converter_base_{ZERO}; RealT vcomp_on_{ONE}; RealT vcomp_off_{ZERO}; RealT ref_on_{ONE}; diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp index b507d88ac..4b76b5ea7 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp @@ -1,7 +1,7 @@ /** * @file RepcaData.hpp * @author Luke Lowery (lukel@tamu.edu) - * @brief Modeling data for the REPCA converter plant-control model. + * @brief Modeling data for the REPCA plant-control model. */ #pragma once @@ -22,7 +22,7 @@ namespace GridKit RefFlag, ///< Reactive-loop reference flag Freqflag, ///< Active-power control output flag Tfltr, ///< Voltage and reactive-power measurement filter time constant - Vfrz, ///< Reactive-PI freeze voltage threshold + Vfrz, ///< Reactive-power PI freeze voltage threshold Rc, ///< Line-drop compensation resistance Xc, ///< Line-drop compensation reactance Kc, ///< Reactive-current compensation gain @@ -60,23 +60,23 @@ namespace GridKit /// Signal inputs for the REPCA plant-control model. enum class RepcaSignalInputs : size_t { - ibranchr, ///< Branch current real-component signal ID - ibranchi, ///< Branch current imaginary-component signal ID - pbranch, ///< Branch active-power signal ID - qbranch, ///< Branch reactive-power signal ID - freq, ///< Frequency input signal ID - freqref, ///< Frequency-reference signal ID - vref, ///< Voltage-reference signal ID - qref, ///< Reactive-power reference signal ID - pplantref, ///< Plant active-power reference signal ID + ibranchr, ///< Required branch-current real component signal ID + ibranchi, ///< Required branch-current imaginary component signal ID + pbranch, ///< Required branch active-power signal ID + qbranch, ///< Required branch reactive-power signal ID + freq, ///< Required frequency input signal ID + freqref, ///< Optional frequency-reference signal ID + vref, ///< Optional voltage-reference signal ID + qref, ///< Optional reactive-power reference signal ID + pplantref, ///< Optional plant active-power reference signal ID SIZE }; /// Signal outputs for the REPCA plant-control model. enum class RepcaSignalOutputs : size_t { - qext, ///< Reactive-power command output signal ID - pext, ///< Active-power command output signal ID + qext, ///< Optional reactive-power command output signal ID + pext, ///< Optional active-power command output signal ID SIZE }; diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaDependencyTracking.cpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaDependencyTracking.cpp index 7b8365122..30d1a5685 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaDependencyTracking.cpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaDependencyTracking.cpp @@ -15,8 +15,8 @@ namespace GridKit template int Repca::evaluateJacobian() { - Log::misc() << "Evaluate Jacobian for Repca..." << std::endl; - Log::misc() << "Jacobian evaluation is not implemented!" << std::endl; + Log::misc() << "Evaluate Jacobian for Repca...\n"; + Log::misc() << "Jacobian evaluation is not implemented!\n"; return 0; } diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp index e484ac001..8fc08c2b0 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp @@ -17,31 +17,32 @@ namespace GridKit template int Repca::evaluateJacobian() { - Log::misc() << "Evaluate Jacobian for Repca..." << std::endl; - Log::misc() << "Jacobian evaluation is experimental!" << std::endl; + Log::misc() << "Evaluate Jacobian for Repca...\n"; + Log::misc() << "Jacobian evaluation is experimental!\n"; if (J_rows_buffer_ == nullptr) { - auto size = static_cast(size_); - auto bus_size = static_cast(bus_->size()); - auto signal_size = static_cast(ws_.size()); - auto buffer_size = 2 * size * size + size * bus_size + size * signal_size; - J_rows_buffer_ = new IdxT[buffer_size]; - J_cols_buffer_ = new IdxT[buffer_size]; - J_vals_buffer_ = new RealT[buffer_size]; + const auto size = static_cast(size_); + const auto bus_size = static_cast(bus_->size()); + const auto signal_size = ws_.size(); + const auto buffer_size = 2 * size * size + size * bus_size + size * signal_size; + + J_rows_buffer_ = new IdxT[buffer_size]; + J_cols_buffer_ = new IdxT[buffer_size]; + J_vals_buffer_ = new RealT[buffer_size]; } - using RepcaT = GridKit::PhasorDynamics::Converter::Repca; + using ModelT = GridKit::PhasorDynamics::Converter::Repca; using Fn = GridKit::Enzyme::Sparse::MemberFunctions; nnz_ = 0; - GridKit::Enzyme::Sparse::DfDy::eval( + GridKit::Enzyme::Sparse::DfDy::eval( this, static_cast(f_.getSize()), static_cast(y_.getSize()), - (this->getResidualIndices()).data(), - (this->getVariableIndices()).data(), + this->getResidualIndices().data(), + this->getVariableIndices().data(), y_.getData(), yp_.getData(), wb_.data(), @@ -51,12 +52,12 @@ namespace GridKit J_vals_buffer_, nnz_); - GridKit::Enzyme::Sparse::DfDyp::eval( + GridKit::Enzyme::Sparse::DfDyp::eval( this, static_cast(f_.getSize()), static_cast(y_.getSize()), - (this->getResidualIndices()).data(), - (this->getVariableIndices()).data(), + this->getResidualIndices().data(), + this->getVariableIndices().data(), y_.getData(), yp_.getData(), wb_.data(), @@ -67,12 +68,12 @@ namespace GridKit J_vals_buffer_, nnz_); - GridKit::Enzyme::Sparse::DfDwb::eval( + GridKit::Enzyme::Sparse::DfDwb::eval( this, static_cast(f_.getSize()), static_cast(bus_->size()), - (this->getResidualIndices()).data(), - (bus_->getVariableIndices()).data(), + this->getResidualIndices().data(), + bus_->getVariableIndices().data(), y_.getData(), yp_.getData(), wb_.data(), @@ -82,11 +83,11 @@ namespace GridKit J_vals_buffer_, nnz_); - GridKit::Enzyme::Sparse::DfDws::eval( + GridKit::Enzyme::Sparse::DfDws::eval( this, static_cast(f_.getSize()), ws_.size(), - (this->getResidualIndices()).data(), + this->getResidualIndices().data(), ws_indices_.data(), y_.getData(), yp_.getData(), @@ -96,7 +97,6 @@ namespace GridKit J_cols_buffer_, J_vals_buffer_, nnz_); - this->constructCoo(); return 0; diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp index b5b888d54..d491f8197 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp @@ -25,14 +25,23 @@ namespace GridKit { using Log = ::GridKit::Utilities::Logger; + /** + * @brief Construct REPCA with its documented parameter defaults. + * @param[in] bus Regulated bus measured by the controller. + */ template Repca::Repca(BusT* bus) : bus_(bus) { - size_ = static_cast(RepcaInternalVariables::MAXIMUM); + size_ = static_cast(RepcaIdx::MAXIMUM); setDerivedParameters(); } + /** + * @brief Construct REPCA from parameters and monitor selections. + * @param[in] bus Regulated bus measured by the controller. + * @param[in] data Model parameters and monitor selections. + */ template Repca::Repca(BusT* bus, const ModelDataT& data) : bus_(bus), @@ -40,8 +49,7 @@ namespace GridKit { initializeParameters(data); initializeMonitor(); - size_ = static_cast(RepcaInternalVariables::MAXIMUM); - setDerivedParameters(); + size_ = static_cast(RepcaIdx::MAXIMUM); } template @@ -49,6 +57,26 @@ namespace GridKit { } + /// Regulated-bus voltage, real component. + template + scalar_type& Repca::Vr() + { + return bus_->Vr(); + } + + /// Regulated-bus voltage, imaginary component. + template + scalar_type& Repca::Vi() + { + return bus_->Vi(); + } + + /** + * @brief Load provided parameters and retain documented defaults for omissions. + * + * Numeric parameters accept real or integer values. Mode switches accept + * bool and numeric 0/1 values; invalid types are counted for verify(). + */ template void Repca::initializeParameters(const ModelDataT& data) { @@ -97,9 +125,9 @@ namespace GridKit target = (*index_value == 1); } else if (const auto* real_value = std::get_if(&value); - real_value && (*real_value == ZERO || *real_value == ONE) ) + real_value && (*real_value == RealT{0} || *real_value == RealT{1})) { - target = (*real_value == ONE); + target = (*real_value == RealT{1}); } else { @@ -108,7 +136,7 @@ namespace GridKit } }; - load_real(Params::mva, mva_, "mva"); + load_real(Params::mva, mva_base_, "mva"); load_switch(Params::VcompFlag, VcompFlag_, "VcompFlag"); load_switch(Params::RefFlag, RefFlag_, "RefFlag"); load_switch(Params::Freqflag, Freqflag_, "Freqflag"); @@ -139,16 +167,27 @@ namespace GridKit load_real(Params::Pmax, Pmax_, "Pmax"); load_real(Params::Pmin, Pmin_, "Pmin"); load_real(Params::Tlag, Tlag_, "Tlag"); + setDerivedParameters(); } + /** + * @brief Resolve time floors, the component power base, and mode masks. + */ template void Repca::setDerivedParameters() { + if (Tfltr_ < TIME_CONSTANT_MINIMUM || Tp_ < TIME_CONSTANT_MINIMUM + || Tlag_ < TIME_CONSTANT_MINIMUM) + { + Log::warning() << "Repca: Tfltr, Tp, and Tlag below " << TIME_CONSTANT_MINIMUM + << " s are raised to that floor to keep the controller lags well posed\n"; + } + Tfltr_ = std::max(Tfltr_, TIME_CONSTANT_MINIMUM); Tp_ = std::max(Tp_, TIME_CONSTANT_MINIMUM); Tlag_ = std::max(Tlag_, TIME_CONSTANT_MINIMUM); - va_component_base_ = mva_ * static_cast(1.0e6); + va_converter_base_ = mva_base_ * static_cast(1.0e6); vcomp_on_ = VcompFlag_ ? ONE : ZERO; vcomp_off_ = ONE - vcomp_on_; @@ -157,45 +196,110 @@ namespace GridKit freq_on_ = Freqflag_ ? ONE : ZERO; } + /// Evaluate log(1 - exp(-x)) accurately for a positive argument. + template + typename Repca::RealT + Repca::logOneMinusExp(RealT x) const + { + const RealT log_two = std::log(static_cast(2.0)); + + if (x < log_two) + { + return log_two - HALF * x + std::log(std::sinh(HALF * x)); + } + return std::log1p(-std::exp(-x)); + } + + /** + * @brief Recover an input for a requested smooth-clamp output. + * @return false when the limits are invalid or the requested output lies outside them. + */ + template + bool Repca::solveLimiterInput( + ScalarT requested_output, + RealT lower_limit, + RealT upper_limit, + ScalarT& limiter_input) const + { + const RealT output_value = static_cast(requested_output); + + if (lower_limit > upper_limit || output_value < lower_limit || output_value > upper_limit) + { + return false; + } + + const RealT width = upper_limit - lower_limit; + if (width <= INITIALIZATION_TOLERANCE) + { + limiter_input = static_cast(lower_limit); + return true; + } + + const RealT distance_from_lower = output_value - lower_limit; + const RealT distance_from_upper = upper_limit - output_value; + if (distance_from_lower <= INITIALIZATION_TOLERANCE) + { + limiter_input = static_cast(lower_limit - INITIALIZATION_LIMIT_OFFSET); + return true; + } + if (distance_from_upper <= INITIALIZATION_TOLERANCE) + { + limiter_input = static_cast(upper_limit + INITIALIZATION_LIMIT_OFFSET); + return true; + } + + const RealT mu = Math::MU; + const RealT scaled_lower_distance = mu * distance_from_lower; + const RealT scaled_upper_distance = mu * distance_from_upper; + const RealT log_lower = logOneMinusExp(scaled_lower_distance); + const RealT log_upper = logOneMinusExp(scaled_upper_distance); + const RealT correction = (scaled_lower_distance + log_lower - log_upper) / mu; + + limiter_input = static_cast(lower_limit + correction); + return true; + } + + /// Convert a system-base power quantity to REPCA component base. template scalar_type Repca::toComponentBase(scalar_type value) const { - return value * va_system_base_ / va_component_base_; + return value * va_system_base_ / va_converter_base_; } + /// Convert a component-base power quantity to system base. template scalar_type Repca::toSystemBase(scalar_type value) const { return value / toComponentBase(static_cast(ONE)); } + /// Access the configured monitor, or nullptr for the default constructor. template const Model::VariableMonitorBase* Repca::getMonitor() const { return monitor_.get(); } + /// Bind monitor selections to REPCA state entries in public monitor order. template void Repca::initializeMonitor() { + using I = RepcaIdx; using Variable = typename ModelDataT::MonitorableVariables; - auto index = [](RepcaInternalVariables variable) - { - return static_cast(variable); - }; - monitor_->set(Variable::qext, [this, index] - { return y_.getData()[index(RepcaInternalVariables::QEXT)]; }); - monitor_->set(Variable::pext, [this, index] - { return y_.getData()[index(RepcaInternalVariables::PEXT)]; }); - monitor_->set(Variable::vmeas, [this, index] - { return y_.getData()[index(RepcaInternalVariables::VMEAS)]; }); - monitor_->set(Variable::qmeas, [this, index] - { return y_.getData()[index(RepcaInternalVariables::QMEAS)]; }); - monitor_->set(Variable::pmeas, [this, index] - { return y_.getData()[index(RepcaInternalVariables::PMEAS)]; }); + monitor_->set(Variable::qext, [this] + { return y_.getData()[I::QEXT]; }); + monitor_->set(Variable::pext, [this] + { return y_.getData()[I::PEXT]; }); + monitor_->set(Variable::vmeas, [this] + { return y_.getData()[I::VMEAS]; }); + monitor_->set(Variable::qmeas, [this] + { return y_.getData()[I::QMEAS]; }); + monitor_->set(Variable::pmeas, [this] + { return y_.getData()[I::PMEAS]; }); } + /// Set the component identifier used by monitor labels. template int Repca::setGridKitComponentID(IdxT component_id) { @@ -203,14 +307,21 @@ namespace GridKit return 0; } + /** + * @brief Allocate model buffers and bind assigned command-output nodes. + * @return 0 on success. + */ template int Repca::allocate() { + using I = RepcaIdx; + using E = RepcaExt; + if (!allocated_) { this->allocateVectors(size_); } - auto size = static_cast(size_); + const auto size = static_cast(size_); tag_.assign(size, false); variable_indices_.resize(size); @@ -218,7 +329,7 @@ namespace GridKit wb_.assign(2, ScalarT{0}); - auto signal_size = static_cast(RepcaExternalVariables::MAXIMUM); + const auto signal_size = E::MAXIMUM; ws_.assign(signal_size, ScalarT{0}); ws_indices_.assign(signal_size, INVALID_INDEX); @@ -228,26 +339,30 @@ namespace GridKit this->setResidualIndex(j, j); } + auto* y = y_.getData(); + if (signals_.template isAssigned()) { - auto* y = y_.getData(); signals_.template getSignalNode()->set( - &y[static_cast(RepcaInternalVariables::QEXT)], - &(this->getVariableIndex(static_cast(RepcaInternalVariables::QEXT)))); + &y[I::QEXT], + &(this->getVariableIndex(static_cast(I::QEXT)))); } if (signals_.template isAssigned()) { - auto* y = y_.getData(); signals_.template getSignalNode()->set( - &y[static_cast(RepcaInternalVariables::PEXT)], - &(this->getVariableIndex(static_cast(RepcaInternalVariables::PEXT)))); + &y[I::PEXT], + &(this->getVariableIndex(static_cast(I::PEXT)))); } allocated_ = true; return 0; } + /** + * @brief Validate parameters, the regulated bus, and signal connections. + * @return The number of configuration errors. + */ template int Repca::verify() const { @@ -268,7 +383,7 @@ namespace GridKit ret += 1; } - check(mva_ > ZERO, "mva must be positive"); + check(mva_base_ > ZERO, "mva must be positive"); check(Tfv_ > ZERO, "Tfv must be positive"); check(dbdlow_ <= ZERO && ZERO <= dbdupper_, "dbdlow <= 0 <= dbdupper is required"); @@ -281,113 +396,64 @@ namespace GridKit "femin <= 0 <= femax is required"); check(Pmin_ <= Pmax_, "Pmin must be less than or equal to Pmax"); - auto check_required_signal = [&](bool attached, bool linked, const char* name) + auto check_required_signal = [&](const char* name) { - if (!attached) + if (!signals_.template isAttached()) { Log::error() << "Repca: " << name << " signal is required\n"; ret += 1; } - else if (!linked) + else if (!signals_.template isLinked()) { Log::error() << "Repca: " << name << " signal attached with no linked source\n"; ret += 1; } }; - check_required_signal( - signals_.template isAttached(), - signals_.template isAttached() - && signals_.template isLinked(), - "ibranchr"); - check_required_signal( - signals_.template isAttached(), - signals_.template isAttached() - && signals_.template isLinked(), - "ibranchi"); - check_required_signal( - signals_.template isAttached(), - signals_.template isAttached() - && signals_.template isLinked(), - "pbranch"); - check_required_signal( - signals_.template isAttached(), - signals_.template isAttached() - && signals_.template isLinked(), - "qbranch"); - check_required_signal( - signals_.template isAttached(), - signals_.template isAttached() - && signals_.template isLinked(), - "freq"); - - auto check_optional_signal = [&](bool attached, bool linked, const char* name) + check_required_signal.template operator()("ibranchr"); + check_required_signal.template operator()("ibranchi"); + check_required_signal.template operator()("pbranch"); + check_required_signal.template operator()("qbranch"); + check_required_signal.template operator()("freq"); + + auto check_optional_signal = [&](const char* name) { - if (attached && !linked) + if (signals_.template isAttached() + && !signals_.template isLinked()) { Log::error() << "Repca: " << name << " signal attached with no linked source\n"; ret += 1; } }; - check_optional_signal( - signals_.template isAttached(), - signals_.template isAttached() - && signals_.template isLinked(), - "freqref"); - check_optional_signal( - signals_.template isAttached(), - signals_.template isAttached() - && signals_.template isLinked(), - "vref"); - check_optional_signal( - signals_.template isAttached(), - signals_.template isAttached() - && signals_.template isLinked(), - "qref"); - check_optional_signal( - signals_.template isAttached(), - signals_.template isAttached() - && signals_.template isLinked(), - "pplantref"); + check_optional_signal.template operator()("freqref"); + check_optional_signal.template operator()("vref"); + check_optional_signal.template operator()("qref"); + check_optional_signal.template operator()("pplantref"); return ret; } + /** + * @brief Initialize from seeded qext and, when frequency control is enabled, pext. + * + * All feasibility checks precede state, derivative, latch, and signal + * writes, so failure leaves model and signal storage unchanged. + * + * @pre allocate() has completed and verify() returned zero. + * @return 0 on success; nonzero for an inadmissible operating point. + */ template int Repca::initialize() { - if (verify() > 0) - { - Log::error() << "Repca: cannot initialize with invalid configuration\n"; - return 1; - } + using I = RepcaIdx; - auto* y = y_.getData(); - auto* yp = yp_.getData(); - - const auto VMEAS = static_cast(RepcaInternalVariables::VMEAS); - const auto QMEAS = static_cast(RepcaInternalVariables::QMEAS); - const auto XQPI = static_cast(RepcaInternalVariables::XQPI); - const auto XQLAG = static_cast(RepcaInternalVariables::XQLAG); - const auto PMEAS = static_cast(RepcaInternalVariables::PMEAS); - const auto XPPI = static_cast(RepcaInternalVariables::XPPI); - const auto PREF = static_cast(RepcaInternalVariables::PREF); - const auto V = static_cast(RepcaInternalVariables::V); - const auto VLDC = static_cast(RepcaInternalVariables::VLDC); - const auto VDROOP = static_cast(RepcaInternalVariables::VDROOP); - const auto VCTRL = static_cast(RepcaInternalVariables::VCTRL); - const auto SFRZ = static_cast(RepcaInternalVariables::SFRZ); - const auto ERQ = static_cast(RepcaInternalVariables::ERQ); - const auto ERQDB = static_cast(RepcaInternalVariables::ERQDB); - const auto ERQLIM = static_cast(RepcaInternalVariables::ERQLIM); - const auto QPI = static_cast(RepcaInternalVariables::QPI); - const auto QEXT = static_cast(RepcaInternalVariables::QEXT); - const auto EF = static_cast(RepcaInternalVariables::EF); - const auto EP = static_cast(RepcaInternalVariables::EP); - const auto EPLIM = static_cast(RepcaInternalVariables::EPLIM); - const auto PPI = static_cast(RepcaInternalVariables::PPI); - const auto PEXT = static_cast(RepcaInternalVariables::PEXT); + auto* y = y_.getData(); + + const ScalarT qext0_system = y[I::QEXT]; + const ScalarT pext0_system = y[I::PEXT]; + const ScalarT qext0 = toComponentBase(qext0_system); + const ScalarT pext0 = toComponentBase(pext0_system); const ScalarT vr = Vr(); const ScalarT vi = Vi(); @@ -395,64 +461,124 @@ namespace GridKit signals_.template readExternalVariable(); const ScalarT ibranchi = signals_.template readExternalVariable(); - const ScalarT pbranch = + const ScalarT pbranch_system = signals_.template readExternalVariable(); - const ScalarT qbranch = + const ScalarT qbranch_system = signals_.template readExternalVariable(); - const ScalarT freq = - signals_.template readExternalVariable(); + const ScalarT freq = signals_.template readExternalVariable(); + + auto is_finite = [](ScalarT value) + { + return std::isfinite(static_cast(value)); + }; + if (!is_finite(vr) || !is_finite(vi) || !is_finite(ibranchr) + || !is_finite(ibranchi) || !is_finite(pbranch_system) + || !is_finite(qbranch_system) + || !is_finite(freq) || !is_finite(qext0) + || (Freqflag_ && !is_finite(pext0))) + { + Log::error() << "Repca: initial bus, signal, and command values must be finite\n"; + return 1; + } - const ScalarT qext0 = toComponentBase(y[QEXT]); - const ScalarT pext0 = toComponentBase(y[PEXT]); + const ScalarT pbranch = toComponentBase(pbranch_system); + const ScalarT qbranch = toComponentBase(qbranch_system); const ScalarT vldc_r = vr - Rc_ * ibranchr + Xc_ * ibranchi; const ScalarT vldc_i = vi - Rc_ * ibranchi - Xc_ * ibranchr; - y[V] = std::sqrt(vr * vr + vi * vi); - y[VLDC] = std::sqrt(vldc_r * vldc_r + vldc_i * vldc_i); - y[VDROOP] = y[V] + Kc_ * toComponentBase(qbranch); - y[VCTRL] = vcomp_on_ * y[VLDC] + vcomp_off_ * y[VDROOP]; - - y[EF] = ZERO; - const ScalarT pfreq0 = Ddn_ * Math::ramp(y[EF]) - Dup_ * Math::ramp(-y[EF]); - - y[VMEAS] = y[VCTRL]; - y[QMEAS] = toComponentBase(qbranch); - y[PMEAS] = toComponentBase(pbranch); - y[SFRZ] = Math::above(y[V], Vfrz_); - - y[ERQ] = ZERO; - y[ERQDB] = Math::deadband2(y[ERQ], dbdlow_, dbdupper_); - y[ERQLIM] = Math::clamp(y[ERQDB], emin_, emax_); - y[QPI] = qext0; - y[XQLAG] = qext0; - y[QEXT] = toSystemBase(qext0); - y[XQPI] = qext0 - Kp_ * y[ERQLIM]; - - y[EP] = ZERO; - y[EPLIM] = Math::clamp(y[EP], femin_, femax_); - y[PREF] = Freqflag_ ? pext0 : Math::clamp(y[PMEAS], Pmin_, Pmax_); - y[PPI] = y[PREF]; - y[XPPI] = y[PREF] - Kpg_ * y[EPLIM]; - y[PEXT] = toSystemBase(freq_on_ * y[PREF]); - - const ScalarT q_aw = Math::antiwindup(y[QPI], Ki_ * y[ERQLIM], Qmin_, Qmax_); - const ScalarT p_aw = Math::antiwindup(y[PPI], Kig_ * y[EPLIM], Pmin_, Pmax_); - if (std::abs(static_cast(q_aw)) > static_cast(1.0e-10)) + const ScalarT v0 = std::sqrt(vr * vr + vi * vi); + const ScalarT vldc0 = std::sqrt(vldc_r * vldc_r + vldc_i * vldc_i); + const ScalarT vdroop0 = v0 + Kc_ * qbranch; + const ScalarT vctrl0 = vcomp_on_ * vldc0 + vcomp_off_ * vdroop0; + const ScalarT vmeas0 = vctrl0; + const ScalarT qmeas0 = qbranch; + const ScalarT pmeas0 = pbranch; + const ScalarT sfrz0 = Math::above(v0, Vfrz_); + + const ScalarT zero = static_cast(ZERO); + const ScalarT erq0 = zero; + const ScalarT erqdb0 = Math::deadband2(erq0, dbdlow_, dbdupper_); + const ScalarT erqlim0 = Math::clamp(erqdb0, emin_, emax_); + const ScalarT qpi0 = qext0; + const ScalarT xqlag0 = qpi0; + + ScalarT qpi_input0{}; + if (!solveLimiterInput(qpi0, Qmin_, Qmax_, qpi_input0)) { - Log::error() << "Repca: reactive PI antiwindup rate is nonzero at initialization\n"; + Log::error() << "Repca: initial reactive-power command is outside Qmin/Qmax\n"; return 1; } - if (std::abs(static_cast(p_aw)) > static_cast(1.0e-10)) + const ScalarT xqpi0 = qpi_input0 - Kp_ * erqlim0; + const ScalarT q_aw_rate0 = Math::antiwindup(qpi0, Ki_ * erqlim0, Qmin_, Qmax_); + if (!is_finite(q_aw_rate0) + || std::abs(static_cast(q_aw_rate0)) > INITIALIZATION_TOLERANCE) { - Log::error() << "Repca: active PI antiwindup rate is nonzero at initialization\n"; + Log::error() << "Repca: reactive-power PI antiwindup rate is nonzero at initialization\n"; return 1; } - freqref_set_ = freq; - vref_set_ = y[VMEAS]; - qref_set_ = toSystemBase(y[QMEAS]); - pplantref_set_ = toSystemBase(y[PMEAS] - pfreq0); + const ScalarT ef0 = Math::deadband2(zero, fdbd1_, fdbd2_); + const ScalarT pfreq0 = Ddn_ * Math::ramp(ef0) - Dup_ * Math::ramp(-ef0); + const ScalarT ep0 = zero; + const ScalarT eplim0 = Math::clamp(ep0, femin_, femax_); + const ScalarT pref0 = Freqflag_ ? pext0 : Math::clamp(pmeas0, Pmin_, Pmax_); + const ScalarT ppi0 = pref0; + ScalarT ppi_input0{}; + if (Freqflag_) + { + if (!solveLimiterInput(ppi0, Pmin_, Pmax_, ppi_input0)) + { + Log::error() << "Repca: initial active-power command is outside Pmin/Pmax\n"; + return 1; + } + } + else + { + ppi_input0 = pmeas0; + } + const ScalarT xppi0 = ppi_input0 - Kpg_ * eplim0; + const ScalarT p_aw_rate0 = Math::antiwindup(ppi0, Kig_ * eplim0, Pmin_, Pmax_); + if (!is_finite(p_aw_rate0) + || std::abs(static_cast(p_aw_rate0)) > INITIALIZATION_TOLERANCE) + { + Log::error() << "Repca: active-power PI antiwindup rate is nonzero at initialization\n"; + return 1; + } + + const ScalarT pext_output0 = Freqflag_ ? pext0_system : zero; + const ScalarT freqref0 = freq; + const ScalarT vref0 = vmeas0; + const ScalarT qref0_system = qbranch_system; + const ScalarT pplantref0_system = toSystemBase(pmeas0 - pfreq0); + + y[I::VMEAS] = vmeas0; + y[I::QMEAS] = qmeas0; + y[I::XQPI] = xqpi0; + y[I::XQLAG] = xqlag0; + y[I::PMEAS] = pmeas0; + y[I::XPPI] = xppi0; + y[I::PREF] = pref0; + y[I::V] = v0; + y[I::VLDC] = vldc0; + y[I::VDROOP] = vdroop0; + y[I::VCTRL] = vctrl0; + y[I::SFRZ] = sfrz0; + y[I::ERQ] = erq0; + y[I::ERQDB] = erqdb0; + y[I::ERQLIM] = erqlim0; + y[I::QPI] = qpi0; + y[I::QEXT] = qext0_system; + y[I::EF] = ef0; + y[I::EP] = ep0; + y[I::EPLIM] = eplim0; + y[I::PPI] = ppi0; + y[I::PEXT] = pext_output0; + + freqref_set_ = freqref0; + vref_set_ = vref0; + qref_set_ = qref0_system; + pplantref_set_ = pplantref0_system; if (signals_.template isAttached()) { @@ -473,30 +599,35 @@ namespace GridKit pplantref_set_); } - for (IdxT i = 0; i < yp_.getSize(); ++i) - { - yp[i] = ZERO; - } - y_.setDataUpdated(); - yp_.setDataUpdated(); + yp_.setToConst(static_cast(ZERO)); return 0; } + /** + * @brief Tag the seven controller states as differential variables. + * @return 0 on success. + */ template int Repca::tagDifferentiable() { + using I = RepcaIdx; + std::fill(tag_.begin(), tag_.end(), false); - tag_[static_cast(RepcaInternalVariables::VMEAS)] = true; - tag_[static_cast(RepcaInternalVariables::QMEAS)] = true; - tag_[static_cast(RepcaInternalVariables::XQPI)] = true; - tag_[static_cast(RepcaInternalVariables::XQLAG)] = true; - tag_[static_cast(RepcaInternalVariables::PMEAS)] = true; - tag_[static_cast(RepcaInternalVariables::XPPI)] = true; - tag_[static_cast(RepcaInternalVariables::PREF)] = true; + tag_[I::VMEAS] = true; + tag_[I::QMEAS] = true; + tag_[I::XQPI] = true; + tag_[I::XQLAG] = true; + tag_[I::PMEAS] = true; + tag_[I::XPPI] = true; + tag_[I::PREF] = true; return 0; } + /** + * @brief Set the common absolute-tolerance floor for all REPCA variables. + * @return 0 on success. + */ template int Repca::setAbsoluteTolerance(RealT rel_tol) { @@ -504,202 +635,161 @@ namespace GridKit return 0; } + /** + * @brief Evaluate the seven differential and fifteen algebraic rows. + * + * Mode selections enter through fixed masks so the residual remains + * branch-free for sparse automatic differentiation. + */ template - __attribute__((always_inline)) inline int Repca::evaluateInternalResidual( + __attribute__((always_inline)) inline int + Repca::evaluateInternalResidual( const ScalarT* y, const ScalarT* yp, const ScalarT* wb, const ScalarT* ws, ScalarT* f) { - const auto VMEAS = static_cast(RepcaInternalVariables::VMEAS); - const auto QMEAS = static_cast(RepcaInternalVariables::QMEAS); - const auto XQPI = static_cast(RepcaInternalVariables::XQPI); - const auto XQLAG = static_cast(RepcaInternalVariables::XQLAG); - const auto PMEAS = static_cast(RepcaInternalVariables::PMEAS); - const auto XPPI = static_cast(RepcaInternalVariables::XPPI); - const auto PREF = static_cast(RepcaInternalVariables::PREF); - const auto V = static_cast(RepcaInternalVariables::V); - const auto VLDC = static_cast(RepcaInternalVariables::VLDC); - const auto VDROOP = static_cast(RepcaInternalVariables::VDROOP); - const auto VCTRL = static_cast(RepcaInternalVariables::VCTRL); - const auto SFRZ = static_cast(RepcaInternalVariables::SFRZ); - const auto ERQ = static_cast(RepcaInternalVariables::ERQ); - const auto ERQDB = static_cast(RepcaInternalVariables::ERQDB); - const auto ERQLIM = static_cast(RepcaInternalVariables::ERQLIM); - const auto QPI = static_cast(RepcaInternalVariables::QPI); - const auto QEXT = static_cast(RepcaInternalVariables::QEXT); - const auto EF = static_cast(RepcaInternalVariables::EF); - const auto EP = static_cast(RepcaInternalVariables::EP); - const auto EPLIM = static_cast(RepcaInternalVariables::EPLIM); - const auto PPI = static_cast(RepcaInternalVariables::PPI); - const auto PEXT = static_cast(RepcaInternalVariables::PEXT); - - const auto IBRANCHR = static_cast(RepcaExternalVariables::IBRANCHR); - const auto IBRANCHI = static_cast(RepcaExternalVariables::IBRANCHI); - const auto PBRANCH = static_cast(RepcaExternalVariables::PBRANCH); - const auto QBRANCH = static_cast(RepcaExternalVariables::QBRANCH); - const auto FREQ = static_cast(RepcaExternalVariables::FREQ); - const auto FREQREF = static_cast(RepcaExternalVariables::FREQREF); - const auto VREF = static_cast(RepcaExternalVariables::VREF); - const auto QREF = static_cast(RepcaExternalVariables::QREF); - const auto PPLANTREF = static_cast(RepcaExternalVariables::PPLANTREF); - - const ScalarT vmeas = y[VMEAS]; - const ScalarT qmeas = y[QMEAS]; - const ScalarT xqpi = y[XQPI]; - const ScalarT xqlag = y[XQLAG]; - const ScalarT pmeas = y[PMEAS]; - const ScalarT xppi = y[XPPI]; - const ScalarT pref = y[PREF]; - const ScalarT v = y[V]; - const ScalarT vldc = y[VLDC]; - const ScalarT vdroop = y[VDROOP]; - const ScalarT vctrl = y[VCTRL]; - const ScalarT sfrz = y[SFRZ]; - const ScalarT erq = y[ERQ]; - const ScalarT erqdb = y[ERQDB]; - const ScalarT erqlim = y[ERQLIM]; - const ScalarT qpi = y[QPI]; - const ScalarT qext = toComponentBase(y[QEXT]); - const ScalarT ef = y[EF]; - const ScalarT ep = y[EP]; - const ScalarT eplim = y[EPLIM]; - const ScalarT ppi = y[PPI]; - const ScalarT pext = toComponentBase(y[PEXT]); - - const ScalarT vmeas_dot = yp[VMEAS]; - const ScalarT qmeas_dot = yp[QMEAS]; - const ScalarT xqpi_dot = yp[XQPI]; - const ScalarT xqlag_dot = yp[XQLAG]; - const ScalarT pmeas_dot = yp[PMEAS]; - const ScalarT xppi_dot = yp[XPPI]; - const ScalarT pref_dot = yp[PREF]; + using I = RepcaIdx; + using E = RepcaExt; + + const ScalarT vmeas = y[I::VMEAS]; + const ScalarT qmeas = y[I::QMEAS]; + const ScalarT xqpi = y[I::XQPI]; + const ScalarT xqlag = y[I::XQLAG]; + const ScalarT pmeas = y[I::PMEAS]; + const ScalarT xppi = y[I::XPPI]; + const ScalarT pref = y[I::PREF]; + const ScalarT v = y[I::V]; + const ScalarT vldc = y[I::VLDC]; + const ScalarT vdroop = y[I::VDROOP]; + const ScalarT vctrl = y[I::VCTRL]; + const ScalarT sfrz = y[I::SFRZ]; + const ScalarT erq = y[I::ERQ]; + const ScalarT erqdb = y[I::ERQDB]; + const ScalarT erqlim = y[I::ERQLIM]; + const ScalarT qpi = y[I::QPI]; + const ScalarT qext = toComponentBase(y[I::QEXT]); + const ScalarT ef = y[I::EF]; + const ScalarT ep = y[I::EP]; + const ScalarT eplim = y[I::EPLIM]; + const ScalarT ppi = y[I::PPI]; + const ScalarT pext = toComponentBase(y[I::PEXT]); + + const ScalarT vmeas_dot = yp[I::VMEAS]; + const ScalarT qmeas_dot = yp[I::QMEAS]; + const ScalarT xqpi_dot = yp[I::XQPI]; + const ScalarT xqlag_dot = yp[I::XQLAG]; + const ScalarT pmeas_dot = yp[I::PMEAS]; + const ScalarT xppi_dot = yp[I::XPPI]; + const ScalarT pref_dot = yp[I::PREF]; const ScalarT vr = wb[0]; const ScalarT vi = wb[1]; - const ScalarT ibranchr = ws[IBRANCHR]; - const ScalarT ibranchi = ws[IBRANCHI]; - const ScalarT pbranch = ws[PBRANCH]; - const ScalarT qbranch = ws[QBRANCH]; - const ScalarT freq = ws[FREQ]; - const ScalarT freqref = ws[FREQREF]; - const ScalarT vref = ws[VREF]; - const ScalarT qref = toComponentBase(ws[QREF]); - const ScalarT pplantref = toComponentBase(ws[PPLANTREF]); + const ScalarT ibranchr = ws[E::IBRANCHR]; + const ScalarT ibranchi = ws[E::IBRANCHI]; + const ScalarT pbranch = toComponentBase(ws[E::PBRANCH]); + const ScalarT qbranch = toComponentBase(ws[E::QBRANCH]); + const ScalarT freq = ws[E::FREQ]; + const ScalarT freqref = ws[E::FREQREF]; + const ScalarT vref = ws[E::VREF]; + const ScalarT qref = toComponentBase(ws[E::QREF]); + const ScalarT pplantref = toComponentBase(ws[E::PPLANTREF]); const ScalarT vldc_r = vr - Rc_ * ibranchr + Xc_ * ibranchi; const ScalarT vldc_i = vi - Rc_ * ibranchi - Xc_ * ibranchr; const ScalarT pfreq = Ddn_ * Math::ramp(ef) - Dup_ * Math::ramp(-ef); - f[VMEAS] = -vmeas_dot + (vctrl - vmeas) / Tfltr_; - f[QMEAS] = -qmeas_dot + (toComponentBase(qbranch) - qmeas) / Tfltr_; - f[XQPI] = -xqpi_dot + sfrz * Math::antiwindup(qpi, Ki_ * erqlim, Qmin_, Qmax_); - f[XQLAG] = -xqlag_dot + (qpi - xqlag) / Tfv_; - f[PMEAS] = -pmeas_dot + (toComponentBase(pbranch) - pmeas) / Tp_; - f[XPPI] = -xppi_dot + Math::antiwindup(ppi, Kig_ * eplim, Pmin_, Pmax_); - f[PREF] = -pref_dot + (ppi - pref) / Tlag_; - - f[V] = -v * v + vr * vr + vi * vi; - f[VLDC] = -vldc * vldc + vldc_r * vldc_r + vldc_i * vldc_i; - f[VDROOP] = -vdroop + v + Kc_ * toComponentBase(qbranch); - f[VCTRL] = -vctrl + vcomp_on_ * vldc + vcomp_off_ * vdroop; - f[SFRZ] = -sfrz + Math::above(v, Vfrz_); - f[ERQ] = -erq + ref_on_ * (vref - vmeas) + ref_off_ * (qref - qmeas); - f[ERQDB] = -erqdb + Math::deadband2(erq, dbdlow_, dbdupper_); - f[ERQLIM] = -erqlim + Math::clamp(erqdb, emin_, emax_); - f[QPI] = -qpi + Math::clamp(Kp_ * erqlim + xqpi, Qmin_, Qmax_); - f[QEXT] = -Tfv_ * (qext - xqlag) + Tft_ * (qpi - xqlag); - - f[EF] = -ef + Math::deadband2(freqref - freq, fdbd1_, fdbd2_); - f[EP] = -ep + pplantref - pmeas + pfreq; - f[EPLIM] = -eplim + Math::clamp(ep, femin_, femax_); - f[PPI] = -ppi + Math::clamp(Kpg_ * eplim + xppi, Pmin_, Pmax_); - f[PEXT] = -pext + freq_on_ * pref; + f[I::VMEAS] = -vmeas_dot + (vctrl - vmeas) / Tfltr_; + f[I::QMEAS] = -qmeas_dot + (qbranch - qmeas) / Tfltr_; + f[I::XQPI] = -xqpi_dot + sfrz * Math::antiwindup(qpi, Ki_ * erqlim, Qmin_, Qmax_); + f[I::XQLAG] = -xqlag_dot + (qpi - xqlag) / Tfv_; + f[I::PMEAS] = -pmeas_dot + (pbranch - pmeas) / Tp_; + f[I::XPPI] = -xppi_dot + Math::antiwindup(ppi, Kig_ * eplim, Pmin_, Pmax_); + f[I::PREF] = -pref_dot + (ppi - pref) / Tlag_; + + f[I::V] = -v * v + vr * vr + vi * vi; + f[I::VLDC] = -vldc * vldc + vldc_r * vldc_r + vldc_i * vldc_i; + f[I::VDROOP] = -vdroop + v + Kc_ * qbranch; + f[I::VCTRL] = -vctrl + vcomp_on_ * vldc + vcomp_off_ * vdroop; + f[I::SFRZ] = -sfrz + Math::above(v, Vfrz_); + f[I::ERQ] = -erq + ref_on_ * (vref - vmeas) + ref_off_ * (qref - qmeas); + f[I::ERQDB] = -erqdb + Math::deadband2(erq, dbdlow_, dbdupper_); + f[I::ERQLIM] = -erqlim + Math::clamp(erqdb, emin_, emax_); + f[I::QPI] = -qpi + Math::clamp(Kp_ * erqlim + xqpi, Qmin_, Qmax_); + f[I::QEXT] = -Tfv_ * (qext - xqlag) + Tft_ * (qpi - xqlag); + + f[I::EF] = -ef + Math::deadband2(freqref - freq, fdbd1_, fdbd2_); + f[I::EP] = -ep + pplantref - pmeas + pfreq; + f[I::EPLIM] = -eplim + Math::clamp(ep, femin_, femax_); + f[I::PPI] = -ppi + Math::clamp(Kpg_ * eplim + xppi, Pmin_, Pmax_); + f[I::PEXT] = -pext + freq_on_ * pref; return 0; } + /** + * @brief Refresh bus and signal buffers and evaluate the REPCA residual. + * + * Required measurements are read directly; unattached optional references + * use the values latched by initialize(). REPCA has no bus residual. + * + * @return 0 on success. + */ template int Repca::evaluateResidual() { - const auto IBRANCHR = static_cast(RepcaExternalVariables::IBRANCHR); - const auto IBRANCHI = static_cast(RepcaExternalVariables::IBRANCHI); - const auto PBRANCH = static_cast(RepcaExternalVariables::PBRANCH); - const auto QBRANCH = static_cast(RepcaExternalVariables::QBRANCH); - const auto FREQ = static_cast(RepcaExternalVariables::FREQ); - const auto FREQREF = static_cast(RepcaExternalVariables::FREQREF); - const auto VREF = static_cast(RepcaExternalVariables::VREF); - const auto QREF = static_cast(RepcaExternalVariables::QREF); - const auto PPLANTREF = static_cast(RepcaExternalVariables::PPLANTREF); - - std::fill(ws_.begin(), ws_.end(), ZERO); + using E = RepcaExt; + + ws_[E::FREQREF] = freqref_set_; + ws_[E::VREF] = vref_set_; + ws_[E::QREF] = qref_set_; + ws_[E::PPLANTREF] = pplantref_set_; std::fill(ws_indices_.begin(), ws_indices_.end(), INVALID_INDEX); - ws_[FREQREF] = freqref_set_; - ws_[VREF] = vref_set_; - ws_[QREF] = qref_set_; - ws_[PPLANTREF] = pplantref_set_; + ws_[E::IBRANCHR] = + signals_.template readExternalVariable(); + ws_indices_[E::IBRANCHR] = + signals_.template readExternalVariableIndex(); + ws_[E::IBRANCHI] = + signals_.template readExternalVariable(); + ws_indices_[E::IBRANCHI] = + signals_.template readExternalVariableIndex(); + ws_[E::PBRANCH] = signals_.template readExternalVariable(); + ws_indices_[E::PBRANCH] = + signals_.template readExternalVariableIndex(); + ws_[E::QBRANCH] = signals_.template readExternalVariable(); + ws_indices_[E::QBRANCH] = + signals_.template readExternalVariableIndex(); + ws_[E::FREQ] = signals_.template readExternalVariable(); + ws_indices_[E::FREQ] = + signals_.template readExternalVariableIndex(); - if (signals_.template isAttached()) - { - ws_[IBRANCHR] = - signals_.template readExternalVariable(); - ws_indices_[IBRANCHR] = - signals_.template readExternalVariableIndex(); - } - if (signals_.template isAttached()) - { - ws_[IBRANCHI] = - signals_.template readExternalVariable(); - ws_indices_[IBRANCHI] = - signals_.template readExternalVariableIndex(); - } - if (signals_.template isAttached()) - { - ws_[PBRANCH] = - signals_.template readExternalVariable(); - ws_indices_[PBRANCH] = - signals_.template readExternalVariableIndex(); - } - if (signals_.template isAttached()) - { - ws_[QBRANCH] = - signals_.template readExternalVariable(); - ws_indices_[QBRANCH] = - signals_.template readExternalVariableIndex(); - } - if (signals_.template isAttached()) - { - ws_[FREQ] = signals_.template readExternalVariable(); - ws_indices_[FREQ] = - signals_.template readExternalVariableIndex(); - } if (signals_.template isAttached()) { - ws_[FREQREF] = + ws_[E::FREQREF] = signals_.template readExternalVariable(); - ws_indices_[FREQREF] = + ws_indices_[E::FREQREF] = signals_.template readExternalVariableIndex(); } if (signals_.template isAttached()) { - ws_[VREF] = signals_.template readExternalVariable(); - ws_indices_[VREF] = + ws_[E::VREF] = signals_.template readExternalVariable(); + ws_indices_[E::VREF] = signals_.template readExternalVariableIndex(); } if (signals_.template isAttached()) { - ws_[QREF] = signals_.template readExternalVariable(); - ws_indices_[QREF] = + ws_[E::QREF] = signals_.template readExternalVariable(); + ws_indices_[E::QREF] = signals_.template readExternalVariableIndex(); } if (signals_.template isAttached()) { - ws_[PPLANTREF] = + ws_[E::PPLANTREF] = signals_.template readExternalVariable(); - ws_indices_[PPLANTREF] = + ws_indices_[E::PPLANTREF] = signals_.template readExternalVariableIndex(); } diff --git a/tests/UnitTests/PhasorDynamics/CMakeLists.txt b/tests/UnitTests/PhasorDynamics/CMakeLists.txt index d8945dd55..8a8de8c9f 100644 --- a/tests/UnitTests/PhasorDynamics/CMakeLists.txt +++ b/tests/UnitTests/PhasorDynamics/CMakeLists.txt @@ -136,8 +136,10 @@ add_executable(test_phasor_converter_repca runConverterRepcaTests.cpp) target_link_libraries( test_phasor_converter_repca GridKit::definitions - GridKit::phasor_dynamics_systemmodel - GridKit::phasor_dynamics_systemmodel_dependency_tracking + GridKit::phasor_dynamics_converter_repca + GridKit::phasor_dynamics_converter_repca_dependency_tracking + GridKit::phasor_dynamics_bus + GridKit::phasor_dynamics_bus_dependency_tracking GridKit::testing) add_executable(test_phasor_stabilizer_ieeest runStabilizerIeeestTests.cpp) diff --git a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp index 01c3a23b9..79b75d380 100644 --- a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp @@ -1,26 +1,33 @@ #pragma once -#include +#include +#include +#include #include +#include #include -#include +#include +#include #include #include +#include #include #include #include #include -#include -#include +#include #include #include +#include #include namespace GridKit { namespace Testing { + using Log = ::GridKit::Utilities::Logger; + template class ConverterRepcaTests { @@ -29,570 +36,1014 @@ namespace GridKit using IdxT = index_type; using RealT = typename PhasorDynamics::Component::RealT; - static constexpr ScalarT kTol = static_cast(1.0e-8); + ConverterRepcaTests() = default; + ~ConverterRepcaTests() = default; + + // Exact-limit limiter seeds leave residual tails below 2e-13. This margin + // absorbs them while resolving the approximately 6.8e-12 boundary response. + static constexpr RealT kBehaviorTol = 1.0e-12; + + // Enzyme traverses the smooth expressions through a separate AD path; + // its double-precision derivatives agree with the fixed key to O(1e-10). + static constexpr RealT kJacobianTol = 1.0e-9; - TestOutcome constructionAndValidation() + /// Validate construction, defaults, parameters, signals, and time floors. + TestOutcome validation() { TestStatus success = true; PhasorDynamics::Bus bus(1.0, 0.0); - bus.allocate(); - bus.initialize(); - - ScalarT ir{0.0}; - ScalarT ii{0.0}; - ScalarT p{0.4}; - ScalarT q{0.1}; - ScalarT freq{1.0}; - IdxT ir_i = 10; - IdxT ii_i = 11; - IdxT p_i = 12; - IdxT q_i = 13; - IdxT f_i = 14; - - PhasorDynamics::SignalNode ir_node; - PhasorDynamics::SignalNode ii_node; - PhasorDynamics::SignalNode p_node; - PhasorDynamics::SignalNode q_node; - PhasorDynamics::SignalNode freq_node; - ir_node.set(&ir, &ir_i); - ii_node.set(&ii, &ii_i); - p_node.set(&p, &p_i); - q_node.set(&q, &q_i); - freq_node.set(&freq, &f_i); - - auto data = makeData(); - PhasorDynamics::Converter::Repca repca(&bus, data); - auto& signals = repca.getSignals(); - signals.template attachSignalNode(&ir_node); - signals.template attachSignalNode(&ii_node); - signals.template attachSignalNode(&p_node); - signals.template attachSignalNode(&q_node); - signals.template attachSignalNode(&freq_node); - - success *= (repca.size() == static_cast(Vars::MAXIMUM)); - success *= (repca.getMonitor() != nullptr); - success *= (repca.verify() == 0); - - auto bad = data; - bad.parameters[Params::mva] = static_cast(0.0); - PhasorDynamics::Converter::Repca bad_mva(&bus, bad); - bad_mva.getSignals().template attachSignalNode(&ir_node); - bad_mva.getSignals().template attachSignalNode(&ii_node); - bad_mva.getSignals().template attachSignalNode(&p_node); - bad_mva.getSignals().template attachSignalNode(&q_node); - bad_mva.getSignals().template attachSignalNode(&freq_node); - success *= (bad_mva.verify() > 0); - - bad = data; - bad.parameters[Params::Tfv] = static_cast(0.0); - PhasorDynamics::Converter::Repca bad_tfv(&bus, bad); - bad_tfv.getSignals().template attachSignalNode(&ir_node); - bad_tfv.getSignals().template attachSignalNode(&ii_node); - bad_tfv.getSignals().template attachSignalNode(&p_node); - bad_tfv.getSignals().template attachSignalNode(&q_node); - bad_tfv.getSignals().template attachSignalNode(&freq_node); - success *= (bad_tfv.verify() > 0); - - bad = data; - bad.parameters[Params::Qmin] = static_cast(2.0); - PhasorDynamics::Converter::Repca bad_q_limits(&bus, bad); - bad_q_limits.getSignals().template attachSignalNode(&ir_node); - bad_q_limits.getSignals().template attachSignalNode(&ii_node); - bad_q_limits.getSignals().template attachSignalNode(&p_node); - bad_q_limits.getSignals().template attachSignalNode(&q_node); - bad_q_limits.getSignals().template attachSignalNode(&freq_node); - success *= (bad_q_limits.verify() > 0); - - bad = data; - bad.parameters[Params::fdbd1] = static_cast(0.1); - PhasorDynamics::Converter::Repca bad_deadband(&bus, bad); - bad_deadband.getSignals().template attachSignalNode(&ir_node); - bad_deadband.getSignals().template attachSignalNode(&ii_node); - bad_deadband.getSignals().template attachSignalNode(&p_node); - bad_deadband.getSignals().template attachSignalNode(&q_node); - bad_deadband.getSignals().template attachSignalNode(&freq_node); - success *= (bad_deadband.verify() > 0); + + PhasorDynamics::Converter::Repca empty(&bus); + success *= (empty.size() == static_cast(I::MAXIMUM)); + success *= (empty.getMonitor() == nullptr); + + Fixture configured(makeData()); + configured.attachAllInputs(); + success *= (configured.repca.size() == static_cast(I::MAXIMUM)); + success *= (configured.repca.getMonitor() != nullptr); + success *= (configured.repca.verify() == 0); + + noteExpectedLogs("Testing REPCA defaults, parameter floors, and invalid " + "configurations. Logged errors and warnings are expected."); + + Fixture documented_defaults(makeMinimalData()); + documented_defaults.attachAllInputs(); + success *= (documented_defaults.repca.verify() == 0); + success *= defaultsMatchDocumentedValues(); + + auto integer_numeric = makeData(); + integer_numeric.parameters[Params::mva] = static_cast(100); + Fixture integer_parameter(integer_numeric); + integer_parameter.attachAllInputs(); + success *= (integer_parameter.repca.verify() == 0); + + PhasorDynamics::Converter::Repca missing_signals(&bus, makeData()); + success *= (missing_signals.verify() > 0); + + success *= invalidParameterCase(Params::mva, 0.0); + success *= invalidParameterCase(Params::Tfv, 0.0); + success *= invalidParameterCase(Params::Tfv, -0.1); + success *= invalidParameterCase(Params::dbdlow, 0.1); + success *= invalidParameterCase(Params::dbdupper, -0.1); + success *= invalidParameterCase(Params::emin, 0.1); + success *= invalidParameterCase(Params::emax, -0.1); + success *= invalidParameterCase(Params::Qmin, 1.1); + success *= invalidParameterCase(Params::fdbd1, 0.1); + success *= invalidParameterCase(Params::fdbd2, -0.1); + success *= invalidParameterCase(Params::femin, 0.1); + success *= invalidParameterCase(Params::femax, -0.1); + success *= invalidParameterCase(Params::Pmin, 2.1); + success *= invalidParameterCase(Params::mva, true); + + for (const Params flag : {Params::VcompFlag, Params::RefFlag, Params::Freqflag}) + { + for (const auto value : {static_cast(0), static_cast(1)}) + { + auto data = makeData(); + data.parameters[flag] = value; + Fixture model(data); + model.attachAllInputs(); + success *= (model.repca.verify() == 0); + } + + for (const auto value : {static_cast(0.0), static_cast(1.0)}) + { + auto data = makeData(); + data.parameters[flag] = value; + Fixture model(data); + model.attachAllInputs(); + success *= (model.repca.verify() == 0); + } + + success *= invalidParameterCase(flag, static_cast(2)); + success *= invalidParameterCase(flag, static_cast(0.5)); + } + + PhasorDynamics::Converter::Repca busless(nullptr, makeData()); + success *= (busless.verify() > 0); + + success *= unlinkedSignalRejected(); + success *= unlinkedSignalRejected(); + success *= unlinkedSignalRejected(); + success *= unlinkedSignalRejected(); + success *= unlinkedSignalRejected(); + success *= unlinkedSignalRejected(); + success *= unlinkedSignalRejected(); + success *= unlinkedSignalRejected(); + success *= unlinkedSignalRejected(); + + auto floor_data = makeInitializationData(); + floor_data.parameters[Params::Tfltr] = -0.2; + floor_data.parameters[Params::Tp] = 0.0; + floor_data.parameters[Params::Tlag] = -0.4; + + Fixture floored(floor_data); + floored.attachAllInputs(); + setInitializationInputs(floored); + success *= floored.initialize(0.25, 0.45); + success *= (floored.repca.evaluateResidual() == 0); + success *= allResidualsZero(floored.repca); + + auto* y = floored.repca.y().getData(); + y[I::VMEAS] -= 0.001; + y[I::QMEAS] -= 0.002; + y[I::PMEAS] -= 0.003; + y[I::PREF] -= 0.004; + floored.repca.y().setDataUpdated(); + success *= (floored.repca.evaluateResidual() == 0); + success *= residualsMatch(floored.repca, + {{I::VMEAS, 1.0}, + {I::QMEAS, 2.0}, + {I::PMEAS, 3.0}, + {I::PREF, 4.0}}, + "floored time constants"); return success.report(__func__); } - TestOutcome signalVerification() + /// Check initialization state, signal publication, monitors, and flag modes. + TestOutcome initializationAndSignals() { TestStatus success = true; - PhasorDynamics::Bus bus(1.0, 0.0); - PhasorDynamics::Converter::Repca repca(&bus, makeData()); - - PhasorDynamics::SignalNode ir_node; - repca.getSignals().template attachSignalNode(&ir_node); - success *= (repca.verify() > 0); - - ScalarT ir{0.0}; - ScalarT ii{0.0}; - ScalarT p{0.4}; - ScalarT q{0.1}; - ScalarT freq{1.0}; - ScalarT vref{1.0}; - IdxT ir_i = 20; - IdxT ii_i = 21; - IdxT p_i = 22; - IdxT q_i = 23; - IdxT f_i = 24; - IdxT v_i = 25; - - PhasorDynamics::SignalNode ii_node; - PhasorDynamics::SignalNode p_node; - PhasorDynamics::SignalNode q_node; - PhasorDynamics::SignalNode freq_node; - PhasorDynamics::SignalNode vref_node; - ir_node.set(&ir, &ir_i); - ii_node.set(&ii, &ii_i); - p_node.set(&p, &p_i); - q_node.set(&q, &q_i); - freq_node.set(&freq, &f_i); - - repca.getSignals().template attachSignalNode(&ii_node); - repca.getSignals().template attachSignalNode(&p_node); - repca.getSignals().template attachSignalNode(&q_node); - repca.getSignals().template attachSignalNode(&freq_node); - success *= (repca.verify() == 0); - - repca.getSignals().template attachSignalNode(&vref_node); - success *= (repca.verify() > 0); - vref_node.set(&vref, &v_i); - success *= (repca.verify() == 0); + Fixture fixture(makeInitializationData(), 0.8, 0.6); + fixture.attachAllInputs(99.0); + setInitializationInputs(fixture); + success *= fixture.initialize(0.25, 0.45); + success *= (fixture.repca.tagDifferentiable() == 0); + success *= (fixture.repca.evaluateResidual() == 0); + + success *= stateMatches(fixture.repca, + {{I::VMEAS, 0.99200050403213}, + {I::QMEAS, 0.2}, + {I::XQPI, 0.5}, + {I::XQLAG, 0.5}, + {I::PMEAS, 0.8}, + {I::XPPI, 0.9}, + {I::PREF, 0.9}, + {I::V, 1.0}, + {I::VLDC, 0.99200050403213}, + {I::VDROOP, 1.08}, + {I::VCTRL, 0.99200050403213}, + {I::SFRZ, 1.0}, + {I::ERQ, 0.0}, + {I::ERQDB, 0.0}, + {I::ERQLIM, 0.0}, + {I::QPI, 0.5}, + {I::QEXT, 0.25}, + {I::EF, -0.00024949607977392432}, + {I::EP, 0.0}, + {I::EPLIM, 0.0}, + {I::PPI, 0.9}, + {I::PEXT, 0.45}}, + "initialization"); + + success *= scalarMatches(fixture.input(E::IBRANCHR), 0.2, "preserved ibranchr"); + success *= scalarMatches(fixture.input(E::IBRANCHI), -0.1, "preserved ibranchi"); + success *= scalarMatches(fixture.input(E::PBRANCH), 0.4, "preserved pbranch"); + success *= scalarMatches(fixture.input(E::QBRANCH), 0.1, "preserved qbranch"); + success *= scalarMatches(fixture.input(E::FREQ), 0.99, "preserved freq"); + success *= scalarMatches(fixture.qext(), 0.25, "preserved qext"); + success *= scalarMatches(fixture.pext(), 0.45, "preserved pext"); + success *= scalarMatches(fixture.input(E::FREQREF), 0.99, "published freqref"); + success *= scalarMatches(fixture.input(E::VREF), 0.99200050403213, "published vref"); + success *= scalarMatches(fixture.input(E::QREF), 0.1, "published qref"); + success *= scalarMatches(fixture.input(E::PPLANTREF), + 0.39874213184871782, + "published pplantref"); + + for (size_t row = 0; row < I::MAXIMUM; ++row) + { + const bool expected = row <= I::PREF; + if (fixture.repca.tag()[row] != expected) + { + std::cout << "REPCA differentiability tag " << row << " mismatch\n"; + success = false; + } + } + + constexpr RealT absolute_tolerance = 2.5e-7; + + success *= (fixture.repca.setAbsoluteTolerance(absolute_tolerance) == 0); + + const auto* tolerances = fixture.repca.absoluteTolerance().getData(); + for (size_t row = 0; row < I::MAXIMUM; ++row) + { + success *= valueUnchanged(tolerances[row], + absolute_tolerance, + "absolute tolerance", + row); + } + + RealT time = 0.0; + Model::VariableMonitorController monitor(time); + monitor.addMonitor(fixture.repca.getMonitor()); + std::stringstream monitor_output; + monitor.addSink({Model::VariableMonitorFormat::CSV}, monitor_output); + monitor.start(); + monitor.print(); + monitor.stop(); + + std::string monitor_header; + std::string monitor_values; + std::getline(monitor_output, monitor_header); + std::getline(monitor_output, monitor_values); + success *= (monitor_header == "t,Repca_repca_test_qext,Repca_repca_test_pext," + "Repca_repca_test_vmeas,Repca_repca_test_qmeas," + "Repca_repca_test_pmeas"); + + const auto monitored = Tokenizer(monitor_values, ',')(); + if (monitored.size() == 6) + { + success *= scalarMatches(monitored[1], 0.25, "monitored qext"); + success *= scalarMatches(monitored[2], 0.45, "monitored pext"); + success *= scalarMatches(monitored[3], 0.99200050403213, "monitored vmeas"); + success *= scalarMatches(monitored[4], 0.2, "monitored qmeas"); + success *= scalarMatches(monitored[5], 0.8, "monitored pmeas"); + } + else + { + std::cout << "REPCA monitor emitted " << monitored.size() + << " values instead of 6\n"; + success = false; + } + + success *= allResidualsZero(fixture.repca); + + { + auto exact_data = makeInitializationData(); + exact_data.parameters[Params::mva] = 73.0; + + // This non-binary base ratio exposes any component-base round trip. + Fixture exact_commands(exact_data, 0.8, 0.6); + exact_commands.attachAllInputs(); + setInitializationInputs(exact_commands); + success *= exact_commands.initialize(0.25, 0.45); + success *= valueUnchanged(exact_commands.qext(), 0.25, "qext signal", I::QEXT); + success *= valueUnchanged(exact_commands.pext(), 0.45, "pext signal", I::PEXT); + success *= valueUnchanged(exact_commands.input(E::QREF), + 0.1, + "qref signal", + E::QREF); + success *= (exact_commands.repca.evaluateResidual() == 0); + success *= allResidualsZero(exact_commands.repca); + } + + Fixture fallback(makeInitializationData(), 0.8, 0.6); + fallback.attachRequiredInputs(); + setInitializationInputs(fallback); + success *= fallback.initialize(0.25, 0.45); + success *= (fallback.repca.evaluateResidual() == 0); + success *= allResidualsZero(fallback.repca); + + struct FlagCase + { + const char* label; + bool voltage_compensation; + bool voltage_reference; + bool frequency_control; + RealT voltage; + RealT pref; + RealT pext; + }; + + const std::array flag_cases{{ + {"droop/reactive-reference/disabled-frequency", false, false, false, 1.08, 0.8, 0.0}, + {"droop/reactive-reference/enabled-frequency", false, false, true, 1.08, 0.9, 0.45}, + {"droop/voltage-reference/disabled-frequency", false, true, false, 1.08, 0.8, 0.0}, + {"droop/voltage-reference/enabled-frequency", false, true, true, 1.08, 0.9, 0.45}, + {"line-drop/reactive-reference/disabled-frequency", true, false, false, 0.99200050403213, 0.8, 0.0}, + {"line-drop/reactive-reference/enabled-frequency", true, false, true, 0.99200050403213, 0.9, 0.45}, + {"line-drop/voltage-reference/disabled-frequency", true, true, false, 0.99200050403213, 0.8, 0.0}, + {"line-drop/voltage-reference/enabled-frequency", true, true, true, 0.99200050403213, 0.9, 0.45}, + }}; + for (const auto& test_case : flag_cases) + { + auto data = makeInitializationData(); + data.parameters[Params::VcompFlag] = test_case.voltage_compensation; + data.parameters[Params::RefFlag] = test_case.voltage_reference; + data.parameters[Params::Freqflag] = test_case.frequency_control; + + Fixture scenario(data, 0.8, 0.6); + scenario.attachAllInputs(99.0); + setInitializationInputs(scenario); + success *= scenario.initialize(0.25, 0.45); + success *= (scenario.repca.evaluateResidual() == 0); + + success *= stateMatches(scenario.repca, + {{I::VMEAS, test_case.voltage}, + {I::VCTRL, test_case.voltage}, + {I::PREF, test_case.pref}, + {I::PPI, test_case.pref}, + {I::PEXT, test_case.pext}}, + test_case.label); + success *= scalarMatches(scenario.qext(), 0.25, test_case.label); + success *= scalarMatches(scenario.pext(), test_case.pext, test_case.label); + success *= allResidualsZero(scenario.repca); + } return success.report(__func__); } - TestOutcome initializationAndResidual() + /// Check initialization rejection, atomicity, and exact command boundaries. + TestOutcome initializationDomain() { TestStatus success = true; - auto data = makeData(); - data.parameters[Params::mva] = static_cast(50.0); - data.parameters[Params::Freqflag] = true; + noteExpectedLogs("Testing inadmissible REPCA command and controller " + "initialization points. Logged errors are expected."); - PhasorDynamics::Bus bus(0.8, 0.6); - bus.allocate(); - bus.initialize(); - - ScalarT ir{0.2}; - ScalarT ii{-0.1}; - ScalarT p{0.4}; - ScalarT q{0.1}; - ScalarT freq{1.0}; - ScalarT freqref{-4.0}; - ScalarT vref{-3.0}; - ScalarT qref{-2.0}; - ScalarT pplantref{-1.0}; - IdxT ir_i = 30; - IdxT ii_i = 31; - IdxT p_i = 32; - IdxT q_i = 33; - IdxT f_i = 34; - IdxT freqref_i = 35; - IdxT vref_i = 36; - IdxT qref_i = 37; - IdxT plantref_i = 38; - - PhasorDynamics::SignalNode ir_node; - PhasorDynamics::SignalNode ii_node; - PhasorDynamics::SignalNode p_node; - PhasorDynamics::SignalNode q_node; - PhasorDynamics::SignalNode freq_node; - PhasorDynamics::SignalNode freqref_node; - PhasorDynamics::SignalNode vref_node; - PhasorDynamics::SignalNode qref_node; - PhasorDynamics::SignalNode plantref_node; - PhasorDynamics::SignalNode qext_node; - PhasorDynamics::SignalNode pext_node; - - ir_node.set(&ir, &ir_i); - ii_node.set(&ii, &ii_i); - p_node.set(&p, &p_i); - q_node.set(&q, &q_i); - freq_node.set(&freq, &f_i); - freqref_node.set(&freqref, &freqref_i); - vref_node.set(&vref, &vref_i); - qref_node.set(&qref, &qref_i); - plantref_node.set(&pplantref, &plantref_i); - - PhasorDynamics::Converter::Repca repca(&bus, data); - auto& signals = repca.getSignals(); - signals.template attachSignalNode(&ir_node); - signals.template attachSignalNode(&ii_node); - signals.template attachSignalNode(&p_node); - signals.template attachSignalNode(&q_node); - signals.template attachSignalNode(&freq_node); - signals.template attachSignalNode(&freqref_node); - signals.template attachSignalNode(&vref_node); - signals.template attachSignalNode(&qref_node); - signals.template attachSignalNode(&plantref_node); - signals.template assignSignalNode(&qext_node); - signals.template assignSignalNode(&pext_node); - - success *= (repca.allocate() == 0); - repca.y().getData()[index(Vars::QEXT)] = static_cast(0.25); - repca.y().getData()[index(Vars::PEXT)] = static_cast(0.70); - repca.y().setDataUpdated(); - success *= (repca.initialize() == 0); - success *= (repca.tagDifferentiable() == 0); - success *= (repca.evaluateResidual() == 0); - - const ScalarT base_ratio = static_cast(2.0); - const ScalarT qmeas = base_ratio * q; - const ScalarT pmeas = base_ratio * p; - const ScalarT pfreq0 = - static_cast(20.0) * Math::ramp(static_cast(0.0)); - - success *= isEqual(repca.y().getData()[index(Vars::QEXT)], static_cast(0.25), kTol); - success *= isEqual(repca.y().getData()[index(Vars::PEXT)], static_cast(0.70), kTol); - success *= isEqual(qext_node.read(), static_cast(0.25), kTol); - success *= isEqual(pext_node.read(), static_cast(0.70), kTol); - success *= isEqual(freqref, freq, kTol); - success *= isEqual(vref, repca.y().getData()[index(Vars::VMEAS)], kTol); - success *= isEqual(qref, qmeas / base_ratio, kTol); - success *= isEqual(pplantref, (pmeas - pfreq0) / base_ratio, kTol); - - for (size_t i = 0; i < repca.getResidual().getSize(); ++i) - { - if (!isEqual(repca.getResidual().getData()[i], static_cast(0.0), kTol)) - { - std::cout << "REPCA residual row " << i << " is " - << repca.getResidual().getData()[i] << "\n"; - success = false; - } + const auto data = makeInitializationData(); + + struct RejectionCase + { + const char* label; + RealT qext; + RealT pext; + }; + + const RealT infinity = std::numeric_limits::infinity(); + const std::array rejection_cases{{ + {"qext below Qmin", -0.4000000001, 0.45}, + {"qext above Qmax", 0.4500000001, 0.45}, + {"pext below Pmin", 0.25, -1.0e-10}, + {"pext above Pmax", 0.25, 0.6000000001}, + {"nonfinite qext", infinity, 0.45}, + {"nonfinite pext", 0.25, infinity}, + }}; + for (const auto& test_case : rejection_cases) + { + success *= initializationRejectedAtomically(data, + test_case.qext, + test_case.pext, + test_case.label); + } + success *= initializationRejectedAtomically(data, + 0.25, + 0.45, + "nonfinite required signal", + NonfiniteTarget::FREQUENCY); + success *= initializationRejectedAtomically(data, + 0.25, + 0.45, + "nonfinite bus voltage", + NonfiniteTarget::BUS_VOLTAGE); + + auto collapsed_data = data; + + collapsed_data.parameters[Params::Qmin] = 0.5; + collapsed_data.parameters[Params::Qmax] = 0.5; + collapsed_data.parameters[Params::Pmin] = 0.9; + collapsed_data.parameters[Params::Pmax] = 0.9; + + success *= initializationRejectedAtomically(collapsed_data, + 0.2500000001, + 0.45, + "qext above collapsed Q limit"); + success *= initializationRejectedAtomically(collapsed_data, + 0.25, + 0.4500000001, + "pext above collapsed P limit"); + + auto reactive_aw_data = makeInitializationData(); + reactive_aw_data.parameters[Params::dbdupper] = 0.03; + + success *= initializationRejectedAtomically(reactive_aw_data, + 0.25, + 0.45, + "nonzero reactive-power PI antiwindup rate"); + + auto active_aw_data = makeInitializationData(); + active_aw_data.parameters[Params::femin] = 0.0; + + success *= initializationRejectedAtomically(active_aw_data, + 0.25, + 0.45, + "nonzero active-power PI antiwindup rate"); + + { + Fixture qmax_pmin_boundary(data, 0.8, 0.6); + qmax_pmin_boundary.attachAllInputs(); + setInitializationInputs(qmax_pmin_boundary); + success *= qmax_pmin_boundary.initialize(0.45, 0.0); + success *= (qmax_pmin_boundary.repca.evaluateResidual() == 0); + success *= stateMatches(qmax_pmin_boundary.repca, + {{I::QPI, 0.9}, + {I::XQLAG, 0.9}, + {I::XQPI, 1.0}, + {I::QEXT, 0.45}, + {I::PREF, 0.0}, + {I::PPI, 0.0}, + {I::XPPI, -0.1}, + {I::PEXT, 0.0}}, + "Qmax/Pmin command boundary"); + success *= allResidualsZero(qmax_pmin_boundary.repca); + } + + { + Fixture qmin_pmax_boundary(data, 0.8, 0.6); + qmin_pmax_boundary.attachAllInputs(); + setInitializationInputs(qmin_pmax_boundary); + success *= qmin_pmax_boundary.initialize(-0.4, 0.6); + success *= (qmin_pmax_boundary.repca.evaluateResidual() == 0); + success *= stateMatches(qmin_pmax_boundary.repca, + {{I::QPI, -0.8}, + {I::XQLAG, -0.8}, + {I::XQPI, -0.9}, + {I::QEXT, -0.4}, + {I::PREF, 1.2}, + {I::PPI, 1.2}, + {I::XPPI, 1.3}, + {I::PEXT, 0.6}}, + "Qmin/Pmax command boundary"); + success *= allResidualsZero(qmin_pmax_boundary.repca); + } + + { + Fixture collapsed_limits(collapsed_data, 0.8, 0.6); + collapsed_limits.attachAllInputs(); + setInitializationInputs(collapsed_limits); + success *= collapsed_limits.initialize(0.25, 0.45); + success *= (collapsed_limits.repca.evaluateResidual() == 0); + success *= stateMatches(collapsed_limits.repca, + {{I::XQPI, 0.5}, + {I::XQLAG, 0.5}, + {I::QPI, 0.5}, + {I::QEXT, 0.25}, + {I::XPPI, 0.9}, + {I::PREF, 0.9}, + {I::PPI, 0.9}, + {I::PEXT, 0.45}}, + "collapsed Q/P limits"); + success *= allResidualsZero(collapsed_limits.repca); } return success.report(__func__); } + /// Check every residual row against an independent numerical answer key. TestOutcome residualEquations() { TestStatus success = true; - auto data = makeResidualData(); - PhasorDynamics::Bus bus(0.9, 0.4); - bus.allocate(); - bus.initialize(); - - ScalarT ir{0.08}; - ScalarT ii{-0.02}; - ScalarT p{0.41}; - ScalarT q{0.13}; - ScalarT freq{0.99}; - ScalarT freqref{1.0}; - ScalarT vref{1.01}; - ScalarT qref{0.12}; - ScalarT pplantref{0.55}; - IdxT ir_i = 40; - IdxT ii_i = 41; - IdxT p_i = 42; - IdxT q_i = 43; - IdxT f_i = 44; - IdxT freqref_i = 45; - IdxT vref_i = 46; - IdxT qref_i = 47; - IdxT plantref_i = 48; - - PhasorDynamics::SignalNode ir_node; - PhasorDynamics::SignalNode ii_node; - PhasorDynamics::SignalNode p_node; - PhasorDynamics::SignalNode q_node; - PhasorDynamics::SignalNode freq_node; - PhasorDynamics::SignalNode freqref_node; - PhasorDynamics::SignalNode vref_node; - PhasorDynamics::SignalNode qref_node; - PhasorDynamics::SignalNode plantref_node; - - ir_node.set(&ir, &ir_i); - ii_node.set(&ii, &ii_i); - p_node.set(&p, &p_i); - q_node.set(&q, &q_i); - freq_node.set(&freq, &f_i); - freqref_node.set(&freqref, &freqref_i); - vref_node.set(&vref, &vref_i); - qref_node.set(&qref, &qref_i); - plantref_node.set(&pplantref, &plantref_i); - - PhasorDynamics::Converter::Repca repca(&bus, data); - repca.getSignals().template attachSignalNode(&ir_node); - repca.getSignals().template attachSignalNode(&ii_node); - repca.getSignals().template attachSignalNode(&p_node); - repca.getSignals().template attachSignalNode(&q_node); - repca.getSignals().template attachSignalNode(&freq_node); - repca.getSignals().template attachSignalNode(&freqref_node); - repca.getSignals().template attachSignalNode(&vref_node); - repca.getSignals().template attachSignalNode(&qref_node); - repca.getSignals().template attachSignalNode(&plantref_node); - - success *= (repca.allocate() == 0); - - repca.y().getData()[index(Vars::VMEAS)] = static_cast(0.98); - repca.y().getData()[index(Vars::QMEAS)] = static_cast(0.11); - repca.y().getData()[index(Vars::XQPI)] = static_cast(0.07); - repca.y().getData()[index(Vars::XQLAG)] = static_cast(0.14); - repca.y().getData()[index(Vars::PMEAS)] = static_cast(0.44); - repca.y().getData()[index(Vars::XPPI)] = static_cast(0.21); - repca.y().getData()[index(Vars::PREF)] = static_cast(0.60); - repca.y().getData()[index(Vars::V)] = static_cast(1.0); - repca.y().getData()[index(Vars::VLDC)] = static_cast(0.99); - repca.y().getData()[index(Vars::VDROOP)] = static_cast(1.05); - repca.y().getData()[index(Vars::VCTRL)] = static_cast(1.02); - repca.y().getData()[index(Vars::SFRZ)] = static_cast(0.8); - repca.y().getData()[index(Vars::ERQ)] = static_cast(0.03); - repca.y().getData()[index(Vars::ERQDB)] = static_cast(0.02); - repca.y().getData()[index(Vars::ERQLIM)] = static_cast(0.02); - repca.y().getData()[index(Vars::QPI)] = static_cast(0.27); - repca.y().getData()[index(Vars::QEXT)] = static_cast(0.20); - repca.y().getData()[index(Vars::EF)] = static_cast(0.01); - repca.y().getData()[index(Vars::EP)] = static_cast(0.04); - repca.y().getData()[index(Vars::EPLIM)] = static_cast(0.04); - repca.y().getData()[index(Vars::PPI)] = static_cast(0.66); - repca.y().getData()[index(Vars::PEXT)] = static_cast(0.61); - - repca.yp().getData()[index(Vars::VMEAS)] = static_cast(0.01); - repca.yp().getData()[index(Vars::QMEAS)] = static_cast(-0.02); - repca.yp().getData()[index(Vars::XQPI)] = static_cast(0.03); - repca.yp().getData()[index(Vars::XQLAG)] = static_cast(-0.04); - repca.yp().getData()[index(Vars::PMEAS)] = static_cast(0.02); - repca.yp().getData()[index(Vars::XPPI)] = static_cast(-0.01); - repca.yp().getData()[index(Vars::PREF)] = static_cast(0.05); - - repca.y().setDataUpdated(); - repca.yp().setDataUpdated(); - - success *= (repca.evaluateResidual() == 0); - - const ScalarT vldc_r = bus.Vr() - static_cast(0.02) * ir - + static_cast(0.03) * ii; - const ScalarT vldc_i = bus.Vi() - static_cast(0.02) * ii - - static_cast(0.03) * ir; - const ScalarT pfreq = static_cast(2.0) * Math::ramp(repca.y().getData()[index(Vars::EF)]) - - Math::ramp(-repca.y().getData()[index(Vars::EF)]); - - std::vector expected(static_cast(Vars::MAXIMUM), - static_cast(0.0)); - expected[index(Vars::VMEAS)] = - -repca.yp().getData()[index(Vars::VMEAS)] - + (repca.y().getData()[index(Vars::VCTRL)] - repca.y().getData()[index(Vars::VMEAS)]) - / static_cast(0.2); - expected[index(Vars::QMEAS)] = - -repca.yp().getData()[index(Vars::QMEAS)] - + (q - repca.y().getData()[index(Vars::QMEAS)]) / static_cast(0.2); - expected[index(Vars::XQPI)] = - -repca.yp().getData()[index(Vars::XQPI)] - + repca.y().getData()[index(Vars::SFRZ)] - * Math::antiwindup(repca.y().getData()[index(Vars::QPI)], - static_cast(3.0) - * repca.y().getData()[index(Vars::ERQLIM)], - static_cast(-0.8), - static_cast(0.9)); - expected[index(Vars::XQLAG)] = - -repca.yp().getData()[index(Vars::XQLAG)] - + (repca.y().getData()[index(Vars::QPI)] - repca.y().getData()[index(Vars::XQLAG)]) - / static_cast(1.5); - expected[index(Vars::PMEAS)] = - -repca.yp().getData()[index(Vars::PMEAS)] - + (p - repca.y().getData()[index(Vars::PMEAS)]) / static_cast(0.4); - expected[index(Vars::XPPI)] = - -repca.yp().getData()[index(Vars::XPPI)] - + Math::antiwindup(repca.y().getData()[index(Vars::PPI)], - static_cast(1.8) * repca.y().getData()[index(Vars::EPLIM)], - static_cast(0.0), - static_cast(1.2)); - expected[index(Vars::PREF)] = - -repca.yp().getData()[index(Vars::PREF)] - + (repca.y().getData()[index(Vars::PPI)] - repca.y().getData()[index(Vars::PREF)]) - / static_cast(0.5); - - expected[index(Vars::V)] = - -repca.y().getData()[index(Vars::V)] * repca.y().getData()[index(Vars::V)] - + bus.Vr() * bus.Vr() + bus.Vi() * bus.Vi(); - expected[index(Vars::VLDC)] = - -repca.y().getData()[index(Vars::VLDC)] * repca.y().getData()[index(Vars::VLDC)] - + vldc_r * vldc_r + vldc_i * vldc_i; - expected[index(Vars::VDROOP)] = - -repca.y().getData()[index(Vars::VDROOP)] + repca.y().getData()[index(Vars::V)] - + static_cast(0.4) * q; - expected[index(Vars::VCTRL)] = - -repca.y().getData()[index(Vars::VCTRL)] + repca.y().getData()[index(Vars::VLDC)]; - expected[index(Vars::SFRZ)] = - -repca.y().getData()[index(Vars::SFRZ)] - + Math::above(repca.y().getData()[index(Vars::V)], static_cast(0.7)); - expected[index(Vars::ERQ)] = - -repca.y().getData()[index(Vars::ERQ)] + vref - repca.y().getData()[index(Vars::VMEAS)]; - expected[index(Vars::ERQDB)] = - -repca.y().getData()[index(Vars::ERQDB)] - + Math::deadband2(repca.y().getData()[index(Vars::ERQ)], - static_cast(-0.02), - static_cast(0.03)); - expected[index(Vars::ERQLIM)] = - -repca.y().getData()[index(Vars::ERQLIM)] - + Math::clamp(repca.y().getData()[index(Vars::ERQDB)], - static_cast(-0.7), - static_cast(0.8)); - expected[index(Vars::QPI)] = - -repca.y().getData()[index(Vars::QPI)] - + Math::clamp(static_cast(2.0) * repca.y().getData()[index(Vars::ERQLIM)] - + repca.y().getData()[index(Vars::XQPI)], - static_cast(-0.8), - static_cast(0.9)); - expected[index(Vars::QEXT)] = - -static_cast(1.5) - * (repca.y().getData()[index(Vars::QEXT)] - repca.y().getData()[index(Vars::XQLAG)]) - + static_cast(0.2) - * (repca.y().getData()[index(Vars::QPI)] - repca.y().getData()[index(Vars::XQLAG)]); - - expected[index(Vars::EF)] = - -repca.y().getData()[index(Vars::EF)] - + Math::deadband2(freqref - freq, - static_cast(-0.01), - static_cast(0.015)); - expected[index(Vars::EP)] = - -repca.y().getData()[index(Vars::EP)] + pplantref - repca.y().getData()[index(Vars::PMEAS)] - + pfreq; - expected[index(Vars::EPLIM)] = - -repca.y().getData()[index(Vars::EPLIM)] - + Math::clamp(repca.y().getData()[index(Vars::EP)], - static_cast(-0.5), - static_cast(0.6)); - expected[index(Vars::PPI)] = - -repca.y().getData()[index(Vars::PPI)] - + Math::clamp(static_cast(1.7) * repca.y().getData()[index(Vars::EPLIM)] - + repca.y().getData()[index(Vars::XPPI)], - static_cast(0.0), - static_cast(1.2)); - expected[index(Vars::PEXT)] = - -repca.y().getData()[index(Vars::PEXT)] + repca.y().getData()[index(Vars::PREF)]; - - for (size_t i = 0; i < expected.size(); ++i) - { - if (!isEqual(repca.getResidual().getData()[i], expected[i], kTol)) + Fixture fixture(makeDynamicData(), kStateVr, kStateVi); + fixture.attachAllInputs(); + setAnswerKeyInputs(fixture); + success *= fixture.prepare(0.0, 0.0); + setAnswerKeyState(fixture.repca); + success *= (fixture.repca.evaluateResidual() == 0); + + const std::array expected{{ + {I::VMEAS, 0.19000000000000017}, + {I::QMEAS, 0.77000000000000013}, + {I::XQPI, 0.018000000000000002}, + {I::XQLAG, 0.12666666666666668}, + {I::PMEAS, 0.92999999999999983}, + {I::XPPI, 0.082000000000000003}, + {I::PREF, 0.070000000000000104}, + {I::V, -0.029999999999999916}, + {I::VLDC, -0.015651160000000081}, + {I::VDROOP, 0.053999999999999965}, + {I::VCTRL, -0.030000000000000027}, + {I::SFRZ, 0.19999999999999996}, + {I::ERQ, 2.7755575615628914e-17}, + {I::ERQDB, -0.017111912348473052}, + {I::ERQLIM, 1.7347234759768071e-17}, + {I::QPI, -0.16000000000000009}, + {I::QEXT, -0.36399999999999999}, + {I::EF, -0.0089371400000009833}, + {I::EP, 0.64036181730064157}, + {I::EPLIM, 3.4694469519536142e-17}, + {I::PPI, -0.38200000000000001}, + {I::PEXT, -0.62}, + }}; + + success *= (static_cast(fixture.repca.getResidual().getSize()) == expected.size()); + for (size_t row = 0; row < expected.size(); ++row) + { + if (expected[row].first != row) { - std::cout << "REPCA residual mismatch at row " << i << ": " - << repca.getResidual().getData()[i] << " != " << expected[i] << "\n"; + std::cout << "REPCA residual key position " << row << " names row " + << expected[row].first << '\n'; success = false; } } + success *= residualsMatch(fixture.repca, expected); return success.report(__func__); } - TestOutcome jsonParseAndSystemAssembly() + /// Check reactive modes, smooth limits, antiwindup, and lead-lag behavior. + TestOutcome reactiveControl() { TestStatus success = true; - std::istringstream input(R"json( -{ - "header": { - "format_version": 0, - "format_revision": 1, - "case_name": "REPCA parser", - "case_description": "REPCA parser behavior test", - "case_comments": "", - "freq_base": 60.0, - "va_base": 100000000.0 - }, - "buses": [ - { - "number": 1, - "class": "bus", - "name": "Bus 1", - "init": { "Vr": 1.0, "Vi": 0.0 }, - "params": { "kv": 1.0 } - } - ], - "signals": [ - { "signal_id": 10, "name": "Ir" }, - { "signal_id": 11, "name": "Ii" }, - { "signal_id": 12, "name": "Pbranch" }, - { "signal_id": 13, "name": "Qbranch" }, - { "signal_id": 14, "name": "Freq" }, - { "signal_id": 15, "name": "Qext" }, - { "signal_id": 16, "name": "Pext" } - ], - "devices": [ - { - "class": "Repca", - "ports": { - "bus": 1, - "ibranchr": 10, - "ibranchi": 11, - "pbranch": 12, - "qbranch": 13, - "freq": 14, - "qext": 15, - "pext": 16 - }, - "id": "RP1", - "params": { "mva": 100.0 }, - "mon": ["qext", "pext", "vmeas", "qmeas", "pmeas"] - } - ] -} -)json"); - - auto data = PhasorDynamics::parseSystemModelData(input); - success *= (data.repca.size() == 1); - success *= (data.repca[0].device_class == "Repca"); - success *= (data.repca[0].buses.at(PhasorDynamics::Converter::RepcaBuses::bus) == 1); - success *= (data.repca[0].signal_inputs.at(PhasorDynamics::Converter::RepcaSignalInputs::ibranchr) == 10); - success *= (data.repca[0].signal_inputs.at(PhasorDynamics::Converter::RepcaSignalInputs::ibranchi) == 11); - success *= (data.repca[0].signal_inputs.at(PhasorDynamics::Converter::RepcaSignalInputs::pbranch) == 12); - success *= (data.repca[0].signal_inputs.at(PhasorDynamics::Converter::RepcaSignalInputs::qbranch) == 13); - success *= (data.repca[0].signal_inputs.at(PhasorDynamics::Converter::RepcaSignalInputs::freq) == 14); - success *= (data.repca[0].signal_outputs.at(PhasorDynamics::Converter::RepcaSignalOutputs::qext) == 15); - success *= (data.repca[0].signal_outputs.at(PhasorDynamics::Converter::RepcaSignalOutputs::pext) == 16); - success *= (std::get_if( - &data.repca[0].parameters.at(Params::mva)) - != nullptr); - - PhasorDynamics::SystemModel system(data); - success *= (system.size() == 0); + struct FlagCase + { + const char* label; + bool voltage_compensation; + bool voltage_reference; + RealT vctrl; + RealT erq; + }; + + const std::array flag_cases{{ + {"droop/reactive-reference", false, false, 0.08, 0.30}, + {"droop/voltage-reference", false, true, 0.08, 0.10}, + {"line-drop/reactive-reference", true, false, -0.08, 0.30}, + {"line-drop/voltage-reference", true, true, -0.08, 0.10}, + }}; + for (const auto& test_case : flag_cases) + { + auto data = makeDynamicData(); + data.parameters[Params::VcompFlag] = test_case.voltage_compensation; + data.parameters[Params::RefFlag] = test_case.voltage_reference; + + Fixture fixture(data); + fixture.attachAllInputs(); + setAnswerKeyInputs(fixture); + fixture.input(E::VREF) = 1.05; + fixture.input(E::QREF) = 0.20; + + success *= fixture.prepare(0.0, 0.0); + setState(fixture.repca, + {{I::VMEAS, 0.95}, + {I::QMEAS, 0.10}, + {I::VLDC, 0.92}, + {I::VDROOP, 1.08}, + {I::VCTRL, 1.0}, + {I::ERQ, 0.0}}); + success *= (fixture.repca.evaluateResidual() == 0); + + success *= residualsMatch(fixture.repca, + {{I::VCTRL, test_case.vctrl}, + {I::ERQ, test_case.erq}}, + test_case.label); + } + + Fixture fixture(makeDynamicData()); + fixture.attachAllInputs(); + setAnswerKeyInputs(fixture); + success *= fixture.prepare(0.0, 0.0); + + const std::array freeze_cases{{ + {0.6, 3.7751357595539048e-11}, + {0.7, 0.5}, + {0.8, 0.99999999996224864}, + }}; + for (const auto& test_case : freeze_cases) + { + setState(fixture.repca, {{I::V, test_case.input}, {I::SFRZ, 0.0}}); + success *= (fixture.repca.evaluateResidual() == 0); + success *= residualsMatch(fixture.repca, {{I::SFRZ, test_case.expected}}, "freeze gate"); + } + + const std::array deadband_cases{{ + {-0.05, -0.030003109594436028}, + {0.0, -3.104066702683552e-5}, + {0.03, 0.002888087651526948}, + {0.06, 0.030003109594436025}, + }}; + for (const auto& test_case : deadband_cases) + { + setState(fixture.repca, {{I::ERQ, test_case.input}, {I::ERQDB, 0.0}}); + success *= (fixture.repca.evaluateResidual() == 0); + success *= residualsMatch(fixture.repca, + {{I::ERQDB, test_case.expected}}, + "reactive-power deadband"); + } + + const std::array error_limit_cases{{ + {-1.0, -0.7}, + {-0.7, -0.69711188674766689}, + {0.2, 0.2}, + {0.8, 0.79711188674766698}, + {1.0, 0.8}, + }}; + for (const auto& test_case : error_limit_cases) + { + setState(fixture.repca, {{I::ERQDB, test_case.input}, {I::ERQLIM, 0.0}}); + success *= (fixture.repca.evaluateResidual() == 0); + success *= residualsMatch(fixture.repca, + {{I::ERQLIM, test_case.expected}}, + "reactive-power error limit"); + } + + const std::array command_limit_cases{{ + {-1.0, -0.8}, + {-0.8, -0.79711188674766698}, + {0.2, 0.2}, + {0.9, 0.89711188674766706}, + {1.1, 0.9}, + }}; + for (const auto& test_case : command_limit_cases) + { + setState(fixture.repca, + {{I::XQPI, test_case.input}, + {I::ERQLIM, 0.0}, + {I::QPI, 0.0}}); + success *= (fixture.repca.evaluateResidual() == 0); + success *= residualsMatch(fixture.repca, + {{I::QPI, test_case.expected}}, + "reactive-power command limit"); + } + + const std::array antiwindup_cases{{ + {-0.9, -0.1, -1.1325407278661714e-11}, + {-0.9, 0.1, 0.3}, + {-0.8, -0.1, -0.15}, + {-0.8, 0.1, 0.3}, + {0.9, -0.1, -0.3}, + {0.9, 0.1, 0.15}, + {1.0, -0.1, -0.3}, + {1.0, 0.1, 1.1325407278661714e-11}, + }}; + for (const auto& test_case : antiwindup_cases) + { + setState(fixture.repca, + {{I::QPI, test_case.output}, + {I::ERQLIM, test_case.error}, + {I::SFRZ, 1.0}}); + setDerivative(fixture.repca, {{I::XQPI, 0.0}}); + success *= (fixture.repca.evaluateResidual() == 0); + success *= residualsMatch(fixture.repca, + {{I::XQPI, test_case.expected}}, + "reactive-power antiwindup"); + } + + setState(fixture.repca, + {{I::XQLAG, 0.14}, + {I::QPI, 0.27}, + {I::QEXT, 0.20}}); + setDerivative(fixture.repca, {{I::XQLAG, -0.04}}); + success *= (fixture.repca.evaluateResidual() == 0); + success *= residualsMatch(fixture.repca, + {{I::XQLAG, 0.12666666666666668}, + {I::QEXT, -0.36399999999999999}}, + "reactive-command lead-lag"); return success.report(__func__); } -#ifdef GRIDKIT_ENABLE_ENZYME - TestOutcome jacobian() + /// Check active-power modes, smooth limits, antiwindup, and command lag. + TestOutcome activePowerControl() { TestStatus success = true; - auto dependency_tracking_jacobian = dependencyTrackingJacobian(); - auto enzyme_jacobian = enzymeJacobian(); + struct FlagCase + { + const char* label; + bool frequency_control; + RealT pext; + }; + + const std::array flag_cases{{ + {"disabled frequency control", false, -0.6}, + {"enabled frequency control", true, 0.2}, + }}; + for (const auto& test_case : flag_cases) + { + auto data = makeDynamicData(); + data.parameters[Params::Freqflag] = test_case.frequency_control; + Fixture fixture(data); + fixture.attachAllInputs(); + setAnswerKeyInputs(fixture); + success *= fixture.prepare(0.0, 0.0); + setState(fixture.repca, {{I::PREF, 0.8}, {I::PEXT, 0.3}}); + success *= (fixture.repca.evaluateResidual() == 0); + success *= residualsMatch(fixture.repca, + {{I::PEXT, test_case.pext}}, + test_case.label); + } + + Fixture fixture(makeDynamicData()); + fixture.attachAllInputs(); + setAnswerKeyInputs(fixture); + success *= fixture.prepare(0.0, 0.0); + + const std::array frequency_deadband_cases{{ + {-0.05, -0.040000281494001103}, + {0.0, -0.00024949607977392432}, + {0.015, 0.0028777978975925616}, + {0.05, 0.035000934519396246}, + }}; + for (const auto& test_case : frequency_deadband_cases) + { + fixture.input(E::FREQ) = 1.0; + fixture.input(E::FREQREF) = 1.0 + test_case.input; + setState(fixture.repca, {{I::EF, 0.0}}); + success *= (fixture.repca.evaluateResidual() == 0); + success *= residualsMatch(fixture.repca, + {{I::EF, test_case.expected}}, + "frequency deadband"); + } - success *= (dependency_tracking_jacobian.size() == enzyme_jacobian.size()); - const auto nrows = - std::min(dependency_tracking_jacobian.size(), enzyme_jacobian.size()); - for (size_t i = 0; i < nrows; ++i) + const std::array droop_cases{{ + {-0.1, -0.099999999999842701}, + {0.0, 0.0028881132523331052}, + {0.1, 0.2000000000001573}, + }}; + fixture.input(E::PPLANTREF) = 0.2; + for (const auto& test_case : droop_cases) { - success *= isEqual(dependency_tracking_jacobian[i], - enzyme_jacobian[i], - static_cast(1.0e-8)); + setState(fixture.repca, + {{I::EF, test_case.input}, + {I::EP, 0.0}, + {I::PMEAS, 0.4}}); + success *= (fixture.repca.evaluateResidual() == 0); + success *= residualsMatch(fixture.repca, + {{I::EP, test_case.expected}}, + "frequency droop"); } + const std::array error_limit_cases{{ + {-1.0, -0.5}, + {-0.5, -0.49711188674766688}, + {0.2, 0.2}, + {0.6, 0.59711188674766702}, + {1.0, 0.6}, + }}; + for (const auto& test_case : error_limit_cases) + { + setState(fixture.repca, {{I::EP, test_case.input}, {I::EPLIM, 0.0}}); + success *= (fixture.repca.evaluateResidual() == 0); + success *= residualsMatch(fixture.repca, + {{I::EPLIM, test_case.expected}}, + "active-power error limit"); + } + + const std::array command_limit_cases{{ + {-0.2, 5.9381836780872301e-24}, + {0.0, 0.0028881132523331052}, + {0.4, 0.4}, + {1.2, 1.1971118867476669}, + {1.4, 1.2}, + }}; + for (const auto& test_case : command_limit_cases) + { + setState(fixture.repca, + {{I::XPPI, test_case.input}, + {I::EPLIM, 0.0}, + {I::PPI, 0.0}}); + success *= (fixture.repca.evaluateResidual() == 0); + success *= residualsMatch(fixture.repca, + {{I::PPI, test_case.expected}}, + "active-power command limit"); + } + + const std::array antiwindup_cases{{ + {-0.1, -0.1, -6.7952443671970281e-12}, + {-0.1, 0.1, 0.18}, + {0.0, -0.1, -0.09}, + {0.0, 0.1, 0.18}, + {1.2, -0.1, -0.18}, + {1.2, 0.1, 0.09}, + {1.3, -0.1, -0.18}, + {1.3, 0.1, 6.7952443671970281e-12}, + }}; + for (const auto& test_case : antiwindup_cases) + { + setState(fixture.repca, + {{I::PPI, test_case.output}, + {I::EPLIM, test_case.error}}); + setDerivative(fixture.repca, {{I::XPPI, 0.0}}); + success *= (fixture.repca.evaluateResidual() == 0); + success *= residualsMatch(fixture.repca, + {{I::XPPI, test_case.expected}}, + "active-power antiwindup"); + } + + setState(fixture.repca, {{I::PPI, 0.66}, {I::PREF, 0.60}}); + setDerivative(fixture.repca, {{I::PREF, 0.05}}); + success *= (fixture.repca.evaluateResidual() == 0); + success *= residualsMatch(fixture.repca, + {{I::PREF, 0.070000000000000104}}, + "active-power command lag"); + + return success.report(__func__); + } + + /// Check every differential residual with nonzero explicit derivatives. + TestOutcome derivatives() + { + TestStatus success = true; + + Fixture fixture(makeDynamicData(), kStateVr, kStateVi); + fixture.attachAllInputs(); + setAnswerKeyInputs(fixture); + success *= fixture.prepare(0.0, 0.0); + setAnswerKeyState(fixture.repca); + setDerivative(fixture.repca, + {{I::VMEAS, 0.11}, + {I::QMEAS, -0.12}, + {I::XQPI, 0.13}, + {I::XQLAG, -0.14}, + {I::PMEAS, 0.15}, + {I::XPPI, -0.16}, + {I::PREF, 0.17}}); + success *= (fixture.repca.evaluateResidual() == 0); + success *= residualsMatch(fixture.repca, + {{I::VMEAS, 0.09000000000000018}, + {I::QMEAS, 0.8700000000000001}, + {I::XQPI, -0.082}, + {I::XQLAG, 0.22666666666666668}, + {I::PMEAS, 0.7999999999999998}, + {I::XPPI, 0.232}, + {I::PREF, -0.04999999999999989}}, + "explicit derivatives"); + return success.report(__func__); } + + /// Check every Jacobian row against an independent numerical answer key. + TestOutcome jacobian() + { + TestStatus success = true; + + const auto data = makeDynamicData(); + const auto expected = expectedJacobian(); + const auto dependency = dependencyTrackingJacobian(data, success); + + success *= jacobianMatches(dependency, + expected, + "dependency tracking", + kBehaviorTol); + +#ifdef GRIDKIT_ENABLE_ENZYME + const auto enzyme = enzymeJacobian(data, success); + + success *= jacobianMatches(enzyme, expected, "Enzyme", kJacobianTol); #endif + return success.report(__func__); + } + private: - using Params = PhasorDynamics::Converter::RepcaParameters; - using Vars = PhasorDynamics::Converter::RepcaInternalVariables; - using Ext = PhasorDynamics::Converter::RepcaExternalVariables; + using Params = PhasorDynamics::Converter::RepcaParameters; + using Vars = PhasorDynamics::Converter::RepcaInternalVariables; + using Ext = PhasorDynamics::Converter::RepcaExternalVariables; + using Mon = PhasorDynamics::Converter::RepcaMonitorableVariables; + using Data = PhasorDynamics::Converter::RepcaData; + using I = PhasorDynamics::Converter::RepcaIdx; + using E = PhasorDynamics::Converter::RepcaExt; + using RepcaT = PhasorDynamics::Converter::Repca; + using Row = std::pair; + using Rows = std::initializer_list; + using JacobianRow = DependencyTracking::Variable::DependencyMap; + + struct DrivenCase + { + RealT input; + RealT expected; + }; - static size_t index(Vars variable) + struct AntiWindupCase { - return static_cast(variable); - } + RealT output; + RealT error; + RealT expected; + }; + + enum class NonfiniteTarget + { + NONE, + FREQUENCY, + BUS_VOLTAGE + }; + + /// Owns the regulated bus, REPCA, assigned command nodes, and attached + /// input nodes. Signal storage precedes the model so every referenced node + /// outlives REPCA; copying would invalidate the model and node pointers. + template + class Fixture + { + private: + std::array input_values_{}; + std::array input_indices_{}; + std::array, E::MAXIMUM> input_nodes_{}; + + PhasorDynamics::SignalNode qext_node_; + PhasorDynamics::SignalNode pext_node_; + + public: + explicit Fixture(const Data& data, + RealT vr = 1.0, + RealT vi = 0.0, + RealT system_va_base = 100.0e6) + : bus(static_cast(vr), static_cast(vi)), + repca(&bus, data) + { + repca.setSystemBase(60.0, system_va_base); + repca.getSignals().template assignSignalNode(&qext_node_); + repca.getSignals().template assignSignalNode(&pext_node_); + } - auto makeData() -> PhasorDynamics::Converter::RepcaData + Fixture(const Fixture&) = delete; + Fixture& operator=(const Fixture&) = delete; + + void attachRequiredInputs(RealT initial_value = 0.0) + { + const IdxT external_index_base = repca.size() + bus.size(); + for (size_t port = 0; port < E::MAXIMUM; ++port) + { + input_values_[port] = static_cast(initial_value); + input_indices_[port] = external_index_base + static_cast(port); + input_nodes_[port].set(&input_values_[port], &input_indices_[port]); + } + + auto& signals = repca.getSignals(); + signals.template attachSignalNode(&input_nodes_[E::IBRANCHR]); + signals.template attachSignalNode(&input_nodes_[E::IBRANCHI]); + signals.template attachSignalNode(&input_nodes_[E::PBRANCH]); + signals.template attachSignalNode(&input_nodes_[E::QBRANCH]); + signals.template attachSignalNode(&input_nodes_[E::FREQ]); + } + + void attachAllInputs(RealT initial_value = 0.0) + { + attachRequiredInputs(initial_value); + + auto& signals = repca.getSignals(); + signals.template attachSignalNode(&input_nodes_[E::FREQREF]); + signals.template attachSignalNode(&input_nodes_[E::VREF]); + signals.template attachSignalNode(&input_nodes_[E::QREF]); + signals.template attachSignalNode(&input_nodes_[E::PPLANTREF]); + } + + void seedCommands(RealT qext, RealT pext) + { + qext_node_.init(static_cast(qext)); + pext_node_.init(static_cast(pext)); + } + + /// Arrange the allocation, verification, bus, and command prerequisites. + bool prepare(RealT qext, RealT pext) + { + const bool success = (bus.allocate() == 0) && (repca.allocate() == 0) + && (repca.verify() == 0) && (bus.initialize() == 0); + if (!success) + { + std::cout << "REPCA fixture preparation failed\n"; + return false; + } + seedCommands(qext, pext); + return true; + } + + bool initialize(RealT qext, RealT pext) + { + if (!prepare(qext, pext)) + { + return false; + } + if (repca.initialize() != 0) + { + std::cout << "REPCA initialization failed\n"; + return false; + } + return true; + } + + T qext() const + { + return qext_node_.read(); + } + + T pext() const + { + return pext_node_.read(); + } + + T& input(size_t port) + { + return input_values_[port]; + } + + IdxT inputIndex(size_t port) const + { + return input_indices_[port]; + } + + PhasorDynamics::Bus bus; + PhasorDynamics::Converter::Repca repca; + }; + + static constexpr RealT kStateVr = 0.9; + static constexpr RealT kStateVi = 0.4; + + static constexpr size_t kBusVrColumn = I::MAXIMUM; + static constexpr size_t kBusViColumn = kBusVrColumn + 1; + static constexpr size_t kExternalColumnBase = kBusViColumn + 1; + + static constexpr size_t externalColumn(size_t port) { - using Mon = PhasorDynamics::Converter::RepcaMonitorableVariables; + return kExternalColumnBase + port; + } - PhasorDynamics::Converter::RepcaData data; + Data makeMinimalData() const + { + Data data; data.device_class = "Repca"; data.disambiguation_string = "repca_test"; data.monitored_variables.insert(Mon::qext); @@ -603,262 +1054,639 @@ namespace GridKit return data; } - auto makeResidualData() -> PhasorDynamics::Converter::RepcaData + Data makeExplicitDefaultData() const { - auto data = makeData(); - - data.parameters[Params::mva] = static_cast(100.0); + auto data = makeMinimalData(); + data.parameters[Params::mva] = 100.0; data.parameters[Params::VcompFlag] = true; data.parameters[Params::RefFlag] = true; - data.parameters[Params::Freqflag] = true; - data.parameters[Params::Tfltr] = static_cast(0.2); - data.parameters[Params::Vfrz] = static_cast(0.7); - data.parameters[Params::Rc] = static_cast(0.02); - data.parameters[Params::Xc] = static_cast(0.03); - data.parameters[Params::Kc] = static_cast(0.4); - data.parameters[Params::dbdlow] = static_cast(-0.02); - data.parameters[Params::dbdupper] = static_cast(0.03); - data.parameters[Params::emax] = static_cast(0.8); - data.parameters[Params::emin] = static_cast(-0.7); - data.parameters[Params::Kp] = static_cast(2.0); - data.parameters[Params::Ki] = static_cast(3.0); - data.parameters[Params::Qmax] = static_cast(0.9); - data.parameters[Params::Qmin] = static_cast(-0.8); - data.parameters[Params::Tft] = static_cast(0.2); - data.parameters[Params::Tfv] = static_cast(1.5); - data.parameters[Params::Tp] = static_cast(0.4); - data.parameters[Params::fdbd1] = static_cast(-0.01); - data.parameters[Params::fdbd2] = static_cast(0.015); - data.parameters[Params::Ddn] = static_cast(2.0); - data.parameters[Params::Dup] = static_cast(1.0); - data.parameters[Params::femax] = static_cast(0.6); - data.parameters[Params::femin] = static_cast(-0.5); - data.parameters[Params::Kpg] = static_cast(1.7); - data.parameters[Params::Kig] = static_cast(1.8); - data.parameters[Params::Pmax] = static_cast(1.2); - data.parameters[Params::Pmin] = static_cast(0.0); - data.parameters[Params::Tlag] = static_cast(0.5); + data.parameters[Params::Freqflag] = false; + data.parameters[Params::Tfltr] = 0.05; + data.parameters[Params::Vfrz] = 0.7; + data.parameters[Params::Rc] = 0.0; + data.parameters[Params::Xc] = 0.0; + data.parameters[Params::Kc] = 1.0; + data.parameters[Params::dbdlow] = 0.0; + data.parameters[Params::dbdupper] = 0.0; + data.parameters[Params::emax] = 1.0; + data.parameters[Params::emin] = -1.0; + data.parameters[Params::Kp] = 10.0; + data.parameters[Params::Ki] = 10.0; + data.parameters[Params::Qmax] = 1.0; + data.parameters[Params::Qmin] = -1.0; + data.parameters[Params::Tft] = 0.0; + data.parameters[Params::Tfv] = 3.0; + data.parameters[Params::Tp] = 0.0; + data.parameters[Params::fdbd1] = 0.0; + data.parameters[Params::fdbd2] = 0.0; + data.parameters[Params::Ddn] = 20.0; + data.parameters[Params::Dup] = 0.0; + data.parameters[Params::femax] = 1.0; + data.parameters[Params::femin] = -1.0; + data.parameters[Params::Kpg] = 10.0; + data.parameters[Params::Kig] = 10.0; + data.parameters[Params::Pmax] = 2.0; + data.parameters[Params::Pmin] = 0.0; + data.parameters[Params::Tlag] = 3.0; return data; } -#ifdef GRIDKIT_ENABLE_ENZYME - void setValue(double& target, double value) - { - target = value; - } - - void setValue(DependencyTracking::Variable& target, double value) - { - target.setValue(value); - } - - template - void setJacobianState(model_type& repca, bus_type& bus) - { - setValue(bus.Vr(), 0.9); - setValue(bus.Vi(), 0.4); - - setValue(repca.y().getData()[index(Vars::VMEAS)], 0.98); - setValue(repca.y().getData()[index(Vars::QMEAS)], 0.11); - setValue(repca.y().getData()[index(Vars::XQPI)], 0.07); - setValue(repca.y().getData()[index(Vars::XQLAG)], 0.14); - setValue(repca.y().getData()[index(Vars::PMEAS)], 0.44); - setValue(repca.y().getData()[index(Vars::XPPI)], 0.21); - setValue(repca.y().getData()[index(Vars::PREF)], 0.60); - setValue(repca.y().getData()[index(Vars::V)], 1.0); - setValue(repca.y().getData()[index(Vars::VLDC)], 0.99); - setValue(repca.y().getData()[index(Vars::VDROOP)], 1.05); - setValue(repca.y().getData()[index(Vars::VCTRL)], 1.02); - setValue(repca.y().getData()[index(Vars::SFRZ)], 0.8); - setValue(repca.y().getData()[index(Vars::ERQ)], 0.03); - setValue(repca.y().getData()[index(Vars::ERQDB)], 0.02); - setValue(repca.y().getData()[index(Vars::ERQLIM)], 0.02); - setValue(repca.y().getData()[index(Vars::QPI)], 0.27); - setValue(repca.y().getData()[index(Vars::QEXT)], 0.20); - setValue(repca.y().getData()[index(Vars::EF)], 0.01); - setValue(repca.y().getData()[index(Vars::EP)], 0.04); - setValue(repca.y().getData()[index(Vars::EPLIM)], 0.04); - setValue(repca.y().getData()[index(Vars::PPI)], 0.66); - setValue(repca.y().getData()[index(Vars::PEXT)], 0.61); - - setValue(repca.yp().getData()[index(Vars::VMEAS)], 0.01); - setValue(repca.yp().getData()[index(Vars::QMEAS)], -0.02); - setValue(repca.yp().getData()[index(Vars::XQPI)], 0.03); - setValue(repca.yp().getData()[index(Vars::XQLAG)], -0.04); - setValue(repca.yp().getData()[index(Vars::PMEAS)], 0.02); - setValue(repca.yp().getData()[index(Vars::XPPI)], -0.01); - setValue(repca.yp().getData()[index(Vars::PREF)], 0.05); - - bus.y().setDataUpdated(); + Data makeData() const + { + auto data = makeExplicitDefaultData(); + data.parameters[Params::Freqflag] = true; + data.parameters[Params::Tp] = 0.05; + return data; + } + + Data makeDynamicData() const + { + auto data = makeData(); + data.parameters[Params::mva] = 50.0; + data.parameters[Params::Tfltr] = 0.2; + data.parameters[Params::Rc] = 0.02; + data.parameters[Params::Xc] = 0.03; + data.parameters[Params::Kc] = 0.4; + data.parameters[Params::dbdlow] = -0.02; + data.parameters[Params::dbdupper] = 0.03; + data.parameters[Params::emax] = 0.8; + data.parameters[Params::emin] = -0.7; + data.parameters[Params::Kp] = 2.0; + data.parameters[Params::Ki] = 3.0; + data.parameters[Params::Qmax] = 0.9; + data.parameters[Params::Qmin] = -0.8; + data.parameters[Params::Tft] = 0.2; + data.parameters[Params::Tfv] = 1.5; + data.parameters[Params::Tp] = 0.4; + data.parameters[Params::fdbd1] = -0.01; + data.parameters[Params::fdbd2] = 0.015; + data.parameters[Params::Ddn] = 2.0; + data.parameters[Params::Dup] = 1.0; + data.parameters[Params::femax] = 0.6; + data.parameters[Params::femin] = -0.5; + data.parameters[Params::Kpg] = 1.7; + data.parameters[Params::Kig] = 1.8; + data.parameters[Params::Pmax] = 1.2; + data.parameters[Params::Tlag] = 0.5; + return data; + } + + Data makeInitializationData() const + { + auto data = makeDynamicData(); + data.parameters[Params::dbdupper] = 0.02; + data.parameters[Params::emin] = -0.8; + data.parameters[Params::femin] = -0.6; + return data; + } + + template + void setInitializationInputs(Fixture& fixture) const + { + fixture.input(E::IBRANCHR) = static_cast(0.2); + fixture.input(E::IBRANCHI) = static_cast(-0.1); + fixture.input(E::PBRANCH) = static_cast(0.4); + fixture.input(E::QBRANCH) = static_cast(0.1); + fixture.input(E::FREQ) = static_cast(0.99); + } + + template + void setAnswerKeyInputs(Fixture& fixture) const + { + fixture.input(E::IBRANCHR) = static_cast(0.08); + fixture.input(E::IBRANCHI) = static_cast(-0.02); + fixture.input(E::PBRANCH) = static_cast(0.41); + fixture.input(E::QBRANCH) = static_cast(0.13); + fixture.input(E::FREQ) = static_cast(0.99); + fixture.input(E::FREQREF) = static_cast(1.0); + fixture.input(E::VREF) = static_cast(1.01); + fixture.input(E::QREF) = static_cast(0.12); + fixture.input(E::PPLANTREF) = static_cast(0.55); + } + + template + void setAnswerKeyState(PhasorDynamics::Converter::Repca& repca) const + { + setState(repca, + {{I::VMEAS, 0.98}, + {I::QMEAS, 0.11}, + {I::XQPI, 0.07}, + {I::XQLAG, 0.14}, + {I::PMEAS, 0.44}, + {I::XPPI, 0.21}, + {I::PREF, 0.60}, + {I::V, 1.0}, + {I::VLDC, 0.99}, + {I::VDROOP, 1.05}, + {I::VCTRL, 1.02}, + {I::SFRZ, 0.8}, + {I::ERQ, 0.03}, + {I::ERQDB, 0.02}, + {I::ERQLIM, 0.02}, + {I::QPI, 0.27}, + {I::QEXT, 0.20}, + {I::EF, 0.01}, + {I::EP, 0.04}, + {I::EPLIM, 0.04}, + {I::PPI, 0.66}, + {I::PEXT, 0.61}}); + setDerivative(repca, + {{I::VMEAS, 0.01}, + {I::QMEAS, -0.02}, + {I::XQPI, 0.03}, + {I::XQLAG, -0.04}, + {I::PMEAS, 0.02}, + {I::XPPI, -0.01}, + {I::PREF, 0.05}}); + } + + bool defaultsMatchDocumentedValues() const + { + Fixture implicit_defaults(makeMinimalData(), 0.9, 0.4); + Fixture explicit_defaults(makeExplicitDefaultData(), 0.9, 0.4); + implicit_defaults.attachAllInputs(); + explicit_defaults.attachAllInputs(); + + implicit_defaults.input(E::PBRANCH) = 0.2; + implicit_defaults.input(E::QBRANCH) = 0.1; + implicit_defaults.input(E::FREQ) = 1.0; + explicit_defaults.input(E::PBRANCH) = 0.2; + explicit_defaults.input(E::QBRANCH) = 0.1; + explicit_defaults.input(E::FREQ) = 1.0; + + bool success = implicit_defaults.initialize(0.1, 0.2) + && explicit_defaults.initialize(0.1, 0.2); + if (!success) + { + std::cout << "REPCA documented-default comparison failed to initialize\n"; + return false; + } + + success &= (implicit_defaults.repca.evaluateResidual() == 0); + success &= (explicit_defaults.repca.evaluateResidual() == 0); + success &= vectorsMatch(implicit_defaults.repca.y(), + explicit_defaults.repca.y(), + "documented-default state"); + success &= vectorsMatch(implicit_defaults.repca.yp(), + explicit_defaults.repca.yp(), + "documented-default derivative"); + success &= vectorsMatch(implicit_defaults.repca.getResidual(), + explicit_defaults.repca.getResidual(), + "documented-default residual"); + for (size_t port = 0; port < E::MAXIMUM; ++port) + { + success &= rowMatches(implicit_defaults.input(port), + explicit_defaults.input(port), + "documented-default signal", + port, + ""); + } + + setAnswerKeyInputs(implicit_defaults); + setAnswerKeyInputs(explicit_defaults); + setAnswerKeyState(implicit_defaults.repca); + setAnswerKeyState(explicit_defaults.repca); + success &= (implicit_defaults.repca.evaluateResidual() == 0); + success &= (explicit_defaults.repca.evaluateResidual() == 0); + success &= vectorsMatch(implicit_defaults.repca.getResidual(), + explicit_defaults.repca.getResidual(), + "documented-default dynamic residual"); + return success; + } + + template + bool invalidParameterCase(Params parameter, ValueT value) const + { + auto data = makeData(); + data.parameters[parameter] = value; + Fixture fixture(data); + fixture.attachAllInputs(); + return fixture.repca.verify() > 0; + } + + template + bool unlinkedSignalRejected() const + { + Fixture fixture(makeData()); + fixture.attachAllInputs(); + PhasorDynamics::SignalNode unlinked_node; + fixture.repca.getSignals().template attachSignalNode(&unlinked_node); + return fixture.repca.verify() > 0; + } + + bool initializationRejectedAtomically(const Data& data, + RealT qext, + RealT pext, + const char* label, + NonfiniteTarget target = NonfiniteTarget::NONE) const + { + Fixture fixture(data, 0.8, 0.6); + fixture.attachAllInputs(77.0); + setInitializationInputs(fixture); + if (!fixture.prepare(qext, pext)) + { + return false; + } + + const RealT infinity = std::numeric_limits::infinity(); + if (target == NonfiniteTarget::FREQUENCY) + { + fixture.input(E::FREQ) = infinity; + } + if (target == NonfiniteTarget::BUS_VOLTAGE) + { + fixture.bus.Vr() = infinity; + fixture.bus.y().setDataUpdated(); + } + + auto* y = fixture.repca.y().getData(); + auto* yp = fixture.repca.yp().getData(); + for (size_t row = 0; row < I::MAXIMUM; ++row) + { + y[row] = 0.125 + 0.01 * static_cast(row); + yp[row] = -0.25 - 0.01 * static_cast(row); + } + // Restore assigned commands after filling model storage with sentinels. + fixture.seedCommands(qext, pext); + fixture.repca.y().setDataUpdated(); + fixture.repca.yp().setDataUpdated(); + + const auto y_before = copyVector(fixture.repca.y()); + const auto yp_before = copyVector(fixture.repca.yp()); + const auto bus_before = copyVector(fixture.bus.y()); + std::array inputs_before{}; + for (size_t port = 0; port < E::MAXIMUM; ++port) + { + inputs_before[port] = fixture.input(port); + } + + bool success = true; + if (fixture.repca.initialize() == 0) + { + std::cout << "Expected REPCA initialization rejection: " << label << '\n'; + success = false; + } + + success &= valueUnchanged(fixture.qext(), qext, "qext signal", I::QEXT); + success &= valueUnchanged(fixture.pext(), pext, "pext signal", I::PEXT); + success &= vectorUnchanged(fixture.repca.y(), y_before, "state"); + success &= vectorUnchanged(fixture.repca.yp(), yp_before, "derivative"); + success &= vectorUnchanged(fixture.bus.y(), bus_before, "bus state"); + for (size_t port = 0; port < E::MAXIMUM; ++port) + { + success &= valueUnchanged(fixture.input(port), + inputs_before[port], + "external signal", + port); + } + return success; + } + + template + void setState(PhasorDynamics::Converter::Repca& repca, Rows rows) const + { + auto* y = repca.y().getData(); + for (const auto& [row, value] : rows) + { + y[row] = static_cast(value); + } repca.y().setDataUpdated(); + } + + template + void setDerivative(PhasorDynamics::Converter::Repca& repca, Rows rows) const + { + auto* yp = repca.yp().getData(); + for (const auto& [row, value] : rows) + { + yp[row] = static_cast(value); + } repca.yp().setDataUpdated(); } - std::vector dependencyTrackingJacobian() + static bool rowMatches(RealT actual, + RealT expected, + const char* what, + size_t row, + const char* context, + RealT tolerance = kBehaviorTol) + { + if (isEqual(actual, expected, tolerance)) + { + return true; + } + std::cout << "REPCA " << what << " row " << row; + if (context[0] != '\0') + { + std::cout << ' ' << context; + } + std::cout << " mismatch: " << std::setprecision(16) << actual + << " != " << expected << '\n'; + return false; + } + + bool scalarMatches(RealT actual, + RealT expected, + const char* label, + RealT tolerance = kBehaviorTol) const + { + if (isEqual(actual, expected, tolerance)) + { + return true; + } + std::cout << label << " mismatch: " << std::setprecision(16) << actual + << " != " << expected << '\n'; + return false; + } + + static bool valueUnchanged(RealT actual, + RealT expected, + const char* what, + size_t index) + { + if (actual == expected) + { + return true; + } + std::cout << "REPCA " << what << ' ' << index + << " changed: " << std::setprecision(16) << actual + << " != " << expected << '\n'; + return false; + } + + template + bool rowsMatch(const VectorT& vector, + const Row* rows, + size_t count, + const char* what, + const char* context) const + { + bool success = true; + const auto* values = vector.getData(); + for (size_t i = 0; i < count; ++i) + { + const auto& [row, expected] = rows[i]; + + success &= rowMatches(static_cast(values[row]), + expected, + what, + row, + context); + } + return success; + } + + bool residualsMatch(const RepcaT& repca, Rows rows, const char* context = "") const + { + return rowsMatch(repca.getResidual(), + rows.begin(), + rows.size(), + "residual", + context); + } + + template + bool residualsMatch(const RepcaT& repca, + const std::array& rows, + const char* context = "") const + { + return rowsMatch(repca.getResidual(), rows.data(), size, "residual", context); + } + + bool stateMatches(const RepcaT& repca, Rows rows, const char* context = "") const + { + return rowsMatch(repca.y(), rows.begin(), rows.size(), "state", context); + } + + bool allResidualsZero(const RepcaT& repca) const + { + bool success = true; + const auto* f = repca.getResidual().getData(); + const auto* yp = repca.yp().getData(); + for (size_t row = 0; row < I::MAXIMUM; ++row) + { + success &= rowMatches(f[row], 0.0, "residual", row, "at rest"); + success &= rowMatches(yp[row], 0.0, "derivative", row, "at rest"); + } + return success; + } + + template + std::vector copyVector(const VectorT& vector) const + { + const auto* values = vector.getData(); + std::vector snapshot(static_cast(vector.getSize())); + for (size_t row = 0; row < snapshot.size(); ++row) + { + snapshot[row] = static_cast(values[row]); + } + return snapshot; + } + + template + bool vectorUnchanged(const VectorT& vector, + const std::vector& snapshot, + const char* what) const + { + bool success = true; + const auto* values = vector.getData(); + for (size_t row = 0; row < snapshot.size(); ++row) + { + success &= valueUnchanged(static_cast(values[row]), + snapshot[row], + what, + row); + } + return success; + } + + template + bool vectorsMatch(const LeftVectorT& left, + const RightVectorT& right, + const char* what) const + { + if (left.getSize() != right.getSize()) + { + std::cout << "REPCA " << what << " size mismatch\n"; + return false; + } + bool success = true; + const auto* left_values = left.getData(); + const auto* right_values = right.getData(); + for (size_t row = 0; row < static_cast(left.getSize()); ++row) + { + success &= rowMatches(static_cast(left_values[row]), + static_cast(right_values[row]), + what, + row, + ""); + } + return success; + } + + void noteExpectedLogs(const char* message) const + { + const auto previous_verbosity = Log::verbosity(); + Log::setVerbosity(Log::Verbosity::EVERYTHING); + Log::misc() << message << '\n'; + Log::setVerbosity(previous_verbosity); + } + + std::array expectedJacobian() const + { + return {{ + {{I::VMEAS, -6.0}, {I::VCTRL, 5.0}}, + {{I::QMEAS, -6.0}, {externalColumn(E::QBRANCH), 10.0}}, + {{I::XQPI, -1.0}, {I::SFRZ, 0.06}, {I::ERQLIM, 2.4}}, + {{I::XQLAG, -1.6666666666666667}, {I::QPI, 0.6666666666666666}}, + {{I::PMEAS, -3.5}, {externalColumn(E::PBRANCH), 5.0}}, + {{I::XPPI, -1.0}, {I::EPLIM, 1.8}}, + {{I::PREF, -3.0}, {I::PPI, 2.0}}, + {{I::V, -2.0}, {kBusVrColumn, 1.8}, {kBusViColumn, 0.8}}, + {{I::VLDC, -1.98}, + {kBusVrColumn, 1.7956}, + {kBusViColumn, 0.796}, + {externalColumn(E::IBRANCHR), -0.059792}, + {externalColumn(E::IBRANCHI), 0.037948}}, + {{I::V, 1.0}, {I::VDROOP, -1.0}, {externalColumn(E::QBRANCH), 0.8}}, + {{I::VLDC, 1.0}, {I::VCTRL, -1.0}}, + {{I::SFRZ, -1.0}}, + {{I::VMEAS, -1.0}, {I::ERQ, -1.0}, {externalColumn(E::VREF), 1.0}}, + {{I::ERQ, 0.50000614417460221}, {I::ERQDB, -1.0}}, + {{I::ERQDB, 1.0}, {I::ERQLIM, -1.0}}, + {{I::XQPI, 1.0}, {I::ERQLIM, 2.0}, {I::QPI, -1.0}}, + {{I::XQLAG, 1.3}, {I::QPI, 0.2}, {I::QEXT, -3.0}}, + {{I::EF, -1.0}, + {externalColumn(E::FREQ), -0.23963778765414229}, + {externalColumn(E::FREQREF), 0.23963778765414229}}, + {{I::PMEAS, -1.0}, + {I::EF, 1.9168273035060777}, + {I::EP, -1.0}, + {externalColumn(E::PPLANTREF), 2.0}}, + {{I::EP, 1.0}, {I::EPLIM, -1.0}}, + {{I::XPPI, 1.0}, {I::EPLIM, 1.7}, {I::PPI, -1.0}}, + {{I::PREF, 1.0}, {I::PEXT, -2.0}}, + }}; + } + + static RealT mapValue(const JacobianRow& row, size_t column) + { + const auto entry = row.find(column); + if (entry == row.end()) + { + return 0.0; + } + return entry->second; + } + + bool jacobianMatches(const std::vector& actual, + const std::array& expected, + const char* source, + RealT tolerance) const + { + if (actual.size() != expected.size()) + { + std::cout << "REPCA " << source << " Jacobian row-count mismatch\n"; + return false; + } + + constexpr size_t column_count = externalColumn(E::MAXIMUM); + bool success = true; + for (size_t row = 0; row < I::MAXIMUM; ++row) + { + for (size_t column = 0; column < column_count; ++column) + { + const RealT actual_value = mapValue(actual[row], column); + const RealT expected_value = mapValue(expected[row], column); + if (!isEqual(actual_value, expected_value, tolerance)) + { + std::cout << "REPCA " << source << " Jacobian row " << row + << " column " << column << " mismatch: " + << std::setprecision(16) << actual_value << " != " + << expected_value << '\n'; + success = false; + } + } + for (const auto& [column, value] : actual[row]) + { + if (column >= column_count && !isEqual(value, 0.0, tolerance)) + { + std::cout << "REPCA " << source << " Jacobian row " << row + << " has unexpected column " << column << '\n'; + success = false; + } + } + } + return success; + } + + void numberVariables(Fixture& fixture) const + { + auto* y = fixture.repca.y().getData(); + auto* yp = fixture.repca.yp().getData(); + auto* bus_y = fixture.bus.y().getData(); + + for (size_t row = 0; row < I::MAXIMUM; ++row) + { + y[row].setVariableNumber(row); + yp[row].setVariableNumber(row); + } + for (size_t row = 0; row < static_cast(fixture.bus.size()); ++row) + { + bus_y[row].setVariableNumber(kBusVrColumn + row); + } + for (size_t port = 0; port < E::MAXIMUM; ++port) + { + fixture.input(port).setVariableNumber(fixture.inputIndex(port)); + } + + fixture.repca.y().setDataUpdated(); + fixture.repca.yp().setDataUpdated(); + fixture.bus.y().setDataUpdated(); + } + + std::vector dependencyTrackingJacobian(const Data& data, + TestStatus& success) const { using DepVar = DependencyTracking::Variable; - auto data = makeResidualData(); - - PhasorDynamics::Bus bus(DepVar{0.9}, DepVar{0.4}); - PhasorDynamics::Converter::Repca repca(&bus, data); - - DepVar ir{0.08}; - DepVar ii{-0.02}; - DepVar p{0.41}; - DepVar q{0.13}; - DepVar freq{0.99}; - DepVar freqref{1.0}; - DepVar vref{1.01}; - DepVar qref{0.12}; - DepVar pplantref{0.55}; - IdxT ir_i = 24; - IdxT ii_i = 25; - IdxT p_i = 26; - IdxT q_i = 27; - IdxT f_i = 28; - IdxT freqref_i = 29; - IdxT vref_i = 30; - IdxT qref_i = 31; - IdxT plantref_i = 32; - - ir.setVariableNumber(ir_i); - ii.setVariableNumber(ii_i); - p.setVariableNumber(p_i); - q.setVariableNumber(q_i); - freq.setVariableNumber(f_i); - freqref.setVariableNumber(freqref_i); - vref.setVariableNumber(vref_i); - qref.setVariableNumber(qref_i); - pplantref.setVariableNumber(plantref_i); - - PhasorDynamics::SignalNode ir_node; - PhasorDynamics::SignalNode ii_node; - PhasorDynamics::SignalNode p_node; - PhasorDynamics::SignalNode q_node; - PhasorDynamics::SignalNode freq_node; - PhasorDynamics::SignalNode freqref_node; - PhasorDynamics::SignalNode vref_node; - PhasorDynamics::SignalNode qref_node; - PhasorDynamics::SignalNode plantref_node; - ir_node.set(&ir, &ir_i); - ii_node.set(&ii, &ii_i); - p_node.set(&p, &p_i); - q_node.set(&q, &q_i); - freq_node.set(&freq, &f_i); - freqref_node.set(&freqref, &freqref_i); - vref_node.set(&vref, &vref_i); - qref_node.set(&qref, &qref_i); - plantref_node.set(&pplantref, &plantref_i); - - repca.getSignals().template attachSignalNode(&ir_node); - repca.getSignals().template attachSignalNode(&ii_node); - repca.getSignals().template attachSignalNode(&p_node); - repca.getSignals().template attachSignalNode(&q_node); - repca.getSignals().template attachSignalNode(&freq_node); - repca.getSignals().template attachSignalNode(&freqref_node); - repca.getSignals().template attachSignalNode(&vref_node); - repca.getSignals().template attachSignalNode(&qref_node); - repca.getSignals().template attachSignalNode(&plantref_node); - - bus.allocate(); - repca.allocate(); - for (IdxT i = 0; i < repca.size(); ++i) - { - repca.y().getData()[static_cast(i)].setVariableNumber(i); - repca.yp().getData()[static_cast(i)].setVariableNumber(i); - } - for (IdxT i = 0; i < bus.size(); ++i) - { - bus.y().getData()[static_cast(i)].setVariableNumber(i + repca.size()); + Fixture fixture(data, kStateVr, kStateVi); + fixture.attachAllInputs(); + setAnswerKeyInputs(fixture); + success *= fixture.prepare(0.0, 0.0); + setAnswerKeyState(fixture.repca); + numberVariables(fixture); + success *= (fixture.repca.evaluateResidual() == 0); + + std::vector rows(I::MAXIMUM); + const auto* f = fixture.repca.getResidual().getData(); + for (size_t row = 0; row < I::MAXIMUM; ++row) + { + rows[row] = f[row].getDependencies(); } + return rows; + } - repca.y().setDataUpdated(); - repca.yp().setDataUpdated(); - bus.y().setDataUpdated(); - - setJacobianState(repca, bus); - repca.evaluateResidual(); - - std::vector dependencies( - static_cast(repca.size())); - for (IdxT i = 0; i < repca.size(); ++i) - { - dependencies[static_cast(i)] = - repca.getResidual().getData()[static_cast(i)].getDependencies(); - } - return dependencies; - } - - std::vector enzymeJacobian() - { - auto data = makeResidualData(); - - PhasorDynamics::Bus bus(0.9, 0.4); - PhasorDynamics::Converter::Repca repca(&bus, data); - - ScalarT ir{0.08}; - ScalarT ii{-0.02}; - ScalarT p{0.41}; - ScalarT q{0.13}; - ScalarT freq{0.99}; - ScalarT freqref{1.0}; - ScalarT vref{1.01}; - ScalarT qref{0.12}; - ScalarT pplantref{0.55}; - IdxT ir_i = 24; - IdxT ii_i = 25; - IdxT p_i = 26; - IdxT q_i = 27; - IdxT f_i = 28; - IdxT freqref_i = 29; - IdxT vref_i = 30; - IdxT qref_i = 31; - IdxT plantref_i = 32; - - PhasorDynamics::SignalNode ir_node; - PhasorDynamics::SignalNode ii_node; - PhasorDynamics::SignalNode p_node; - PhasorDynamics::SignalNode q_node; - PhasorDynamics::SignalNode freq_node; - PhasorDynamics::SignalNode freqref_node; - PhasorDynamics::SignalNode vref_node; - PhasorDynamics::SignalNode qref_node; - PhasorDynamics::SignalNode plantref_node; - ir_node.set(&ir, &ir_i); - ii_node.set(&ii, &ii_i); - p_node.set(&p, &p_i); - q_node.set(&q, &q_i); - freq_node.set(&freq, &f_i); - freqref_node.set(&freqref, &freqref_i); - vref_node.set(&vref, &vref_i); - qref_node.set(&qref, &qref_i); - plantref_node.set(&pplantref, &plantref_i); - - repca.getSignals().template attachSignalNode(&ir_node); - repca.getSignals().template attachSignalNode(&ii_node); - repca.getSignals().template attachSignalNode(&p_node); - repca.getSignals().template attachSignalNode(&q_node); - repca.getSignals().template attachSignalNode(&freq_node); - repca.getSignals().template attachSignalNode(&freqref_node); - repca.getSignals().template attachSignalNode(&vref_node); - repca.getSignals().template attachSignalNode(&qref_node); - repca.getSignals().template attachSignalNode(&plantref_node); - - bus.allocate(); - repca.allocate(); - for (IdxT i = 0; i < bus.size(); ++i) - { - bus.setVariableIndex(i, i + repca.size()); - bus.setResidualIndex(i, i + repca.size()); - } - - setJacobianState(repca, bus); - repca.updateTime(0.0, 1.0); - repca.evaluateResidual(); - repca.evaluateJacobian(); - repca.constructCsr(); - return MapFromCsr(repca.getCsrJacobian()); +#ifdef GRIDKIT_ENABLE_ENZYME + std::vector enzymeJacobian(const Data& data, + TestStatus& success) const + { + Fixture fixture(data, kStateVr, kStateVi); + fixture.attachAllInputs(); + setAnswerKeyInputs(fixture); + success *= fixture.prepare(0.0, 0.0); + + for (IdxT row = 0; row < fixture.bus.size(); ++row) + { + fixture.bus.setVariableIndex(row, fixture.repca.size() + row); + } + + setAnswerKeyState(fixture.repca); + fixture.repca.updateTime(0.0, 1.0); + success *= (fixture.repca.evaluateResidual() == 0); + success *= (fixture.repca.evaluateJacobian() == 0); + success *= (fixture.repca.constructCsr() == 0); + return MapFromCsr(fixture.repca.getCsrJacobian()); } #endif }; diff --git a/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp b/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp index 3103f8106..06f90e5f4 100644 --- a/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp @@ -6,14 +6,14 @@ int main() GridKit::Testing::ConverterRepcaTests test; - result += test.constructionAndValidation(); - result += test.signalVerification(); - result += test.initializationAndResidual(); + result += test.validation(); + result += test.initializationAndSignals(); + result += test.initializationDomain(); result += test.residualEquations(); - result += test.jsonParseAndSystemAssembly(); -#ifdef GRIDKIT_ENABLE_ENZYME + result += test.reactiveControl(); + result += test.activePowerControl(); + result += test.derivatives(); result += test.jacobian(); -#endif return result.summary(); } From 257751cb15b6a2fbc531c7810044e1bc9afb59bf Mon Sep 17 00:00:00 2001 From: lukelowry Date: Sun, 2 Aug 2026 04:11:32 -0500 Subject: [PATCH 03/19] initial cleanup --- .../PhasorDynamics/Converter/REPCA/README.md | 525 +++---- .../PhasorDynamics/Converter/REPCA/Repca.cpp | 3 + .../PhasorDynamics/Converter/REPCA/Repca.hpp | 181 +-- .../Converter/REPCA/RepcaData.hpp | 112 +- .../REPCA/RepcaDependencyTracking.cpp | 4 + .../Converter/REPCA/RepcaEnzyme.cpp | 13 + .../Converter/REPCA/RepcaImpl.hpp | 1266 +++++++++++------ .../Model/PhasorDynamics/SystemModelImpl.hpp | 6 +- .../PhasorDynamics/ConverterRepcaTests.hpp | 1010 ++++++++----- .../SystemSingleComponentTests.hpp | 134 ++ .../runSystemSingleComponentTests.cpp | 1 + tests/UnitTests/Utilities/CaseFormatTests.hpp | 40 +- 12 files changed, 2072 insertions(+), 1223 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md index e1df1cec0..deb8d0188 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md @@ -5,76 +5,77 @@ resources. ## Notes -- REPCA is a control model only. It measures the regulated bus and branch - signals, publishes plant power commands, and injects no current into the - network. -- Branch-current, branch-power, power-reference, and command signals are on - system base. Internal power states and controller limits are on component - base. +- Fig. 1 hard nonlinearities use the linked CommonMath smooth approximations; + transition-point values may differ. +- The PowerWorld Governor Response Limits `Down Only` and `Fixed` modes are not + implemented; $P^{\min}$ and $P^{\max}$ remain static parameters. ## Block Diagram ![REPCA plant-control block diagram](../../../../../docs/Figures/PhasorDynamics/REPCA/diagram.png) Figure 1: REPCA plant-control model. Figure courtesy of -[PowerWorld](https://www.powerworld.com/WebHelp/). +[PowerWorld REPC_A model reference](https://www.powerworld.com/WebHelp/Content/TransientModels_PDF/Generator/Others/Plant%20Controller%20REPC_A.pdf). ## Model Parameters Symbol | Units | JSON | Description | Typical Value | Note ------------------------------------|----------|-------------|---------------------------------------------------------|---------------|------ -$S^\text{base}$ | [MVA] | `mva` | REPCA component power base | 100.0 | -$s_\text{comp}$ | [binary] | `VcompFlag` | Voltage-compensation mode flag | 1 | 1 = line-drop compensation, 0 = reactive droop -$s_\text{ref}$ | [binary] | `RefFlag` | Reactive-loop reference flag | 1 | 1 = voltage control, 0 = reactive-power control -$s_\text{freq}$ | [binary] | `Freqflag` | Active-power control output flag | 0 | 1 = command enabled, 0 = zero output -$T_\text{fltr}$ | [sec] | `Tfltr` | Voltage and reactive-power measurement filter time constant | 0.05 | -$V^\text{frz}$ | [p.u.] | `Vfrz` | Regulated-voltage threshold below which the reactive-power PI state freezes | 0.7 | -$R_c$ | [p.u.] | `Rc` | Line-drop compensation resistance | 0.0 | -$X_c$ | [p.u.] | `Xc` | Line-drop compensation reactance | 0.0 | +$S^\mathrm{base}$ | [MVA] | `mva` | REPCA component power base | 100.0 | Component base +$s_\mathrm{comp}$ | [binary] | `VcompFlag` | Voltage-compensation selector | 1 | 1 = line-drop compensation, 0 = reactive droop +$s_\mathrm{ref}$ | [binary] | `RefFlag` | Reactive-loop reference selector | 1 | 1 = voltage control, 0 = reactive-power control +$s_\mathrm{freq}$ | [binary] | `Freqflag` | Active-power output selector | 0 | 1 = command enabled, 0 = zero output +$T_\mathrm{fltr}$ | [sec] | `Tfltr` | Voltage and reactive-power measurement filter time constant | 0.05 | +$V^\mathrm{frz}$ | [p.u.] | `Vfrz` | Reactive-power PI freeze-voltage threshold | 0.7 | +$R_c$ | [p.u.] | `Rc` | Line-drop compensation resistance | 0.0 | System base +$X_c$ | [p.u.] | `Xc` | Line-drop compensation reactance | 0.0 | System base $K_c$ | [p.u.] | `Kc` | Reactive-current compensation gain | 1.0 | -$D_\text{bd1}$ | [p.u.] | `dbdlow` | Lower reactive-loop deadband threshold | 0.0 | -$D_\text{bd2}$ | [p.u.] | `dbdupper` | Upper reactive-loop deadband threshold | 0.0 | +$D_\mathrm{bd1}$ | [p.u.] | `dbdlow` | Lower reactive-loop deadband threshold | 0.0 | +$D_\mathrm{bd2}$ | [p.u.] | `dbdupper` | Upper reactive-loop deadband threshold | 0.0 | $e^{\max}$ | [p.u.] | `emax` | Maximum reactive-loop error limit | 1.0 | $e^{\min}$ | [p.u.] | `emin` | Minimum reactive-loop error limit | -1.0 | -$K_\text{p}$ | [p.u.] | `Kp` | Reactive-power controller proportional gain | 10.0 | -$K_\text{i}$ | [p.u./s] | `Ki` | Reactive-power controller integral gain | 10.0 | -$Q^{\max}$ | [p.u.] | `Qmax` | Maximum reactive-power command | 1.0 | -$Q^{\min}$ | [p.u.] | `Qmin` | Minimum reactive-power command | -1.0 | -$T_\text{ft}$ | [sec] | `Tft` | Reactive-command lead time constant | 0.0 | -$T_\text{fv}$ | [sec] | `Tfv` | Reactive-command lag time constant | 3.0 | -$T_\text{p}$ | [sec] | `Tp` | Active-power measurement filter time constant | 0.0 | -$D_\text{bd1}^f$ | [p.u.] | `fdbd1` | Lower frequency-error deadband threshold | 0.0 | -$D_\text{bd2}^f$ | [p.u.] | `fdbd2` | Upper frequency-error deadband threshold | 0.0 | -$D_\text{dn}$ | [p.u.] | `Ddn` | Down-frequency droop response gain | 20.0 | -$D_\text{up}$ | [p.u.] | `Dup` | Up-frequency droop response gain | 0.0 | +$K_\mathrm{p}$ | [p.u.] | `Kp` | Reactive-power controller proportional gain | 10.0 | +$K_\mathrm{i}$ | [p.u./s] | `Ki` | Reactive-power controller integral gain | 10.0 | +$Q^{\max}$ | [p.u.] | `Qmax` | Maximum reactive-power command | 1.0 | Component base +$Q^{\min}$ | [p.u.] | `Qmin` | Minimum reactive-power command | -1.0 | Component base +$T_\mathrm{ft}$ | [sec] | `Tft` | Reactive-command lead time constant | 0.0 | +$T_\mathrm{fv}$ | [sec] | `Tfv` | Reactive-command lag time constant | 3.0 | +$T_\mathrm{p}$ | [sec] | `Tp` | Active-power measurement filter time constant | 0.0 | +$D_\mathrm{bd1}^{f}$ | [p.u.] | `fdbd1` | Lower frequency-error deadband threshold | 0.0 | +$D_\mathrm{bd2}^{f}$ | [p.u.] | `fdbd2` | Upper frequency-error deadband threshold | 0.0 | +$D_\mathrm{dn}$ | [p.u.] | `Ddn` | Down-frequency droop response gain | 20.0 | +$D_\mathrm{up}$ | [p.u.] | `Dup` | Up-frequency droop response gain | 0.0 | $e_P^{\max}$ | [p.u.] | `femax` | Maximum active-power error limit | 1.0 | $e_P^{\min}$ | [p.u.] | `femin` | Minimum active-power error limit | -1.0 | -$K_\text{pg}$ | [p.u.] | `Kpg` | Active-power controller proportional gain | 10.0 | -$K_\text{ig}$ | [p.u./s] | `Kig` | Active-power controller integral gain | 10.0 | -$P^{\max}$ | [p.u.] | `Pmax` | Maximum active-power command | 2.0 | -$P^{\min}$ | [p.u.] | `Pmin` | Minimum active-power command | 0.0 | -$T_\text{lag}$ | [sec] | `Tlag` | Active-power command lag time constant | 3.0 | +$K_\mathrm{pg}$ | [p.u.] | `Kpg` | Active-power controller proportional gain | 10.0 | +$K_\mathrm{ig}$ | [p.u./s] | `Kig` | Active-power controller integral gain | 10.0 | +$P^{\max}$ | [p.u.] | `Pmax` | Maximum active-power command | 2.0 | Component base +$P^{\min}$ | [p.u.] | `Pmin` | Minimum active-power command | 0.0 | Component base +$T_\mathrm{lag}$ | [sec] | `Tlag` | Active-power command lag time constant | 3.0 | + +Every parameter is optional and uses the Typical Value when omitted. Real-valued +parameters accept real or integer JSON values; selectors accept Boolean or numeric 0/1. ### Parameter Validation -Invalid REPCA parameter sets are rejected by the following checks: +All real parameters must be finite. Invalid parameter sets are rejected by: ```math \begin{aligned} - S^\text{base} + S^\mathrm{base} &> 0 \\ - s_\text{comp}, s_\text{ref}, s_\text{freq} + s_\mathrm{comp}, s_\mathrm{ref}, s_\mathrm{freq} &\in \{0,1\} \\ - T_\text{fv} + T_\mathrm{fv} &> 0 \\ - D_\text{bd1} - &\le 0 \le D_\text{bd2} \\ + D_\mathrm{bd1} + &\le 0 \le D_\mathrm{bd2} \\ e^{\min} &\le 0 \le e^{\max} \\ Q^{\min} &\le Q^{\max} \\ - D_\text{bd1}^f - &\le 0 \le D_\text{bd2}^f \\ + D_\mathrm{bd1}^{f} + &\le 0 \le D_\mathrm{bd2}^{f} \\ e_P^{\min} &\le 0 \le e_P^{\max} \\ P^{\min} @@ -82,49 +83,50 @@ Invalid REPCA parameter sets are rejected by the following checks: \end{aligned} ``` -`VcompFlag`, `RefFlag`, and `Freqflag` accept Boolean values or integer/real -values equal to 0 or 1. +The power bases and both conversion ratios must also be finite and positive; +`verify()` also enforces Model Ports. ### Model Derived Parameters -Let $\epsilon_T=10^{-3}\ \text{s}$. Each time constant below $\epsilon_T$ is -raised to that floor and logged as a warning: +Let $\epsilon_T=10^{-3}\ \mathrm{s}$. Smaller time constants are raised to that +floor with a warning: ```math \begin{aligned} T_x &\leftarrow \max\!\left(T_x,\epsilon_T\right), - \quad x\in\{\text{fltr},\text{p},\text{lag}\} \\ - s_\text{comp}^\text{off} &= 1 - s_\text{comp} \\ - s_\text{ref}^\text{off} &= 1 - s_\text{ref} \\ - k_\text{base} &= \dfrac{S^\text{sys}}{S^\text{base}} + \quad x\in\{\mathrm{fltr},\mathrm{p},\mathrm{lag}\} \\ + s_\mathrm{comp}^{\mathrm{off}} &= 1 - s_\mathrm{comp} \\ + s_\mathrm{ref}^{\mathrm{off}} &= 1 - s_\mathrm{ref} \\ + k_\mathrm{base} &= \dfrac{S^\mathrm{sys}}{S^\mathrm{base}} \end{aligned} ``` -Multiplying by $k_\text{base}$ converts system base to component base. +$S^\mathrm{sys}$ is the system power base; $k_\mathrm{base}$ converts system-base +power to component base. ## Model Ports -Name | Port | Init | Base | Description -------------|--------|---------|--------|------ -`bus` | Bus | Known | - | Regulated-bus voltage -`ibranchr` | Input | Known | System | Branch-current real component input -`ibranchi` | Input | Known | System | Branch-current imaginary component input -`pbranch` | Input | Known | System | Branch active-power input -`qbranch` | Input | Known | System | Branch reactive-power input -`freq` | Input | Known | - | Frequency input -`freqref` | Input | Unknown | - | Frequency reference -`vref` | Input | Unknown | - | Voltage-control reference -`qref` | Input | Unknown | System | Reactive-power reference -`pplantref` | Input | Unknown | System | Plant active-power reference -`qext` | Output | Known | System | Reactive-power command output -`pext` | Output | Known | System | Active-power command output - -The known bus and input ports are seeded before `initialize()` and preserved by -it. `qext` is also seeded and preserved. `pext` is preserved when -$s_\text{freq}=1$ and set to zero when $s_\text{freq}=0$. Unknown reference -inputs are resolved during initialization and written to attached signal -storage, or retained as constant inputs when the port is unattached. +Name | Port | Init | Description +------------|--------|---------|------------ +`bus` | Bus | Known | Regulated-bus voltage +`ibranchr` | Input | Known | Branch-current real component on system base +`ibranchi` | Input | Known | Branch-current imaginary component on system base +`pbranch` | Input | Known | Branch active power on system base +`qbranch` | Input | Known | Branch reactive power on system base +`freq` | Input | Known | Frequency input +`freqref` | Input | Unknown | Frequency reference +`vref` | Input | Unknown | Voltage-control reference +`qref` | Input | Unknown | Reactive-power reference on system base +`pplantref` | Input | Unknown | Plant active-power reference on system base +`qext` | Output | Known | Reactive-power command on system base +`pext` | Output | Known | Active-power command on system base + +The bus and measurement inputs are required; attached inputs must be linked. +Optional references are published when attached and latched otherwise. The +`qext` and `pext` assignments are optional; both remain monitorable. +Initialization preserves `qext` and preserves `pext` only when +$s_\mathrm{freq}=1$; otherwise `pext` becomes zero. ## Model Variables @@ -134,280 +136,297 @@ storage, or retained as constant inputs when the port is unattached. Symbol | Units | Description | Note ------------------------|--------|-------------------------------------|------ -$V^\text{meas}$ | [p.u.] | Filtered regulated voltage | State 1 in Fig. 1 -$Q^\text{meas}$ | [p.u.] | Filtered reactive-power signal | State 2 in Fig. 1; component base -$x_Q^\text{PI}$ | [p.u.] | Reactive-power PI controller state | State 3 in Fig. 1; component base -$x_Q^\text{lag}$ | [p.u.] | Reactive-command lead-lag state | State 4 in Fig. 1; component base -$P^\text{meas}$ | [p.u.] | Filtered active-power signal | State 5 in Fig. 1; component base -$x_P^\text{PI}$ | [p.u.] | Active-power PI controller state | State 6 in Fig. 1; component base -$P^\text{ref}$ | [p.u.] | Active-power command lag state | State 7 in Fig. 1; component base +$V^\mathrm{meas}$ | [p.u.] | Filtered regulated voltage | State 1 in Fig. 1 +$Q^\mathrm{meas}$ | [p.u.] | Filtered reactive-power signal | State 2 in Fig. 1; component base +$x_Q^\mathrm{PI}$ | [p.u.] | Reactive-power PI controller state | State 3 in Fig. 1; component base +$x_Q^\mathrm{lag}$ | [p.u.] | Reactive-command lead-lag state | State 4 in Fig. 1; component base +$P^\mathrm{meas}$ | [p.u.] | Filtered active-power signal | State 5 in Fig. 1; component base +$x_P^\mathrm{PI}$ | [p.u.] | Active-power PI controller state | State 6 in Fig. 1; component base +$P^\mathrm{ref}$ | [p.u.] | Active-power command lag state | State 7 in Fig. 1; component base #### Algebraic Symbol | Units | Description | Note --------------------------|--------|-------------------------------------|------ $V$ | [p.u.] | Regulated-bus voltage magnitude | -$V^\text{ldc}$ | [p.u.] | Line-drop compensated voltage magnitude | -$V^\text{droop}$ | [p.u.] | Reactive-droop-compensated voltage | -$V^\text{ctrl}$ | [p.u.] | Selected voltage-measurement input | -$s_\text{frz}$ | [-] | Smooth reactive-power PI voltage-enable gate | -$e_\text{RQ}$ | [p.u.] | Selected reactive-loop error | -$e_\text{RQ}^\text{db}$ | [p.u.] | Deadbanded reactive-loop error | -$e_\text{RQ}^\text{lim}$ | [p.u.] | Limited reactive-loop error | -$Q^\text{PI}$ | [p.u.] | Reactive-power PI output | Component base -$Q^\text{ext}$ | [p.u.] | Reactive-power command output | System base +$V^\mathrm{ldc}$ | [p.u.] | Line-drop compensated voltage magnitude | +$V^\mathrm{droop}$ | [p.u.] | Reactive-droop-compensated voltage | +$V^\mathrm{ctrl}$ | [p.u.] | Selected voltage-measurement input | +$s_\mathrm{frz}$ | [-] | Smooth reactive-power PI voltage-enable gate | +$e_\mathrm{RQ}$ | [p.u.] | Selected reactive-loop error | +$e_\mathrm{RQ}^{\mathrm{db}}$ | [p.u.] | Deadbanded reactive-loop error | +$e_\mathrm{RQ}^{\mathrm{lim}}$ | [p.u.] | Limited reactive-loop error | +$Q^\mathrm{PI}$ | [p.u.] | Reactive-power PI output | Component base +$Q^\mathrm{ext}$ | [p.u.] | Reactive-power command output | System base $e_f$ | [p.u.] | Frequency error after deadband | $e_P$ | [p.u.] | Active-power control error | Component base -$e_P^\text{lim}$ | [p.u.] | Limited active-power control error | Component base -$P^\text{PI}$ | [p.u.] | Active-power PI output | Component base -$P^\text{ext}$ | [p.u.] | Active-power command output | System base +$e_P^{\mathrm{lim}}$ | [p.u.] | Limited active-power control error | Component base +$P^\mathrm{PI}$ | [p.u.] | Active-power PI output | Component base +$P^\mathrm{ext}$ | [p.u.] | Active-power command output | System base ### External Variables #### Differential + None. #### Algebraic Symbol | Units | Init | Description | Note -------------------------------|--------|---------|-----------------------------------|------ -$V_\text{r}$ | [p.u.] | Known | Regulated-bus voltage, real component | Bus input -$V_\text{i}$ | [p.u.] | Known | Regulated-bus voltage, imaginary component | Bus input -$I_\text{r}$ | [p.u.] | Known | Branch-current real component | Signal port `ibranchr`; system base -$I_\text{i}$ | [p.u.] | Known | Branch-current imaginary component | Signal port `ibranchi`; system base -$P^\text{br}$ | [p.u.] | Known | Branch active power | Signal port `pbranch`; system base -$Q^\text{br}$ | [p.u.] | Known | Branch reactive power | Signal port `qbranch`; system base +$V_\mathrm{r}$ | [p.u.] | Known | Regulated-bus voltage, real component | Bus input +$V_\mathrm{i}$ | [p.u.] | Known | Regulated-bus voltage, imaginary component | Bus input +$I_\mathrm{r}$ | [p.u.] | Known | Branch-current real component | Signal port `ibranchr`; system base +$I_\mathrm{i}$ | [p.u.] | Known | Branch-current imaginary component | Signal port `ibranchi`; system base +$P^\mathrm{br}$ | [p.u.] | Known | Branch active power | Signal port `pbranch`; system base +$Q^\mathrm{br}$ | [p.u.] | Known | Branch reactive power | Signal port `qbranch`; system base $f$ | [p.u.] | Known | Frequency input | Signal port `freq` -$f^\text{ref}$ | [p.u.] | Unknown | Frequency reference | Optional signal port `freqref` -$V^\text{ref}$ | [p.u.] | Unknown | Voltage-control reference | Optional signal port `vref` -$Q^\text{ref}$ | [p.u.] | Unknown | Reactive-power reference | Optional signal port `qref`; system base -$P_\text{plant}^\text{ref}$ | [p.u.] | Unknown | Plant active-power reference | Optional signal port `pplantref`; system base +$f^\mathrm{ref}$ | [p.u.] | Unknown | Frequency reference | Optional signal port `freqref` +$V^\mathrm{ref}$ | [p.u.] | Unknown | Voltage-control reference | Optional signal port `vref` +$Q^\mathrm{ref}$ | [p.u.] | Unknown | Reactive-power reference | Optional signal port `qref`; system base +$P_\mathrm{plant}^\mathrm{ref}$ | [p.u.] | Unknown | Plant active-power reference | Optional signal port `pplantref`; system base ## Model Equations -### Differential Equations +### Internal Equations + +#### Differential ```math \begin{aligned} 0 &= - -\dot{V}^\text{meas} - + \dfrac{1}{T_\text{fltr}} - \left(V^\text{ctrl} - V^\text{meas}\right) \\ + -\dot{V}^{\mathrm{meas}} + + \dfrac{1}{T_\mathrm{fltr}} + \left(V^\mathrm{ctrl} - V^\mathrm{meas}\right) \\ 0 &= - -\dot{Q}^\text{meas} - + \dfrac{1}{T_\text{fltr}} - \left(k_\text{base}Q^\text{br} - Q^\text{meas}\right) \\ + -\dot{Q}^{\mathrm{meas}} + + \dfrac{1}{T_\mathrm{fltr}} + \left(k_\mathrm{base}Q^\mathrm{br} - Q^\mathrm{meas}\right) \\ 0 &= - -\dot{x}_Q^\text{PI} - + s_\text{frz}\, + -\dot{x}_Q^{\mathrm{PI}} + + s_\mathrm{frz}\, \text{antiwindup}\!\left( - Q^\text{PI}, - K_\text{i}e_\text{RQ}^\text{lim};\, + Q^\mathrm{PI}, + K_\mathrm{i}e_\mathrm{RQ}^{\mathrm{lim}};\, Q^{\min}, Q^{\max} \right) \\ 0 &= - -\dot{x}_Q^\text{lag} - + \dfrac{1}{T_\text{fv}} - \left(Q^\text{PI} - x_Q^\text{lag}\right) \\ + -\dot{x}_Q^{\mathrm{lag}} + + \dfrac{1}{T_\mathrm{fv}} + \left(Q^\mathrm{PI} - x_Q^\mathrm{lag}\right) \\ 0 &= - -\dot{P}^\text{meas} - + \dfrac{1}{T_\text{p}} - \left(k_\text{base}P^\text{br} - P^\text{meas}\right) \\ + -\dot{P}^{\mathrm{meas}} + + \dfrac{1}{T_\mathrm{p}} + \left(k_\mathrm{base}P^\mathrm{br} - P^\mathrm{meas}\right) \\ 0 &= - -\dot{x}_P^\text{PI} + -\dot{x}_P^{\mathrm{PI}} + \text{antiwindup}\!\left( - P^\text{PI}, - K_\text{ig}e_P^\text{lim};\, + P^\mathrm{PI}, + K_\mathrm{ig}e_P^{\mathrm{lim}};\, P^{\min}, P^{\max} \right) \\ 0 &= - -\dot{P}^\text{ref} - + \dfrac{1}{T_\text{lag}} - \left(P^\text{PI} - P^\text{ref}\right) + -\dot{P}^{\mathrm{ref}} + + \dfrac{1}{T_\mathrm{lag}} + \left(P^\mathrm{PI} - P^\mathrm{ref}\right). \end{aligned} ``` CommonMath defines the [`antiwindup`](../../../../CommonMath.md#antiwindup) target and smooth approximation. -### Algebraic Equations +#### Algebraic ```math \begin{aligned} 0 &= -V^2 - + V_\text{r}^2 - + V_\text{i}^2 \\ + + V_\mathrm{r}^2 + + V_\mathrm{i}^2 \\ 0 &= - -\left(V^\text{ldc}\right)^2 - + \left(V_\text{r} - R_c I_\text{r} + X_c I_\text{i}\right)^2 - + \left(V_\text{i} - R_c I_\text{i} - X_c I_\text{r}\right)^2 \\ - 0 &= -V^\text{droop} + V + K_c k_\text{base}Q^\text{br} \\ - 0 &= -V^\text{ctrl} - + s_\text{comp}V^\text{ldc} - + s_\text{comp}^\text{off}V^\text{droop} \\ - 0 &= -s_\text{frz} + \text{above}(V;\,V^\text{frz}) \\[0.5ex] - 0 &= -e_\text{RQ} - + s_\text{ref}\left(V^\text{ref} - V^\text{meas}\right) - + s_\text{ref}^\text{off} - \left(k_\text{base}Q^\text{ref} - Q^\text{meas}\right) \\ - 0 &= -e_\text{RQ}^\text{db} + -\left(V^\mathrm{ldc}\right)^2 + + \left(V_\mathrm{r} - R_c I_\mathrm{r} + X_c I_\mathrm{i}\right)^2 + + \left(V_\mathrm{i} - R_c I_\mathrm{i} - X_c I_\mathrm{r}\right)^2 \\ + 0 &= -V^\mathrm{droop} + V + K_c k_\mathrm{base}Q^\mathrm{br} \\ + 0 &= -V^\mathrm{ctrl} + + s_\mathrm{comp}V^\mathrm{ldc} + + s_\mathrm{comp}^{\mathrm{off}}V^\mathrm{droop} \\ + 0 &= -s_\mathrm{frz} + \text{above}(V;\,V^\mathrm{frz}) \\ + 0 &= -e_\mathrm{RQ} + + s_\mathrm{ref}\left(V^\mathrm{ref} - V^\mathrm{meas}\right) + + s_\mathrm{ref}^{\mathrm{off}} + \left(k_\mathrm{base}Q^\mathrm{ref} - Q^\mathrm{meas}\right) \\ + 0 &= -e_\mathrm{RQ}^{\mathrm{db}} + \text{deadband2} - \left(e_\text{RQ};\,D_\text{bd1},D_\text{bd2}\right) \\ - 0 &= -e_\text{RQ}^\text{lim} + \left(e_\mathrm{RQ};\,D_\mathrm{bd1},D_\mathrm{bd2}\right) \\ + 0 &= -e_\mathrm{RQ}^{\mathrm{lim}} + \text{clamp} - \left(e_\text{RQ}^\text{db};\,e^{\min},e^{\max}\right) \\ - 0 &= -Q^\text{PI} + \left(e_\mathrm{RQ}^{\mathrm{db}};\,e^{\min},e^{\max}\right) \\ + 0 &= -Q^\mathrm{PI} + \text{clamp} - \left(K_\text{p}e_\text{RQ}^\text{lim}+x_Q^\text{PI};\, + \left(K_\mathrm{p}e_\mathrm{RQ}^{\mathrm{lim}}+x_Q^\mathrm{PI};\, Q^{\min},Q^{\max}\right) \\ - 0 &= -T_\text{fv} - \left(k_\text{base}Q^\text{ext}-x_Q^\text{lag}\right) - + T_\text{ft} - \left(Q^\text{PI}-x_Q^\text{lag}\right) \\[0.5ex] + 0 &= -T_\mathrm{fv} + \left(k_\mathrm{base}Q^\mathrm{ext}-x_Q^\mathrm{lag}\right) + + T_\mathrm{ft} + \left(Q^\mathrm{PI}-x_Q^\mathrm{lag}\right) \\ 0 &= -e_f + \text{deadband2} - \left(f^\text{ref}-f;\,D_\text{bd1}^f,D_\text{bd2}^f\right) \\ + \left(f^\mathrm{ref}-f;\,D_\mathrm{bd1}^{f},D_\mathrm{bd2}^{f}\right) \\ 0 &= -e_P - + k_\text{base}P_\text{plant}^\text{ref} - - P^\text{meas} - + D_\text{dn}\text{ramp}(e_f) - - D_\text{up}\text{ramp}(-e_f) \\ - 0 &= -e_P^\text{lim} + + k_\mathrm{base}P_\mathrm{plant}^\mathrm{ref} + - P^\mathrm{meas} + + D_\mathrm{dn}\text{ramp}(e_f) + - D_\mathrm{up}\text{ramp}(-e_f) \\ + 0 &= -e_P^{\mathrm{lim}} + \text{clamp}\left(e_P;\,e_P^{\min},e_P^{\max}\right) \\ - 0 &= -P^\text{PI} + 0 &= -P^\mathrm{PI} + \text{clamp} - \left(K_\text{pg}e_P^\text{lim}+x_P^\text{PI};\, + \left(K_\mathrm{pg}e_P^{\mathrm{lim}}+x_P^\mathrm{PI};\, P^{\min},P^{\max}\right) \\ - 0 &= -k_\text{base}P^\text{ext} + s_\text{freq}P^\text{ref} + 0 &= -k_\mathrm{base}P^\mathrm{ext} + s_\mathrm{freq}P^\mathrm{ref}. \end{aligned} ``` CommonMath defines the [derived limiter functions](../../../../CommonMath.md#derived-functions) and [ramp primitive](../../../../CommonMath.md#primitives) used above. +### External Equations + +#### Differential + +None. + +#### Algebraic + +None. + ## Initialization +REPCA reconstructs a steady operating point; arbitrary-state restart is unsupported. + ### Input Initialization ```math \begin{aligned} - V_\text{r}, V_\text{i} + V_\mathrm{r}, V_\mathrm{i} &\leftarrow \text{regulated-bus voltage} \\ - I_\text{r}, I_\text{i} + I_\mathrm{r}, I_\mathrm{i} &\leftarrow \text{branch current} \\ - P^\text{br}, Q^\text{br} + P^\mathrm{br}, Q^\mathrm{br} &\leftarrow \text{branch power} \\ f &\leftarrow \text{frequency input} \\ - Q_0^\text{seed} - &\leftarrow k_\text{base}Q_0^\text{ext} \\ - P_0^\text{seed} - &\leftarrow k_\text{base}P_0^\text{ext} + Q^\mathrm{ext} + &\leftarrow \text{known reactive-power command on system base} \\ + P^\mathrm{ext} + &\leftarrow \text{known active-power command on system base}, + \quad s_\mathrm{freq}\ \text{enabled}. \end{aligned} ``` ### Internal Initialization -With initialization tolerance $\epsilon_0=10^{-10}$, -$\text{clamp}^{-1}(z;\ell,u)$ denotes a finite smooth-limiter input whose output -matches $z$ within $\epsilon_0$. It is available for $z\in[\ell,u]$, -including at both exact limits. - -Subscript $0$ denotes initial values; all internal derivatives start at zero: +For initialization tolerance $\epsilon_\mathrm{init}$ of $10^{-10}$, +$\text{clamp}^{-1}(z;\ell,u)$ returns a finite input matching +$z\in[\ell,u]$ within that tolerance, including the limits. ```math \begin{aligned} - V_0 - &= \sqrt{ - V_{\text{r},0}^2 - + V_{\text{i},0}^2 + V + &\leftarrow \sqrt{ + V_\mathrm{r}^2 + + V_\mathrm{i}^2 } \\ - V_0^\text{ldc} - &= \sqrt{ - \left(V_{\text{r},0}-R_c I_{\text{r},0}+X_c I_{\text{i},0}\right)^2 - + \left(V_{\text{i},0}-R_c I_{\text{i},0}-X_c I_{\text{r},0}\right)^2 + V^\mathrm{ldc} + &\leftarrow \sqrt{ + \left(V_\mathrm{r}-R_c I_\mathrm{r}+X_c I_\mathrm{i}\right)^2 + + \left(V_\mathrm{i}-R_c I_\mathrm{i}-X_c I_\mathrm{r}\right)^2 } \\ - V_0^\text{droop} - &= V_0 - + K_c k_\text{base}Q_0^\text{br} \\ - V_0^\text{ctrl} - &= s_\text{comp}V_0^\text{ldc} - + s_\text{comp}^\text{off}V_0^\text{droop} \\ - V_0^\text{meas} &= V_0^\text{ctrl} \\ - Q_0^\text{meas} &= k_\text{base}Q_0^\text{br} \\ - P_0^\text{meas} &= k_\text{base}P_0^\text{br} \\ - s_{\text{frz},0} - &= \text{above}\left(V_0;\,V^\text{frz}\right) \\ - e_{\text{RQ},0} &= 0 \\ - e_{\text{RQ},0}^\text{db} - &= \text{deadband2} - \left(e_{\text{RQ},0};\,D_\text{bd1},D_\text{bd2}\right) \\ - e_{\text{RQ},0}^\text{lim} - &= \text{clamp} - \left(e_{\text{RQ},0}^\text{db};\,e^{\min},e^{\max}\right) \\ - Q_0^\text{PI} &= Q_0^\text{seed} \\ - x_{Q,0}^\text{lag} &= Q_0^\text{PI} \\ - u_{Q,0}^\text{PI} - &= \text{clamp}^{-1} - \left(Q_0^\text{PI};\,Q^{\min},Q^{\max}\right) \\ - x_{Q,0}^\text{PI} - &= u_{Q,0}^\text{PI}-K_\text{p}e_{\text{RQ},0}^\text{lim} \\ - e_{f,0} - &= \text{deadband2}\left(0;\,D_\text{bd1}^f,D_\text{bd2}^f\right) \\ - P_0^\text{freq} - &= D_\text{dn}\text{ramp}(e_{f,0}) - - D_\text{up}\text{ramp}(-e_{f,0}) \\ - e_{P,0} &= 0 \\ - e_{P,0}^\text{lim} - &= \text{clamp}\left(e_{P,0};\,e_P^{\min},e_P^{\max}\right) \\ - P_0^\text{ref} - &= + V^\mathrm{droop} + &\leftarrow V + K_c k_\mathrm{base}Q^\mathrm{br} \\ + V^\mathrm{ctrl} + &\leftarrow s_\mathrm{comp}V^\mathrm{ldc} + + s_\mathrm{comp}^{\mathrm{off}}V^\mathrm{droop} \\ + V^\mathrm{meas} &\leftarrow V^\mathrm{ctrl} \\ + Q^\mathrm{meas} &\leftarrow k_\mathrm{base}Q^\mathrm{br} \\ + P^\mathrm{meas} &\leftarrow k_\mathrm{base}P^\mathrm{br} \\ + s_\mathrm{frz} + &\leftarrow \text{above}\left(V;\,V^\mathrm{frz}\right) \\ + e_\mathrm{RQ} &\leftarrow 0 \\ + e_\mathrm{RQ}^{\mathrm{db}} + &\leftarrow \text{deadband2} + \left(e_\mathrm{RQ};\,D_\mathrm{bd1},D_\mathrm{bd2}\right) \\ + e_\mathrm{RQ}^{\mathrm{lim}} + &\leftarrow \text{clamp} + \left(e_\mathrm{RQ}^{\mathrm{db}};\,e^{\min},e^{\max}\right) \\ + Q^\mathrm{PI} &\leftarrow k_\mathrm{base}Q^\mathrm{ext} \\ + x_Q^\mathrm{lag} &\leftarrow Q^\mathrm{PI} \\ + u_Q^\mathrm{PI} + &\leftarrow \text{clamp}^{-1} + \left(Q^\mathrm{PI};\,Q^{\min},Q^{\max}\right) \\ + x_Q^\mathrm{PI} + &\leftarrow u_Q^\mathrm{PI} + -K_\mathrm{p}e_\mathrm{RQ}^{\mathrm{lim}} \\ + e_f + &\leftarrow \text{deadband2} + \left(0;\,D_\mathrm{bd1}^{f},D_\mathrm{bd2}^{f}\right) \\ + P^\mathrm{freq} + &\leftarrow D_\mathrm{dn}\text{ramp}(e_f) + - D_\mathrm{up}\text{ramp}(-e_f) \\ + e_P &\leftarrow 0 \\ + e_P^{\mathrm{lim}} + &\leftarrow \text{clamp}\left(e_P;\,e_P^{\min},e_P^{\max}\right) \\ + P^\mathrm{ref} + &\leftarrow \begin{cases} - P_0^\text{seed} & s_\text{freq}=1 \\ + k_\mathrm{base}P^\mathrm{ext} & s_\mathrm{freq}\ \text{enabled} \\ \text{clamp} - \left(P_0^\text{meas};\,P^{\min},P^{\max}\right) - & s_\text{freq}=0 + \left(P^\mathrm{meas};\,P^{\min},P^{\max}\right) + & s_\mathrm{freq}\ \text{disabled} \end{cases} \\ - P_0^\text{PI} &= P_0^\text{ref} \\ - u_{P,0}^\text{PI} - &= + P^\mathrm{PI} &\leftarrow P^\mathrm{ref} \\ + u_P^\mathrm{PI} + &\leftarrow \begin{cases} \text{clamp}^{-1} - \left(P_0^\text{PI};\,P^{\min},P^{\max}\right) - & s_\text{freq}=1 \\ - P_0^\text{meas} & s_\text{freq}=0 + \left(P^\mathrm{PI};\,P^{\min},P^{\max}\right) + & s_\mathrm{freq}\ \text{enabled} \\ + P^\mathrm{meas} & s_\mathrm{freq}\ \text{disabled} \end{cases} \\ - x_{P,0}^\text{PI} - &= u_{P,0}^\text{PI}-K_\text{pg}e_{P,0}^\text{lim} \\ - P_0^\text{ext} - &= \dfrac{s_\text{freq}}{k_\text{base}}P_0^\text{ref} + x_P^\mathrm{PI} + &\leftarrow u_P^\mathrm{PI} + -K_\mathrm{pg}e_P^{\mathrm{lim}} \\ + P^\mathrm{ext} + &\leftarrow \dfrac{s_\mathrm{freq}}{k_\mathrm{base}}P^\mathrm{ref} \\ + \dot{V}^\mathrm{meas},\dot{Q}^\mathrm{meas}, + \dot{x}_Q^\mathrm{PI},\dot{x}_Q^\mathrm{lag}, + \dot{P}^\mathrm{meas},\dot{x}_P^\mathrm{PI},\dot{P}^\mathrm{ref} + &\leftarrow 0. \end{aligned} ``` -Initialization rejects an operating point when any of the following holds: +Initialization rejects an operating point if: -- a required bus or signal value, $Q_0^\text{seed}$, or a frequency-enabled - $P_0^\text{seed}$ is not finite; -- $Q_0^\text{seed}\notin[Q^{\min},Q^{\max}]$, or - $s_\text{freq}=1$ and - $P_0^\text{seed}\notin[P^{\min},P^{\max}]$; or -- either PI antiwindup rate is nonfinite or has magnitude greater than - $\epsilon_0$. +- a required input or derived value is not finite; +- $k_\mathrm{base}Q^\mathrm{ext}\notin[Q^{\min},Q^{\max}]$, or + $s_\mathrm{freq}$ is enabled and + $k_\mathrm{base}P^\mathrm{ext}\notin[P^{\min},P^{\max}]$; or +- the gated reactive-power or active-power PI state rate is nonfinite or + exceeds $\epsilon_\mathrm{init}$ in magnitude. -Every check resolves before any storage is written, so rejected initialization -leaves model and signal storage unchanged. +Initialization is atomic; candidates are validated before state or signal writes. ### Output Initialization ```math \begin{aligned} - f^\text{ref} &\leftarrow f_0 \\ - V^\text{ref} &\leftarrow V_0^\text{meas} \\ - Q^\text{ref} &\leftarrow Q_0^\text{br} \\ - P_\text{plant}^\text{ref} - &\leftarrow \dfrac{P_0^\text{meas}-P_0^\text{freq}}{k_\text{base}} + f^\mathrm{ref} &\leftarrow f \\ + V^\mathrm{ref} &\leftarrow V^\mathrm{meas} \\ + Q^\mathrm{ref} &\leftarrow Q^\mathrm{br} \\ + P_\mathrm{plant}^\mathrm{ref} + &\leftarrow \dfrac{P^\mathrm{meas}-P^\mathrm{freq}}{k_\mathrm{base}}. \end{aligned} ``` @@ -415,8 +434,24 @@ leaves model and signal storage unchanged. Output | Units | Description | Note ----------------|--------|-------------------------------------|------ -`qext` | [p.u.] | Reactive-power command output | $Q^\text{ext}$ (system base) -`pext` | [p.u.] | Active-power command output | $P^\text{ext}$ (system base) -`vmeas` | [p.u.] | Filtered regulated voltage | $V^\text{meas}$ -`qmeas` | [p.u.] | Filtered reactive-power signal | $Q^\text{meas}$ (component base) -`pmeas` | [p.u.] | Filtered active-power signal | $P^\text{meas}$ (component base) +`qext` | [p.u.] | Reactive-power command output | $Q^\mathrm{ext}$; system base +`pext` | [p.u.] | Active-power command output | $P^\mathrm{ext}$; system base +`vmeas` | [p.u.] | Filtered regulated voltage | $V^\mathrm{meas}$ +`qmeas` | [p.u.] | Filtered reactive-power signal | $Q^\mathrm{meas}$; component base +`pmeas` | [p.u.] | Filtered active-power signal | $P^\mathrm{meas}$; component base + +## Testing + +- `validation()` checks defaults, parameter domains, signal contracts, and time floors. +- `initializationAndSignals()` checks reconstruction, bases, signals, monitors, + tags, and selectors. +- `initializationDomain()` checks rejection, exact and collapsed limits, + nonfinite values, and atomicity. +- `residualEquations()` checks every residual against a fixed answer key. +- `reactiveControl()` checks compensation and reference modes, voltage freeze, + deadbands, smooth limits, anti-windup, and lead-lag behavior. +- `activePowerControl()` checks frequency selection, deadband, droop, smooth + limits, anti-windup, and the output lag. +- `derivatives()` checks differential-row derivative signs. +- `jacobian()` checks fixed numerical and structural oracles, plus Enzyme + agreement to $10^{-9}$ when enabled. diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp index 9d7911e1f..8705053b3 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp @@ -12,6 +12,9 @@ namespace GridKit { namespace Converter { + /** + * @brief Report that a separate Jacobian is unavailable in the plain-real build. + */ template int Repca::evaluateJacobian() { diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp index a5fcc2461..3ab1922fe 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp @@ -27,98 +27,55 @@ namespace GridKit namespace Converter { - /// Internal variables of a `Repca`. + /// Internal variables and residual rows of a `Repca`. enum class RepcaInternalVariables : size_t { - VMEAS, ///< Filtered regulated voltage - QMEAS, ///< Filtered reactive-power signal - XQPI, ///< Reactive-power PI controller state - XQLAG, ///< Reactive-command lead-lag state - PMEAS, ///< Filtered active-power signal - XPPI, ///< Active-power PI controller state - PREF, ///< Active-power command lag state - V, ///< Regulated-bus voltage magnitude - VLDC, ///< Line-drop compensated voltage magnitude - VDROOP, ///< Reactive-droop-compensated voltage - VCTRL, ///< Selected voltage-measurement input - SFRZ, ///< Reactive-power PI voltage-enable gate - ERQ, ///< Selected reactive-loop error - ERQDB, ///< Deadbanded reactive-loop error - ERQLIM, ///< Limited reactive-loop error - QPI, ///< Reactive-power PI output - QEXT, ///< Reactive-power command output - EF, ///< Frequency error after deadband - EP, ///< Active-power control error - EPLIM, ///< Limited active-power control error - PPI, ///< Active-power PI output - PEXT, ///< Active-power command output - MAXIMUM, + VMEAS, ///< \f$V^\mathrm{meas}\f$ Differential filtered regulated voltage [p.u.] + QMEAS, ///< \f$Q^\mathrm{meas}\f$ Differential filtered reactive power on component base [p.u.] + XQPI, ///< \f$x_Q^\mathrm{PI}\f$ Differential reactive-power PI state on component base [p.u.] + XQLAG, ///< \f$x_Q^\mathrm{lag}\f$ Differential reactive-command lead-lag state on component base [p.u.] + PMEAS, ///< \f$P^\mathrm{meas}\f$ Differential filtered active power on component base [p.u.] + XPPI, ///< \f$x_P^\mathrm{PI}\f$ Differential active-power PI state on component base [p.u.] + PREF, ///< \f$P^\mathrm{ref}\f$ Differential active-power command lag state on component base [p.u.] + V, ///< \f$V\f$ Algebraic regulated-bus voltage magnitude [p.u.] + VLDC, ///< \f$V^\mathrm{ldc}\f$ Algebraic line-drop compensated voltage magnitude [p.u.] + VDROOP, ///< \f$V^\mathrm{droop}\f$ Algebraic reactive-droop-compensated voltage [p.u.] + VCTRL, ///< \f$V^\mathrm{ctrl}\f$ Algebraic selected voltage-measurement input [p.u.] + SFRZ, ///< \f$s_\mathrm{frz}\f$ Algebraic reactive-power PI voltage-enable gate [-] + ERQ, ///< \f$e_\mathrm{RQ}\f$ Algebraic selected reactive-loop error [p.u.] + ERQDB, ///< \f$e_\mathrm{RQ}^\mathrm{db}\f$ Algebraic deadbanded reactive-loop error [p.u.] + ERQLIM, ///< \f$e_\mathrm{RQ}^\mathrm{lim}\f$ Algebraic limited reactive-loop error [p.u.] + QPI, ///< \f$Q^\mathrm{PI}\f$ Algebraic reactive-power PI output on component base [p.u.] + QEXT, ///< \f$Q^\mathrm{ext}\f$ Algebraic reactive-power command on system base [p.u.] + EF, ///< \f$e_f\f$ Algebraic frequency error after deadband [p.u.] + EP, ///< \f$e_P\f$ Algebraic active-power control error on component base [p.u.] + EPLIM, ///< \f$e_P^\mathrm{lim}\f$ Algebraic limited active-power control error on component base [p.u.] + PPI, ///< \f$P^\mathrm{PI}\f$ Algebraic active-power PI output on component base [p.u.] + PEXT, ///< \f$P^\mathrm{ext}\f$ Algebraic active-power command on system base [p.u.] + MAXIMUM ///< Number of REPCA internal variables and residual rows }; - /// External variables of a `Repca`. + /// External signal variables read or initialized by a `Repca`. enum class RepcaExternalVariables : size_t { - IBRANCHR, ///< Required branch-current real component - IBRANCHI, ///< Required branch-current imaginary component - PBRANCH, ///< Required branch active power - QBRANCH, ///< Required branch reactive power - FREQ, ///< Required frequency input - FREQREF, ///< Optional frequency reference - VREF, ///< Optional voltage-control reference - QREF, ///< Optional reactive-power reference - PPLANTREF, ///< Optional plant active-power reference - MAXIMUM, - }; - - /// Indices into the REPCA state, derivative, and residual vectors. - struct RepcaIdx - { - static constexpr size_t VMEAS = static_cast(RepcaInternalVariables::VMEAS); - static constexpr size_t QMEAS = static_cast(RepcaInternalVariables::QMEAS); - static constexpr size_t XQPI = static_cast(RepcaInternalVariables::XQPI); - static constexpr size_t XQLAG = static_cast(RepcaInternalVariables::XQLAG); - static constexpr size_t PMEAS = static_cast(RepcaInternalVariables::PMEAS); - static constexpr size_t XPPI = static_cast(RepcaInternalVariables::XPPI); - static constexpr size_t PREF = static_cast(RepcaInternalVariables::PREF); - static constexpr size_t V = static_cast(RepcaInternalVariables::V); - static constexpr size_t VLDC = static_cast(RepcaInternalVariables::VLDC); - static constexpr size_t VDROOP = static_cast(RepcaInternalVariables::VDROOP); - static constexpr size_t VCTRL = static_cast(RepcaInternalVariables::VCTRL); - static constexpr size_t SFRZ = static_cast(RepcaInternalVariables::SFRZ); - static constexpr size_t ERQ = static_cast(RepcaInternalVariables::ERQ); - static constexpr size_t ERQDB = static_cast(RepcaInternalVariables::ERQDB); - static constexpr size_t ERQLIM = static_cast(RepcaInternalVariables::ERQLIM); - static constexpr size_t QPI = static_cast(RepcaInternalVariables::QPI); - static constexpr size_t QEXT = static_cast(RepcaInternalVariables::QEXT); - static constexpr size_t EF = static_cast(RepcaInternalVariables::EF); - static constexpr size_t EP = static_cast(RepcaInternalVariables::EP); - static constexpr size_t EPLIM = static_cast(RepcaInternalVariables::EPLIM); - static constexpr size_t PPI = static_cast(RepcaInternalVariables::PPI); - static constexpr size_t PEXT = static_cast(RepcaInternalVariables::PEXT); - static constexpr size_t MAXIMUM = static_cast(RepcaInternalVariables::MAXIMUM); - }; - - /// Indices into the REPCA external-signal buffers. - struct RepcaExt - { - static constexpr size_t IBRANCHR = static_cast(RepcaExternalVariables::IBRANCHR); - static constexpr size_t IBRANCHI = static_cast(RepcaExternalVariables::IBRANCHI); - static constexpr size_t PBRANCH = static_cast(RepcaExternalVariables::PBRANCH); - static constexpr size_t QBRANCH = static_cast(RepcaExternalVariables::QBRANCH); - static constexpr size_t FREQ = static_cast(RepcaExternalVariables::FREQ); - static constexpr size_t FREQREF = static_cast(RepcaExternalVariables::FREQREF); - static constexpr size_t VREF = static_cast(RepcaExternalVariables::VREF); - static constexpr size_t QREF = static_cast(RepcaExternalVariables::QREF); - static constexpr size_t PPLANTREF = static_cast(RepcaExternalVariables::PPLANTREF); - static constexpr size_t MAXIMUM = static_cast(RepcaExternalVariables::MAXIMUM); + IBRANCHR, ///< \f$I_\mathrm{r}\f$ Required branch-current real component on system base [p.u.] + IBRANCHI, ///< \f$I_\mathrm{i}\f$ Required branch-current imaginary component on system base [p.u.] + PBRANCH, ///< \f$P^\mathrm{br}\f$ Required branch active power on system base [p.u.] + QBRANCH, ///< \f$Q^\mathrm{br}\f$ Required branch reactive power on system base [p.u.] + FREQ, ///< \f$f\f$ Required frequency input [p.u.] + FREQREF, ///< \f$f^\mathrm{ref}\f$ Optional frequency reference [p.u.] + VREF, ///< \f$V^\mathrm{ref}\f$ Optional voltage-control reference [p.u.] + QREF, ///< \f$Q^\mathrm{ref}\f$ Optional reactive-power reference on system base [p.u.] + PPLANTREF, ///< \f$P_\mathrm{plant}^\mathrm{ref}\f$ Optional plant active-power reference on system base [p.u.] + MAXIMUM ///< Number of REPCA external signal variables }; template class Repca : public Component { - using Component::alpha_; using Component::abs_tol_; using Component::allocated_; + using Component::alpha_; using Component::f_; using Component::gridkit_component_id_; using Component::J_cols_buffer_; @@ -135,24 +92,26 @@ namespace GridKit using Component::yp_; public: - using ScalarT = scalar_type; - using IdxT = index_type; - using RealT = typename Component::RealT; - using BusT = BusBase; - using SignalT = SignalNode; - using ModelDataT = RepcaData; - using MonitorT = Model::VariableMonitor; + using ScalarT = scalar_type; + using IdxT = index_type; + using RealT = typename Component::RealT; + using BusT = BusBase; + using SignalT = SignalNode; + using ModelDataT = RepcaData; + using MonitorT = Model::VariableMonitor; + using InternalVariablesT = RepcaInternalVariables; + using ExternalVariablesT = RepcaExternalVariables; Repca(BusT* bus); Repca(BusT* bus, const ModelDataT& data); ~Repca(); - int setGridKitComponentID(IdxT) override final; + int setGridKitComponentID(IdxT component_id) override final; int allocate() override final; int verify() const override final; int initialize() override final; int tagDifferentiable() override final; - int setAbsoluteTolerance(RealT) override final; + int setAbsoluteTolerance(RealT rel_tol) override final; int evaluateResidual() override final; int evaluateJacobian() override final; @@ -160,31 +119,51 @@ namespace GridKit -> ComponentSignals& - { - return signals_; - } + RepcaExternalVariables>&; const Model::VariableMonitorBase* getMonitor() const override; - __attribute__((always_inline)) inline int evaluateInternalResidual( - const ScalarT*, const ScalarT*, const ScalarT*, const ScalarT*, ScalarT*); + [[gnu::always_inline]] inline int evaluateInternalResidual( + const ScalarT* y, + const ScalarT* yp, + const ScalarT* wb, + const ScalarT* ws, + ScalarT* f); private: - void initializeParameters(const ModelDataT& data); - void initializeMonitor(); - void setDerivedParameters(); + static constexpr size_t index(RepcaInternalVariables variable) + { + return static_cast(variable); + } + + static constexpr size_t index(RepcaExternalVariables variable) + { + return static_cast(variable); + } + + static void checkConfiguration(bool condition, const char* message, int& errors); + void loadRealParameter(const ModelDataT& data, + RepcaParameters parameter, + RealT& target, + const char* name); + void loadSwitchParameter(const ModelDataT& data, + RepcaParameters parameter, + bool& target, + const char* name); + bool floorTimeConstant(RealT& value, const char* name); + void initializeParameters(const ModelDataT& data); + void initializeMonitor(); + void setDerivedParameters(); bool solveLimiterInput(ScalarT requested_output, RealT lower_limit, RealT upper_limit, ScalarT& limiter_input) const; - /// Evaluate log(1 - exp(-x)) without cancellation for positive x. RealT logOneMinusExp(RealT x) const; - ScalarT toComponentBase(ScalarT value) const; - ScalarT toSystemBase(ScalarT value) const; + [[gnu::always_inline]] inline ScalarT toComponentBase(ScalarT value) const; + ScalarT toSystemBase(ScalarT value) const; ScalarT& Vr(); ScalarT& Vi(); @@ -192,8 +171,6 @@ namespace GridKit static constexpr RealT TIME_CONSTANT_MINIMUM = static_cast(1.0e-3); static constexpr RealT INITIALIZATION_TOLERANCE = static_cast(1.0e-10); - /// Distance past a smooth-clamp bound; with CommonMath MU = 240, - /// this keeps exact-limit initialization residuals below 2e-13. static constexpr RealT INITIALIZATION_LIMIT_OFFSET = static_cast(0.1); BusT* bus_{nullptr}; diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp index 4b76b5ea7..17bbf9f6a 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp @@ -14,82 +14,90 @@ namespace GridKit { namespace Converter { - /// Parameter keys for the REPCA plant-control model. + /// Parameter keys for REPCA; all are optional with documented defaults. enum class RepcaParameters { - mva, ///< REPCA component power base - VcompFlag, ///< Voltage-compensation mode flag - RefFlag, ///< Reactive-loop reference flag - Freqflag, ///< Active-power control output flag - Tfltr, ///< Voltage and reactive-power measurement filter time constant - Vfrz, ///< Reactive-power PI freeze voltage threshold - Rc, ///< Line-drop compensation resistance - Xc, ///< Line-drop compensation reactance - Kc, ///< Reactive-current compensation gain - dbdlow, ///< Lower reactive-loop deadband threshold - dbdupper, ///< Upper reactive-loop deadband threshold - emax, ///< Maximum reactive-loop error limit - emin, ///< Minimum reactive-loop error limit - Kp, ///< Reactive-power controller proportional gain - Ki, ///< Reactive-power controller integral gain - Qmax, ///< Maximum reactive-power command - Qmin, ///< Minimum reactive-power command - Tft, ///< Reactive-command lead time constant - Tfv, ///< Reactive-command lag time constant - Tp, ///< Active-power measurement filter time constant - fdbd1, ///< Lower frequency-error deadband threshold - fdbd2, ///< Upper frequency-error deadband threshold - Ddn, ///< Down-frequency droop response gain - Dup, ///< Up-frequency droop response gain - femax, ///< Maximum active-power error limit - femin, ///< Minimum active-power error limit - Kpg, ///< Active-power controller proportional gain - Kig, ///< Active-power controller integral gain - Pmax, ///< Maximum active-power command - Pmin, ///< Minimum active-power command - Tlag ///< Active-power command lag time constant + mva, ///< \f$S^\mathrm{base}\f$ Component power base [MVA] + VcompFlag, ///< \f$s_\mathrm{comp}\f$ Voltage-compensation selector [binary] + RefFlag, ///< \f$s_\mathrm{ref}\f$ Reactive-loop reference selector [binary] + Freqflag, ///< \f$s_\mathrm{freq}\f$ Active-power output selector [binary] + Tfltr, ///< \f$T_\mathrm{fltr}\f$ Voltage and reactive-power filter time constant [sec] + Vfrz, ///< \f$V^\mathrm{frz}\f$ Reactive-power PI freeze threshold [p.u.] + Rc, ///< \f$R_c\f$ Line-drop resistance on system base [p.u.] + Xc, ///< \f$X_c\f$ Line-drop reactance on system base [p.u.] + Kc, ///< \f$K_c\f$ Reactive-current compensation gain [p.u.] + dbdlow, ///< \f$D_\mathrm{bd1}\f$ Lower reactive-loop deadband threshold [p.u.] + dbdupper, ///< \f$D_\mathrm{bd2}\f$ Upper reactive-loop deadband threshold [p.u.] + emax, ///< \f$e^{\max}\f$ Maximum reactive-loop error [p.u.] + emin, ///< \f$e^{\min}\f$ Minimum reactive-loop error [p.u.] + Kp, ///< \f$K_\mathrm{p}\f$ Reactive-power proportional gain [p.u.] + Ki, ///< \f$K_\mathrm{i}\f$ Reactive-power integral gain [p.u./s] + Qmax, ///< \f$Q^{\max}\f$ Maximum reactive-power command on component base [p.u.] + Qmin, ///< \f$Q^{\min}\f$ Minimum reactive-power command on component base [p.u.] + Tft, ///< \f$T_\mathrm{ft}\f$ Reactive-command lead time constant [sec] + Tfv, ///< \f$T_\mathrm{fv}\f$ Reactive-command lag time constant [sec] + Tp, ///< \f$T_\mathrm{p}\f$ Active-power measurement filter time constant [sec] + fdbd1, ///< \f$D_\mathrm{bd1}^{f}\f$ Lower frequency-error deadband threshold [p.u.] + fdbd2, ///< \f$D_\mathrm{bd2}^{f}\f$ Upper frequency-error deadband threshold [p.u.] + Ddn, ///< \f$D_\mathrm{dn}\f$ Down-frequency droop response gain [p.u.] + Dup, ///< \f$D_\mathrm{up}\f$ Up-frequency droop response gain [p.u.] + femax, ///< \f$e_P^{\max}\f$ Maximum active-power error [p.u.] + femin, ///< \f$e_P^{\min}\f$ Minimum active-power error [p.u.] + Kpg, ///< \f$K_\mathrm{pg}\f$ Active-power proportional gain [p.u.] + Kig, ///< \f$K_\mathrm{ig}\f$ Active-power integral gain [p.u./s] + Pmax, ///< \f$P^{\max}\f$ Maximum active-power command on component base [p.u.] + Pmin, ///< \f$P^{\min}\f$ Minimum active-power command on component base [p.u.] + Tlag ///< \f$T_\mathrm{lag}\f$ Active-power command lag time constant [sec] }; /// Buses for the REPCA plant-control model. enum class RepcaBuses : size_t { - bus, ///< Regulated bus ID - SIZE + bus, ///< \f$V_\mathrm{r},V_\mathrm{i}\f$ Required Known regulated-bus voltage [p.u.] + SIZE ///< Number of REPCA bus ports }; /// Signal inputs for the REPCA plant-control model. enum class RepcaSignalInputs : size_t { - ibranchr, ///< Required branch-current real component signal ID - ibranchi, ///< Required branch-current imaginary component signal ID - pbranch, ///< Required branch active-power signal ID - qbranch, ///< Required branch reactive-power signal ID - freq, ///< Required frequency input signal ID - freqref, ///< Optional frequency-reference signal ID - vref, ///< Optional voltage-reference signal ID - qref, ///< Optional reactive-power reference signal ID - pplantref, ///< Optional plant active-power reference signal ID - SIZE + ibranchr, ///< \f$I_\mathrm{r}\f$ Required Known branch-current real input on system base [p.u.] + ibranchi, ///< \f$I_\mathrm{i}\f$ Required Known branch-current imaginary input on system base [p.u.] + pbranch, ///< \f$P^\mathrm{br}\f$ Required Known branch active-power input on system base [p.u.] + qbranch, ///< \f$Q^\mathrm{br}\f$ Required Known branch reactive-power input on system base [p.u.] + freq, ///< \f$f\f$ Required Known frequency input [p.u.] + freqref, ///< \f$f^\mathrm{ref}\f$ Optional Unknown frequency-reference input [p.u.] + vref, ///< \f$V^\mathrm{ref}\f$ Optional Unknown voltage-reference input [p.u.] + qref, ///< \f$Q^\mathrm{ref}\f$ Optional Unknown reactive-power reference on system base [p.u.] + pplantref, ///< \f$P_\mathrm{plant}^\mathrm{ref}\f$ Optional Unknown plant active-power reference on system base [p.u.] + SIZE ///< Number of REPCA signal-input ports }; /// Signal outputs for the REPCA plant-control model. enum class RepcaSignalOutputs : size_t { - qext, ///< Optional reactive-power command output signal ID - pext, ///< Optional active-power command output signal ID - SIZE + qext, ///< \f$Q^\mathrm{ext}\f$ Optional Known reactive-power command output on system base [p.u.] + pext, ///< \f$P^\mathrm{ext}\f$ Optional Known active-power command output on system base [p.u.] + SIZE ///< Number of REPCA signal-output ports }; /// Variables available through the monitor interface. enum class RepcaMonitorableVariables { - qext, ///< Reactive-power command output - pext, ///< Active-power command output - vmeas, ///< Filtered regulated voltage - qmeas, ///< Filtered reactive-power signal - pmeas ///< Filtered active-power signal + qext, ///< \f$Q^\mathrm{ext}\f$ Reactive-power command output on system base [p.u.] + pext, ///< \f$P^\mathrm{ext}\f$ Active-power command output on system base [p.u.] + vmeas, ///< \f$V^\mathrm{meas}\f$ Filtered regulated voltage [p.u.] + qmeas, ///< \f$Q^\mathrm{meas}\f$ Filtered reactive-power signal on component base [p.u.] + pmeas ///< \f$P^\mathrm{meas}\f$ Filtered active-power signal on component base [p.u.] }; + /** + * @brief Model data for REPCA parameters, bus and signal ports, and monitored variables. + * + * @tparam real_type Real parameter value type. + * @tparam index_type Integer index and serialized selector type. + * + * @see Repca + */ template struct RepcaData : public ComponentData int Repca::evaluateJacobian() { diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp index 8fc08c2b0..2f5782de2 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp @@ -14,6 +14,19 @@ namespace GridKit { namespace Converter { + /** + * @brief Assemble the sparse REPCA Jacobian with Enzyme. + * + * Differentiates the internal residual with respect to state, derivative, + * regulated-bus, and linked signal variables, then constructs the model + * COO matrix. + * + * @pre allocate() has sized the model and Jacobian index maps. + * @pre evaluateResidual() has refreshed the current bus/signal values and + * signal indices. + * @pre The containing solver has set the current integration coefficient + * and global variable/residual indices. + */ template int Repca::evaluateJacobian() { diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp index d491f8197..c5e6cd6fe 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp @@ -23,22 +23,38 @@ namespace GridKit { namespace Converter { + /// Logger used for REPCA diagnostics. using Log = ::GridKit::Utilities::Logger; /** - * @brief Construct REPCA with its documented parameter defaults. + * @class Repca + * @brief WECC renewable plant controller with reactive-power and + * active-power control paths. + * + * @tparam scalar_type Plain real or differentiable scalar type. + * @tparam index_type Integer index type. + */ + + /** + * @brief Construct REPCA with its documented parameter defaults + * + * The regulated bus is retained, the model is sized, and every + * parameter keeps its documented default. No monitor or signal + * connection is created. + * * @param[in] bus Regulated bus measured by the controller. */ template Repca::Repca(BusT* bus) : bus_(bus) { - size_ = static_cast(RepcaIdx::MAXIMUM); + size_ = static_cast(index(RepcaInternalVariables::MAXIMUM)); setDerivedParameters(); } /** - * @brief Construct REPCA from parameters and monitor selections. + * @brief Construct REPCA from model data + * * @param[in] bus Regulated bus measured by the controller. * @param[in] data Model parameters and monitor selections. */ @@ -49,258 +65,23 @@ namespace GridKit { initializeParameters(data); initializeMonitor(); - size_ = static_cast(RepcaIdx::MAXIMUM); - } - - template - Repca::~Repca() - { - } - - /// Regulated-bus voltage, real component. - template - scalar_type& Repca::Vr() - { - return bus_->Vr(); - } - - /// Regulated-bus voltage, imaginary component. - template - scalar_type& Repca::Vi() - { - return bus_->Vi(); + size_ = static_cast(index(RepcaInternalVariables::MAXIMUM)); } /** - * @brief Load provided parameters and retain documented defaults for omissions. - * - * Numeric parameters accept real or integer values. Mode switches accept - * bool and numeric 0/1 values; invalid types are counted for verify(). + * @brief Destroy the plant controller and its optional variable monitor. */ template - void Repca::initializeParameters(const ModelDataT& data) - { - using Params = typename ModelDataT::Parameters; - - parameter_error_count_ = 0; - - auto load_real = [&](auto key, RealT& target, const char* name) - { - if (!data.parameters.contains(key)) - { - return; - } - - const auto& value = data.parameters.at(key); - if (const auto* real_value = std::get_if(&value)) - { - target = *real_value; - } - else if (const auto* index_value = std::get_if(&value)) - { - target = static_cast(*index_value); - } - else - { - Log::error() << "Repca: parameter '" << name << "' must be numeric\n"; - ++parameter_error_count_; - } - }; - - auto load_switch = [&](auto key, bool& target, const char* name) - { - if (!data.parameters.contains(key)) - { - return; - } - - const auto& value = data.parameters.at(key); - if (const auto* bool_value = std::get_if(&value)) - { - target = *bool_value; - } - else if (const auto* index_value = std::get_if(&value); - index_value && (*index_value == 0 || *index_value == 1)) - { - target = (*index_value == 1); - } - else if (const auto* real_value = std::get_if(&value); - real_value && (*real_value == RealT{0} || *real_value == RealT{1})) - { - target = (*real_value == RealT{1}); - } - else - { - Log::error() << "Repca: parameter '" << name << "' must be bool or 0/1\n"; - ++parameter_error_count_; - } - }; - - load_real(Params::mva, mva_base_, "mva"); - load_switch(Params::VcompFlag, VcompFlag_, "VcompFlag"); - load_switch(Params::RefFlag, RefFlag_, "RefFlag"); - load_switch(Params::Freqflag, Freqflag_, "Freqflag"); - load_real(Params::Tfltr, Tfltr_, "Tfltr"); - load_real(Params::Vfrz, Vfrz_, "Vfrz"); - load_real(Params::Rc, Rc_, "Rc"); - load_real(Params::Xc, Xc_, "Xc"); - load_real(Params::Kc, Kc_, "Kc"); - load_real(Params::dbdlow, dbdlow_, "dbdlow"); - load_real(Params::dbdupper, dbdupper_, "dbdupper"); - load_real(Params::emax, emax_, "emax"); - load_real(Params::emin, emin_, "emin"); - load_real(Params::Kp, Kp_, "Kp"); - load_real(Params::Ki, Ki_, "Ki"); - load_real(Params::Qmax, Qmax_, "Qmax"); - load_real(Params::Qmin, Qmin_, "Qmin"); - load_real(Params::Tft, Tft_, "Tft"); - load_real(Params::Tfv, Tfv_, "Tfv"); - load_real(Params::Tp, Tp_, "Tp"); - load_real(Params::fdbd1, fdbd1_, "fdbd1"); - load_real(Params::fdbd2, fdbd2_, "fdbd2"); - load_real(Params::Ddn, Ddn_, "Ddn"); - load_real(Params::Dup, Dup_, "Dup"); - load_real(Params::femax, femax_, "femax"); - load_real(Params::femin, femin_, "femin"); - load_real(Params::Kpg, Kpg_, "Kpg"); - load_real(Params::Kig, Kig_, "Kig"); - load_real(Params::Pmax, Pmax_, "Pmax"); - load_real(Params::Pmin, Pmin_, "Pmin"); - load_real(Params::Tlag, Tlag_, "Tlag"); - setDerivedParameters(); - } - - /** - * @brief Resolve time floors, the component power base, and mode masks. - */ - template - void Repca::setDerivedParameters() - { - if (Tfltr_ < TIME_CONSTANT_MINIMUM || Tp_ < TIME_CONSTANT_MINIMUM - || Tlag_ < TIME_CONSTANT_MINIMUM) - { - Log::warning() << "Repca: Tfltr, Tp, and Tlag below " << TIME_CONSTANT_MINIMUM - << " s are raised to that floor to keep the controller lags well posed\n"; - } - - Tfltr_ = std::max(Tfltr_, TIME_CONSTANT_MINIMUM); - Tp_ = std::max(Tp_, TIME_CONSTANT_MINIMUM); - Tlag_ = std::max(Tlag_, TIME_CONSTANT_MINIMUM); - - va_converter_base_ = mva_base_ * static_cast(1.0e6); - - vcomp_on_ = VcompFlag_ ? ONE : ZERO; - vcomp_off_ = ONE - vcomp_on_; - ref_on_ = RefFlag_ ? ONE : ZERO; - ref_off_ = ONE - ref_on_; - freq_on_ = Freqflag_ ? ONE : ZERO; - } - - /// Evaluate log(1 - exp(-x)) accurately for a positive argument. - template - typename Repca::RealT - Repca::logOneMinusExp(RealT x) const + Repca::~Repca() { - const RealT log_two = std::log(static_cast(2.0)); - - if (x < log_two) - { - return log_two - HALF * x + std::log(std::sinh(HALF * x)); - } - return std::log1p(-std::exp(-x)); } /** - * @brief Recover an input for a requested smooth-clamp output. - * @return false when the limits are invalid or the requested output lies outside them. + * @brief Set the component ID + * + * @param[in] component_id Identifier assigned by the system model. */ template - bool Repca::solveLimiterInput( - ScalarT requested_output, - RealT lower_limit, - RealT upper_limit, - ScalarT& limiter_input) const - { - const RealT output_value = static_cast(requested_output); - - if (lower_limit > upper_limit || output_value < lower_limit || output_value > upper_limit) - { - return false; - } - - const RealT width = upper_limit - lower_limit; - if (width <= INITIALIZATION_TOLERANCE) - { - limiter_input = static_cast(lower_limit); - return true; - } - - const RealT distance_from_lower = output_value - lower_limit; - const RealT distance_from_upper = upper_limit - output_value; - if (distance_from_lower <= INITIALIZATION_TOLERANCE) - { - limiter_input = static_cast(lower_limit - INITIALIZATION_LIMIT_OFFSET); - return true; - } - if (distance_from_upper <= INITIALIZATION_TOLERANCE) - { - limiter_input = static_cast(upper_limit + INITIALIZATION_LIMIT_OFFSET); - return true; - } - - const RealT mu = Math::MU; - const RealT scaled_lower_distance = mu * distance_from_lower; - const RealT scaled_upper_distance = mu * distance_from_upper; - const RealT log_lower = logOneMinusExp(scaled_lower_distance); - const RealT log_upper = logOneMinusExp(scaled_upper_distance); - const RealT correction = (scaled_lower_distance + log_lower - log_upper) / mu; - - limiter_input = static_cast(lower_limit + correction); - return true; - } - - /// Convert a system-base power quantity to REPCA component base. - template - scalar_type Repca::toComponentBase(scalar_type value) const - { - return value * va_system_base_ / va_converter_base_; - } - - /// Convert a component-base power quantity to system base. - template - scalar_type Repca::toSystemBase(scalar_type value) const - { - return value / toComponentBase(static_cast(ONE)); - } - - /// Access the configured monitor, or nullptr for the default constructor. - template - const Model::VariableMonitorBase* Repca::getMonitor() const - { - return monitor_.get(); - } - - /// Bind monitor selections to REPCA state entries in public monitor order. - template - void Repca::initializeMonitor() - { - using I = RepcaIdx; - using Variable = typename ModelDataT::MonitorableVariables; - - monitor_->set(Variable::qext, [this] - { return y_.getData()[I::QEXT]; }); - monitor_->set(Variable::pext, [this] - { return y_.getData()[I::PEXT]; }); - monitor_->set(Variable::vmeas, [this] - { return y_.getData()[I::VMEAS]; }); - monitor_->set(Variable::qmeas, [this] - { return y_.getData()[I::QMEAS]; }); - monitor_->set(Variable::pmeas, [this] - { return y_.getData()[I::PMEAS]; }); - } - - /// Set the component identifier used by monitor labels. - template int Repca::setGridKitComponentID(IdxT component_id) { gridkit_component_id_ = component_id; @@ -308,14 +89,18 @@ namespace GridKit } /** - * @brief Allocate model buffers and bind assigned command-output nodes. - * @return 0 on success. + * @brief Allocate model vectors and wire assigned command outputs + * + * Sizes the state, residual, bus, and signal-interface buffers, initializes + * identity index maps, and points assigned `qext` and `pext` nodes at + * the internal system-base states that REPCA publishes. Repeated + * allocation reuses the existing model vectors and signal links. */ template int Repca::allocate() { - using I = RepcaIdx; - using E = RepcaExt; + using I = RepcaInternalVariables; + using E = RepcaExternalVariables; if (!allocated_) { @@ -329,7 +114,7 @@ namespace GridKit wb_.assign(2, ScalarT{0}); - const auto signal_size = E::MAXIMUM; + const auto signal_size = index(E::MAXIMUM); ws_.assign(signal_size, ScalarT{0}); ws_indices_.assign(signal_size, INVALID_INDEX); @@ -344,15 +129,15 @@ namespace GridKit if (signals_.template isAssigned()) { signals_.template getSignalNode()->set( - &y[I::QEXT], - &(this->getVariableIndex(static_cast(I::QEXT)))); + &y[index(I::QEXT)], + &(this->getVariableIndex(static_cast(index(I::QEXT))))); } if (signals_.template isAssigned()) { signals_.template getSignalNode()->set( - &y[I::PEXT], - &(this->getVariableIndex(static_cast(I::PEXT)))); + &y[index(I::PEXT)], + &(this->getVariableIndex(static_cast(index(I::PEXT))))); } allocated_ = true; @@ -360,41 +145,142 @@ namespace GridKit } /** - * @brief Validate parameters, the regulated bus, and signal connections. - * @return The number of configuration errors. + * @brief Validate the REPCA configuration + * + * Checks parameter-loading errors, finiteness and static parameter + * relationships, system/component bases and both conversion ratios, + * the regulated bus, required measurement signals, and attached + * optional reference signals. Command-output assignment is optional. + * Operating-point feasibility is checked by initialize(). + * + * @return Number of configuration errors; zero when valid. */ template int Repca::verify() const { int ret = static_cast(parameter_error_count_); - auto check = [&](bool condition, const char* message) + checkConfiguration(bus_ != nullptr, "regulated bus is required", ret); + + const bool valid_converter_base = std::isfinite(mva_base_) + && mva_base_ > ZERO + && std::isfinite(va_converter_base_) + && va_converter_base_ > ZERO; + const bool valid_system_base = std::isfinite(va_system_base_) + && va_system_base_ > ZERO; + checkConfiguration(valid_converter_base, + "mva must define a finite positive converter power base", + ret); + checkConfiguration(valid_system_base, + "system power base must be finite and positive", + ret); + if (valid_converter_base && valid_system_base) { - if (!condition) - { - Log::error() << "Repca: " << message << '\n'; - ret += 1; - } - }; + const RealT system_to_converter = va_system_base_ / va_converter_base_; + const RealT converter_to_system = va_converter_base_ / va_system_base_; + checkConfiguration( + std::isfinite(system_to_converter) + && system_to_converter > ZERO + && std::isfinite(converter_to_system) + && converter_to_system > ZERO, + "system/converter power-base conversion ratios must be finite and positive", + ret); + } + + checkConfiguration(std::isfinite(Tfltr_), "Tfltr must be finite", ret); + checkConfiguration(std::isfinite(Vfrz_), "Vfrz must be finite", ret); + checkConfiguration(std::isfinite(Rc_), "Rc must be finite", ret); + checkConfiguration(std::isfinite(Xc_), "Xc must be finite", ret); + checkConfiguration(std::isfinite(Kc_), "Kc must be finite", ret); + + const bool finite_reactive_deadband = std::isfinite(dbdlow_) + && std::isfinite(dbdupper_); + checkConfiguration(finite_reactive_deadband, + "dbdlow and dbdupper must be finite", + ret); + if (finite_reactive_deadband) + { + checkConfiguration(dbdlow_ <= ZERO && ZERO <= dbdupper_, + "dbdlow <= 0 <= dbdupper is required", + ret); + } - if (bus_ == nullptr) + const bool finite_reactive_error_limits = std::isfinite(emin_) + && std::isfinite(emax_); + checkConfiguration(finite_reactive_error_limits, + "emin and emax must be finite", + ret); + if (finite_reactive_error_limits) { - Log::error() << "Repca: bus pointer is null\n"; - ret += 1; + checkConfiguration(emin_ <= ZERO && ZERO <= emax_, + "emin <= 0 <= emax is required", + ret); } - check(mva_base_ > ZERO, "mva must be positive"); - check(Tfv_ > ZERO, "Tfv must be positive"); - check(dbdlow_ <= ZERO && ZERO <= dbdupper_, - "dbdlow <= 0 <= dbdupper is required"); - check(emin_ <= ZERO && ZERO <= emax_, - "emin <= 0 <= emax is required"); - check(Qmin_ <= Qmax_, "Qmin must be less than or equal to Qmax"); - check(fdbd1_ <= ZERO && ZERO <= fdbd2_, - "fdbd1 <= 0 <= fdbd2 is required"); - check(femin_ <= ZERO && ZERO <= femax_, - "femin <= 0 <= femax is required"); - check(Pmin_ <= Pmax_, "Pmin must be less than or equal to Pmax"); + checkConfiguration(std::isfinite(Kp_), "Kp must be finite", ret); + checkConfiguration(std::isfinite(Ki_), "Ki must be finite", ret); + + const bool finite_reactive_limits = std::isfinite(Qmin_) + && std::isfinite(Qmax_); + checkConfiguration(finite_reactive_limits, + "Qmin and Qmax must be finite", + ret); + if (finite_reactive_limits) + { + checkConfiguration(Qmin_ <= Qmax_, + "Qmin must be less than or equal to Qmax", + ret); + } + + checkConfiguration(std::isfinite(Tft_), "Tft must be finite", ret); + checkConfiguration(std::isfinite(Tfv_) && Tfv_ > ZERO, + "Tfv must be finite and positive", + ret); + checkConfiguration(std::isfinite(Tp_), "Tp must be finite", ret); + + const bool finite_frequency_deadband = std::isfinite(fdbd1_) + && std::isfinite(fdbd2_); + checkConfiguration(finite_frequency_deadband, + "fdbd1 and fdbd2 must be finite", + ret); + if (finite_frequency_deadband) + { + checkConfiguration(fdbd1_ <= ZERO && ZERO <= fdbd2_, + "fdbd1 <= 0 <= fdbd2 is required", + ret); + } + + checkConfiguration(std::isfinite(Ddn_), "Ddn must be finite", ret); + checkConfiguration(std::isfinite(Dup_), "Dup must be finite", ret); + + const bool finite_active_error_limits = std::isfinite(femin_) + && std::isfinite(femax_); + checkConfiguration(finite_active_error_limits, + "femin and femax must be finite", + ret); + if (finite_active_error_limits) + { + checkConfiguration(femin_ <= ZERO && ZERO <= femax_, + "femin <= 0 <= femax is required", + ret); + } + + checkConfiguration(std::isfinite(Kpg_), "Kpg must be finite", ret); + checkConfiguration(std::isfinite(Kig_), "Kig must be finite", ret); + + const bool finite_active_limits = std::isfinite(Pmin_) + && std::isfinite(Pmax_); + checkConfiguration(finite_active_limits, + "Pmin and Pmax must be finite", + ret); + if (finite_active_limits) + { + checkConfiguration(Pmin_ <= Pmax_, + "Pmin must be less than or equal to Pmax", + ret); + } + + checkConfiguration(std::isfinite(Tlag_), "Tlag must be finite", ret); auto check_required_signal = [&](const char* name) { @@ -435,23 +321,45 @@ namespace GridKit } /** - * @brief Initialize from seeded qext and, when frequency control is enabled, pext. + * @brief Initialize REPCA from the initial plant commands + * + * Reads the required bus and measurement ports, preserves the initial + * system-base `qext` value and, when frequency control is enabled, `pext`, + * reconstructs the steady controller state, and publishes the resolved + * optional references. * - * All feasibility checks precede state, derivative, latch, and signal - * writes, so failure leaves model and signal storage unchanged. + * @pre allocate() has completed. + * @pre verify() reports a valid parameter and port configuration. + * @pre `qext` and, when frequency control is enabled, `pext` contain the + * initial plant commands. * - * @pre allocate() has completed and verify() returned zero. - * @return 0 on success; nonzero for an inadmissible operating point. + * @post On failure no state, derivative, latch, or signal storage is + * modified. + * + * @return 0 on success; nonzero when allocation, configuration, initial- + * value, candidate, command-limit, or steady-state checks fail. */ template int Repca::initialize() { - using I = RepcaIdx; + using I = RepcaInternalVariables; + + if (!allocated_) + { + Log::error() << "Repca: allocate must complete before initialize\n"; + return 1; + } + + if (verify() > 0) + { + Log::error() << "Repca: cannot initialize with invalid configuration\n"; + return 1; + } auto* y = y_.getData(); - const ScalarT qext0_system = y[I::QEXT]; - const ScalarT pext0_system = y[I::PEXT]; + const ScalarT qext0_system = y[index(I::QEXT)]; + const ScalarT pext0_system = y[index(I::PEXT)]; const ScalarT qext0 = toComponentBase(qext0_system); const ScalarT pext0 = toComponentBase(pext0_system); @@ -511,10 +419,11 @@ namespace GridKit } const ScalarT xqpi0 = qpi_input0 - Kp_ * erqlim0; const ScalarT q_aw_rate0 = Math::antiwindup(qpi0, Ki_ * erqlim0, Qmin_, Qmax_); - if (!is_finite(q_aw_rate0) - || std::abs(static_cast(q_aw_rate0)) > INITIALIZATION_TOLERANCE) + const ScalarT xqpi_rate0 = sfrz0 * q_aw_rate0; + if (!is_finite(q_aw_rate0) || !is_finite(xqpi_rate0) + || std::abs(static_cast(xqpi_rate0)) > INITIALIZATION_TOLERANCE) { - Log::error() << "Repca: reactive-power PI antiwindup rate is nonzero at initialization\n"; + Log::error() << "Repca: reactive-power PI state rate is nonzero at initialization\n"; return 1; } @@ -552,28 +461,74 @@ namespace GridKit const ScalarT qref0_system = qbranch_system; const ScalarT pplantref0_system = toSystemBase(pmeas0 - pfreq0); - y[I::VMEAS] = vmeas0; - y[I::QMEAS] = qmeas0; - y[I::XQPI] = xqpi0; - y[I::XQLAG] = xqlag0; - y[I::PMEAS] = pmeas0; - y[I::XPPI] = xppi0; - y[I::PREF] = pref0; - y[I::V] = v0; - y[I::VLDC] = vldc0; - y[I::VDROOP] = vdroop0; - y[I::VCTRL] = vctrl0; - y[I::SFRZ] = sfrz0; - y[I::ERQ] = erq0; - y[I::ERQDB] = erqdb0; - y[I::ERQLIM] = erqlim0; - y[I::QPI] = qpi0; - y[I::QEXT] = qext0_system; - y[I::EF] = ef0; - y[I::EP] = ep0; - y[I::EPLIM] = eplim0; - y[I::PPI] = ppi0; - y[I::PEXT] = pext_output0; + const bool candidates_are_finite = + is_finite(qext0_system) + && (!Freqflag_ || is_finite(pext0_system)) + && is_finite(qext0) + && (!Freqflag_ || is_finite(pext0)) + && is_finite(pbranch) + && is_finite(qbranch) + && is_finite(vldc_r) + && is_finite(vldc_i) + && is_finite(v0) + && is_finite(vldc0) + && is_finite(vdroop0) + && is_finite(vctrl0) + && is_finite(vmeas0) + && is_finite(qmeas0) + && is_finite(pmeas0) + && is_finite(sfrz0) + && is_finite(erq0) + && is_finite(erqdb0) + && is_finite(erqlim0) + && is_finite(qpi0) + && is_finite(xqlag0) + && is_finite(qpi_input0) + && is_finite(xqpi0) + && is_finite(q_aw_rate0) + && is_finite(xqpi_rate0) + && is_finite(ef0) + && is_finite(pfreq0) + && is_finite(ep0) + && is_finite(eplim0) + && is_finite(pref0) + && is_finite(ppi0) + && is_finite(ppi_input0) + && is_finite(xppi0) + && is_finite(p_aw_rate0) + && is_finite(pext_output0) + && is_finite(freqref0) + && is_finite(vref0) + && is_finite(qref0_system) + && is_finite(pplantref0_system); + if (!candidates_are_finite) + { + Log::error() << "Repca: derived initial values must be finite\n"; + return 1; + } + + y[index(I::VMEAS)] = vmeas0; + y[index(I::QMEAS)] = qmeas0; + y[index(I::XQPI)] = xqpi0; + y[index(I::XQLAG)] = xqlag0; + y[index(I::PMEAS)] = pmeas0; + y[index(I::XPPI)] = xppi0; + y[index(I::PREF)] = pref0; + y[index(I::V)] = v0; + y[index(I::VLDC)] = vldc0; + y[index(I::VDROOP)] = vdroop0; + y[index(I::VCTRL)] = vctrl0; + y[index(I::SFRZ)] = sfrz0; + y[index(I::ERQ)] = erq0; + y[index(I::ERQDB)] = erqdb0; + y[index(I::ERQLIM)] = erqlim0; + y[index(I::QPI)] = qpi0; + y[index(I::QEXT)] = qext0_system; + y[index(I::EF)] = ef0; + y[index(I::EP)] = ep0; + y[index(I::EPLIM)] = eplim0; + y[index(I::PPI)] = ppi0; + y[index(I::PEXT)] = pext_output0; freqref_set_ = freqref0; vref_set_ = vref0; @@ -605,28 +560,36 @@ namespace GridKit } /** - * @brief Tag the seven controller states as differential variables. - * @return 0 on success. + * @brief Identify the differential variables + * + * The voltage and power filters, reactive PI and lead-lag states, and + * active PI and command-lag states carry derivatives; every other + * internal variable is algebraic. */ template int Repca::tagDifferentiable() { - using I = RepcaIdx; + using I = RepcaInternalVariables; std::fill(tag_.begin(), tag_.end(), false); - tag_[I::VMEAS] = true; - tag_[I::QMEAS] = true; - tag_[I::XQPI] = true; - tag_[I::XQLAG] = true; - tag_[I::PMEAS] = true; - tag_[I::XPPI] = true; - tag_[I::PREF] = true; + tag_[index(I::VMEAS)] = true; + tag_[index(I::QMEAS)] = true; + tag_[index(I::XQPI)] = true; + tag_[index(I::XQLAG)] = true; + tag_[index(I::PMEAS)] = true; + tag_[index(I::XPPI)] = true; + tag_[index(I::PREF)] = true; return 0; } /** - * @brief Set the common absolute-tolerance floor for all REPCA variables. - * @return 0 on success. + * @brief Compute the absolute tolerance for each variable in the model + * + * REPCA variables are dimensionless per-unit signals and controller + * states of the same order, so they share the relative tolerance as + * their absolute floor. + * + * @param[in] rel_tol Solver relative tolerance. */ template int Repca::setAbsoluteTolerance(RealT rel_tol) @@ -636,160 +599,70 @@ namespace GridKit } /** - * @brief Evaluate the seven differential and fifteen algebraic rows. + * @brief Evaluate the REPCA-owned residual rows * - * Mode selections enter through fixed masks so the residual remains - * branch-free for sparse automatic differentiation. - */ - template - __attribute__((always_inline)) inline int - Repca::evaluateInternalResidual( - const ScalarT* y, - const ScalarT* yp, - const ScalarT* wb, - const ScalarT* ws, - ScalarT* f) - { - using I = RepcaIdx; - using E = RepcaExt; - - const ScalarT vmeas = y[I::VMEAS]; - const ScalarT qmeas = y[I::QMEAS]; - const ScalarT xqpi = y[I::XQPI]; - const ScalarT xqlag = y[I::XQLAG]; - const ScalarT pmeas = y[I::PMEAS]; - const ScalarT xppi = y[I::XPPI]; - const ScalarT pref = y[I::PREF]; - const ScalarT v = y[I::V]; - const ScalarT vldc = y[I::VLDC]; - const ScalarT vdroop = y[I::VDROOP]; - const ScalarT vctrl = y[I::VCTRL]; - const ScalarT sfrz = y[I::SFRZ]; - const ScalarT erq = y[I::ERQ]; - const ScalarT erqdb = y[I::ERQDB]; - const ScalarT erqlim = y[I::ERQLIM]; - const ScalarT qpi = y[I::QPI]; - const ScalarT qext = toComponentBase(y[I::QEXT]); - const ScalarT ef = y[I::EF]; - const ScalarT ep = y[I::EP]; - const ScalarT eplim = y[I::EPLIM]; - const ScalarT ppi = y[I::PPI]; - const ScalarT pext = toComponentBase(y[I::PEXT]); - - const ScalarT vmeas_dot = yp[I::VMEAS]; - const ScalarT qmeas_dot = yp[I::QMEAS]; - const ScalarT xqpi_dot = yp[I::XQPI]; - const ScalarT xqlag_dot = yp[I::XQLAG]; - const ScalarT pmeas_dot = yp[I::PMEAS]; - const ScalarT xppi_dot = yp[I::XPPI]; - const ScalarT pref_dot = yp[I::PREF]; - - const ScalarT vr = wb[0]; - const ScalarT vi = wb[1]; - - const ScalarT ibranchr = ws[E::IBRANCHR]; - const ScalarT ibranchi = ws[E::IBRANCHI]; - const ScalarT pbranch = toComponentBase(ws[E::PBRANCH]); - const ScalarT qbranch = toComponentBase(ws[E::QBRANCH]); - const ScalarT freq = ws[E::FREQ]; - const ScalarT freqref = ws[E::FREQREF]; - const ScalarT vref = ws[E::VREF]; - const ScalarT qref = toComponentBase(ws[E::QREF]); - const ScalarT pplantref = toComponentBase(ws[E::PPLANTREF]); - - const ScalarT vldc_r = vr - Rc_ * ibranchr + Xc_ * ibranchi; - const ScalarT vldc_i = vi - Rc_ * ibranchi - Xc_ * ibranchr; - const ScalarT pfreq = Ddn_ * Math::ramp(ef) - Dup_ * Math::ramp(-ef); - - f[I::VMEAS] = -vmeas_dot + (vctrl - vmeas) / Tfltr_; - f[I::QMEAS] = -qmeas_dot + (qbranch - qmeas) / Tfltr_; - f[I::XQPI] = -xqpi_dot + sfrz * Math::antiwindup(qpi, Ki_ * erqlim, Qmin_, Qmax_); - f[I::XQLAG] = -xqlag_dot + (qpi - xqlag) / Tfv_; - f[I::PMEAS] = -pmeas_dot + (pbranch - pmeas) / Tp_; - f[I::XPPI] = -xppi_dot + Math::antiwindup(ppi, Kig_ * eplim, Pmin_, Pmax_); - f[I::PREF] = -pref_dot + (ppi - pref) / Tlag_; - - f[I::V] = -v * v + vr * vr + vi * vi; - f[I::VLDC] = -vldc * vldc + vldc_r * vldc_r + vldc_i * vldc_i; - f[I::VDROOP] = -vdroop + v + Kc_ * qbranch; - f[I::VCTRL] = -vctrl + vcomp_on_ * vldc + vcomp_off_ * vdroop; - f[I::SFRZ] = -sfrz + Math::above(v, Vfrz_); - f[I::ERQ] = -erq + ref_on_ * (vref - vmeas) + ref_off_ * (qref - qmeas); - f[I::ERQDB] = -erqdb + Math::deadband2(erq, dbdlow_, dbdupper_); - f[I::ERQLIM] = -erqlim + Math::clamp(erqdb, emin_, emax_); - f[I::QPI] = -qpi + Math::clamp(Kp_ * erqlim + xqpi, Qmin_, Qmax_); - f[I::QEXT] = -Tfv_ * (qext - xqlag) + Tft_ * (qpi - xqlag); - - f[I::EF] = -ef + Math::deadband2(freqref - freq, fdbd1_, fdbd2_); - f[I::EP] = -ep + pplantref - pmeas + pfreq; - f[I::EPLIM] = -eplim + Math::clamp(ep, femin_, femax_); - f[I::PPI] = -ppi + Math::clamp(Kpg_ * eplim + xppi, Pmin_, Pmax_); - f[I::PEXT] = -pext + freq_on_ * pref; - - return 0; - } - - /** - * @brief Refresh bus and signal buffers and evaluate the REPCA residual. - * - * Required measurements are read directly; unattached optional references - * use the values latched by initialize(). REPCA has no bus residual. - * - * @return 0 on success. + * Refreshes required bus and measurement inputs, starts optional + * references from the values latched by initialize(), then overwrites + * them from attached signals. REPCA contributes no bus residual. */ template int Repca::evaluateResidual() { - using E = RepcaExt; + using E = RepcaExternalVariables; - ws_[E::FREQREF] = freqref_set_; - ws_[E::VREF] = vref_set_; - ws_[E::QREF] = qref_set_; - ws_[E::PPLANTREF] = pplantref_set_; + ws_[index(E::FREQREF)] = freqref_set_; + ws_[index(E::VREF)] = vref_set_; + ws_[index(E::QREF)] = qref_set_; + ws_[index(E::PPLANTREF)] = pplantref_set_; std::fill(ws_indices_.begin(), ws_indices_.end(), INVALID_INDEX); - ws_[E::IBRANCHR] = + ws_[index(E::IBRANCHR)] = signals_.template readExternalVariable(); - ws_indices_[E::IBRANCHR] = + ws_indices_[index(E::IBRANCHR)] = signals_.template readExternalVariableIndex(); - ws_[E::IBRANCHI] = + ws_[index(E::IBRANCHI)] = signals_.template readExternalVariable(); - ws_indices_[E::IBRANCHI] = + ws_indices_[index(E::IBRANCHI)] = signals_.template readExternalVariableIndex(); - ws_[E::PBRANCH] = signals_.template readExternalVariable(); - ws_indices_[E::PBRANCH] = + ws_[index(E::PBRANCH)] = + signals_.template readExternalVariable(); + ws_indices_[index(E::PBRANCH)] = signals_.template readExternalVariableIndex(); - ws_[E::QBRANCH] = signals_.template readExternalVariable(); - ws_indices_[E::QBRANCH] = + ws_[index(E::QBRANCH)] = + signals_.template readExternalVariable(); + ws_indices_[index(E::QBRANCH)] = signals_.template readExternalVariableIndex(); - ws_[E::FREQ] = signals_.template readExternalVariable(); - ws_indices_[E::FREQ] = + ws_[index(E::FREQ)] = + signals_.template readExternalVariable(); + ws_indices_[index(E::FREQ)] = signals_.template readExternalVariableIndex(); if (signals_.template isAttached()) { - ws_[E::FREQREF] = + ws_[index(E::FREQREF)] = signals_.template readExternalVariable(); - ws_indices_[E::FREQREF] = + ws_indices_[index(E::FREQREF)] = signals_.template readExternalVariableIndex(); } if (signals_.template isAttached()) { - ws_[E::VREF] = signals_.template readExternalVariable(); - ws_indices_[E::VREF] = + ws_[index(E::VREF)] = + signals_.template readExternalVariable(); + ws_indices_[index(E::VREF)] = signals_.template readExternalVariableIndex(); } if (signals_.template isAttached()) { - ws_[E::QREF] = signals_.template readExternalVariable(); - ws_indices_[E::QREF] = + ws_[index(E::QREF)] = + signals_.template readExternalVariable(); + ws_indices_[index(E::QREF)] = signals_.template readExternalVariableIndex(); } if (signals_.template isAttached()) { - ws_[E::PPLANTREF] = + ws_[index(E::PPLANTREF)] = signals_.template readExternalVariable(); - ws_indices_[E::PPLANTREF] = + ws_indices_[index(E::PPLANTREF)] = signals_.template readExternalVariableIndex(); } @@ -804,6 +677,511 @@ namespace GridKit f_.setDataUpdated(); return 0; } + + /** + * @brief Access the REPCA signal interface + * + * @return Interface used to assign optional plant-command outputs and + * attach required measurements and optional references. + */ + template + auto Repca::getSignals() + -> ComponentSignals& + { + return signals_; + } + + /** + * @brief Access the configured monitor + * + * @return Monitor for this model, or nullptr when constructed without data. + */ + template + const Model::VariableMonitorBase* Repca::getMonitor() const + { + return monitor_.get(); + } + + /** + * @brief Evaluate the REPCA internal residual + * + * Evaluates seven differential and fifteen algebraic rows in enum order. + * Precomputed mode masks keep the differentiated path branch- and + * loop-free with a fixed dependency structure. + * + * @param[in] y Internal variables in `RepcaInternalVariables` order and + * on the bases documented by their enums. + * @param[in] yp Internal derivatives in the same enum order and bases. + * @param[in] wb Regulated-bus real and imaginary voltage components. + * @param[in] ws External signals in `RepcaExternalVariables` order. + * @param[out] f Model-owned residuals in `RepcaInternalVariables` order. + */ + template + [[gnu::always_inline]] inline int + Repca::evaluateInternalResidual( + const ScalarT* y, + const ScalarT* yp, + const ScalarT* wb, + const ScalarT* ws, + ScalarT* f) + { + using I = RepcaInternalVariables; + using E = RepcaExternalVariables; + + const ScalarT vmeas = y[index(I::VMEAS)]; + const ScalarT qmeas = y[index(I::QMEAS)]; + const ScalarT xqpi = y[index(I::XQPI)]; + const ScalarT xqlag = y[index(I::XQLAG)]; + const ScalarT pmeas = y[index(I::PMEAS)]; + const ScalarT xppi = y[index(I::XPPI)]; + const ScalarT pref = y[index(I::PREF)]; + const ScalarT v = y[index(I::V)]; + const ScalarT vldc = y[index(I::VLDC)]; + const ScalarT vdroop = y[index(I::VDROOP)]; + const ScalarT vctrl = y[index(I::VCTRL)]; + const ScalarT sfrz = y[index(I::SFRZ)]; + const ScalarT erq = y[index(I::ERQ)]; + const ScalarT erqdb = y[index(I::ERQDB)]; + const ScalarT erqlim = y[index(I::ERQLIM)]; + const ScalarT qpi = y[index(I::QPI)]; + const ScalarT qext = toComponentBase(y[index(I::QEXT)]); + const ScalarT ef = y[index(I::EF)]; + const ScalarT ep = y[index(I::EP)]; + const ScalarT eplim = y[index(I::EPLIM)]; + const ScalarT ppi = y[index(I::PPI)]; + const ScalarT pext = toComponentBase(y[index(I::PEXT)]); + + const ScalarT vmeas_dot = yp[index(I::VMEAS)]; + const ScalarT qmeas_dot = yp[index(I::QMEAS)]; + const ScalarT xqpi_dot = yp[index(I::XQPI)]; + const ScalarT xqlag_dot = yp[index(I::XQLAG)]; + const ScalarT pmeas_dot = yp[index(I::PMEAS)]; + const ScalarT xppi_dot = yp[index(I::XPPI)]; + const ScalarT pref_dot = yp[index(I::PREF)]; + + const ScalarT vr = wb[0]; + const ScalarT vi = wb[1]; + + const ScalarT ibranchr = ws[index(E::IBRANCHR)]; + const ScalarT ibranchi = ws[index(E::IBRANCHI)]; + const ScalarT pbranch = toComponentBase(ws[index(E::PBRANCH)]); + const ScalarT qbranch = toComponentBase(ws[index(E::QBRANCH)]); + const ScalarT freq = ws[index(E::FREQ)]; + const ScalarT freqref = ws[index(E::FREQREF)]; + const ScalarT vref = ws[index(E::VREF)]; + const ScalarT qref = toComponentBase(ws[index(E::QREF)]); + const ScalarT pplantref = toComponentBase(ws[index(E::PPLANTREF)]); + + const ScalarT vldc_r = vr - Rc_ * ibranchr + Xc_ * ibranchi; + const ScalarT vldc_i = vi - Rc_ * ibranchi - Xc_ * ibranchr; + const ScalarT pfreq = Ddn_ * Math::ramp(ef) - Dup_ * Math::ramp(-ef); + + f[index(I::VMEAS)] = -vmeas_dot + (vctrl - vmeas) / Tfltr_; + f[index(I::QMEAS)] = -qmeas_dot + (qbranch - qmeas) / Tfltr_; + f[index(I::XQPI)] = -xqpi_dot + sfrz * Math::antiwindup(qpi, Ki_ * erqlim, Qmin_, Qmax_); + f[index(I::XQLAG)] = -xqlag_dot + (qpi - xqlag) / Tfv_; + f[index(I::PMEAS)] = -pmeas_dot + (pbranch - pmeas) / Tp_; + f[index(I::XPPI)] = -xppi_dot + Math::antiwindup(ppi, Kig_ * eplim, Pmin_, Pmax_); + f[index(I::PREF)] = -pref_dot + (ppi - pref) / Tlag_; + + f[index(I::V)] = -v * v + vr * vr + vi * vi; + f[index(I::VLDC)] = -vldc * vldc + vldc_r * vldc_r + vldc_i * vldc_i; + f[index(I::VDROOP)] = -vdroop + v + Kc_ * qbranch; + f[index(I::VCTRL)] = -vctrl + vcomp_on_ * vldc + vcomp_off_ * vdroop; + f[index(I::SFRZ)] = -sfrz + Math::above(v, Vfrz_); + f[index(I::ERQ)] = -erq + ref_on_ * (vref - vmeas) + ref_off_ * (qref - qmeas); + f[index(I::ERQDB)] = -erqdb + Math::deadband2(erq, dbdlow_, dbdupper_); + f[index(I::ERQLIM)] = -erqlim + Math::clamp(erqdb, emin_, emax_); + f[index(I::QPI)] = -qpi + Math::clamp(Kp_ * erqlim + xqpi, Qmin_, Qmax_); + f[index(I::QEXT)] = -Tfv_ * (qext - xqlag) + Tft_ * (qpi - xqlag); + + f[index(I::EF)] = -ef + Math::deadband2(freqref - freq, fdbd1_, fdbd2_); + f[index(I::EP)] = -ep + pplantref - pmeas + pfreq; + f[index(I::EPLIM)] = -eplim + Math::clamp(ep, femin_, femax_); + f[index(I::PPI)] = -ppi + Math::clamp(Kpg_ * eplim + xppi, Pmin_, Pmax_); + f[index(I::PEXT)] = -pext + freq_on_ * pref; + + return 0; + } + + // + // Private methods + // + + /** + * @brief Record one failed configuration condition + * + * @param[in] condition Required condition. + * @param[in] message Error message when `condition` is false. + * @param[in,out] errors Accumulated configuration-error count. + */ + template + void Repca::checkConfiguration( + bool condition, const char* message, int& errors) + { + if (!condition) + { + Log::error() << "Repca: " << message << '\n'; + errors += 1; + } + } + + /** + * @brief Load one optional real-valued parameter + * + * Real and integer serialized values are accepted. Any other stored + * type records a loading error while preserving the existing default. + * + * @param[in] data Model parameter data. + * @param[in] parameter Parameter key to load. + * @param[in,out] target Stored parameter value. + * @param[in] name Serialized parameter name for diagnostics. + */ + template + void Repca::loadRealParameter( + const ModelDataT& data, + RepcaParameters parameter, + RealT& target, + const char* name) + { + if (!data.parameters.contains(parameter)) + { + return; + } + + const auto& value = data.parameters.at(parameter); + if (const auto* real_value = std::get_if(&value)) + { + target = *real_value; + } + else if (const auto* index_value = std::get_if(&value)) + { + target = static_cast(*index_value); + } + else + { + Log::error() << "Repca: parameter '" << name << "' must be numeric\n"; + ++parameter_error_count_; + } + } + + /** + * @brief Load one optional binary selector + * + * Boolean values and numeric values exactly equal to zero or one are + * accepted. Any other value or stored type records a loading error + * while preserving the existing default. + * + * @param[in] data Model parameter data. + * @param[in] parameter Parameter key to load. + * @param[in,out] target Stored selector value. + * @param[in] name Serialized parameter name for diagnostics. + */ + template + void Repca::loadSwitchParameter( + const ModelDataT& data, + RepcaParameters parameter, + bool& target, + const char* name) + { + if (!data.parameters.contains(parameter)) + { + return; + } + + const auto& value = data.parameters.at(parameter); + if (const auto* bool_value = std::get_if(&value)) + { + target = *bool_value; + } + else if (const auto* index_value = std::get_if(&value); + index_value && (*index_value == 0 || *index_value == 1)) + { + target = (*index_value == 1); + } + else if (const auto* real_value = std::get_if(&value); + real_value && (*real_value == ZERO || *real_value == ONE) ) + { + target = (*real_value == ONE); + } + else + { + Log::error() << "Repca: parameter '" << name << "' must be bool or 0/1\n"; + ++parameter_error_count_; + } + } + + /** + * @brief Validate and floor one controller lag + * + * A nonfinite value records a loading error and is replaced by the + * floor so later calculations remain well posed. A finite value below + * the floor is raised and reported through the return value. + * + * @param[in,out] value Time constant to validate and floor. + * @param[in] name Parameter name for diagnostics. + * @return true when a finite value was raised to the floor. + */ + template + bool Repca::floorTimeConstant( + RealT& value, const char* name) + { + if (!std::isfinite(value)) + { + Log::error() << "Repca: " << name << " must be finite\n"; + ++parameter_error_count_; + value = TIME_CONSTANT_MINIMUM; + return false; + } + + const bool raised = value < TIME_CONSTANT_MINIMUM; + value = std::max(value, TIME_CONSTANT_MINIMUM); + return raised; + } + + /** + * @brief Read optional parameters from model data + * + * Omitted parameters retain their documented defaults. Numeric parameters + * accept real and integer values; selectors also accept Boolean values. + * Loading errors are counted for verify() rather than thrown. + * + * @param[in] data Parameters and monitored-variable selections. + */ + template + void Repca::initializeParameters(const ModelDataT& data) + { + using Params = typename ModelDataT::Parameters; + + parameter_error_count_ = 0; + + loadRealParameter(data, Params::mva, mva_base_, "mva"); + loadSwitchParameter(data, Params::VcompFlag, VcompFlag_, "VcompFlag"); + loadSwitchParameter(data, Params::RefFlag, RefFlag_, "RefFlag"); + loadSwitchParameter(data, Params::Freqflag, Freqflag_, "Freqflag"); + loadRealParameter(data, Params::Tfltr, Tfltr_, "Tfltr"); + loadRealParameter(data, Params::Vfrz, Vfrz_, "Vfrz"); + loadRealParameter(data, Params::Rc, Rc_, "Rc"); + loadRealParameter(data, Params::Xc, Xc_, "Xc"); + loadRealParameter(data, Params::Kc, Kc_, "Kc"); + loadRealParameter(data, Params::dbdlow, dbdlow_, "dbdlow"); + loadRealParameter(data, Params::dbdupper, dbdupper_, "dbdupper"); + loadRealParameter(data, Params::emax, emax_, "emax"); + loadRealParameter(data, Params::emin, emin_, "emin"); + loadRealParameter(data, Params::Kp, Kp_, "Kp"); + loadRealParameter(data, Params::Ki, Ki_, "Ki"); + loadRealParameter(data, Params::Qmax, Qmax_, "Qmax"); + loadRealParameter(data, Params::Qmin, Qmin_, "Qmin"); + loadRealParameter(data, Params::Tft, Tft_, "Tft"); + loadRealParameter(data, Params::Tfv, Tfv_, "Tfv"); + loadRealParameter(data, Params::Tp, Tp_, "Tp"); + loadRealParameter(data, Params::fdbd1, fdbd1_, "fdbd1"); + loadRealParameter(data, Params::fdbd2, fdbd2_, "fdbd2"); + loadRealParameter(data, Params::Ddn, Ddn_, "Ddn"); + loadRealParameter(data, Params::Dup, Dup_, "Dup"); + loadRealParameter(data, Params::femax, femax_, "femax"); + loadRealParameter(data, Params::femin, femin_, "femin"); + loadRealParameter(data, Params::Kpg, Kpg_, "Kpg"); + loadRealParameter(data, Params::Kig, Kig_, "Kig"); + loadRealParameter(data, Params::Pmax, Pmax_, "Pmax"); + loadRealParameter(data, Params::Pmin, Pmin_, "Pmin"); + loadRealParameter(data, Params::Tlag, Tlag_, "Tlag"); + + setDerivedParameters(); + } + + /** + * @brief Bind monitor selections to REPCA internal states + */ + template + void Repca::initializeMonitor() + { + using I = RepcaInternalVariables; + using Variable = typename ModelDataT::MonitorableVariables; + + monitor_->set(Variable::qext, [this] + { return y_.getData()[index(I::QEXT)]; }); + monitor_->set(Variable::pext, [this] + { return y_.getData()[index(I::PEXT)]; }); + monitor_->set(Variable::vmeas, [this] + { return y_.getData()[index(I::VMEAS)]; }); + monitor_->set(Variable::qmeas, [this] + { return y_.getData()[index(I::QMEAS)]; }); + monitor_->set(Variable::pmeas, [this] + { return y_.getData()[index(I::PMEAS)]; }); + } + + /** + * @brief Resolve parameter-derived constants + * + * Raises the explicit controller lags in place, computes the converter + * power base, and resolves selector masks. Nonfinite lag inputs are + * recorded before replacement so verify() retains the error. + */ + template + void Repca::setDerivedParameters() + { + bool floor_warning = false; + + floor_warning |= floorTimeConstant(Tfltr_, "Tfltr"); + floor_warning |= floorTimeConstant(Tp_, "Tp"); + floor_warning |= floorTimeConstant(Tlag_, "Tlag"); + + if (floor_warning) + { + Log::warning() << "Repca: any of Tfltr, Tp, or Tlag below " + << TIME_CONSTANT_MINIMUM + << " s is raised to that floor to keep the controller lags well posed\n"; + } + + va_converter_base_ = mva_base_ * static_cast(1.0e6); + + vcomp_on_ = VcompFlag_ ? ONE : ZERO; + vcomp_off_ = ONE - vcomp_on_; + ref_on_ = RefFlag_ ? ONE : ZERO; + ref_off_ = ONE - ref_on_; + freq_on_ = Freqflag_ ? ONE : ZERO; + } + + /** + * @brief Recover an input for a requested smooth-clamp output + * + * This initialization-only helper inverts the CommonMath smooth clamp, + * including collapsed limits. Exact-bound requests use a 0.1 offset, + * leaving less than 2e-13 at CommonMath MU = 240. It must not be called + * from residual evaluation. + * + * @param[in] requested_output Requested smooth-clamp output. + * @param[in] lower_limit Lower clamp limit. + * @param[in] upper_limit Upper clamp limit. + * @param[out] limiter_input Recovered clamp input on success. + * @return true when a finite admissible input was recovered. + */ + template + bool Repca::solveLimiterInput( + ScalarT requested_output, + RealT lower_limit, + RealT upper_limit, + ScalarT& limiter_input) const + { + const RealT output_value = static_cast(requested_output); + + if (!std::isfinite(output_value) + || !std::isfinite(lower_limit) + || !std::isfinite(upper_limit) + || lower_limit > upper_limit + || output_value < lower_limit + || output_value > upper_limit) + { + return false; + } + + const RealT width = upper_limit - lower_limit; + if (width <= INITIALIZATION_TOLERANCE) + { + limiter_input = static_cast(lower_limit); + return true; + } + + const RealT distance_from_lower = output_value - lower_limit; + const RealT distance_from_upper = upper_limit - output_value; + if (distance_from_lower <= INITIALIZATION_TOLERANCE) + { + limiter_input = static_cast(lower_limit - INITIALIZATION_LIMIT_OFFSET); + return true; + } + if (distance_from_upper <= INITIALIZATION_TOLERANCE) + { + limiter_input = static_cast(upper_limit + INITIALIZATION_LIMIT_OFFSET); + return true; + } + + const RealT mu = Math::MU; + const RealT scaled_lower_distance = mu * distance_from_lower; + const RealT scaled_upper_distance = mu * distance_from_upper; + const RealT log_lower = logOneMinusExp(scaled_lower_distance); + const RealT log_upper = logOneMinusExp(scaled_upper_distance); + const RealT correction = + (scaled_lower_distance + log_lower - log_upper) / mu; + + limiter_input = static_cast(lower_limit + correction); + return std::isfinite(static_cast(limiter_input)); + } + + /** + * @brief Evaluate log(1 - exp(-x)) accurately for positive x + * + * The small-x form avoids cancellation in `1 - exp(-x)`; the large-x + * form uses `log1p`. The two algebraically equivalent forms agree in + * value and first derivative at x = log(2). + * + * @param[in] x Positive argument. + * @return Numerically stable value of log(1 - exp(-x)). + */ + template + typename Repca::RealT + Repca::logOneMinusExp(RealT x) const + { + const RealT log_two = std::log(static_cast(2.0)); + + if (x < log_two) + { + return log_two - HALF * x + + std::log(std::sinh(HALF * x)); + } + return std::log1p(-std::exp(-x)); + } + + /** + * @brief Convert a system-base power quantity to REPCA component base + * + * @param[in] value Quantity on the system base. + * @return The same quantity on the REPCA component base. + */ + template + [[gnu::always_inline]] inline scalar_type + Repca::toComponentBase(scalar_type value) const + { + return value * (va_system_base_ / va_converter_base_); + } + + /** + * @brief Convert a component-base power quantity to system base + * + * @param[in] value Quantity on the REPCA component base. + * @return The same quantity on the system base. + */ + template + scalar_type Repca::toSystemBase(scalar_type value) const + { + return value * (va_converter_base_ / va_system_base_); + } + + /** + * @brief Access the regulated-bus real voltage component + * + * @return Mutable reference to the bus real voltage state. + */ + template + scalar_type& Repca::Vr() + { + return bus_->Vr(); + } + + /** + * @brief Access the regulated-bus imaginary voltage component + * + * @return Mutable reference to the bus imaginary voltage state. + */ + template + scalar_type& Repca::Vi() + { + return bus_->Vi(); + } + } // namespace Converter } // namespace PhasorDynamics } // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp index b09281aaf..1051b8669 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp @@ -165,13 +165,13 @@ namespace GridKit // Add REPCA plant controllers for (const auto& repcadata : data.repca) { - IdxT bus_index = 0; + BusT* bus = nullptr; if (repcadata.buses.contains(RepcaBuses::bus)) { - bus_index = repcadata.buses.at(RepcaBuses::bus); + bus = getBus(repcadata.buses.at(RepcaBuses::bus)); } - auto* repca = new Repca(getBus(bus_index), repcadata); + auto* repca = new Repca(bus, repcadata); if (repcadata.signal_inputs.contains(RepcaSignalInputs::ibranchr)) { diff --git a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp index 79b75d380..35438833d 100644 --- a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp @@ -39,8 +39,8 @@ namespace GridKit ConverterRepcaTests() = default; ~ConverterRepcaTests() = default; - // Exact-limit limiter seeds leave residual tails below 2e-13. This margin - // absorbs them while resolving the approximately 6.8e-12 boundary response. + // Exact-limit initialization leaves residual tails below 2e-13; this + // tolerance still resolves the 6.8e-12 boundary response. static constexpr RealT kBehaviorTol = 1.0e-12; // Enzyme traverses the smooth expressions through a separate AD path; @@ -52,21 +52,21 @@ namespace GridKit { TestStatus success = true; + noteExpectedLogs("Testing REPCA defaults, parameter floors, and invalid " + "configurations. Logged errors and warnings are expected."); + PhasorDynamics::Bus bus(1.0, 0.0); PhasorDynamics::Converter::Repca empty(&bus); - success *= (empty.size() == static_cast(I::MAXIMUM)); + success *= (empty.size() == static_cast(index(Vars::MAXIMUM))); success *= (empty.getMonitor() == nullptr); Fixture configured(makeData()); configured.attachAllInputs(); - success *= (configured.repca.size() == static_cast(I::MAXIMUM)); + success *= (configured.repca.size() == static_cast(index(Vars::MAXIMUM))); success *= (configured.repca.getMonitor() != nullptr); success *= (configured.repca.verify() == 0); - noteExpectedLogs("Testing REPCA defaults, parameter floors, and invalid " - "configurations. Logged errors and warnings are expected."); - Fixture documented_defaults(makeMinimalData()); documented_defaults.attachAllInputs(); success *= (documented_defaults.repca.verify() == 0); @@ -96,6 +96,44 @@ namespace GridKit success *= invalidParameterCase(Params::Pmin, 2.1); success *= invalidParameterCase(Params::mva, true); + const RealT nan = std::numeric_limits::quiet_NaN(); + const RealT infinity = std::numeric_limits::infinity(); + for (const Params parameter : {Params::mva, + Params::Tfltr, + Params::Vfrz, + Params::Rc, + Params::Kc, + Params::dbdlow, + Params::emax, + Params::Kp, + Params::Qmax, + Params::Tft, + Params::Ddn, + Params::Pmax, + Params::Tlag}) + { + success *= invalidParameterCase(parameter, nan); + success *= invalidParameterCase(parameter, infinity); + success *= invalidParameterCase(parameter, -infinity); + } + success *= invalidParameterCase(Params::mva, std::numeric_limits::max()); + + { + Fixture nonfinite_system_base(makeData(), 1.0, 0.0, infinity); + nonfinite_system_base.attachAllInputs(); + success *= (nonfinite_system_base.repca.verify() > 0); + } + { + auto tiny_base_data = makeData(); + tiny_base_data.parameters[Params::mva] = std::numeric_limits::min(); + Fixture overflowing_base_ratio(tiny_base_data, + 1.0, + 0.0, + std::numeric_limits::max()); + overflowing_base_ratio.attachAllInputs(); + success *= (overflowing_base_ratio.repca.verify() > 0); + } + for (const Params flag : {Params::VcompFlag, Params::RefFlag, Params::Freqflag}) { for (const auto value : {static_cast(0), static_cast(1)}) @@ -118,6 +156,8 @@ namespace GridKit success *= invalidParameterCase(flag, static_cast(2)); success *= invalidParameterCase(flag, static_cast(0.5)); + success *= invalidParameterCase(flag, nan); + success *= invalidParameterCase(flag, infinity); } PhasorDynamics::Converter::Repca busless(nullptr, makeData()); @@ -145,18 +185,18 @@ namespace GridKit success *= (floored.repca.evaluateResidual() == 0); success *= allResidualsZero(floored.repca); - auto* y = floored.repca.y().getData(); - y[I::VMEAS] -= 0.001; - y[I::QMEAS] -= 0.002; - y[I::PMEAS] -= 0.003; - y[I::PREF] -= 0.004; + auto* y = floored.repca.y().getData(); + y[index(Vars::VMEAS)] -= 0.001; + y[index(Vars::QMEAS)] -= 0.002; + y[index(Vars::PMEAS)] -= 0.003; + y[index(Vars::PREF)] -= 0.004; floored.repca.y().setDataUpdated(); success *= (floored.repca.evaluateResidual() == 0); success *= residualsMatch(floored.repca, - {{I::VMEAS, 1.0}, - {I::QMEAS, 2.0}, - {I::PMEAS, 3.0}, - {I::PREF, 4.0}}, + {{Vars::VMEAS, 1.0}, + {Vars::QMEAS, 2.0}, + {Vars::PMEAS, 3.0}, + {Vars::PREF, 4.0}}, "floored time constants"); return success.report(__func__); @@ -175,47 +215,47 @@ namespace GridKit success *= (fixture.repca.evaluateResidual() == 0); success *= stateMatches(fixture.repca, - {{I::VMEAS, 0.99200050403213}, - {I::QMEAS, 0.2}, - {I::XQPI, 0.5}, - {I::XQLAG, 0.5}, - {I::PMEAS, 0.8}, - {I::XPPI, 0.9}, - {I::PREF, 0.9}, - {I::V, 1.0}, - {I::VLDC, 0.99200050403213}, - {I::VDROOP, 1.08}, - {I::VCTRL, 0.99200050403213}, - {I::SFRZ, 1.0}, - {I::ERQ, 0.0}, - {I::ERQDB, 0.0}, - {I::ERQLIM, 0.0}, - {I::QPI, 0.5}, - {I::QEXT, 0.25}, - {I::EF, -0.00024949607977392432}, - {I::EP, 0.0}, - {I::EPLIM, 0.0}, - {I::PPI, 0.9}, - {I::PEXT, 0.45}}, + {{Vars::VMEAS, 0.99200050403213}, + {Vars::QMEAS, 0.2}, + {Vars::XQPI, 0.5}, + {Vars::XQLAG, 0.5}, + {Vars::PMEAS, 0.8}, + {Vars::XPPI, 0.9}, + {Vars::PREF, 0.9}, + {Vars::V, 1.0}, + {Vars::VLDC, 0.99200050403213}, + {Vars::VDROOP, 1.08}, + {Vars::VCTRL, 0.99200050403213}, + {Vars::SFRZ, 1.0}, + {Vars::ERQ, 0.0}, + {Vars::ERQDB, 0.0}, + {Vars::ERQLIM, 0.0}, + {Vars::QPI, 0.5}, + {Vars::QEXT, 0.25}, + {Vars::EF, -0.00024949607977392432}, + {Vars::EP, 0.0}, + {Vars::EPLIM, 0.0}, + {Vars::PPI, 0.9}, + {Vars::PEXT, 0.45}}, "initialization"); - success *= scalarMatches(fixture.input(E::IBRANCHR), 0.2, "preserved ibranchr"); - success *= scalarMatches(fixture.input(E::IBRANCHI), -0.1, "preserved ibranchi"); - success *= scalarMatches(fixture.input(E::PBRANCH), 0.4, "preserved pbranch"); - success *= scalarMatches(fixture.input(E::QBRANCH), 0.1, "preserved qbranch"); - success *= scalarMatches(fixture.input(E::FREQ), 0.99, "preserved freq"); + success *= scalarMatches(fixture.input(index(Ext::IBRANCHR)), 0.2, "preserved ibranchr"); + success *= scalarMatches(fixture.input(index(Ext::IBRANCHI)), -0.1, "preserved ibranchi"); + success *= scalarMatches(fixture.input(index(Ext::PBRANCH)), 0.4, "preserved pbranch"); + success *= scalarMatches(fixture.input(index(Ext::QBRANCH)), 0.1, "preserved qbranch"); + success *= scalarMatches(fixture.input(index(Ext::FREQ)), 0.99, "preserved freq"); success *= scalarMatches(fixture.qext(), 0.25, "preserved qext"); success *= scalarMatches(fixture.pext(), 0.45, "preserved pext"); - success *= scalarMatches(fixture.input(E::FREQREF), 0.99, "published freqref"); - success *= scalarMatches(fixture.input(E::VREF), 0.99200050403213, "published vref"); - success *= scalarMatches(fixture.input(E::QREF), 0.1, "published qref"); - success *= scalarMatches(fixture.input(E::PPLANTREF), + success *= scalarMatches(fixture.input(index(Ext::FREQREF)), 0.99, "published freqref"); + success *= scalarMatches(fixture.input(index(Ext::VREF)), 0.99200050403213, "published vref"); + success *= scalarMatches(fixture.input(index(Ext::QREF)), 0.1, "published qref"); + success *= scalarMatches(fixture.input(index(Ext::PPLANTREF)), 0.39874213184871782, "published pplantref"); - for (size_t row = 0; row < I::MAXIMUM; ++row) + for (size_t row = 0; row < index(Vars::MAXIMUM); ++row) { - const bool expected = row <= I::PREF; + const bool expected = row <= index(Vars::PREF); if (fixture.repca.tag()[row] != expected) { std::cout << "REPCA differentiability tag " << row << " mismatch\n"; @@ -228,7 +268,7 @@ namespace GridKit success *= (fixture.repca.setAbsoluteTolerance(absolute_tolerance) == 0); const auto* tolerances = fixture.repca.absoluteTolerance().getData(); - for (size_t row = 0; row < I::MAXIMUM; ++row) + for (size_t row = 0; row < index(Vars::MAXIMUM); ++row) { success *= valueUnchanged(tolerances[row], absolute_tolerance, @@ -236,38 +276,9 @@ namespace GridKit row); } - RealT time = 0.0; - Model::VariableMonitorController monitor(time); - monitor.addMonitor(fixture.repca.getMonitor()); - std::stringstream monitor_output; - monitor.addSink({Model::VariableMonitorFormat::CSV}, monitor_output); - monitor.start(); - monitor.print(); - monitor.stop(); - - std::string monitor_header; - std::string monitor_values; - std::getline(monitor_output, monitor_header); - std::getline(monitor_output, monitor_values); - success *= (monitor_header == "t,Repca_repca_test_qext,Repca_repca_test_pext," - "Repca_repca_test_vmeas,Repca_repca_test_qmeas," - "Repca_repca_test_pmeas"); - - const auto monitored = Tokenizer(monitor_values, ',')(); - if (monitored.size() == 6) - { - success *= scalarMatches(monitored[1], 0.25, "monitored qext"); - success *= scalarMatches(monitored[2], 0.45, "monitored pext"); - success *= scalarMatches(monitored[3], 0.99200050403213, "monitored vmeas"); - success *= scalarMatches(monitored[4], 0.2, "monitored qmeas"); - success *= scalarMatches(monitored[5], 0.8, "monitored pmeas"); - } - else - { - std::cout << "REPCA monitor emitted " << monitored.size() - << " values instead of 6\n"; - success = false; - } + success *= monitorMatches(fixture.repca, + {{0.25, 0.45, 0.99200050403213, 0.2, 0.8}}, + "initialization"); success *= allResidualsZero(fixture.repca); @@ -280,12 +291,12 @@ namespace GridKit exact_commands.attachAllInputs(); setInitializationInputs(exact_commands); success *= exact_commands.initialize(0.25, 0.45); - success *= valueUnchanged(exact_commands.qext(), 0.25, "qext signal", I::QEXT); - success *= valueUnchanged(exact_commands.pext(), 0.45, "pext signal", I::PEXT); - success *= valueUnchanged(exact_commands.input(E::QREF), + success *= valueUnchanged(exact_commands.qext(), 0.25, "qext signal", index(Vars::QEXT)); + success *= valueUnchanged(exact_commands.pext(), 0.45, "pext signal", index(Vars::PEXT)); + success *= valueUnchanged(exact_commands.input(index(Ext::QREF)), 0.1, "qref signal", - E::QREF); + index(Ext::QREF)); success *= (exact_commands.repca.evaluateResidual() == 0); success *= allResidualsZero(exact_commands.repca); } @@ -297,6 +308,24 @@ namespace GridKit success *= (fallback.repca.evaluateResidual() == 0); success *= allResidualsZero(fallback.repca); + Fixture outputless(makeInitializationData(), + 0.8, + 0.6, + 100.0e6, + false); + outputless.attachAllInputs(); + setInitializationInputs(outputless); + success *= outputless.initialize(0.25, 0.45); + success *= (outputless.repca.evaluateResidual() == 0); + success *= stateMatches(outputless.repca, + {{Vars::QEXT, 0.25}, + {Vars::PEXT, 0.45}}, + "unassigned command outputs"); + success *= monitorMatches(outputless.repca, + {{0.25, 0.45, 0.99200050403213, 0.2, 0.8}}, + "unassigned command outputs"); + success *= allResidualsZero(outputless.repca); + struct FlagCase { const char* label; @@ -332,11 +361,11 @@ namespace GridKit success *= (scenario.repca.evaluateResidual() == 0); success *= stateMatches(scenario.repca, - {{I::VMEAS, test_case.voltage}, - {I::VCTRL, test_case.voltage}, - {I::PREF, test_case.pref}, - {I::PPI, test_case.pref}, - {I::PEXT, test_case.pext}}, + {{Vars::VMEAS, test_case.voltage}, + {Vars::VCTRL, test_case.voltage}, + {Vars::PREF, test_case.pref}, + {Vars::PPI, test_case.pref}, + {Vars::PEXT, test_case.pext}}, test_case.label); success *= scalarMatches(scenario.qext(), 0.25, test_case.label); success *= scalarMatches(scenario.pext(), test_case.pext, test_case.label); @@ -413,6 +442,23 @@ namespace GridKit 0.25, 0.45, "nonzero reactive-power PI antiwindup rate"); + success *= initializationRejectedAtomically( + reactive_aw_data, + 0.25, + 0.45, + "nonzero gated reactive-power PI state rate at the freeze transition", + NonfiniteTarget::NONE, + 0.7, + 0.0); + + { + Fixture frozen_reactive_rate(reactive_aw_data, 0.5, 0.0); + frozen_reactive_rate.attachAllInputs(); + setInitializationInputs(frozen_reactive_rate); + success *= frozen_reactive_rate.initialize(0.25, 0.45); + success *= (frozen_reactive_rate.repca.evaluateResidual() == 0); + success *= allResidualsZero(frozen_reactive_rate.repca); + } auto active_aw_data = makeInitializationData(); active_aw_data.parameters[Params::femin] = 0.0; @@ -422,6 +468,13 @@ namespace GridKit 0.45, "nonzero active-power PI antiwindup rate"); + auto overflow_data = makeInitializationData(); + overflow_data.parameters[Params::Rc] = std::numeric_limits::max(); + success *= initializationRejectedAtomically(overflow_data, + 0.25, + 0.45, + "nonfinite derived initialization candidate"); + { Fixture qmax_pmin_boundary(data, 0.8, 0.6); qmax_pmin_boundary.attachAllInputs(); @@ -429,14 +482,14 @@ namespace GridKit success *= qmax_pmin_boundary.initialize(0.45, 0.0); success *= (qmax_pmin_boundary.repca.evaluateResidual() == 0); success *= stateMatches(qmax_pmin_boundary.repca, - {{I::QPI, 0.9}, - {I::XQLAG, 0.9}, - {I::XQPI, 1.0}, - {I::QEXT, 0.45}, - {I::PREF, 0.0}, - {I::PPI, 0.0}, - {I::XPPI, -0.1}, - {I::PEXT, 0.0}}, + {{Vars::QPI, 0.9}, + {Vars::XQLAG, 0.9}, + {Vars::XQPI, 1.0}, + {Vars::QEXT, 0.45}, + {Vars::PREF, 0.0}, + {Vars::PPI, 0.0}, + {Vars::XPPI, -0.1}, + {Vars::PEXT, 0.0}}, "Qmax/Pmin command boundary"); success *= allResidualsZero(qmax_pmin_boundary.repca); } @@ -448,14 +501,14 @@ namespace GridKit success *= qmin_pmax_boundary.initialize(-0.4, 0.6); success *= (qmin_pmax_boundary.repca.evaluateResidual() == 0); success *= stateMatches(qmin_pmax_boundary.repca, - {{I::QPI, -0.8}, - {I::XQLAG, -0.8}, - {I::XQPI, -0.9}, - {I::QEXT, -0.4}, - {I::PREF, 1.2}, - {I::PPI, 1.2}, - {I::XPPI, 1.3}, - {I::PEXT, 0.6}}, + {{Vars::QPI, -0.8}, + {Vars::XQLAG, -0.8}, + {Vars::XQPI, -0.9}, + {Vars::QEXT, -0.4}, + {Vars::PREF, 1.2}, + {Vars::PPI, 1.2}, + {Vars::XPPI, 1.3}, + {Vars::PEXT, 0.6}}, "Qmin/Pmax command boundary"); success *= allResidualsZero(qmin_pmax_boundary.repca); } @@ -467,14 +520,14 @@ namespace GridKit success *= collapsed_limits.initialize(0.25, 0.45); success *= (collapsed_limits.repca.evaluateResidual() == 0); success *= stateMatches(collapsed_limits.repca, - {{I::XQPI, 0.5}, - {I::XQLAG, 0.5}, - {I::QPI, 0.5}, - {I::QEXT, 0.25}, - {I::XPPI, 0.9}, - {I::PREF, 0.9}, - {I::PPI, 0.9}, - {I::PEXT, 0.45}}, + {{Vars::XQPI, 0.5}, + {Vars::XQLAG, 0.5}, + {Vars::QPI, 0.5}, + {Vars::QEXT, 0.25}, + {Vars::XPPI, 0.9}, + {Vars::PREF, 0.9}, + {Vars::PPI, 0.9}, + {Vars::PEXT, 0.45}}, "collapsed Q/P limits"); success *= allResidualsZero(collapsed_limits.repca); } @@ -494,42 +547,45 @@ namespace GridKit setAnswerKeyState(fixture.repca); success *= (fixture.repca.evaluateResidual() == 0); - const std::array expected{{ - {I::VMEAS, 0.19000000000000017}, - {I::QMEAS, 0.77000000000000013}, - {I::XQPI, 0.018000000000000002}, - {I::XQLAG, 0.12666666666666668}, - {I::PMEAS, 0.92999999999999983}, - {I::XPPI, 0.082000000000000003}, - {I::PREF, 0.070000000000000104}, - {I::V, -0.029999999999999916}, - {I::VLDC, -0.015651160000000081}, - {I::VDROOP, 0.053999999999999965}, - {I::VCTRL, -0.030000000000000027}, - {I::SFRZ, 0.19999999999999996}, - {I::ERQ, 2.7755575615628914e-17}, - {I::ERQDB, -0.017111912348473052}, - {I::ERQLIM, 1.7347234759768071e-17}, - {I::QPI, -0.16000000000000009}, - {I::QEXT, -0.36399999999999999}, - {I::EF, -0.0089371400000009833}, - {I::EP, 0.64036181730064157}, - {I::EPLIM, 3.4694469519536142e-17}, - {I::PPI, -0.38200000000000001}, - {I::PEXT, -0.62}, + const std::array expected{{ + {Vars::VMEAS, "VMEAS", 0.19000000000000017}, + {Vars::QMEAS, "QMEAS", 0.77000000000000013}, + {Vars::XQPI, "XQPI", 0.018000000000000002}, + {Vars::XQLAG, "XQLAG", 0.12666666666666668}, + {Vars::PMEAS, "PMEAS", 0.92999999999999983}, + {Vars::XPPI, "XPPI", 0.082000000000000003}, + {Vars::PREF, "PREF", 0.070000000000000104}, + {Vars::V, "V", -0.029999999999999916}, + {Vars::VLDC, "VLDC", -0.015651160000000081}, + {Vars::VDROOP, "VDROOP", 0.053999999999999965}, + {Vars::VCTRL, "VCTRL", -0.030000000000000027}, + {Vars::SFRZ, "SFRZ", 0.19999999999999996}, + {Vars::ERQ, "ERQ", 2.7755575615628914e-17}, + {Vars::ERQDB, "ERQDB", -0.017111912348473052}, + {Vars::ERQLIM, "ERQLIM", 1.7347234759768071e-17}, + {Vars::QPI, "QPI", -0.16000000000000009}, + {Vars::QEXT, "QEXT", -0.36399999999999999}, + {Vars::EF, "EF", -0.0089371400000009833}, + {Vars::EP, "EP", 0.64036181730064157}, + {Vars::EPLIM, "EPLIM", 3.4694469519536142e-17}, + {Vars::PPI, "PPI", -0.38200000000000001}, + {Vars::PEXT, "PEXT", -0.62}, }}; - success *= (static_cast(fixture.repca.getResidual().getSize()) == expected.size()); + success *= (static_cast(fixture.repca.getResidual().getSize()) == expected.size()); + const auto* residual = fixture.repca.getResidual().getData(); for (size_t row = 0; row < expected.size(); ++row) { - if (expected[row].first != row) + if (index(expected[row].variable) != row) { std::cout << "REPCA residual key position " << row << " names row " - << expected[row].first << '\n'; + << expected[row].name << '\n'; success = false; } + success *= scalarMatches(residual[index(expected[row].variable)], + expected[row].value, + expected[row].name); } - success *= residualsMatch(fixture.repca, expected); return success.report(__func__); } @@ -563,22 +619,22 @@ namespace GridKit Fixture fixture(data); fixture.attachAllInputs(); setAnswerKeyInputs(fixture); - fixture.input(E::VREF) = 1.05; - fixture.input(E::QREF) = 0.20; + fixture.input(index(Ext::VREF)) = 1.05; + fixture.input(index(Ext::QREF)) = 0.20; success *= fixture.prepare(0.0, 0.0); setState(fixture.repca, - {{I::VMEAS, 0.95}, - {I::QMEAS, 0.10}, - {I::VLDC, 0.92}, - {I::VDROOP, 1.08}, - {I::VCTRL, 1.0}, - {I::ERQ, 0.0}}); + {{Vars::VMEAS, 0.95}, + {Vars::QMEAS, 0.10}, + {Vars::VLDC, 0.92}, + {Vars::VDROOP, 1.08}, + {Vars::VCTRL, 1.0}, + {Vars::ERQ, 0.0}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{I::VCTRL, test_case.vctrl}, - {I::ERQ, test_case.erq}}, + {{Vars::VCTRL, test_case.vctrl}, + {Vars::ERQ, test_case.erq}}, test_case.label); } @@ -594,23 +650,24 @@ namespace GridKit }}; for (const auto& test_case : freeze_cases) { - setState(fixture.repca, {{I::V, test_case.input}, {I::SFRZ, 0.0}}); + setState(fixture.repca, {{Vars::V, test_case.input}, {Vars::SFRZ, 0.0}}); success *= (fixture.repca.evaluateResidual() == 0); - success *= residualsMatch(fixture.repca, {{I::SFRZ, test_case.expected}}, "freeze gate"); + success *= residualsMatch(fixture.repca, {{Vars::SFRZ, test_case.expected}}, "freeze gate"); } - const std::array deadband_cases{{ + const std::array deadband_cases{{ {-0.05, -0.030003109594436028}, + {-0.02, -0.002888087651526948}, {0.0, -3.104066702683552e-5}, {0.03, 0.002888087651526948}, {0.06, 0.030003109594436025}, }}; for (const auto& test_case : deadband_cases) { - setState(fixture.repca, {{I::ERQ, test_case.input}, {I::ERQDB, 0.0}}); + setState(fixture.repca, {{Vars::ERQ, test_case.input}, {Vars::ERQDB, 0.0}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{I::ERQDB, test_case.expected}}, + {{Vars::ERQDB, test_case.expected}}, "reactive-power deadband"); } @@ -623,10 +680,10 @@ namespace GridKit }}; for (const auto& test_case : error_limit_cases) { - setState(fixture.repca, {{I::ERQDB, test_case.input}, {I::ERQLIM, 0.0}}); + setState(fixture.repca, {{Vars::ERQDB, test_case.input}, {Vars::ERQLIM, 0.0}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{I::ERQLIM, test_case.expected}}, + {{Vars::ERQLIM, test_case.expected}}, "reactive-power error limit"); } @@ -640,12 +697,12 @@ namespace GridKit for (const auto& test_case : command_limit_cases) { setState(fixture.repca, - {{I::XQPI, test_case.input}, - {I::ERQLIM, 0.0}, - {I::QPI, 0.0}}); + {{Vars::XQPI, test_case.input}, + {Vars::ERQLIM, 0.0}, + {Vars::QPI, 0.0}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{I::QPI, test_case.expected}}, + {{Vars::QPI, test_case.expected}}, "reactive-power command limit"); } @@ -662,25 +719,25 @@ namespace GridKit for (const auto& test_case : antiwindup_cases) { setState(fixture.repca, - {{I::QPI, test_case.output}, - {I::ERQLIM, test_case.error}, - {I::SFRZ, 1.0}}); - setDerivative(fixture.repca, {{I::XQPI, 0.0}}); + {{Vars::QPI, test_case.output}, + {Vars::ERQLIM, test_case.error}, + {Vars::SFRZ, 1.0}}); + setDerivative(fixture.repca, {{Vars::XQPI, 0.0}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{I::XQPI, test_case.expected}}, + {{Vars::XQPI, test_case.expected}}, "reactive-power antiwindup"); } setState(fixture.repca, - {{I::XQLAG, 0.14}, - {I::QPI, 0.27}, - {I::QEXT, 0.20}}); - setDerivative(fixture.repca, {{I::XQLAG, -0.04}}); + {{Vars::XQLAG, 0.14}, + {Vars::QPI, 0.27}, + {Vars::QEXT, 0.20}}); + setDerivative(fixture.repca, {{Vars::XQLAG, -0.04}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{I::XQLAG, 0.12666666666666668}, - {I::QEXT, -0.36399999999999999}}, + {{Vars::XQLAG, 0.12666666666666668}, + {Vars::QEXT, -0.36399999999999999}}, "reactive-command lead-lag"); return success.report(__func__); @@ -710,10 +767,10 @@ namespace GridKit fixture.attachAllInputs(); setAnswerKeyInputs(fixture); success *= fixture.prepare(0.0, 0.0); - setState(fixture.repca, {{I::PREF, 0.8}, {I::PEXT, 0.3}}); + setState(fixture.repca, {{Vars::PREF, 0.8}, {Vars::PEXT, 0.3}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{I::PEXT, test_case.pext}}, + {{Vars::PEXT, test_case.pext}}, test_case.label); } @@ -722,20 +779,21 @@ namespace GridKit setAnswerKeyInputs(fixture); success *= fixture.prepare(0.0, 0.0); - const std::array frequency_deadband_cases{{ + const std::array frequency_deadband_cases{{ {-0.05, -0.040000281494001103}, + {-0.01, -0.0028777978975925616}, {0.0, -0.00024949607977392432}, {0.015, 0.0028777978975925616}, {0.05, 0.035000934519396246}, }}; for (const auto& test_case : frequency_deadband_cases) { - fixture.input(E::FREQ) = 1.0; - fixture.input(E::FREQREF) = 1.0 + test_case.input; - setState(fixture.repca, {{I::EF, 0.0}}); + fixture.input(index(Ext::FREQ)) = 1.0; + fixture.input(index(Ext::FREQREF)) = 1.0 + test_case.input; + setState(fixture.repca, {{Vars::EF, 0.0}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{I::EF, test_case.expected}}, + {{Vars::EF, test_case.expected}}, "frequency deadband"); } @@ -744,16 +802,16 @@ namespace GridKit {0.0, 0.0028881132523331052}, {0.1, 0.2000000000001573}, }}; - fixture.input(E::PPLANTREF) = 0.2; + fixture.input(index(Ext::PPLANTREF)) = 0.2; for (const auto& test_case : droop_cases) { setState(fixture.repca, - {{I::EF, test_case.input}, - {I::EP, 0.0}, - {I::PMEAS, 0.4}}); + {{Vars::EF, test_case.input}, + {Vars::EP, 0.0}, + {Vars::PMEAS, 0.4}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{I::EP, test_case.expected}}, + {{Vars::EP, test_case.expected}}, "frequency droop"); } @@ -766,10 +824,10 @@ namespace GridKit }}; for (const auto& test_case : error_limit_cases) { - setState(fixture.repca, {{I::EP, test_case.input}, {I::EPLIM, 0.0}}); + setState(fixture.repca, {{Vars::EP, test_case.input}, {Vars::EPLIM, 0.0}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{I::EPLIM, test_case.expected}}, + {{Vars::EPLIM, test_case.expected}}, "active-power error limit"); } @@ -783,12 +841,12 @@ namespace GridKit for (const auto& test_case : command_limit_cases) { setState(fixture.repca, - {{I::XPPI, test_case.input}, - {I::EPLIM, 0.0}, - {I::PPI, 0.0}}); + {{Vars::XPPI, test_case.input}, + {Vars::EPLIM, 0.0}, + {Vars::PPI, 0.0}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{I::PPI, test_case.expected}}, + {{Vars::PPI, test_case.expected}}, "active-power command limit"); } @@ -805,20 +863,20 @@ namespace GridKit for (const auto& test_case : antiwindup_cases) { setState(fixture.repca, - {{I::PPI, test_case.output}, - {I::EPLIM, test_case.error}}); - setDerivative(fixture.repca, {{I::XPPI, 0.0}}); + {{Vars::PPI, test_case.output}, + {Vars::EPLIM, test_case.error}}); + setDerivative(fixture.repca, {{Vars::XPPI, 0.0}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{I::XPPI, test_case.expected}}, + {{Vars::XPPI, test_case.expected}}, "active-power antiwindup"); } - setState(fixture.repca, {{I::PPI, 0.66}, {I::PREF, 0.60}}); - setDerivative(fixture.repca, {{I::PREF, 0.05}}); + setState(fixture.repca, {{Vars::PPI, 0.66}, {Vars::PREF, 0.60}}); + setDerivative(fixture.repca, {{Vars::PREF, 0.05}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{I::PREF, 0.070000000000000104}}, + {{Vars::PREF, 0.070000000000000104}}, "active-power command lag"); return success.report(__func__); @@ -835,22 +893,22 @@ namespace GridKit success *= fixture.prepare(0.0, 0.0); setAnswerKeyState(fixture.repca); setDerivative(fixture.repca, - {{I::VMEAS, 0.11}, - {I::QMEAS, -0.12}, - {I::XQPI, 0.13}, - {I::XQLAG, -0.14}, - {I::PMEAS, 0.15}, - {I::XPPI, -0.16}, - {I::PREF, 0.17}}); + {{Vars::VMEAS, 0.11}, + {Vars::QMEAS, -0.12}, + {Vars::XQPI, 0.13}, + {Vars::XQLAG, -0.14}, + {Vars::PMEAS, 0.15}, + {Vars::XPPI, -0.16}, + {Vars::PREF, 0.17}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{I::VMEAS, 0.09000000000000018}, - {I::QMEAS, 0.8700000000000001}, - {I::XQPI, -0.082}, - {I::XQLAG, 0.22666666666666668}, - {I::PMEAS, 0.7999999999999998}, - {I::XPPI, 0.232}, - {I::PREF, -0.04999999999999989}}, + {{Vars::VMEAS, 0.09000000000000018}, + {Vars::QMEAS, 0.8700000000000001}, + {Vars::XQPI, -0.082}, + {Vars::XQLAG, 0.22666666666666668}, + {Vars::PMEAS, 0.7999999999999998}, + {Vars::XPPI, 0.232}, + {Vars::PREF, -0.04999999999999989}}, "explicit derivatives"); return success.report(__func__); @@ -862,6 +920,8 @@ namespace GridKit TestStatus success = true; const auto data = makeDynamicData(); + // The fixed state places ERQ exactly at dbdupper, so the key also + // covers the CommonMath deadband transition derivative. const auto expected = expectedJacobian(); const auto dependency = dependencyTrackingJacobian(data, success); @@ -870,10 +930,42 @@ namespace GridKit "dependency tracking", kBehaviorTol); + auto all_flags_off_data = data; + all_flags_off_data.parameters[Params::VcompFlag] = false; + all_flags_off_data.parameters[Params::RefFlag] = false; + all_flags_off_data.parameters[Params::Freqflag] = false; + const auto all_flags_off = + dependencyTrackingJacobian(all_flags_off_data, success); + success *= jacobianMatches(all_flags_off, + expectedJacobianAllFlagsOff(), + "all-flags-off dependency tracking", + kBehaviorTol); + + constexpr RealT nonunit_alpha = 2.5; + const auto nonunit_alpha_dependency = + dependencyTrackingJacobian(data, success, nonunit_alpha); + success *= jacobianMatches(nonunit_alpha_dependency, + expectedJacobianNonunitAlpha(), + "non-unit-alpha dependency tracking", + kBehaviorTol); + #ifdef GRIDKIT_ENABLE_ENZYME const auto enzyme = enzymeJacobian(data, success); success *= jacobianMatches(enzyme, expected, "Enzyme", kJacobianTol); + + const auto all_flags_off_enzyme = enzymeJacobian(all_flags_off_data, success); + success *= jacobianMatches(all_flags_off_enzyme, + expectedJacobianAllFlagsOff(), + "all-flags-off Enzyme", + kJacobianTol); + + const auto nonunit_alpha_enzyme = + enzymeJacobian(data, success, nonunit_alpha); + success *= jacobianMatches(nonunit_alpha_enzyme, + expectedJacobianNonunitAlpha(), + "non-unit-alpha Enzyme", + kJacobianTol); #endif return success.report(__func__); @@ -885,13 +977,41 @@ namespace GridKit using Ext = PhasorDynamics::Converter::RepcaExternalVariables; using Mon = PhasorDynamics::Converter::RepcaMonitorableVariables; using Data = PhasorDynamics::Converter::RepcaData; - using I = PhasorDynamics::Converter::RepcaIdx; - using E = PhasorDynamics::Converter::RepcaExt; using RepcaT = PhasorDynamics::Converter::Repca; - using Row = std::pair; - using Rows = std::initializer_list; using JacobianRow = DependencyTracking::Variable::DependencyMap; + static constexpr size_t index(Vars variable) + { + return static_cast(variable); + } + + static constexpr size_t index(Ext variable) + { + return static_cast(variable); + } + + struct Row + { + constexpr Row(Vars row, RealT expected_value) + : variable(row), + value(expected_value) + { + } + + Vars variable; + RealT value; + }; + + struct ExpectedResidual + { + Vars variable; + const char* name; + RealT value; + }; + + using Rows = std::initializer_list; + using JacobianKey = std::array; + struct DrivenCase { RealT input; @@ -919,24 +1039,28 @@ namespace GridKit class Fixture { private: - std::array input_values_{}; - std::array input_indices_{}; - std::array, E::MAXIMUM> input_nodes_{}; + std::array input_values_{}; + std::array input_indices_{}; + std::array, index(Ext::MAXIMUM)> input_nodes_{}; PhasorDynamics::SignalNode qext_node_; PhasorDynamics::SignalNode pext_node_; public: explicit Fixture(const Data& data, - RealT vr = 1.0, - RealT vi = 0.0, - RealT system_va_base = 100.0e6) + RealT vr = 1.0, + RealT vi = 0.0, + RealT system_va_base = 100.0e6, + bool assign_command_outputs = true) : bus(static_cast(vr), static_cast(vi)), repca(&bus, data) { repca.setSystemBase(60.0, system_va_base); - repca.getSignals().template assignSignalNode(&qext_node_); - repca.getSignals().template assignSignalNode(&pext_node_); + if (assign_command_outputs) + { + repca.getSignals().template assignSignalNode(&qext_node_); + repca.getSignals().template assignSignalNode(&pext_node_); + } } Fixture(const Fixture&) = delete; @@ -945,7 +1069,7 @@ namespace GridKit void attachRequiredInputs(RealT initial_value = 0.0) { const IdxT external_index_base = repca.size() + bus.size(); - for (size_t port = 0; port < E::MAXIMUM; ++port) + for (size_t port = 0; port < index(Ext::MAXIMUM); ++port) { input_values_[port] = static_cast(initial_value); input_indices_[port] = external_index_base + static_cast(port); @@ -953,11 +1077,11 @@ namespace GridKit } auto& signals = repca.getSignals(); - signals.template attachSignalNode(&input_nodes_[E::IBRANCHR]); - signals.template attachSignalNode(&input_nodes_[E::IBRANCHI]); - signals.template attachSignalNode(&input_nodes_[E::PBRANCH]); - signals.template attachSignalNode(&input_nodes_[E::QBRANCH]); - signals.template attachSignalNode(&input_nodes_[E::FREQ]); + signals.template attachSignalNode(&input_nodes_[index(Ext::IBRANCHR)]); + signals.template attachSignalNode(&input_nodes_[index(Ext::IBRANCHI)]); + signals.template attachSignalNode(&input_nodes_[index(Ext::PBRANCH)]); + signals.template attachSignalNode(&input_nodes_[index(Ext::QBRANCH)]); + signals.template attachSignalNode(&input_nodes_[index(Ext::FREQ)]); } void attachAllInputs(RealT initial_value = 0.0) @@ -965,16 +1089,18 @@ namespace GridKit attachRequiredInputs(initial_value); auto& signals = repca.getSignals(); - signals.template attachSignalNode(&input_nodes_[E::FREQREF]); - signals.template attachSignalNode(&input_nodes_[E::VREF]); - signals.template attachSignalNode(&input_nodes_[E::QREF]); - signals.template attachSignalNode(&input_nodes_[E::PPLANTREF]); + signals.template attachSignalNode(&input_nodes_[index(Ext::FREQREF)]); + signals.template attachSignalNode(&input_nodes_[index(Ext::VREF)]); + signals.template attachSignalNode(&input_nodes_[index(Ext::QREF)]); + signals.template attachSignalNode(&input_nodes_[index(Ext::PPLANTREF)]); } - void seedCommands(RealT qext, RealT pext) + void setCommands(RealT qext, RealT pext) { - qext_node_.init(static_cast(qext)); - pext_node_.init(static_cast(pext)); + auto* y = repca.y().getData(); + y[index(Vars::QEXT)] = static_cast(qext); + y[index(Vars::PEXT)] = static_cast(pext); + repca.y().setDataUpdated(); } /// Arrange the allocation, verification, bus, and command prerequisites. @@ -987,7 +1113,7 @@ namespace GridKit std::cout << "REPCA fixture preparation failed\n"; return false; } - seedCommands(qext, pext); + setCommands(qext, pext); return true; } @@ -1007,12 +1133,12 @@ namespace GridKit T qext() const { - return qext_node_.read(); + return repca.y().getData()[index(Vars::QEXT)]; } T pext() const { - return pext_node_.read(); + return repca.y().getData()[index(Vars::PEXT)]; } T& input(size_t port) @@ -1032,7 +1158,7 @@ namespace GridKit static constexpr RealT kStateVr = 0.9; static constexpr RealT kStateVi = 0.4; - static constexpr size_t kBusVrColumn = I::MAXIMUM; + static constexpr size_t kBusVrColumn = index(Vars::MAXIMUM); static constexpr size_t kBusViColumn = kBusVrColumn + 1; static constexpr size_t kExternalColumnBase = kBusViColumn + 1; @@ -1143,61 +1269,61 @@ namespace GridKit template void setInitializationInputs(Fixture& fixture) const { - fixture.input(E::IBRANCHR) = static_cast(0.2); - fixture.input(E::IBRANCHI) = static_cast(-0.1); - fixture.input(E::PBRANCH) = static_cast(0.4); - fixture.input(E::QBRANCH) = static_cast(0.1); - fixture.input(E::FREQ) = static_cast(0.99); + fixture.input(index(Ext::IBRANCHR)) = static_cast(0.2); + fixture.input(index(Ext::IBRANCHI)) = static_cast(-0.1); + fixture.input(index(Ext::PBRANCH)) = static_cast(0.4); + fixture.input(index(Ext::QBRANCH)) = static_cast(0.1); + fixture.input(index(Ext::FREQ)) = static_cast(0.99); } template void setAnswerKeyInputs(Fixture& fixture) const { - fixture.input(E::IBRANCHR) = static_cast(0.08); - fixture.input(E::IBRANCHI) = static_cast(-0.02); - fixture.input(E::PBRANCH) = static_cast(0.41); - fixture.input(E::QBRANCH) = static_cast(0.13); - fixture.input(E::FREQ) = static_cast(0.99); - fixture.input(E::FREQREF) = static_cast(1.0); - fixture.input(E::VREF) = static_cast(1.01); - fixture.input(E::QREF) = static_cast(0.12); - fixture.input(E::PPLANTREF) = static_cast(0.55); + fixture.input(index(Ext::IBRANCHR)) = static_cast(0.08); + fixture.input(index(Ext::IBRANCHI)) = static_cast(-0.02); + fixture.input(index(Ext::PBRANCH)) = static_cast(0.41); + fixture.input(index(Ext::QBRANCH)) = static_cast(0.13); + fixture.input(index(Ext::FREQ)) = static_cast(0.99); + fixture.input(index(Ext::FREQREF)) = static_cast(1.0); + fixture.input(index(Ext::VREF)) = static_cast(1.01); + fixture.input(index(Ext::QREF)) = static_cast(0.12); + fixture.input(index(Ext::PPLANTREF)) = static_cast(0.55); } template void setAnswerKeyState(PhasorDynamics::Converter::Repca& repca) const { setState(repca, - {{I::VMEAS, 0.98}, - {I::QMEAS, 0.11}, - {I::XQPI, 0.07}, - {I::XQLAG, 0.14}, - {I::PMEAS, 0.44}, - {I::XPPI, 0.21}, - {I::PREF, 0.60}, - {I::V, 1.0}, - {I::VLDC, 0.99}, - {I::VDROOP, 1.05}, - {I::VCTRL, 1.02}, - {I::SFRZ, 0.8}, - {I::ERQ, 0.03}, - {I::ERQDB, 0.02}, - {I::ERQLIM, 0.02}, - {I::QPI, 0.27}, - {I::QEXT, 0.20}, - {I::EF, 0.01}, - {I::EP, 0.04}, - {I::EPLIM, 0.04}, - {I::PPI, 0.66}, - {I::PEXT, 0.61}}); + {{Vars::VMEAS, 0.98}, + {Vars::QMEAS, 0.11}, + {Vars::XQPI, 0.07}, + {Vars::XQLAG, 0.14}, + {Vars::PMEAS, 0.44}, + {Vars::XPPI, 0.21}, + {Vars::PREF, 0.60}, + {Vars::V, 1.0}, + {Vars::VLDC, 0.99}, + {Vars::VDROOP, 1.05}, + {Vars::VCTRL, 1.02}, + {Vars::SFRZ, 0.8}, + {Vars::ERQ, 0.03}, + {Vars::ERQDB, 0.02}, + {Vars::ERQLIM, 0.02}, + {Vars::QPI, 0.27}, + {Vars::QEXT, 0.20}, + {Vars::EF, 0.01}, + {Vars::EP, 0.04}, + {Vars::EPLIM, 0.04}, + {Vars::PPI, 0.66}, + {Vars::PEXT, 0.61}}); setDerivative(repca, - {{I::VMEAS, 0.01}, - {I::QMEAS, -0.02}, - {I::XQPI, 0.03}, - {I::XQLAG, -0.04}, - {I::PMEAS, 0.02}, - {I::XPPI, -0.01}, - {I::PREF, 0.05}}); + {{Vars::VMEAS, 0.01}, + {Vars::QMEAS, -0.02}, + {Vars::XQPI, 0.03}, + {Vars::XQLAG, -0.04}, + {Vars::PMEAS, 0.02}, + {Vars::XPPI, -0.01}, + {Vars::PREF, 0.05}}); } bool defaultsMatchDocumentedValues() const @@ -1207,12 +1333,12 @@ namespace GridKit implicit_defaults.attachAllInputs(); explicit_defaults.attachAllInputs(); - implicit_defaults.input(E::PBRANCH) = 0.2; - implicit_defaults.input(E::QBRANCH) = 0.1; - implicit_defaults.input(E::FREQ) = 1.0; - explicit_defaults.input(E::PBRANCH) = 0.2; - explicit_defaults.input(E::QBRANCH) = 0.1; - explicit_defaults.input(E::FREQ) = 1.0; + implicit_defaults.input(index(Ext::PBRANCH)) = 0.2; + implicit_defaults.input(index(Ext::QBRANCH)) = 0.1; + implicit_defaults.input(index(Ext::FREQ)) = 1.0; + explicit_defaults.input(index(Ext::PBRANCH)) = 0.2; + explicit_defaults.input(index(Ext::QBRANCH)) = 0.1; + explicit_defaults.input(index(Ext::FREQ)) = 1.0; bool success = implicit_defaults.initialize(0.1, 0.2) && explicit_defaults.initialize(0.1, 0.2); @@ -1233,7 +1359,7 @@ namespace GridKit success &= vectorsMatch(implicit_defaults.repca.getResidual(), explicit_defaults.repca.getResidual(), "documented-default residual"); - for (size_t port = 0; port < E::MAXIMUM; ++port) + for (size_t port = 0; port < index(Ext::MAXIMUM); ++port) { success &= rowMatches(implicit_defaults.input(port), explicit_defaults.input(port), @@ -1278,9 +1404,11 @@ namespace GridKit RealT qext, RealT pext, const char* label, - NonfiniteTarget target = NonfiniteTarget::NONE) const + NonfiniteTarget target = NonfiniteTarget::NONE, + RealT initial_vr = 0.8, + RealT initial_vi = 0.6) const { - Fixture fixture(data, 0.8, 0.6); + Fixture fixture(data, initial_vr, initial_vi); fixture.attachAllInputs(77.0); setInitializationInputs(fixture); if (!fixture.prepare(qext, pext)) @@ -1291,7 +1419,7 @@ namespace GridKit const RealT infinity = std::numeric_limits::infinity(); if (target == NonfiniteTarget::FREQUENCY) { - fixture.input(E::FREQ) = infinity; + fixture.input(index(Ext::FREQ)) = infinity; } if (target == NonfiniteTarget::BUS_VOLTAGE) { @@ -1301,21 +1429,21 @@ namespace GridKit auto* y = fixture.repca.y().getData(); auto* yp = fixture.repca.yp().getData(); - for (size_t row = 0; row < I::MAXIMUM; ++row) + for (size_t row = 0; row < index(Vars::MAXIMUM); ++row) { y[row] = 0.125 + 0.01 * static_cast(row); yp[row] = -0.25 - 0.01 * static_cast(row); } // Restore assigned commands after filling model storage with sentinels. - fixture.seedCommands(qext, pext); + fixture.setCommands(qext, pext); fixture.repca.y().setDataUpdated(); fixture.repca.yp().setDataUpdated(); - const auto y_before = copyVector(fixture.repca.y()); - const auto yp_before = copyVector(fixture.repca.yp()); - const auto bus_before = copyVector(fixture.bus.y()); - std::array inputs_before{}; - for (size_t port = 0; port < E::MAXIMUM; ++port) + const auto y_before = copyVector(fixture.repca.y()); + const auto yp_before = copyVector(fixture.repca.yp()); + const auto bus_before = copyVector(fixture.bus.y()); + std::array inputs_before{}; + for (size_t port = 0; port < index(Ext::MAXIMUM); ++port) { inputs_before[port] = fixture.input(port); } @@ -1327,12 +1455,12 @@ namespace GridKit success = false; } - success &= valueUnchanged(fixture.qext(), qext, "qext signal", I::QEXT); - success &= valueUnchanged(fixture.pext(), pext, "pext signal", I::PEXT); + success &= valueUnchanged(fixture.qext(), qext, "qext signal", index(Vars::QEXT)); + success &= valueUnchanged(fixture.pext(), pext, "pext signal", index(Vars::PEXT)); success &= vectorUnchanged(fixture.repca.y(), y_before, "state"); success &= vectorUnchanged(fixture.repca.yp(), yp_before, "derivative"); success &= vectorUnchanged(fixture.bus.y(), bus_before, "bus state"); - for (size_t port = 0; port < E::MAXIMUM; ++port) + for (size_t port = 0; port < index(Ext::MAXIMUM); ++port) { success &= valueUnchanged(fixture.input(port), inputs_before[port], @@ -1346,9 +1474,9 @@ namespace GridKit void setState(PhasorDynamics::Converter::Repca& repca, Rows rows) const { auto* y = repca.y().getData(); - for (const auto& [row, value] : rows) + for (const auto& [variable, value] : rows) { - y[row] = static_cast(value); + y[index(variable)] = static_cast(value); } repca.y().setDataUpdated(); } @@ -1357,13 +1485,64 @@ namespace GridKit void setDerivative(PhasorDynamics::Converter::Repca& repca, Rows rows) const { auto* yp = repca.yp().getData(); - for (const auto& [row, value] : rows) + for (const auto& [variable, value] : rows) { - yp[row] = static_cast(value); + yp[index(variable)] = static_cast(value); } repca.yp().setDataUpdated(); } + static const char* variableName(Vars variable) + { + static constexpr std::array names{{ + "VMEAS", + "QMEAS", + "XQPI", + "XQLAG", + "PMEAS", + "XPPI", + "PREF", + "V", + "VLDC", + "VDROOP", + "VCTRL", + "SFRZ", + "ERQ", + "ERQDB", + "ERQLIM", + "QPI", + "QEXT", + "EF", + "EP", + "EPLIM", + "PPI", + "PEXT", + }}; + return names[index(variable)]; + } + + static bool variableMatches(RealT actual, + RealT expected, + const char* what, + Vars variable, + const char* context, + RealT tolerance = kBehaviorTol) + { + if (isEqual(actual, expected, tolerance)) + { + return true; + } + std::cout << "REPCA " << what << ' ' << variableName(variable); + if (context[0] != '\0') + { + std::cout << ' ' << context; + } + std::cout << " mismatch: " + << std::setprecision(std::numeric_limits::max_digits10) + << actual << " != " << expected << '\n'; + return false; + } + static bool rowMatches(RealT actual, RealT expected, const char* what, @@ -1380,7 +1559,7 @@ namespace GridKit { std::cout << ' ' << context; } - std::cout << " mismatch: " << std::setprecision(16) << actual + std::cout << " mismatch: " << std::setprecision(std::numeric_limits::max_digits10) << actual << " != " << expected << '\n'; return false; } @@ -1394,11 +1573,53 @@ namespace GridKit { return true; } - std::cout << label << " mismatch: " << std::setprecision(16) << actual + std::cout << label << " mismatch: " << std::setprecision(std::numeric_limits::max_digits10) << actual << " != " << expected << '\n'; return false; } + bool monitorMatches(const RepcaT& repca, + const std::array& expected, + const char* context) const + { + RealT time = 0.0; + Model::VariableMonitorController monitor(time); + monitor.addMonitor(repca.getMonitor()); + std::stringstream output; + monitor.addSink({Model::VariableMonitorFormat::CSV}, output); + monitor.start(); + monitor.print(); + monitor.stop(); + + std::string header; + std::string values_line; + std::getline(output, header); + std::getline(output, values_line); + + bool success = + header == "t,Repca_repca_test_qext,Repca_repca_test_pext," + "Repca_repca_test_vmeas,Repca_repca_test_qmeas," + "Repca_repca_test_pmeas"; + + const auto values = Tokenizer(values_line, ',')(); + if (values.size() != expected.size() + 1) + { + std::cout << "REPCA monitor emitted " << values.size() + << " values instead of " << expected.size() + 1 << '\n'; + return false; + } + + for (size_t i = 0; i < expected.size(); ++i) + { + success &= rowMatches(values[i + 1], + expected[i], + "monitor", + i, + context); + } + return success; + } + static bool valueUnchanged(RealT actual, RealT expected, const char* what, @@ -1409,7 +1630,7 @@ namespace GridKit return true; } std::cout << "REPCA " << what << ' ' << index - << " changed: " << std::setprecision(16) << actual + << " changed: " << std::setprecision(std::numeric_limits::max_digits10) << actual << " != " << expected << '\n'; return false; } @@ -1425,13 +1646,14 @@ namespace GridKit const auto* values = vector.getData(); for (size_t i = 0; i < count; ++i) { - const auto& [row, expected] = rows[i]; + const auto& [variable, expected] = rows[i]; + const size_t row = index(variable); - success &= rowMatches(static_cast(values[row]), - expected, - what, - row, - context); + success &= variableMatches(static_cast(values[row]), + expected, + what, + variable, + context); } return success; } @@ -1445,14 +1667,6 @@ namespace GridKit context); } - template - bool residualsMatch(const RepcaT& repca, - const std::array& rows, - const char* context = "") const - { - return rowsMatch(repca.getResidual(), rows.data(), size, "residual", context); - } - bool stateMatches(const RepcaT& repca, Rows rows, const char* context = "") const { return rowsMatch(repca.y(), rows.begin(), rows.size(), "state", context); @@ -1463,10 +1677,19 @@ namespace GridKit bool success = true; const auto* f = repca.getResidual().getData(); const auto* yp = repca.yp().getData(); - for (size_t row = 0; row < I::MAXIMUM; ++row) - { - success &= rowMatches(f[row], 0.0, "residual", row, "at rest"); - success &= rowMatches(yp[row], 0.0, "derivative", row, "at rest"); + for (size_t row = 0; row < index(Vars::MAXIMUM); ++row) + { + const auto variable = static_cast(row); + success &= variableMatches(f[row], + 0.0, + "residual", + variable, + "at rest"); + success &= variableMatches(yp[row], + 0.0, + "derivative", + variable, + "at rest"); } return success; } @@ -1532,43 +1755,74 @@ namespace GridKit Log::setVerbosity(previous_verbosity); } - std::array expectedJacobian() const + JacobianKey expectedJacobian() const { return {{ - {{I::VMEAS, -6.0}, {I::VCTRL, 5.0}}, - {{I::QMEAS, -6.0}, {externalColumn(E::QBRANCH), 10.0}}, - {{I::XQPI, -1.0}, {I::SFRZ, 0.06}, {I::ERQLIM, 2.4}}, - {{I::XQLAG, -1.6666666666666667}, {I::QPI, 0.6666666666666666}}, - {{I::PMEAS, -3.5}, {externalColumn(E::PBRANCH), 5.0}}, - {{I::XPPI, -1.0}, {I::EPLIM, 1.8}}, - {{I::PREF, -3.0}, {I::PPI, 2.0}}, - {{I::V, -2.0}, {kBusVrColumn, 1.8}, {kBusViColumn, 0.8}}, - {{I::VLDC, -1.98}, + {{index(Vars::VMEAS), -6.0}, {index(Vars::VCTRL), 5.0}}, + {{index(Vars::QMEAS), -6.0}, {externalColumn(index(Ext::QBRANCH)), 10.0}}, + {{index(Vars::XQPI), -1.0}, {index(Vars::SFRZ), 0.06}, {index(Vars::ERQLIM), 2.4}}, + {{index(Vars::XQLAG), -1.6666666666666667}, {index(Vars::QPI), 0.6666666666666666}}, + {{index(Vars::PMEAS), -3.5}, {externalColumn(index(Ext::PBRANCH)), 5.0}}, + {{index(Vars::XPPI), -1.0}, {index(Vars::EPLIM), 1.8}}, + {{index(Vars::PREF), -3.0}, {index(Vars::PPI), 2.0}}, + {{index(Vars::V), -2.0}, {kBusVrColumn, 1.8}, {kBusViColumn, 0.8}}, + {{index(Vars::VLDC), -1.98}, {kBusVrColumn, 1.7956}, {kBusViColumn, 0.796}, - {externalColumn(E::IBRANCHR), -0.059792}, - {externalColumn(E::IBRANCHI), 0.037948}}, - {{I::V, 1.0}, {I::VDROOP, -1.0}, {externalColumn(E::QBRANCH), 0.8}}, - {{I::VLDC, 1.0}, {I::VCTRL, -1.0}}, - {{I::SFRZ, -1.0}}, - {{I::VMEAS, -1.0}, {I::ERQ, -1.0}, {externalColumn(E::VREF), 1.0}}, - {{I::ERQ, 0.50000614417460221}, {I::ERQDB, -1.0}}, - {{I::ERQDB, 1.0}, {I::ERQLIM, -1.0}}, - {{I::XQPI, 1.0}, {I::ERQLIM, 2.0}, {I::QPI, -1.0}}, - {{I::XQLAG, 1.3}, {I::QPI, 0.2}, {I::QEXT, -3.0}}, - {{I::EF, -1.0}, - {externalColumn(E::FREQ), -0.23963778765414229}, - {externalColumn(E::FREQREF), 0.23963778765414229}}, - {{I::PMEAS, -1.0}, - {I::EF, 1.9168273035060777}, - {I::EP, -1.0}, - {externalColumn(E::PPLANTREF), 2.0}}, - {{I::EP, 1.0}, {I::EPLIM, -1.0}}, - {{I::XPPI, 1.0}, {I::EPLIM, 1.7}, {I::PPI, -1.0}}, - {{I::PREF, 1.0}, {I::PEXT, -2.0}}, + {externalColumn(index(Ext::IBRANCHR)), -0.059792}, + {externalColumn(index(Ext::IBRANCHI)), 0.037948}}, + {{index(Vars::V), 1.0}, {index(Vars::VDROOP), -1.0}, {externalColumn(index(Ext::QBRANCH)), 0.8}}, + {{index(Vars::VLDC), 1.0}, {index(Vars::VCTRL), -1.0}}, + {{index(Vars::SFRZ), -1.0}}, + {{index(Vars::VMEAS), -1.0}, {index(Vars::ERQ), -1.0}, {externalColumn(index(Ext::VREF)), 1.0}}, + {{index(Vars::ERQ), 0.50000614417460221}, {index(Vars::ERQDB), -1.0}}, + {{index(Vars::ERQDB), 1.0}, {index(Vars::ERQLIM), -1.0}}, + {{index(Vars::XQPI), 1.0}, {index(Vars::ERQLIM), 2.0}, {index(Vars::QPI), -1.0}}, + {{index(Vars::XQLAG), 1.3}, {index(Vars::QPI), 0.2}, {index(Vars::QEXT), -3.0}}, + {{index(Vars::EF), -1.0}, + {externalColumn(index(Ext::FREQ)), -0.23963778765414229}, + {externalColumn(index(Ext::FREQREF)), 0.23963778765414229}}, + {{index(Vars::PMEAS), -1.0}, + {index(Vars::EF), 1.9168273035060777}, + {index(Vars::EP), -1.0}, + {externalColumn(index(Ext::PPLANTREF)), 2.0}}, + {{index(Vars::EP), 1.0}, {index(Vars::EPLIM), -1.0}}, + {{index(Vars::XPPI), 1.0}, {index(Vars::EPLIM), 1.7}, {index(Vars::PPI), -1.0}}, + {{index(Vars::PREF), 1.0}, {index(Vars::PEXT), -2.0}}, }}; } + JacobianKey expectedJacobianAllFlagsOff() const + { + auto expected = expectedJacobian(); + expected[index(Vars::VCTRL)] = { + {index(Vars::VDROOP), 1.0}, + {index(Vars::VCTRL), -1.0}, + }; + expected[index(Vars::ERQ)] = { + {index(Vars::QMEAS), -1.0}, + {index(Vars::ERQ), -1.0}, + {externalColumn(index(Ext::QREF)), 2.0}, + }; + expected[index(Vars::PEXT)] = { + {index(Vars::PEXT), -2.0}, + }; + return expected; + } + + JacobianKey expectedJacobianNonunitAlpha() const + { + auto expected = expectedJacobian(); + expected[index(Vars::VMEAS)][index(Vars::VMEAS)] = -7.5; + expected[index(Vars::QMEAS)][index(Vars::QMEAS)] = -7.5; + expected[index(Vars::XQPI)][index(Vars::XQPI)] = -2.5; + expected[index(Vars::XQLAG)][index(Vars::XQLAG)] = -3.1666666666666665; + expected[index(Vars::PMEAS)][index(Vars::PMEAS)] = -5.0; + expected[index(Vars::XPPI)][index(Vars::XPPI)] = -2.5; + expected[index(Vars::PREF)][index(Vars::PREF)] = -4.5; + return expected; + } + static RealT mapValue(const JacobianRow& row, size_t column) { const auto entry = row.find(column); @@ -1579,10 +1833,10 @@ namespace GridKit return entry->second; } - bool jacobianMatches(const std::vector& actual, - const std::array& expected, - const char* source, - RealT tolerance) const + bool jacobianMatches(const std::vector& actual, + const JacobianKey& expected, + const char* source, + RealT tolerance) const { if (actual.size() != expected.size()) { @@ -1590,9 +1844,9 @@ namespace GridKit return false; } - constexpr size_t column_count = externalColumn(E::MAXIMUM); + constexpr size_t column_count = externalColumn(index(Ext::MAXIMUM)); bool success = true; - for (size_t row = 0; row < I::MAXIMUM; ++row) + for (size_t row = 0; row < index(Vars::MAXIMUM); ++row) { for (size_t column = 0; column < column_count; ++column) { @@ -1602,7 +1856,7 @@ namespace GridKit { std::cout << "REPCA " << source << " Jacobian row " << row << " column " << column << " mismatch: " - << std::setprecision(16) << actual_value << " != " + << std::setprecision(std::numeric_limits::max_digits10) << actual_value << " != " << expected_value << '\n'; success = false; } @@ -1620,22 +1874,24 @@ namespace GridKit return success; } - void numberVariables(Fixture& fixture) const + void numberVariables(Fixture& fixture, + RealT alpha) const { auto* y = fixture.repca.y().getData(); auto* yp = fixture.repca.yp().getData(); auto* bus_y = fixture.bus.y().getData(); - for (size_t row = 0; row < I::MAXIMUM; ++row) + for (size_t row = 0; row < index(Vars::MAXIMUM); ++row) { y[row].setVariableNumber(row); yp[row].setVariableNumber(row); + yp[row].scaleDependencies(alpha); } for (size_t row = 0; row < static_cast(fixture.bus.size()); ++row) { bus_y[row].setVariableNumber(kBusVrColumn + row); } - for (size_t port = 0; port < E::MAXIMUM; ++port) + for (size_t port = 0; port < index(Ext::MAXIMUM); ++port) { fixture.input(port).setVariableNumber(fixture.inputIndex(port)); } @@ -1646,7 +1902,8 @@ namespace GridKit } std::vector dependencyTrackingJacobian(const Data& data, - TestStatus& success) const + TestStatus& success, + RealT alpha = 1.0) const { using DepVar = DependencyTracking::Variable; @@ -1655,12 +1912,12 @@ namespace GridKit setAnswerKeyInputs(fixture); success *= fixture.prepare(0.0, 0.0); setAnswerKeyState(fixture.repca); - numberVariables(fixture); + numberVariables(fixture, alpha); success *= (fixture.repca.evaluateResidual() == 0); - std::vector rows(I::MAXIMUM); + std::vector rows(index(Vars::MAXIMUM)); const auto* f = fixture.repca.getResidual().getData(); - for (size_t row = 0; row < I::MAXIMUM; ++row) + for (size_t row = 0; row < index(Vars::MAXIMUM); ++row) { rows[row] = f[row].getDependencies(); } @@ -1669,7 +1926,8 @@ namespace GridKit #ifdef GRIDKIT_ENABLE_ENZYME std::vector enzymeJacobian(const Data& data, - TestStatus& success) const + TestStatus& success, + RealT alpha = 1.0) const { Fixture fixture(data, kStateVr, kStateVi); fixture.attachAllInputs(); @@ -1682,7 +1940,7 @@ namespace GridKit } setAnswerKeyState(fixture.repca); - fixture.repca.updateTime(0.0, 1.0); + fixture.repca.updateTime(0.0, alpha); success *= (fixture.repca.evaluateResidual() == 0); success *= (fixture.repca.evaluateJacobian() == 0); success *= (fixture.repca.constructCsr() == 0); diff --git a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp index de98a440f..e9d875808 100644 --- a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp @@ -1,3 +1,4 @@ +#include #include #include @@ -252,6 +253,139 @@ namespace GridKit return success.report(__func__); } + /// Construct REPCA through SystemModel, check its required bus mapping, + /// and prove assigned command outputs alias the assembled signals. + TestOutcome repca() + { + using Repca = PhasorDynamics::Converter::Repca; + using Buses = PhasorDynamics::Converter::RepcaBuses; + using Inputs = PhasorDynamics::Converter::RepcaSignalInputs; + using Outputs = PhasorDynamics::Converter::RepcaSignalOutputs; + using Params = PhasorDynamics::Converter::RepcaParameters; + using Vars = PhasorDynamics::Converter::RepcaInternalVariables; + + constexpr size_t input_count = static_cast(Inputs::SIZE); + constexpr IdxT bus_id = static_cast(1); + constexpr IdxT qext_id = static_cast(input_count + 1); + constexpr IdxT pext_id = static_cast(input_count + 2); + + constexpr std::array input_ports{{ + Inputs::ibranchr, + Inputs::ibranchi, + Inputs::pbranch, + Inputs::qbranch, + Inputs::freq, + Inputs::freqref, + Inputs::vref, + Inputs::qref, + Inputs::pplantref, + }}; + constexpr std::array input_names{{ + "Branch Current Real", + "Branch Current Imaginary", + "Branch Active Power", + "Branch Reactive Power", + "Frequency", + "Frequency Reference", + "Voltage Reference", + "Reactive Power Reference", + "Plant Active Power Reference", + }}; + + PhasorDynamics::SystemModelData data; + data.bus.resize(1); + data.bus[0].bus_id = bus_id; + data.bus[0].bus_type = PhasorDynamics::BusData::BusType::SLACK; + data.bus[0].Vr0 = static_cast(1.0); + data.bus[0].Vi0 = static_cast(0.0); + + data.signal.resize(input_count + 2); + for (size_t port = 0; port < input_count; ++port) + { + data.signal[port].signal_id = static_cast(port + 1); + data.signal[port].name = input_names[port]; + } + data.signal[input_count].signal_id = qext_id; + data.signal[input_count].name = "Reactive Power Command"; + data.signal[input_count + 1].signal_id = pext_id; + data.signal[input_count + 1].name = "Active Power Command"; + + typename PhasorDynamics::SystemModelData::RepcaDataT repca_data; + repca_data.device_class = "Repca"; + repca_data.disambiguation_string = "repca_system"; + repca_data.buses[Buses::bus] = bus_id; + repca_data.parameters[Params::Tp] = static_cast(0.05); + for (size_t port = 0; port < input_count; ++port) + { + repca_data.signal_inputs[input_ports[port]] = static_cast(port + 1); + } + repca_data.signal_outputs[Outputs::qext] = qext_id; + repca_data.signal_outputs[Outputs::pext] = pext_id; + data.repca.push_back(repca_data); + + std::array input_values{}; + std::array input_indices{}; + input_values[static_cast(Inputs::freq)] = static_cast(1.0); + input_values[static_cast(Inputs::freqref)] = static_cast(1.0); + input_values[static_cast(Inputs::vref)] = static_cast(1.0); + input_indices.fill(INVALID_INDEX); + + PhasorDynamics::SystemModel system(data); + TestStatus success = true; + for (size_t port = 0; port < input_count; ++port) + { + auto* signal = system.getSignal(static_cast(port + 1)); + signal->set(&input_values[port], &input_indices[port]); + success *= signal->linked(); + } + + success *= system.allocate() == 0; + success *= system.initialize() == 0; + success *= system.tagDifferentiable() == 0; + success *= system.evaluateResidual() == 0; + success *= system.evaluateJacobian() == 0; + success *= system.size() == static_cast(Vars::MAXIMUM); + + auto* repca = dynamic_cast(system.getComponent(0)); + success *= repca != nullptr; + auto* qext = system.getSignal(qext_id); + auto* pext = system.getSignal(pext_id); + success *= qext != nullptr; + success *= pext != nullptr; + if (repca != nullptr && qext != nullptr && pext != nullptr) + { + success *= qext->linked(); + success *= pext->linked(); + if (qext->linked() && pext->linked()) + { + success *= qext->getVariableIndex() + == repca->getVariableIndex(static_cast(Vars::QEXT)); + success *= pext->getVariableIndex() + == repca->getVariableIndex(static_cast(Vars::PEXT)); + success *= isEqual(qext->read(), + repca->y().getData()[static_cast(Vars::QEXT)], + static_cast(1.0e-12)); + success *= isEqual(pext->read(), + repca->y().getData()[static_cast(Vars::PEXT)], + static_cast(1.0e-12)); + } + } + + auto missing_bus_data = data; + missing_bus_data.bus[0].bus_id = static_cast(0); + missing_bus_data.repca[0].buses.clear(); + PhasorDynamics::SystemModel missing_bus_system(missing_bus_data); + for (size_t port = 0; port < input_count; ++port) + { + missing_bus_system.getSignal(static_cast(port + 1)) + ->set(&input_values[port], &input_indices[port]); + } + std::cout << "Testing expected REPCA missing-bus configuration error.\n"; + success *= missing_bus_system.verify() > 0; + + return success.report(__func__); + } + TestOutcome genrou() { TestStatus success = true; diff --git a/tests/UnitTests/PhasorDynamics/runSystemSingleComponentTests.cpp b/tests/UnitTests/PhasorDynamics/runSystemSingleComponentTests.cpp index abb274a0f..65ec715a8 100644 --- a/tests/UnitTests/PhasorDynamics/runSystemSingleComponentTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runSystemSingleComponentTests.cpp @@ -16,6 +16,7 @@ int main() result += test.load(); result += test.loadZIP(); result += test.regca(); + result += test.repca(); result += test.genrou(); result += test.genClassical(); result += test.tgov1(); diff --git a/tests/UnitTests/Utilities/CaseFormatTests.hpp b/tests/UnitTests/Utilities/CaseFormatTests.hpp index ce342a321..93b853e29 100644 --- a/tests/UnitTests/Utilities/CaseFormatTests.hpp +++ b/tests/UnitTests/Utilities/CaseFormatTests.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -193,6 +194,7 @@ namespace GridKit using BusType = typename BusData::BusType; using Esdc1aData = Exciter::Esdc1aData; using HygovData = Governor::HygovData; + using RepcaData = Converter::RepcaData; const char data[] = R"({ @@ -220,7 +222,18 @@ namespace GridKit { "signal_id": 6, "name": "Under-excitation Limiter"}, { "signal_id": 7, "name": "Hydro Mechanical Power"}, { "signal_id": 8, "name": "Governor Load Reference"}, - { "signal_id": 9, "name": "Governor Auxiliary Power"} + { "signal_id": 9, "name": "Governor Auxiliary Power"}, + { "signal_id": 11, "name": "Branch Current Real"}, + { "signal_id": 12, "name": "Branch Current Imaginary"}, + { "signal_id": 13, "name": "Branch Active Power"}, + { "signal_id": 14, "name": "Branch Reactive Power"}, + { "signal_id": 15, "name": "Frequency"}, + { "signal_id": 16, "name": "Frequency Reference"}, + { "signal_id": 17, "name": "Plant Voltage Reference"}, + { "signal_id": 18, "name": "Reactive Power Reference"}, + { "signal_id": 19, "name": "Plant Active Power Reference"}, + { "signal_id": 20, "name": "Reactive Power Command"}, + { "signal_id": 21, "name": "Active Power Command"} ], "devices": [ { "class": "Branch", "ports": {"bus1":1, "bus2":2}, "id": "BR1", "params": {"R":0.0, "X":0.1, "G":0.0, "B":0.0, "tap":1.05, "phase":0.1} }, @@ -231,6 +244,7 @@ namespace GridKit "Velm": 0.2, "Gmax": 0.98, "Gmin": 0.02, "Tw": 1.2, "At": 1.1, "Dturb": 0.4, "Qnl": 0.08, "Tn": 0.7, "Tnp": 1.4, "db1": 0.01, "db2": 0.02, "Hdam": 1.05, "Gv0": 0.0, "Gv1": 0.2, "Gv2": 0.4, "Gv3": 0.6, "Gv4": 0.8, "Gv5": 1.0, "Pgv0": 0.0, "Pgv1": 0.15, "Pgv2": 0.42, "Pgv3": 0.66, "Pgv4": 0.85, "Pgv5": 1.0}, "mon": ["pmech", "filter", "desiredgate", "gate", "flow", "head"]}, + { "class": "Repca", "ports": {"bus":1, "ibranchr":11, "ibranchi":12, "pbranch":13, "qbranch":14, "freq":15, "freqref":16, "vref":17, "qref":18, "pplantref":19, "qext":20, "pext":21}, "id": "PC1", "params": {"mva":50, "VcompFlag":false, "Tfltr":0.2}, "mon": ["qext", "pext", "vmeas", "qmeas", "pmeas"] }, { "class": "Ieeet1", "ports": {"bus":1, "speed": 1, "efd":3}, "id": "DV3", "params": {"Tr":0.0, "Ka":50.0, "Ta":0.04, "Ke":-0.06, "Te":0.6, "Kf":0.09, "Tf":1.46, "Vrmin":-1.0, "Vrmax":1.0, "E1":2.8, "E2":3.373, "Se1":0.04, "Se2":0.33, "Ispdlim":0.0}}, { "class": "SexsPti", "ports": {"bus":1, "efd":3}, "id": "DV4", "params": {"Ta":0.1, "Tb":0.5, "Te":0.8, "K":10.0, "Efdmax":5.0, "Efdmin":-5.0}}, { "class": "BusFault", "ports": {"bus":1}, "id": "1", "params": {"state0": false, "R":0.0, "X":1e-3} } @@ -256,9 +270,11 @@ namespace GridKit success *= result.gov.size() == 1; success *= result.esdc1a.size() == 1; success *= result.hygov.size() == 1; + success *= result.repca.size() == 1; success *= result.loadz.size() == 0; success *= result.exciter.size() == 1; success *= result.sexspti.size() == 1; + success *= result.signal.size() == 20; success *= result.bus[0].bus_id == 1; success *= result.bus[0].bus_type == BusType::DEFAULT; @@ -418,6 +434,28 @@ namespace GridKit success *= result.hygov[0].monitored_variables.contains(HygovData::MonitorableVariables::flow); success *= result.hygov[0].monitored_variables.contains(HygovData::MonitorableVariables::head); + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::mva]) == 50; + success *= !std::get(result.repca[0].parameters[RepcaData::Parameters::VcompFlag]); + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Tfltr]) == 0.2; + success *= result.repca[0].buses[RepcaData::Buses::bus] == 1; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::ibranchr] == 11; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::ibranchi] == 12; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::pbranch] == 13; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::qbranch] == 14; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::freq] == 15; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::freqref] == 16; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::vref] == 17; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::qref] == 18; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::pplantref] == 19; + success *= result.repca[0].signal_outputs[RepcaData::SignalOutputs::qext] == 20; + success *= result.repca[0].signal_outputs[RepcaData::SignalOutputs::pext] == 21; + success *= result.repca[0].disambiguation_string == "PC1"; + success *= result.repca[0].monitored_variables.contains(RepcaData::MonitorableVariables::qext); + success *= result.repca[0].monitored_variables.contains(RepcaData::MonitorableVariables::pext); + success *= result.repca[0].monitored_variables.contains(RepcaData::MonitorableVariables::vmeas); + success *= result.repca[0].monitored_variables.contains(RepcaData::MonitorableVariables::qmeas); + success *= result.repca[0].monitored_variables.contains(RepcaData::MonitorableVariables::pmeas); + success *= std::get(result.exciter[0].parameters[Exciter::Ieeet1Parameters::Tr]) == 0.0; success *= std::get(result.exciter[0].parameters[Exciter::Ieeet1Parameters::Ka]) == 50.0; success *= std::get(result.exciter[0].parameters[Exciter::Ieeet1Parameters::Ta]) == 0.04; From 8fd09fb83a71ef1f3d5b3b02d75b65f3cfc2e482 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Sun, 2 Aug 2026 06:07:23 -0500 Subject: [PATCH 04/19] bus freq port clarification --- .../Model/PhasorDynamics/Converter/REPCA/README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md index deb8d0188..b552bea38 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md @@ -114,7 +114,7 @@ Name | Port | Init | Description `ibranchi` | Input | Known | Branch-current imaginary component on system base `pbranch` | Input | Known | Branch active power on system base `qbranch` | Input | Known | Branch reactive power on system base -`freq` | Input | Known | Frequency input +`freq` | Input | Known | Frequency input[^frequency-measurement] Not supported yet. `freqref` | Input | Unknown | Frequency reference `vref` | Input | Unknown | Voltage-control reference `qref` | Input | Unknown | Reactive-power reference on system base @@ -294,12 +294,6 @@ and [ramp primitive](../../../../CommonMath.md#primitives) used above. ### External Equations -#### Differential - -None. - -#### Algebraic - None. ## Initialization @@ -455,3 +449,7 @@ Output | Units | Description | Note - `derivatives()` checks differential-row derivative signs. - `jacobian()` checks fixed numerical and structural oracles, plus Enzyme agreement to $10^{-9}$ when enabled. + +[^frequency-measurement]: Background for phase-derived, filtered frequency + measurement: [PSCAD Frequency/Phase/Magnitude Meter](https://www.pscad.com/webhelp-pscad-v5.1.0-ol/Master_Library_Models/Meters/Frequency_Phase_Magnitude_Meter.htm) and + [Ting et al., *Evaluating Methods for Measuring Grid Frequency in Low-Inertia Power Systems*](https://research-hub.nrel.gov/en/publications/evaluating-methods-for-measuring-grid-frequency-in-low-inertia-po-3/). From e270eb289bedb35f9613018e6c8e3d635336b2d9 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Sun, 2 Aug 2026 23:50:44 -0500 Subject: [PATCH 05/19] flags are bool type --- .../PhasorDynamics/Converter/REPCA/README.md | 70 +++++++++---------- .../PhasorDynamics/Converter/REPCA/Repca.hpp | 8 +-- .../Converter/REPCA/RepcaData.hpp | 8 +-- .../Converter/REPCA/RepcaImpl.hpp | 29 +++----- .../PhasorDynamics/ConverterRepcaTests.hpp | 24 ++++--- 5 files changed, 64 insertions(+), 75 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md index b552bea38..f26bddd33 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md @@ -19,42 +19,42 @@ Figure 1: REPCA plant-control model. Figure courtesy of ## Model Parameters -Symbol | Units | JSON | Description | Typical Value | Note -------------------------------------|----------|-------------|---------------------------------------------------------|---------------|------ -$S^\mathrm{base}$ | [MVA] | `mva` | REPCA component power base | 100.0 | Component base -$s_\mathrm{comp}$ | [binary] | `VcompFlag` | Voltage-compensation selector | 1 | 1 = line-drop compensation, 0 = reactive droop -$s_\mathrm{ref}$ | [binary] | `RefFlag` | Reactive-loop reference selector | 1 | 1 = voltage control, 0 = reactive-power control -$s_\mathrm{freq}$ | [binary] | `Freqflag` | Active-power output selector | 0 | 1 = command enabled, 0 = zero output -$T_\mathrm{fltr}$ | [sec] | `Tfltr` | Voltage and reactive-power measurement filter time constant | 0.05 | -$V^\mathrm{frz}$ | [p.u.] | `Vfrz` | Reactive-power PI freeze-voltage threshold | 0.7 | -$R_c$ | [p.u.] | `Rc` | Line-drop compensation resistance | 0.0 | System base -$X_c$ | [p.u.] | `Xc` | Line-drop compensation reactance | 0.0 | System base -$K_c$ | [p.u.] | `Kc` | Reactive-current compensation gain | 1.0 | -$D_\mathrm{bd1}$ | [p.u.] | `dbdlow` | Lower reactive-loop deadband threshold | 0.0 | -$D_\mathrm{bd2}$ | [p.u.] | `dbdupper` | Upper reactive-loop deadband threshold | 0.0 | -$e^{\max}$ | [p.u.] | `emax` | Maximum reactive-loop error limit | 1.0 | -$e^{\min}$ | [p.u.] | `emin` | Minimum reactive-loop error limit | -1.0 | -$K_\mathrm{p}$ | [p.u.] | `Kp` | Reactive-power controller proportional gain | 10.0 | -$K_\mathrm{i}$ | [p.u./s] | `Ki` | Reactive-power controller integral gain | 10.0 | -$Q^{\max}$ | [p.u.] | `Qmax` | Maximum reactive-power command | 1.0 | Component base -$Q^{\min}$ | [p.u.] | `Qmin` | Minimum reactive-power command | -1.0 | Component base -$T_\mathrm{ft}$ | [sec] | `Tft` | Reactive-command lead time constant | 0.0 | -$T_\mathrm{fv}$ | [sec] | `Tfv` | Reactive-command lag time constant | 3.0 | -$T_\mathrm{p}$ | [sec] | `Tp` | Active-power measurement filter time constant | 0.0 | -$D_\mathrm{bd1}^{f}$ | [p.u.] | `fdbd1` | Lower frequency-error deadband threshold | 0.0 | -$D_\mathrm{bd2}^{f}$ | [p.u.] | `fdbd2` | Upper frequency-error deadband threshold | 0.0 | -$D_\mathrm{dn}$ | [p.u.] | `Ddn` | Down-frequency droop response gain | 20.0 | -$D_\mathrm{up}$ | [p.u.] | `Dup` | Up-frequency droop response gain | 0.0 | -$e_P^{\max}$ | [p.u.] | `femax` | Maximum active-power error limit | 1.0 | -$e_P^{\min}$ | [p.u.] | `femin` | Minimum active-power error limit | -1.0 | -$K_\mathrm{pg}$ | [p.u.] | `Kpg` | Active-power controller proportional gain | 10.0 | -$K_\mathrm{ig}$ | [p.u./s] | `Kig` | Active-power controller integral gain | 10.0 | -$P^{\max}$ | [p.u.] | `Pmax` | Maximum active-power command | 2.0 | Component base -$P^{\min}$ | [p.u.] | `Pmin` | Minimum active-power command | 0.0 | Component base -$T_\mathrm{lag}$ | [sec] | `Tlag` | Active-power command lag time constant | 3.0 | +Symbol | Units | JSON | Description | Typical Value | Note +------------------------------------|-----------|-------------|---------------------------------------------------------|---------------|------ +$S^\mathrm{base}$ | [MVA] | `mva` | REPCA component power base | 100.0 | Component base +$s_\mathrm{comp}$ | [boolean] | `VcompFlag` | Voltage-compensation selector | `true` | `true` = line-drop compensation, `false` = reactive droop +$s_\mathrm{ref}$ | [boolean] | `RefFlag` | Reactive-loop reference selector | `true` | `true` = voltage control, `false` = reactive-power control +$s_\mathrm{freq}$ | [boolean] | `Freqflag` | Active-power output selector | `false` | `true` = command enabled, `false` = zero output +$T_\mathrm{fltr}$ | [sec] | `Tfltr` | Voltage and reactive-power filter time constant | 0.05 | +$V^\mathrm{frz}$ | [p.u.] | `Vfrz` | Reactive-power PI freeze-voltage threshold | 0.7 | +$R_c$ | [p.u.] | `Rc` | Line-drop compensation resistance | 0.0 | System base +$X_c$ | [p.u.] | `Xc` | Line-drop compensation reactance | 0.0 | System base +$K_c$ | [p.u.] | `Kc` | Reactive-current compensation gain | 1.0 | +$D_\mathrm{bd1}$ | [p.u.] | `dbdlow` | Lower reactive-loop deadband threshold | 0.0 | +$D_\mathrm{bd2}$ | [p.u.] | `dbdupper` | Upper reactive-loop deadband threshold | 0.0 | +$e^{\max}$ | [p.u.] | `emax` | Maximum reactive-loop error limit | 1.0 | +$e^{\min}$ | [p.u.] | `emin` | Minimum reactive-loop error limit | -1.0 | +$K_\mathrm{p}$ | [p.u.] | `Kp` | Reactive-power controller proportional gain | 10.0 | +$K_\mathrm{i}$ | [p.u./s] | `Ki` | Reactive-power controller integral gain | 10.0 | +$Q^{\max}$ | [p.u.] | `Qmax` | Maximum reactive-power command | 1.0 | Component base +$Q^{\min}$ | [p.u.] | `Qmin` | Minimum reactive-power command | -1.0 | Component base +$T_\mathrm{ft}$ | [sec] | `Tft` | Reactive-command lead time constant | 0.0 | +$T_\mathrm{fv}$ | [sec] | `Tfv` | Reactive-command lag time constant | 3.0 | +$T_\mathrm{p}$ | [sec] | `Tp` | Active-power measurement filter time constant | 0.0 | +$D_\mathrm{bd1}^{f}$ | [p.u.] | `fdbd1` | Lower frequency-error deadband threshold | 0.0 | +$D_\mathrm{bd2}^{f}$ | [p.u.] | `fdbd2` | Upper frequency-error deadband threshold | 0.0 | +$D_\mathrm{dn}$ | [p.u.] | `Ddn` | Down-frequency droop response gain | 20.0 | +$D_\mathrm{up}$ | [p.u.] | `Dup` | Up-frequency droop response gain | 0.0 | +$e_P^{\max}$ | [p.u.] | `femax` | Maximum active-power error limit | 1.0 | +$e_P^{\min}$ | [p.u.] | `femin` | Minimum active-power error limit | -1.0 | +$K_\mathrm{pg}$ | [p.u.] | `Kpg` | Active-power controller proportional gain | 10.0 | +$K_\mathrm{ig}$ | [p.u./s] | `Kig` | Active-power controller integral gain | 10.0 | +$P^{\max}$ | [p.u.] | `Pmax` | Maximum active-power command | 2.0 | Component base +$P^{\min}$ | [p.u.] | `Pmin` | Minimum active-power command | 0.0 | Component base +$T_\mathrm{lag}$ | [sec] | `Tlag` | Active-power command lag time constant | 3.0 | Every parameter is optional and uses the Typical Value when omitted. Real-valued -parameters accept real or integer JSON values; selectors accept Boolean or numeric 0/1. +parameters accept real or integer JSON values; selectors require Boolean JSON values. ### Parameter Validation @@ -64,8 +64,6 @@ All real parameters must be finite. Invalid parameter sets are rejected by: \begin{aligned} S^\mathrm{base} &> 0 \\ - s_\mathrm{comp}, s_\mathrm{ref}, s_\mathrm{freq} - &\in \{0,1\} \\ T_\mathrm{fv} &> 0 \\ D_\mathrm{bd1} diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp index 3ab1922fe..3262346cf 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp @@ -146,10 +146,10 @@ namespace GridKit RepcaParameters parameter, RealT& target, const char* name); - void loadSwitchParameter(const ModelDataT& data, - RepcaParameters parameter, - bool& target, - const char* name); + void loadBooleanParameter(const ModelDataT& data, + RepcaParameters parameter, + bool& target, + const char* name); bool floorTimeConstant(RealT& value, const char* name); void initializeParameters(const ModelDataT& data); void initializeMonitor(); diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp index 17bbf9f6a..fed8f163c 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp @@ -18,9 +18,9 @@ namespace GridKit enum class RepcaParameters { mva, ///< \f$S^\mathrm{base}\f$ Component power base [MVA] - VcompFlag, ///< \f$s_\mathrm{comp}\f$ Voltage-compensation selector [binary] - RefFlag, ///< \f$s_\mathrm{ref}\f$ Reactive-loop reference selector [binary] - Freqflag, ///< \f$s_\mathrm{freq}\f$ Active-power output selector [binary] + VcompFlag, ///< \f$s_\mathrm{comp}\f$ Voltage-compensation selector [boolean] + RefFlag, ///< \f$s_\mathrm{ref}\f$ Reactive-loop reference selector [boolean] + Freqflag, ///< \f$s_\mathrm{freq}\f$ Active-power output selector [boolean] Tfltr, ///< \f$T_\mathrm{fltr}\f$ Voltage and reactive-power filter time constant [sec] Vfrz, ///< \f$V^\mathrm{frz}\f$ Reactive-power PI freeze threshold [p.u.] Rc, ///< \f$R_c\f$ Line-drop resistance on system base [p.u.] @@ -94,7 +94,7 @@ namespace GridKit * @brief Model data for REPCA parameters, bus and signal ports, and monitored variables. * * @tparam real_type Real parameter value type. - * @tparam index_type Integer index and serialized selector type. + * @tparam index_type Integer index type. * * @see Repca */ diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp index c5e6cd6fe..ae218f4df 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp @@ -869,19 +869,18 @@ namespace GridKit } /** - * @brief Load one optional binary selector + * @brief Load one optional Boolean parameter * - * Boolean values and numeric values exactly equal to zero or one are - * accepted. Any other value or stored type records a loading error - * while preserving the existing default. + * Any non-Boolean stored type records a loading error while preserving + * the existing default. * * @param[in] data Model parameter data. * @param[in] parameter Parameter key to load. - * @param[in,out] target Stored selector value. + * @param[in,out] target Stored Boolean value. * @param[in] name Serialized parameter name for diagnostics. */ template - void Repca::loadSwitchParameter( + void Repca::loadBooleanParameter( const ModelDataT& data, RepcaParameters parameter, bool& target, @@ -897,19 +896,9 @@ namespace GridKit { target = *bool_value; } - else if (const auto* index_value = std::get_if(&value); - index_value && (*index_value == 0 || *index_value == 1)) - { - target = (*index_value == 1); - } - else if (const auto* real_value = std::get_if(&value); - real_value && (*real_value == ZERO || *real_value == ONE) ) - { - target = (*real_value == ONE); - } else { - Log::error() << "Repca: parameter '" << name << "' must be bool or 0/1\n"; + Log::error() << "Repca: parameter '" << name << "' must be boolean\n"; ++parameter_error_count_; } } @@ -959,9 +948,9 @@ namespace GridKit parameter_error_count_ = 0; loadRealParameter(data, Params::mva, mva_base_, "mva"); - loadSwitchParameter(data, Params::VcompFlag, VcompFlag_, "VcompFlag"); - loadSwitchParameter(data, Params::RefFlag, RefFlag_, "RefFlag"); - loadSwitchParameter(data, Params::Freqflag, Freqflag_, "Freqflag"); + loadBooleanParameter(data, Params::VcompFlag, VcompFlag_, "VcompFlag"); + loadBooleanParameter(data, Params::RefFlag, RefFlag_, "RefFlag"); + loadBooleanParameter(data, Params::Freqflag, Freqflag_, "Freqflag"); loadRealParameter(data, Params::Tfltr, Tfltr_, "Tfltr"); loadRealParameter(data, Params::Vfrz, Vfrz_, "Vfrz"); loadRealParameter(data, Params::Rc, Rc_, "Rc"); diff --git a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp index 35438833d..a006cf105 100644 --- a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp @@ -136,7 +136,7 @@ namespace GridKit for (const Params flag : {Params::VcompFlag, Params::RefFlag, Params::Freqflag}) { - for (const auto value : {static_cast(0), static_cast(1)}) + for (const bool value : {false, true}) { auto data = makeData(); data.parameters[flag] = value; @@ -145,19 +145,21 @@ namespace GridKit success *= (model.repca.verify() == 0); } - for (const auto value : {static_cast(0.0), static_cast(1.0)}) + for (const IdxT value : {static_cast(0), + static_cast(1), + static_cast(2)}) { - auto data = makeData(); - data.parameters[flag] = value; - Fixture model(data); - model.attachAllInputs(); - success *= (model.repca.verify() == 0); + success *= invalidParameterCase(flag, value); } - success *= invalidParameterCase(flag, static_cast(2)); - success *= invalidParameterCase(flag, static_cast(0.5)); - success *= invalidParameterCase(flag, nan); - success *= invalidParameterCase(flag, infinity); + for (const RealT value : {static_cast(0.0), + static_cast(0.5), + static_cast(1.0), + nan, + infinity}) + { + success *= invalidParameterCase(flag, value); + } } PhasorDynamics::Converter::Repca busless(nullptr, makeData()); From cf03bf6527163fd6b67ccf0cf2ef772a1e16135c Mon Sep 17 00:00:00 2001 From: lukelowry Date: Mon, 3 Aug 2026 12:46:42 -0500 Subject: [PATCH 06/19] test style consistancy --- .../PhasorDynamics/ConverterRepcaTests.hpp | 183 ++++++++++++------ 1 file changed, 123 insertions(+), 60 deletions(-) diff --git a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp index a006cf105..516d9a108 100644 --- a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp @@ -1350,35 +1350,62 @@ namespace GridKit return false; } - success &= (implicit_defaults.repca.evaluateResidual() == 0); - success &= (explicit_defaults.repca.evaluateResidual() == 0); - success &= vectorsMatch(implicit_defaults.repca.y(), - explicit_defaults.repca.y(), - "documented-default state"); - success &= vectorsMatch(implicit_defaults.repca.yp(), - explicit_defaults.repca.yp(), - "documented-default derivative"); - success &= vectorsMatch(implicit_defaults.repca.getResidual(), - explicit_defaults.repca.getResidual(), - "documented-default residual"); + if (implicit_defaults.repca.evaluateResidual() != 0) + { + success = false; + } + if (explicit_defaults.repca.evaluateResidual() != 0) + { + success = false; + } + if (!vectorsMatch(implicit_defaults.repca.y(), + explicit_defaults.repca.y(), + "documented-default state")) + { + success = false; + } + if (!vectorsMatch(implicit_defaults.repca.yp(), + explicit_defaults.repca.yp(), + "documented-default derivative")) + { + success = false; + } + if (!vectorsMatch(implicit_defaults.repca.getResidual(), + explicit_defaults.repca.getResidual(), + "documented-default residual")) + { + success = false; + } for (size_t port = 0; port < index(Ext::MAXIMUM); ++port) { - success &= rowMatches(implicit_defaults.input(port), - explicit_defaults.input(port), - "documented-default signal", - port, - ""); + if (!rowMatches(implicit_defaults.input(port), + explicit_defaults.input(port), + "documented-default signal", + port, + "")) + { + success = false; + } } setAnswerKeyInputs(implicit_defaults); setAnswerKeyInputs(explicit_defaults); setAnswerKeyState(implicit_defaults.repca); setAnswerKeyState(explicit_defaults.repca); - success &= (implicit_defaults.repca.evaluateResidual() == 0); - success &= (explicit_defaults.repca.evaluateResidual() == 0); - success &= vectorsMatch(implicit_defaults.repca.getResidual(), - explicit_defaults.repca.getResidual(), - "documented-default dynamic residual"); + if (implicit_defaults.repca.evaluateResidual() != 0) + { + success = false; + } + if (explicit_defaults.repca.evaluateResidual() != 0) + { + success = false; + } + if (!vectorsMatch(implicit_defaults.repca.getResidual(), + explicit_defaults.repca.getResidual(), + "documented-default dynamic residual")) + { + success = false; + } return success; } @@ -1457,17 +1484,35 @@ namespace GridKit success = false; } - success &= valueUnchanged(fixture.qext(), qext, "qext signal", index(Vars::QEXT)); - success &= valueUnchanged(fixture.pext(), pext, "pext signal", index(Vars::PEXT)); - success &= vectorUnchanged(fixture.repca.y(), y_before, "state"); - success &= vectorUnchanged(fixture.repca.yp(), yp_before, "derivative"); - success &= vectorUnchanged(fixture.bus.y(), bus_before, "bus state"); + if (!valueUnchanged(fixture.qext(), qext, "qext signal", index(Vars::QEXT))) + { + success = false; + } + if (!valueUnchanged(fixture.pext(), pext, "pext signal", index(Vars::PEXT))) + { + success = false; + } + if (!vectorUnchanged(fixture.repca.y(), y_before, "state")) + { + success = false; + } + if (!vectorUnchanged(fixture.repca.yp(), yp_before, "derivative")) + { + success = false; + } + if (!vectorUnchanged(fixture.bus.y(), bus_before, "bus state")) + { + success = false; + } for (size_t port = 0; port < index(Ext::MAXIMUM); ++port) { - success &= valueUnchanged(fixture.input(port), - inputs_before[port], - "external signal", - port); + if (!valueUnchanged(fixture.input(port), + inputs_before[port], + "external signal", + port)) + { + success = false; + } } return success; } @@ -1613,11 +1658,14 @@ namespace GridKit for (size_t i = 0; i < expected.size(); ++i) { - success &= rowMatches(values[i + 1], - expected[i], - "monitor", - i, - context); + if (!rowMatches(values[i + 1], + expected[i], + "monitor", + i, + context)) + { + success = false; + } } return success; } @@ -1651,11 +1699,14 @@ namespace GridKit const auto& [variable, expected] = rows[i]; const size_t row = index(variable); - success &= variableMatches(static_cast(values[row]), - expected, - what, - variable, - context); + if (!variableMatches(static_cast(values[row]), + expected, + what, + variable, + context)) + { + success = false; + } } return success; } @@ -1681,17 +1732,23 @@ namespace GridKit const auto* yp = repca.yp().getData(); for (size_t row = 0; row < index(Vars::MAXIMUM); ++row) { - const auto variable = static_cast(row); - success &= variableMatches(f[row], - 0.0, - "residual", - variable, - "at rest"); - success &= variableMatches(yp[row], - 0.0, - "derivative", - variable, - "at rest"); + const auto variable = static_cast(row); + if (!variableMatches(f[row], + 0.0, + "residual", + variable, + "at rest")) + { + success = false; + } + if (!variableMatches(yp[row], + 0.0, + "derivative", + variable, + "at rest")) + { + success = false; + } } return success; } @@ -1717,10 +1774,13 @@ namespace GridKit const auto* values = vector.getData(); for (size_t row = 0; row < snapshot.size(); ++row) { - success &= valueUnchanged(static_cast(values[row]), - snapshot[row], - what, - row); + if (!valueUnchanged(static_cast(values[row]), + snapshot[row], + what, + row)) + { + success = false; + } } return success; } @@ -1740,11 +1800,14 @@ namespace GridKit const auto* right_values = right.getData(); for (size_t row = 0; row < static_cast(left.getSize()); ++row) { - success &= rowMatches(static_cast(left_values[row]), - static_cast(right_values[row]), - what, - row, - ""); + if (!rowMatches(static_cast(left_values[row]), + static_cast(right_values[row]), + what, + row, + "")) + { + success = false; + } } return success; } From b0953384193d966464bab5c7b2bd8f93a499e986 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Tue, 4 Aug 2026 22:18:04 -0500 Subject: [PATCH 07/19] Consistant tests and style as esdc1a --- .../PhasorDynamics/Converter/REPCA/README.md | 248 +++------- .../PhasorDynamics/Converter/REPCA/Repca.hpp | 16 +- .../Converter/REPCA/RepcaImpl.hpp | 416 ++++++---------- .../Model/PhasorDynamics/SystemModelImpl.hpp | 163 +++---- .../ComponentConnectionTests.hpp | 91 ++++ .../PhasorDynamics/ConverterRepcaTests.hpp | 444 ++++++++++++------ .../SystemSingleComponentTests.hpp | 21 +- .../runComponentConnectionTests.cpp | 1 + .../PhasorDynamics/runConverterRepcaTests.cpp | 3 + tests/UnitTests/Utilities/CaseFormatTests.hpp | 30 +- 10 files changed, 718 insertions(+), 715 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md index f26bddd33..5d2933adf 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md @@ -53,31 +53,20 @@ $P^{\max}$ | [p.u.] | `Pmax` | Maximum active-p $P^{\min}$ | [p.u.] | `Pmin` | Minimum active-power command | 0.0 | Component base $T_\mathrm{lag}$ | [sec] | `Tlag` | Active-power command lag time constant | 3.0 | -Every parameter is optional and uses the Typical Value when omitted. Real-valued -parameters accept real or integer JSON values; selectors require Boolean JSON values. - ### Parameter Validation All real parameters must be finite. Invalid parameter sets are rejected by: ```math \begin{aligned} - S^\mathrm{base} - &> 0 \\ - T_\mathrm{fv} - &> 0 \\ - D_\mathrm{bd1} - &\le 0 \le D_\mathrm{bd2} \\ - e^{\min} - &\le 0 \le e^{\max} \\ - Q^{\min} - &\le Q^{\max} \\ - D_\mathrm{bd1}^{f} - &\le 0 \le D_\mathrm{bd2}^{f} \\ - e_P^{\min} - &\le 0 \le e_P^{\max} \\ - P^{\min} - &\le P^{\max} + S^\mathrm{base} &> 0 \\ + T_\mathrm{fv} &> 0 \\ + D_\mathrm{bd1} &\le 0 \le D_\mathrm{bd2} \\ + e^{\min} &\le 0 \le e^{\max} \\ + Q^{\min} &\le Q^{\max} \\ + D_\mathrm{bd1}^{f} &\le 0 \le D_\mathrm{bd2}^{f} \\ + e_P^{\min} &\le 0 \le e_P^{\max} \\ + P^{\min} &\le P^{\max} \end{aligned} ``` @@ -91,11 +80,9 @@ floor with a warning: ```math \begin{aligned} - T_x - &\leftarrow \max\!\left(T_x,\epsilon_T\right), - \quad x\in\{\mathrm{fltr},\mathrm{p},\mathrm{lag}\} \\ - s_\mathrm{comp}^{\mathrm{off}} &= 1 - s_\mathrm{comp} \\ - s_\mathrm{ref}^{\mathrm{off}} &= 1 - s_\mathrm{ref} \\ + T_x &\leftarrow \max(T_x,\epsilon_T), \quad x\in\{\mathrm{fltr},\mathrm{p},\mathrm{lag}\} \\ + s_\mathrm{comp}^\mathrm{off} &= 1 - s_\mathrm{comp} \\ + s_\mathrm{ref}^\mathrm{off} &= 1 - s_\mathrm{ref} \\ k_\mathrm{base} &= \dfrac{S^\mathrm{sys}}{S^\mathrm{base}} \end{aligned} ``` @@ -152,13 +139,13 @@ $V^\mathrm{droop}$ | [p.u.] | Reactive-droop-compensated voltage | $V^\mathrm{ctrl}$ | [p.u.] | Selected voltage-measurement input | $s_\mathrm{frz}$ | [-] | Smooth reactive-power PI voltage-enable gate | $e_\mathrm{RQ}$ | [p.u.] | Selected reactive-loop error | -$e_\mathrm{RQ}^{\mathrm{db}}$ | [p.u.] | Deadbanded reactive-loop error | -$e_\mathrm{RQ}^{\mathrm{lim}}$ | [p.u.] | Limited reactive-loop error | +$e_\mathrm{RQ}^\mathrm{db}$ | [p.u.] | Deadbanded reactive-loop error | +$e_\mathrm{RQ}^\mathrm{lim}$ | [p.u.] | Limited reactive-loop error | $Q^\mathrm{PI}$ | [p.u.] | Reactive-power PI output | Component base $Q^\mathrm{ext}$ | [p.u.] | Reactive-power command output | System base $e_f$ | [p.u.] | Frequency error after deadband | $e_P$ | [p.u.] | Active-power control error | Component base -$e_P^{\mathrm{lim}}$ | [p.u.] | Limited active-power control error | Component base +$e_P^\mathrm{lim}$ | [p.u.] | Limited active-power control error | Component base $P^\mathrm{PI}$ | [p.u.] | Active-power PI output | Component base $P^\mathrm{ext}$ | [p.u.] | Active-power command output | System base @@ -178,7 +165,7 @@ $I_\mathrm{r}$ | [p.u.] | Known | Branch-current real component $I_\mathrm{i}$ | [p.u.] | Known | Branch-current imaginary component | Signal port `ibranchi`; system base $P^\mathrm{br}$ | [p.u.] | Known | Branch active power | Signal port `pbranch`; system base $Q^\mathrm{br}$ | [p.u.] | Known | Branch reactive power | Signal port `qbranch`; system base -$f$ | [p.u.] | Known | Frequency input | Signal port `freq` +$f$ | [p.u.] | Known | Frequency input | Signal port `freq` $f^\mathrm{ref}$ | [p.u.] | Unknown | Frequency reference | Optional signal port `freqref` $V^\mathrm{ref}$ | [p.u.] | Unknown | Voltage-control reference | Optional signal port `vref` $Q^\mathrm{ref}$ | [p.u.] | Unknown | Reactive-power reference | Optional signal port `qref`; system base @@ -192,43 +179,13 @@ $P_\mathrm{plant}^\mathrm{ref}$ | [p.u.] | Unknown | Plant active-power referenc ```math \begin{aligned} - 0 &= - -\dot{V}^{\mathrm{meas}} - + \dfrac{1}{T_\mathrm{fltr}} - \left(V^\mathrm{ctrl} - V^\mathrm{meas}\right) \\ - 0 &= - -\dot{Q}^{\mathrm{meas}} - + \dfrac{1}{T_\mathrm{fltr}} - \left(k_\mathrm{base}Q^\mathrm{br} - Q^\mathrm{meas}\right) \\ - 0 &= - -\dot{x}_Q^{\mathrm{PI}} - + s_\mathrm{frz}\, - \text{antiwindup}\!\left( - Q^\mathrm{PI}, - K_\mathrm{i}e_\mathrm{RQ}^{\mathrm{lim}};\, - Q^{\min}, - Q^{\max} - \right) \\ - 0 &= - -\dot{x}_Q^{\mathrm{lag}} - + \dfrac{1}{T_\mathrm{fv}} - \left(Q^\mathrm{PI} - x_Q^\mathrm{lag}\right) \\ - 0 &= - -\dot{P}^{\mathrm{meas}} - + \dfrac{1}{T_\mathrm{p}} - \left(k_\mathrm{base}P^\mathrm{br} - P^\mathrm{meas}\right) \\ - 0 &= - -\dot{x}_P^{\mathrm{PI}} - + \text{antiwindup}\!\left( - P^\mathrm{PI}, - K_\mathrm{ig}e_P^{\mathrm{lim}};\, - P^{\min}, - P^{\max} - \right) \\ - 0 &= - -\dot{P}^{\mathrm{ref}} - + \dfrac{1}{T_\mathrm{lag}} - \left(P^\mathrm{PI} - P^\mathrm{ref}\right). + 0 &= -\dot{V}^\mathrm{meas} + \dfrac{1}{T_\mathrm{fltr}} (V^\mathrm{ctrl} - V^\mathrm{meas}) \\ + 0 &= -\dot{Q}^\mathrm{meas} + \dfrac{1}{T_\mathrm{fltr}} (k_\mathrm{base}Q^\mathrm{br} - Q^\mathrm{meas}) \\ + 0 &= -\dot{x}_Q^\mathrm{PI} + s_\mathrm{frz}\, \text{antiwindup}(Q^\mathrm{PI}, K_\mathrm{i}e_\mathrm{RQ}^\mathrm{lim};\,Q^{\min}, Q^{\max}) \\ + 0 &= -\dot{x}_Q^\mathrm{lag} + \dfrac{1}{T_\mathrm{fv}} (Q^\mathrm{PI} - x_Q^\mathrm{lag}) \\ + 0 &= -\dot{P}^\mathrm{meas} + \dfrac{1}{T_\mathrm{p}} (k_\mathrm{base}P^\mathrm{br} - P^\mathrm{meas}) \\ + 0 &= -\dot{x}_P^\mathrm{PI} + \text{antiwindup}(P^\mathrm{PI}, K_\mathrm{ig}e_P^\mathrm{lim};\,P^{\min}, P^{\max}) \\ + 0 &= -\dot{P}^\mathrm{ref} + \dfrac{1}{T_\mathrm{lag}} (P^\mathrm{PI} - P^\mathrm{ref}). \end{aligned} ``` @@ -239,50 +196,20 @@ target and smooth approximation. ```math \begin{aligned} - 0 &= -V^2 - + V_\mathrm{r}^2 - + V_\mathrm{i}^2 \\ - 0 &= - -\left(V^\mathrm{ldc}\right)^2 - + \left(V_\mathrm{r} - R_c I_\mathrm{r} + X_c I_\mathrm{i}\right)^2 - + \left(V_\mathrm{i} - R_c I_\mathrm{i} - X_c I_\mathrm{r}\right)^2 \\ + 0 &= -V^2 + V_\mathrm{r}^2 + V_\mathrm{i}^2 \\ + 0 &= -(V^\mathrm{ldc})^2 + (V_\mathrm{r} - R_c I_\mathrm{r} + X_c I_\mathrm{i})^2 + (V_\mathrm{i} - R_c I_\mathrm{i} - X_c I_\mathrm{r})^2 \\ 0 &= -V^\mathrm{droop} + V + K_c k_\mathrm{base}Q^\mathrm{br} \\ - 0 &= -V^\mathrm{ctrl} - + s_\mathrm{comp}V^\mathrm{ldc} - + s_\mathrm{comp}^{\mathrm{off}}V^\mathrm{droop} \\ + 0 &= -V^\mathrm{ctrl} + s_\mathrm{comp}V^\mathrm{ldc} + s_\mathrm{comp}^\mathrm{off}V^\mathrm{droop} \\ 0 &= -s_\mathrm{frz} + \text{above}(V;\,V^\mathrm{frz}) \\ - 0 &= -e_\mathrm{RQ} - + s_\mathrm{ref}\left(V^\mathrm{ref} - V^\mathrm{meas}\right) - + s_\mathrm{ref}^{\mathrm{off}} - \left(k_\mathrm{base}Q^\mathrm{ref} - Q^\mathrm{meas}\right) \\ - 0 &= -e_\mathrm{RQ}^{\mathrm{db}} - + \text{deadband2} - \left(e_\mathrm{RQ};\,D_\mathrm{bd1},D_\mathrm{bd2}\right) \\ - 0 &= -e_\mathrm{RQ}^{\mathrm{lim}} - + \text{clamp} - \left(e_\mathrm{RQ}^{\mathrm{db}};\,e^{\min},e^{\max}\right) \\ - 0 &= -Q^\mathrm{PI} - + \text{clamp} - \left(K_\mathrm{p}e_\mathrm{RQ}^{\mathrm{lim}}+x_Q^\mathrm{PI};\, - Q^{\min},Q^{\max}\right) \\ - 0 &= -T_\mathrm{fv} - \left(k_\mathrm{base}Q^\mathrm{ext}-x_Q^\mathrm{lag}\right) - + T_\mathrm{ft} - \left(Q^\mathrm{PI}-x_Q^\mathrm{lag}\right) \\ - 0 &= -e_f - + \text{deadband2} - \left(f^\mathrm{ref}-f;\,D_\mathrm{bd1}^{f},D_\mathrm{bd2}^{f}\right) \\ - 0 &= -e_P - + k_\mathrm{base}P_\mathrm{plant}^\mathrm{ref} - - P^\mathrm{meas} - + D_\mathrm{dn}\text{ramp}(e_f) - - D_\mathrm{up}\text{ramp}(-e_f) \\ - 0 &= -e_P^{\mathrm{lim}} - + \text{clamp}\left(e_P;\,e_P^{\min},e_P^{\max}\right) \\ - 0 &= -P^\mathrm{PI} - + \text{clamp} - \left(K_\mathrm{pg}e_P^{\mathrm{lim}}+x_P^\mathrm{PI};\, - P^{\min},P^{\max}\right) \\ + 0 &= -e_\mathrm{RQ} + s_\mathrm{ref}(V^\mathrm{ref} - V^\mathrm{meas}) + s_\mathrm{ref}^\mathrm{off} (k_\mathrm{base}Q^\mathrm{ref} - Q^\mathrm{meas}) \\ + 0 &= -e_\mathrm{RQ}^\mathrm{db} + \text{deadband2}(e_\mathrm{RQ};\,D_\mathrm{bd1},D_\mathrm{bd2}) \\ + 0 &= -e_\mathrm{RQ}^\mathrm{lim} + \text{clamp}(e_\mathrm{RQ}^\mathrm{db};\,e^{\min},e^{\max}) \\ + 0 &= -Q^\mathrm{PI} + \text{clamp}(K_\mathrm{p}e_\mathrm{RQ}^\mathrm{lim}+x_Q^\mathrm{PI};\,Q^{\min},Q^{\max}) \\ + 0 &= -T_\mathrm{fv} (k_\mathrm{base}Q^\mathrm{ext}-x_Q^\mathrm{lag}) + T_\mathrm{ft} (Q^\mathrm{PI}-x_Q^\mathrm{lag}) \\ + 0 &= -e_f + \text{deadband2}(f^\mathrm{ref}-f;\,D_\mathrm{bd1}^{f},D_\mathrm{bd2}^{f}) \\ + 0 &= -e_P + k_\mathrm{base}P_\mathrm{plant}^\mathrm{ref} - P^\mathrm{meas} + D_\mathrm{dn}\text{ramp}(e_f) - D_\mathrm{up}\text{ramp}(-e_f) \\ + 0 &= -e_P^\mathrm{lim} + \text{clamp}(e_P;\,e_P^{\min},e_P^{\max}) \\ + 0 &= -P^\mathrm{PI} + \text{clamp}(K_\mathrm{pg}e_P^\mathrm{lim}+x_P^\mathrm{PI};\,P^{\min},P^{\max}) \\ 0 &= -k_\mathrm{base}P^\mathrm{ext} + s_\mathrm{freq}P^\mathrm{ref}. \end{aligned} ``` @@ -302,19 +229,12 @@ REPCA reconstructs a steady operating point; arbitrary-state restart is unsuppor ```math \begin{aligned} - V_\mathrm{r}, V_\mathrm{i} - &\leftarrow \text{regulated-bus voltage} \\ - I_\mathrm{r}, I_\mathrm{i} - &\leftarrow \text{branch current} \\ - P^\mathrm{br}, Q^\mathrm{br} - &\leftarrow \text{branch power} \\ - f - &\leftarrow \text{frequency input} \\ - Q^\mathrm{ext} - &\leftarrow \text{known reactive-power command on system base} \\ - P^\mathrm{ext} - &\leftarrow \text{known active-power command on system base}, - \quad s_\mathrm{freq}\ \text{enabled}. + V_\mathrm{r}, V_\mathrm{i} &\leftarrow \text{regulated-bus voltage} \\ + I_\mathrm{r}, I_\mathrm{i} &\leftarrow \text{branch current} \\ + P^\mathrm{br}, Q^\mathrm{br} &\leftarrow \text{branch power} \\ + f &\leftarrow \text{frequency input} \\ + Q^\mathrm{ext} &\leftarrow \text{known reactive-power command on system base} \\ + P^\mathrm{ext} &\leftarrow \text{known active-power command on system base}, \quad s_\mathrm{freq}\ \text{enabled}. \end{aligned} ``` @@ -326,76 +246,37 @@ $z\in[\ell,u]$ within that tolerance, including the limits. ```math \begin{aligned} - V - &\leftarrow \sqrt{ - V_\mathrm{r}^2 - + V_\mathrm{i}^2 - } \\ - V^\mathrm{ldc} - &\leftarrow \sqrt{ - \left(V_\mathrm{r}-R_c I_\mathrm{r}+X_c I_\mathrm{i}\right)^2 - + \left(V_\mathrm{i}-R_c I_\mathrm{i}-X_c I_\mathrm{r}\right)^2 - } \\ - V^\mathrm{droop} - &\leftarrow V + K_c k_\mathrm{base}Q^\mathrm{br} \\ - V^\mathrm{ctrl} - &\leftarrow s_\mathrm{comp}V^\mathrm{ldc} - + s_\mathrm{comp}^{\mathrm{off}}V^\mathrm{droop} \\ + V &\leftarrow \sqrt{V_\mathrm{r}^2 + V_\mathrm{i}^2} \\ + V^\mathrm{ldc} &\leftarrow \sqrt{(V_\mathrm{r}-R_c I_\mathrm{r}+X_c I_\mathrm{i})^2 + (V_\mathrm{i}-R_c I_\mathrm{i}-X_c I_\mathrm{r})^2} \\ + V^\mathrm{droop} &\leftarrow V + K_c k_\mathrm{base}Q^\mathrm{br} \\ + V^\mathrm{ctrl} &\leftarrow s_\mathrm{comp}V^\mathrm{ldc} + s_\mathrm{comp}^\mathrm{off}V^\mathrm{droop} \\ V^\mathrm{meas} &\leftarrow V^\mathrm{ctrl} \\ Q^\mathrm{meas} &\leftarrow k_\mathrm{base}Q^\mathrm{br} \\ P^\mathrm{meas} &\leftarrow k_\mathrm{base}P^\mathrm{br} \\ - s_\mathrm{frz} - &\leftarrow \text{above}\left(V;\,V^\mathrm{frz}\right) \\ + s_\mathrm{frz} &\leftarrow \text{above}(V;\,V^\mathrm{frz}) \\ e_\mathrm{RQ} &\leftarrow 0 \\ - e_\mathrm{RQ}^{\mathrm{db}} - &\leftarrow \text{deadband2} - \left(e_\mathrm{RQ};\,D_\mathrm{bd1},D_\mathrm{bd2}\right) \\ - e_\mathrm{RQ}^{\mathrm{lim}} - &\leftarrow \text{clamp} - \left(e_\mathrm{RQ}^{\mathrm{db}};\,e^{\min},e^{\max}\right) \\ + e_\mathrm{RQ}^\mathrm{db} &\leftarrow \text{deadband2}(e_\mathrm{RQ};\,D_\mathrm{bd1},D_\mathrm{bd2}) \\ + e_\mathrm{RQ}^\mathrm{lim} &\leftarrow \text{clamp}(e_\mathrm{RQ}^\mathrm{db};\,e^{\min},e^{\max}) \\ Q^\mathrm{PI} &\leftarrow k_\mathrm{base}Q^\mathrm{ext} \\ x_Q^\mathrm{lag} &\leftarrow Q^\mathrm{PI} \\ - u_Q^\mathrm{PI} - &\leftarrow \text{clamp}^{-1} - \left(Q^\mathrm{PI};\,Q^{\min},Q^{\max}\right) \\ - x_Q^\mathrm{PI} - &\leftarrow u_Q^\mathrm{PI} - -K_\mathrm{p}e_\mathrm{RQ}^{\mathrm{lim}} \\ - e_f - &\leftarrow \text{deadband2} - \left(0;\,D_\mathrm{bd1}^{f},D_\mathrm{bd2}^{f}\right) \\ - P^\mathrm{freq} - &\leftarrow D_\mathrm{dn}\text{ramp}(e_f) - - D_\mathrm{up}\text{ramp}(-e_f) \\ + u_Q^\mathrm{PI} &\leftarrow \text{clamp}^{-1}(Q^\mathrm{PI};\,Q^{\min},Q^{\max}) \\ + x_Q^\mathrm{PI} &\leftarrow u_Q^\mathrm{PI} - K_\mathrm{p}e_\mathrm{RQ}^\mathrm{lim} \\ + e_f &\leftarrow \text{deadband2}(0;\,D_\mathrm{bd1}^{f},D_\mathrm{bd2}^{f}) \\ + P^\mathrm{freq} &\leftarrow D_\mathrm{dn}\text{ramp}(e_f) - D_\mathrm{up}\text{ramp}(-e_f) \\ e_P &\leftarrow 0 \\ - e_P^{\mathrm{lim}} - &\leftarrow \text{clamp}\left(e_P;\,e_P^{\min},e_P^{\max}\right) \\ - P^\mathrm{ref} - &\leftarrow - \begin{cases} - k_\mathrm{base}P^\mathrm{ext} & s_\mathrm{freq}\ \text{enabled} \\ - \text{clamp} - \left(P^\mathrm{meas};\,P^{\min},P^{\max}\right) - & s_\mathrm{freq}\ \text{disabled} - \end{cases} \\ + e_P^\mathrm{lim} &\leftarrow \text{clamp}(e_P;\,e_P^{\min},e_P^{\max}) \\ + P^\mathrm{ref} &\leftarrow \begin{cases} + k_\mathrm{base}P^\mathrm{ext} & s_\mathrm{freq}\ \text{enabled} \\ + \text{clamp}(P^\mathrm{meas};\,P^{\min},P^{\max}) & s_\mathrm{freq}\ \text{disabled} + \end{cases} \\ P^\mathrm{PI} &\leftarrow P^\mathrm{ref} \\ - u_P^\mathrm{PI} - &\leftarrow - \begin{cases} - \text{clamp}^{-1} - \left(P^\mathrm{PI};\,P^{\min},P^{\max}\right) - & s_\mathrm{freq}\ \text{enabled} \\ - P^\mathrm{meas} & s_\mathrm{freq}\ \text{disabled} - \end{cases} \\ - x_P^\mathrm{PI} - &\leftarrow u_P^\mathrm{PI} - -K_\mathrm{pg}e_P^{\mathrm{lim}} \\ - P^\mathrm{ext} - &\leftarrow \dfrac{s_\mathrm{freq}}{k_\mathrm{base}}P^\mathrm{ref} \\ - \dot{V}^\mathrm{meas},\dot{Q}^\mathrm{meas}, - \dot{x}_Q^\mathrm{PI},\dot{x}_Q^\mathrm{lag}, - \dot{P}^\mathrm{meas},\dot{x}_P^\mathrm{PI},\dot{P}^\mathrm{ref} - &\leftarrow 0. + u_P^\mathrm{PI} &\leftarrow \begin{cases} + \text{clamp}^{-1}(P^\mathrm{PI};\,P^{\min},P^{\max}) & s_\mathrm{freq}\ \text{enabled} \\ + P^\mathrm{meas} & s_\mathrm{freq}\ \text{disabled} + \end{cases} \\ + x_P^\mathrm{PI} &\leftarrow u_P^\mathrm{PI} - K_\mathrm{pg}e_P^\mathrm{lim} \\ + P^\mathrm{ext} &\leftarrow \dfrac{s_\mathrm{freq}}{k_\mathrm{base}}P^\mathrm{ref} \\ + \dot{V}^\mathrm{meas},\dot{Q}^\mathrm{meas}, \dot{x}_Q^\mathrm{PI},\dot{x}_Q^\mathrm{lag}, \dot{P}^\mathrm{meas},\dot{x}_P^\mathrm{PI},\dot{P}^\mathrm{ref} &\leftarrow 0. \end{aligned} ``` @@ -417,8 +298,7 @@ Initialization is atomic; candidates are validated before state or signal writes f^\mathrm{ref} &\leftarrow f \\ V^\mathrm{ref} &\leftarrow V^\mathrm{meas} \\ Q^\mathrm{ref} &\leftarrow Q^\mathrm{br} \\ - P_\mathrm{plant}^\mathrm{ref} - &\leftarrow \dfrac{P^\mathrm{meas}-P^\mathrm{freq}}{k_\mathrm{base}}. + P_\mathrm{plant}^\mathrm{ref} &\leftarrow \dfrac{P^\mathrm{meas}-P^\mathrm{freq}}{k_\mathrm{base}}. \end{aligned} ``` diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp index 3262346cf..38d2f92a4 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp @@ -141,19 +141,9 @@ namespace GridKit return static_cast(variable); } - static void checkConfiguration(bool condition, const char* message, int& errors); - void loadRealParameter(const ModelDataT& data, - RepcaParameters parameter, - RealT& target, - const char* name); - void loadBooleanParameter(const ModelDataT& data, - RepcaParameters parameter, - bool& target, - const char* name); - bool floorTimeConstant(RealT& value, const char* name); - void initializeParameters(const ModelDataT& data); - void initializeMonitor(); - void setDerivedParameters(); + void initializeParameters(const ModelDataT& data); + void initializeMonitor(); + void setDerivedParameters(); bool solveLimiterInput(ScalarT requested_output, RealT lower_limit, diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp index ae218f4df..30f1a800f 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp @@ -7,7 +7,6 @@ #pragma once #include -#include #include #include @@ -147,10 +146,10 @@ namespace GridKit /** * @brief Validate the REPCA configuration * - * Checks parameter-loading errors, finiteness and static parameter - * relationships, system/component bases and both conversion ratios, - * the regulated bus, required measurement signals, and attached - * optional reference signals. Command-output assignment is optional. + * Checks parameter-loading errors, static parameter relationships, + * system/component bases and both conversion ratios, the regulated + * bus, required measurement signals, and attached optional reference + * signals. Command-output assignment is optional. * Operating-point feasibility is checked by initialize(). * * @return Number of configuration errors; zero when valid. @@ -160,7 +159,16 @@ namespace GridKit { int ret = static_cast(parameter_error_count_); - checkConfiguration(bus_ != nullptr, "regulated bus is required", ret); + auto check = [&](bool condition, const char* message) + { + if (!condition) + { + Log::error() << "Repca: " << message << '\n'; + ret += 1; + } + }; + + check(bus_ != nullptr, "regulated bus is required"); const bool valid_converter_base = std::isfinite(mva_base_) && mva_base_ > ZERO @@ -168,119 +176,31 @@ namespace GridKit && va_converter_base_ > ZERO; const bool valid_system_base = std::isfinite(va_system_base_) && va_system_base_ > ZERO; - checkConfiguration(valid_converter_base, - "mva must define a finite positive converter power base", - ret); - checkConfiguration(valid_system_base, - "system power base must be finite and positive", - ret); + check(valid_converter_base, + "mva must define a finite positive converter power base"); + check(valid_system_base, "system power base must be finite and positive"); if (valid_converter_base && valid_system_base) { const RealT system_to_converter = va_system_base_ / va_converter_base_; const RealT converter_to_system = va_converter_base_ / va_system_base_; - checkConfiguration( - std::isfinite(system_to_converter) - && system_to_converter > ZERO - && std::isfinite(converter_to_system) - && converter_to_system > ZERO, - "system/converter power-base conversion ratios must be finite and positive", - ret); - } - - checkConfiguration(std::isfinite(Tfltr_), "Tfltr must be finite", ret); - checkConfiguration(std::isfinite(Vfrz_), "Vfrz must be finite", ret); - checkConfiguration(std::isfinite(Rc_), "Rc must be finite", ret); - checkConfiguration(std::isfinite(Xc_), "Xc must be finite", ret); - checkConfiguration(std::isfinite(Kc_), "Kc must be finite", ret); - - const bool finite_reactive_deadband = std::isfinite(dbdlow_) - && std::isfinite(dbdupper_); - checkConfiguration(finite_reactive_deadband, - "dbdlow and dbdupper must be finite", - ret); - if (finite_reactive_deadband) - { - checkConfiguration(dbdlow_ <= ZERO && ZERO <= dbdupper_, - "dbdlow <= 0 <= dbdupper is required", - ret); - } - - const bool finite_reactive_error_limits = std::isfinite(emin_) - && std::isfinite(emax_); - checkConfiguration(finite_reactive_error_limits, - "emin and emax must be finite", - ret); - if (finite_reactive_error_limits) - { - checkConfiguration(emin_ <= ZERO && ZERO <= emax_, - "emin <= 0 <= emax is required", - ret); - } - - checkConfiguration(std::isfinite(Kp_), "Kp must be finite", ret); - checkConfiguration(std::isfinite(Ki_), "Ki must be finite", ret); - - const bool finite_reactive_limits = std::isfinite(Qmin_) - && std::isfinite(Qmax_); - checkConfiguration(finite_reactive_limits, - "Qmin and Qmax must be finite", - ret); - if (finite_reactive_limits) - { - checkConfiguration(Qmin_ <= Qmax_, - "Qmin must be less than or equal to Qmax", - ret); - } - - checkConfiguration(std::isfinite(Tft_), "Tft must be finite", ret); - checkConfiguration(std::isfinite(Tfv_) && Tfv_ > ZERO, - "Tfv must be finite and positive", - ret); - checkConfiguration(std::isfinite(Tp_), "Tp must be finite", ret); - - const bool finite_frequency_deadband = std::isfinite(fdbd1_) - && std::isfinite(fdbd2_); - checkConfiguration(finite_frequency_deadband, - "fdbd1 and fdbd2 must be finite", - ret); - if (finite_frequency_deadband) - { - checkConfiguration(fdbd1_ <= ZERO && ZERO <= fdbd2_, - "fdbd1 <= 0 <= fdbd2 is required", - ret); - } - - checkConfiguration(std::isfinite(Ddn_), "Ddn must be finite", ret); - checkConfiguration(std::isfinite(Dup_), "Dup must be finite", ret); - - const bool finite_active_error_limits = std::isfinite(femin_) - && std::isfinite(femax_); - checkConfiguration(finite_active_error_limits, - "femin and femax must be finite", - ret); - if (finite_active_error_limits) - { - checkConfiguration(femin_ <= ZERO && ZERO <= femax_, - "femin <= 0 <= femax is required", - ret); + check(std::isfinite(system_to_converter) + && system_to_converter > ZERO + && std::isfinite(converter_to_system) + && converter_to_system > ZERO, + "system/converter power-base conversion ratios must be finite and positive"); } - checkConfiguration(std::isfinite(Kpg_), "Kpg must be finite", ret); - checkConfiguration(std::isfinite(Kig_), "Kig must be finite", ret); - - const bool finite_active_limits = std::isfinite(Pmin_) - && std::isfinite(Pmax_); - checkConfiguration(finite_active_limits, - "Pmin and Pmax must be finite", - ret); - if (finite_active_limits) - { - checkConfiguration(Pmin_ <= Pmax_, - "Pmin must be less than or equal to Pmax", - ret); - } - - checkConfiguration(std::isfinite(Tlag_), "Tlag must be finite", ret); + check(dbdlow_ <= ZERO && ZERO <= dbdupper_, + "dbdlow <= 0 <= dbdupper is required"); + check(emin_ <= ZERO && ZERO <= emax_, + "emin <= 0 <= emax is required"); + check(Qmin_ <= Qmax_, "Qmin must be less than or equal to Qmax"); + check(Tfv_ > ZERO, "Tfv must be positive"); + check(fdbd1_ <= ZERO && ZERO <= fdbd2_, + "fdbd1 <= 0 <= fdbd2 is required"); + check(femin_ <= ZERO && ZERO <= femax_, + "femin <= 0 <= femax is required"); + check(Pmin_ <= Pmax_, "Pmin must be less than or equal to Pmax"); auto check_required_signal = [&](const char* name) { @@ -812,172 +732,106 @@ namespace GridKit // /** - * @brief Record one failed configuration condition - * - * @param[in] condition Required condition. - * @param[in] message Error message when `condition` is false. - * @param[in,out] errors Accumulated configuration-error count. - */ - template - void Repca::checkConfiguration( - bool condition, const char* message, int& errors) - { - if (!condition) - { - Log::error() << "Repca: " << message << '\n'; - errors += 1; - } - } - - /** - * @brief Load one optional real-valued parameter + * @brief Read optional parameters from model data * - * Real and integer serialized values are accepted. Any other stored - * type records a loading error while preserving the existing default. + * Omitted parameters retain their documented defaults. Real parameters + * accept real and integer values and must be finite; selectors require + * Boolean values. Loading errors are counted for verify() rather than + * thrown. * - * @param[in] data Model parameter data. - * @param[in] parameter Parameter key to load. - * @param[in,out] target Stored parameter value. - * @param[in] name Serialized parameter name for diagnostics. + * @param[in] data Parameters and monitored-variable selections. */ template - void Repca::loadRealParameter( - const ModelDataT& data, - RepcaParameters parameter, - RealT& target, - const char* name) + void Repca::initializeParameters(const ModelDataT& data) { - if (!data.parameters.contains(parameter)) - { - return; - } + using Params = typename ModelDataT::Parameters; - const auto& value = data.parameters.at(parameter); - if (const auto* real_value = std::get_if(&value)) - { - target = *real_value; - } - else if (const auto* index_value = std::get_if(&value)) - { - target = static_cast(*index_value); - } - else - { - Log::error() << "Repca: parameter '" << name << "' must be numeric\n"; - ++parameter_error_count_; - } - } + parameter_error_count_ = 0; - /** - * @brief Load one optional Boolean parameter - * - * Any non-Boolean stored type records a loading error while preserving - * the existing default. - * - * @param[in] data Model parameter data. - * @param[in] parameter Parameter key to load. - * @param[in,out] target Stored Boolean value. - * @param[in] name Serialized parameter name for diagnostics. - */ - template - void Repca::loadBooleanParameter( - const ModelDataT& data, - RepcaParameters parameter, - bool& target, - const char* name) - { - if (!data.parameters.contains(parameter)) + auto load_real = [&](auto key, RealT& target, const char* name) { - return; - } + if (!data.parameters.contains(key)) + { + return; + } - const auto& value = data.parameters.at(parameter); - if (const auto* bool_value = std::get_if(&value)) - { - target = *bool_value; - } - else - { - Log::error() << "Repca: parameter '" << name << "' must be boolean\n"; - ++parameter_error_count_; - } - } + const auto& value = data.parameters.at(key); + RealT parsed_value{}; + if (const auto* real_value = std::get_if(&value)) + { + parsed_value = *real_value; + } + else if (const auto* index_value = std::get_if(&value)) + { + parsed_value = static_cast(*index_value); + } + else + { + Log::error() << "Repca: parameter '" << name << "' must be numeric\n"; + ++parameter_error_count_; + return; + } - /** - * @brief Validate and floor one controller lag - * - * A nonfinite value records a loading error and is replaced by the - * floor so later calculations remain well posed. A finite value below - * the floor is raised and reported through the return value. - * - * @param[in,out] value Time constant to validate and floor. - * @param[in] name Parameter name for diagnostics. - * @return true when a finite value was raised to the floor. - */ - template - bool Repca::floorTimeConstant( - RealT& value, const char* name) - { - if (!std::isfinite(value)) - { - Log::error() << "Repca: " << name << " must be finite\n"; - ++parameter_error_count_; - value = TIME_CONSTANT_MINIMUM; - return false; - } + if (!std::isfinite(parsed_value)) + { + Log::error() << "Repca: parameter '" << name << "' must be finite\n"; + ++parameter_error_count_; + return; + } - const bool raised = value < TIME_CONSTANT_MINIMUM; - value = std::max(value, TIME_CONSTANT_MINIMUM); - return raised; - } + target = parsed_value; + }; - /** - * @brief Read optional parameters from model data - * - * Omitted parameters retain their documented defaults. Numeric parameters - * accept real and integer values; selectors also accept Boolean values. - * Loading errors are counted for verify() rather than thrown. - * - * @param[in] data Parameters and monitored-variable selections. - */ - template - void Repca::initializeParameters(const ModelDataT& data) - { - using Params = typename ModelDataT::Parameters; + auto load_switch = [&](auto key, bool& target, const char* name) + { + if (!data.parameters.contains(key)) + { + return; + } - parameter_error_count_ = 0; + const auto& value = data.parameters.at(key); + if (const auto* bool_value = std::get_if(&value)) + { + target = *bool_value; + } + else + { + Log::error() << "Repca: parameter '" << name << "' must be boolean\n"; + ++parameter_error_count_; + } + }; - loadRealParameter(data, Params::mva, mva_base_, "mva"); - loadBooleanParameter(data, Params::VcompFlag, VcompFlag_, "VcompFlag"); - loadBooleanParameter(data, Params::RefFlag, RefFlag_, "RefFlag"); - loadBooleanParameter(data, Params::Freqflag, Freqflag_, "Freqflag"); - loadRealParameter(data, Params::Tfltr, Tfltr_, "Tfltr"); - loadRealParameter(data, Params::Vfrz, Vfrz_, "Vfrz"); - loadRealParameter(data, Params::Rc, Rc_, "Rc"); - loadRealParameter(data, Params::Xc, Xc_, "Xc"); - loadRealParameter(data, Params::Kc, Kc_, "Kc"); - loadRealParameter(data, Params::dbdlow, dbdlow_, "dbdlow"); - loadRealParameter(data, Params::dbdupper, dbdupper_, "dbdupper"); - loadRealParameter(data, Params::emax, emax_, "emax"); - loadRealParameter(data, Params::emin, emin_, "emin"); - loadRealParameter(data, Params::Kp, Kp_, "Kp"); - loadRealParameter(data, Params::Ki, Ki_, "Ki"); - loadRealParameter(data, Params::Qmax, Qmax_, "Qmax"); - loadRealParameter(data, Params::Qmin, Qmin_, "Qmin"); - loadRealParameter(data, Params::Tft, Tft_, "Tft"); - loadRealParameter(data, Params::Tfv, Tfv_, "Tfv"); - loadRealParameter(data, Params::Tp, Tp_, "Tp"); - loadRealParameter(data, Params::fdbd1, fdbd1_, "fdbd1"); - loadRealParameter(data, Params::fdbd2, fdbd2_, "fdbd2"); - loadRealParameter(data, Params::Ddn, Ddn_, "Ddn"); - loadRealParameter(data, Params::Dup, Dup_, "Dup"); - loadRealParameter(data, Params::femax, femax_, "femax"); - loadRealParameter(data, Params::femin, femin_, "femin"); - loadRealParameter(data, Params::Kpg, Kpg_, "Kpg"); - loadRealParameter(data, Params::Kig, Kig_, "Kig"); - loadRealParameter(data, Params::Pmax, Pmax_, "Pmax"); - loadRealParameter(data, Params::Pmin, Pmin_, "Pmin"); - loadRealParameter(data, Params::Tlag, Tlag_, "Tlag"); + load_real(Params::mva, mva_base_, "mva"); + load_switch(Params::VcompFlag, VcompFlag_, "VcompFlag"); + load_switch(Params::RefFlag, RefFlag_, "RefFlag"); + load_switch(Params::Freqflag, Freqflag_, "Freqflag"); + load_real(Params::Tfltr, Tfltr_, "Tfltr"); + load_real(Params::Vfrz, Vfrz_, "Vfrz"); + load_real(Params::Rc, Rc_, "Rc"); + load_real(Params::Xc, Xc_, "Xc"); + load_real(Params::Kc, Kc_, "Kc"); + load_real(Params::dbdlow, dbdlow_, "dbdlow"); + load_real(Params::dbdupper, dbdupper_, "dbdupper"); + load_real(Params::emax, emax_, "emax"); + load_real(Params::emin, emin_, "emin"); + load_real(Params::Kp, Kp_, "Kp"); + load_real(Params::Ki, Ki_, "Ki"); + load_real(Params::Qmax, Qmax_, "Qmax"); + load_real(Params::Qmin, Qmin_, "Qmin"); + load_real(Params::Tft, Tft_, "Tft"); + load_real(Params::Tfv, Tfv_, "Tfv"); + load_real(Params::Tp, Tp_, "Tp"); + load_real(Params::fdbd1, fdbd1_, "fdbd1"); + load_real(Params::fdbd2, fdbd2_, "fdbd2"); + load_real(Params::Ddn, Ddn_, "Ddn"); + load_real(Params::Dup, Dup_, "Dup"); + load_real(Params::femax, femax_, "femax"); + load_real(Params::femin, femin_, "femin"); + load_real(Params::Kpg, Kpg_, "Kpg"); + load_real(Params::Kig, Kig_, "Kig"); + load_real(Params::Pmax, Pmax_, "Pmax"); + load_real(Params::Pmin, Pmin_, "Pmin"); + load_real(Params::Tlag, Tlag_, "Tlag"); setDerivedParameters(); } @@ -1007,25 +861,39 @@ namespace GridKit * @brief Resolve parameter-derived constants * * Raises the explicit controller lags in place, computes the converter - * power base, and resolves selector masks. Nonfinite lag inputs are - * recorded before replacement so verify() retains the error. + * power base, and resolves selector masks. */ template void Repca::setDerivedParameters() { - bool floor_warning = false; + // The lags are raised to the floor below, so negative values must be + // rejected here while the value as read is still available. + auto check_non_negative = [&](RealT value, const char* name) + { + if (value < ZERO) + { + Log::error() << "Repca: " << name << " must be non-negative\n"; + ++parameter_error_count_; + } + }; - floor_warning |= floorTimeConstant(Tfltr_, "Tfltr"); - floor_warning |= floorTimeConstant(Tp_, "Tp"); - floor_warning |= floorTimeConstant(Tlag_, "Tlag"); + check_non_negative(Tfltr_, "Tfltr"); + check_non_negative(Tft_, "Tft"); + check_non_negative(Tp_, "Tp"); + check_non_negative(Tlag_, "Tlag"); - if (floor_warning) + if (Tfltr_ < TIME_CONSTANT_MINIMUM || Tp_ < TIME_CONSTANT_MINIMUM + || Tlag_ < TIME_CONSTANT_MINIMUM) { - Log::warning() << "Repca: any of Tfltr, Tp, or Tlag below " + Log::warning() << "Repca: Tfltr, Tp, and Tlag below " << TIME_CONSTANT_MINIMUM - << " s is raised to that floor to keep the controller lags well posed\n"; + << " s are raised to that floor to keep the controller lags well posed\n"; } + Tfltr_ = std::max(Tfltr_, TIME_CONSTANT_MINIMUM); + Tp_ = std::max(Tp_, TIME_CONSTANT_MINIMUM); + Tlag_ = std::max(Tlag_, TIME_CONSTANT_MINIMUM); + va_converter_base_ = mva_base_ * static_cast(1.0e6); vcomp_on_ = VcompFlag_ ? ONE : ZERO; diff --git a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp index 1051b8669..9d9bde040 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp @@ -162,87 +162,6 @@ namespace GridKit addComponent(regca); } - // Add REPCA plant controllers - for (const auto& repcadata : data.repca) - { - BusT* bus = nullptr; - if (repcadata.buses.contains(RepcaBuses::bus)) - { - bus = getBus(repcadata.buses.at(RepcaBuses::bus)); - } - - auto* repca = new Repca(bus, repcadata); - - if (repcadata.signal_inputs.contains(RepcaSignalInputs::ibranchr)) - { - const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::ibranchr); - constexpr auto IBRANCHR = RepcaExternalVariables::IBRANCHR; - repca->getSignals().template attachSignalNode(getSignal(signal)); - } - if (repcadata.signal_inputs.contains(RepcaSignalInputs::ibranchi)) - { - const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::ibranchi); - constexpr auto IBRANCHI = RepcaExternalVariables::IBRANCHI; - repca->getSignals().template attachSignalNode(getSignal(signal)); - } - if (repcadata.signal_inputs.contains(RepcaSignalInputs::pbranch)) - { - const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::pbranch); - constexpr auto PBRANCH = RepcaExternalVariables::PBRANCH; - repca->getSignals().template attachSignalNode(getSignal(signal)); - } - if (repcadata.signal_inputs.contains(RepcaSignalInputs::qbranch)) - { - const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::qbranch); - constexpr auto QBRANCH = RepcaExternalVariables::QBRANCH; - repca->getSignals().template attachSignalNode(getSignal(signal)); - } - if (repcadata.signal_inputs.contains(RepcaSignalInputs::freq)) - { - const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::freq); - constexpr auto FREQ = RepcaExternalVariables::FREQ; - repca->getSignals().template attachSignalNode(getSignal(signal)); - } - if (repcadata.signal_inputs.contains(RepcaSignalInputs::freqref)) - { - const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::freqref); - constexpr auto FREQREF = RepcaExternalVariables::FREQREF; - repca->getSignals().template attachSignalNode(getSignal(signal)); - } - if (repcadata.signal_inputs.contains(RepcaSignalInputs::vref)) - { - const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::vref); - constexpr auto VREF = RepcaExternalVariables::VREF; - repca->getSignals().template attachSignalNode(getSignal(signal)); - } - if (repcadata.signal_inputs.contains(RepcaSignalInputs::qref)) - { - const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::qref); - constexpr auto QREF = RepcaExternalVariables::QREF; - repca->getSignals().template attachSignalNode(getSignal(signal)); - } - if (repcadata.signal_inputs.contains(RepcaSignalInputs::pplantref)) - { - const IdxT signal = repcadata.signal_inputs.at(RepcaSignalInputs::pplantref); - constexpr auto PPLANTREF = RepcaExternalVariables::PPLANTREF; - repca->getSignals().template attachSignalNode(getSignal(signal)); - } - if (repcadata.signal_outputs.contains(RepcaSignalOutputs::qext)) - { - const IdxT signal = repcadata.signal_outputs.at(RepcaSignalOutputs::qext); - constexpr auto QEXT = RepcaInternalVariables::QEXT; - repca->getSignals().template assignSignalNode(getSignal(signal)); - } - if (repcadata.signal_outputs.contains(RepcaSignalOutputs::pext)) - { - const IdxT signal = repcadata.signal_outputs.at(RepcaSignalOutputs::pext); - constexpr auto PEXT = RepcaInternalVariables::PEXT; - repca->getSignals().template assignSignalNode(getSignal(signal)); - } - - addComponent(repca); - } - // Add branches for (const auto& branchdata : data.branch) { @@ -563,6 +482,88 @@ namespace GridKit addComponent(stabilizer); } + // Add REPCA plant controllers after the signal producers they read at + // initialization + for (const auto& repcadata : data.repca) + { + BusT* bus = nullptr; + if (repcadata.buses.contains(RepcaBuses::bus)) + { + bus = getBus(repcadata.buses.at(RepcaBuses::bus)); + } + + auto* repca = new Repca(bus, repcadata); + + if (repcadata.signal_inputs.contains(RepcaSignalInputs::ibranchr)) + { + const IdxT ibranchr = repcadata.signal_inputs.at(RepcaSignalInputs::ibranchr); + constexpr auto IBRANCHR = RepcaExternalVariables::IBRANCHR; + repca->getSignals().template attachSignalNode(getSignal(ibranchr)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::ibranchi)) + { + const IdxT ibranchi = repcadata.signal_inputs.at(RepcaSignalInputs::ibranchi); + constexpr auto IBRANCHI = RepcaExternalVariables::IBRANCHI; + repca->getSignals().template attachSignalNode(getSignal(ibranchi)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::pbranch)) + { + const IdxT pbranch = repcadata.signal_inputs.at(RepcaSignalInputs::pbranch); + constexpr auto PBRANCH = RepcaExternalVariables::PBRANCH; + repca->getSignals().template attachSignalNode(getSignal(pbranch)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::qbranch)) + { + const IdxT qbranch = repcadata.signal_inputs.at(RepcaSignalInputs::qbranch); + constexpr auto QBRANCH = RepcaExternalVariables::QBRANCH; + repca->getSignals().template attachSignalNode(getSignal(qbranch)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::freq)) + { + const IdxT freq = repcadata.signal_inputs.at(RepcaSignalInputs::freq); + constexpr auto FREQ = RepcaExternalVariables::FREQ; + repca->getSignals().template attachSignalNode(getSignal(freq)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::freqref)) + { + const IdxT freqref = repcadata.signal_inputs.at(RepcaSignalInputs::freqref); + constexpr auto FREQREF = RepcaExternalVariables::FREQREF; + repca->getSignals().template attachSignalNode(getSignal(freqref)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::vref)) + { + const IdxT vref = repcadata.signal_inputs.at(RepcaSignalInputs::vref); + constexpr auto VREF = RepcaExternalVariables::VREF; + repca->getSignals().template attachSignalNode(getSignal(vref)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::qref)) + { + const IdxT qref = repcadata.signal_inputs.at(RepcaSignalInputs::qref); + constexpr auto QREF = RepcaExternalVariables::QREF; + repca->getSignals().template attachSignalNode(getSignal(qref)); + } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::pplantref)) + { + const IdxT pplantref = repcadata.signal_inputs.at(RepcaSignalInputs::pplantref); + constexpr auto PPLANTREF = RepcaExternalVariables::PPLANTREF; + repca->getSignals().template attachSignalNode(getSignal(pplantref)); + } + if (repcadata.signal_outputs.contains(RepcaSignalOutputs::qext)) + { + const IdxT qext = repcadata.signal_outputs.at(RepcaSignalOutputs::qext); + constexpr auto QEXT = RepcaInternalVariables::QEXT; + repca->getSignals().template assignSignalNode(getSignal(qext)); + } + if (repcadata.signal_outputs.contains(RepcaSignalOutputs::pext)) + { + const IdxT pext = repcadata.signal_outputs.at(RepcaSignalOutputs::pext); + constexpr auto PEXT = RepcaInternalVariables::PEXT; + repca->getSignals().template assignSignalNode(getSignal(pext)); + } + + addComponent(repca); + } + // Add constant signal sources for (const auto& srcdata : data.constant_source) { diff --git a/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp b/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp index 3b50a9f0b..de1075c44 100644 --- a/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp @@ -131,6 +131,97 @@ namespace GridKit return success.report(__func__); } + + /// REGCA initializes first and publishes its branch current and power + /// to the four shared nodes. REPCA then initializes around those + /// measurements and must hold a steady state. REGCA has no frequency + /// output, so that required input keeps a locally owned node. + TestOutcome regcaRepca() + { + using ConverterInternal = PhasorDynamics::Converter::RegcaInternalVariables; + using ConverterParams = PhasorDynamics::Converter::RegcaParameters; + using PlantExternal = PhasorDynamics::Converter::RepcaExternalVariables; + using PlantParams = PhasorDynamics::Converter::RepcaParameters; + + TestStatus success = true; + + PhasorDynamics::SystemModel system; + PhasorDynamics::BusInfinite bus( + static_cast(1.0), + static_cast(0.0)); + PhasorDynamics::SignalNode ibranchr; + PhasorDynamics::SignalNode ibranchi; + PhasorDynamics::SignalNode pbranch; + PhasorDynamics::SignalNode qbranch; + + ScalarT freq_value = static_cast(1.0); + IdxT freq_index = INVALID_INDEX; + + PhasorDynamics::SignalNode freq; + freq.set(&freq_value, &freq_index); + + PhasorDynamics::Converter::RegcaData converter_data; + converter_data.parameters[ConverterParams::p0] = static_cast(0.0); + converter_data.parameters[ConverterParams::q0] = static_cast(0.0); + converter_data.parameters[ConverterParams::mva] = static_cast(100.0); + converter_data.parameters[ConverterParams::Tg] = static_cast(0.02); + converter_data.parameters[ConverterParams::TM] = static_cast(0.02); + converter_data.parameters[ConverterParams::Rqmax] = static_cast(999.0); + converter_data.parameters[ConverterParams::Rqmin] = static_cast(-999.0); + converter_data.parameters[ConverterParams::Rpmax] = static_cast(999.0); + converter_data.parameters[ConverterParams::sL] = true; + converter_data.parameters[ConverterParams::IL1] = static_cast(1.1); + converter_data.parameters[ConverterParams::VL0] = static_cast(0.4); + converter_data.parameters[ConverterParams::VL1] = static_cast(0.9); + converter_data.parameters[ConverterParams::VA0] = static_cast(0.4); + converter_data.parameters[ConverterParams::VA1] = static_cast(0.9); + converter_data.parameters[ConverterParams::Vhvmax] = static_cast(1.2); + + PhasorDynamics::Converter::RepcaData plant_data; + plant_data.parameters[PlantParams::Tp] = static_cast(0.05); + + PhasorDynamics::Converter::Regca converter(&bus, converter_data); + PhasorDynamics::Converter::Repca plant(&bus, plant_data); + + auto& converter_signals = converter.getSignals(); + converter_signals.template assignSignalNode(&ibranchr); + converter_signals.template assignSignalNode(&ibranchi); + converter_signals.template assignSignalNode(&pbranch); + converter_signals.template assignSignalNode(&qbranch); + + auto& plant_signals = plant.getSignals(); + plant_signals.template attachSignalNode(&ibranchr); + plant_signals.template attachSignalNode(&ibranchi); + plant_signals.template attachSignalNode(&pbranch); + plant_signals.template attachSignalNode(&qbranch); + plant_signals.template attachSignalNode(&freq); + + system.addBus(&bus); + system.addComponent(&converter); + system.addComponent(&plant); + + success *= system.allocate() == 0; + success *= ibranchr.linked(); + success *= ibranchi.linked(); + success *= pbranch.linked(); + success *= qbranch.linked(); + success *= system.initialize() == 0; + success *= system.evaluateResidual() == 0; + + // At zero power the converter draws no branch current or power. + success *= isEqual(ibranchr.read(), static_cast(0.0), kTol); + success *= isEqual(ibranchi.read(), static_cast(0.0), kTol); + success *= isEqual(pbranch.read(), static_cast(0.0), kTol); + success *= isEqual(qbranch.read(), static_cast(0.0), kTol); + + const auto* residual = plant.getResidual().getData(); + for (IdxT row = 0; row < plant.size(); ++row) + { + success *= isEqual(residual[row], static_cast(0.0), kTol); + } + + return success.report(__func__); + } }; } // namespace Testing diff --git a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp index 516d9a108..4c52253fe 100644 --- a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp @@ -39,13 +39,15 @@ namespace GridKit ConverterRepcaTests() = default; ~ConverterRepcaTests() = default; - // Exact-limit initialization leaves residual tails below 2e-13; this - // tolerance still resolves the 6.8e-12 boundary response. - static constexpr RealT kBehaviorTol = 1.0e-12; + // A command initialized exactly at a limit leaves the widest residual + // tail: QPI at 708 eps. + static constexpr RealT kBehaviorTol = + static_cast(1000.0) * std::numeric_limits::epsilon(); - // Enzyme traverses the smooth expressions through a separate AD path; - // its double-precision derivatives agree with the fixed key to O(1e-10). - static constexpr RealT kJacobianTol = 1.0e-9; + // The widest Enzyme-versus-dependency-tracking gap is QMEAS at the + // qbranch column, 0.7 eps. + static constexpr RealT kJacobianTol = + static_cast(10.0) * std::numeric_limits::epsilon(); /// Validate construction, defaults, parameters, signals, and time floors. TestOutcome validation() @@ -60,6 +62,7 @@ namespace GridKit PhasorDynamics::Converter::Repca empty(&bus); success *= (empty.size() == static_cast(index(Vars::MAXIMUM))); success *= (empty.getMonitor() == nullptr); + success *= (empty.verify() > 0); Fixture configured(makeData()); configured.attachAllInputs(); @@ -96,20 +99,40 @@ namespace GridKit success *= invalidParameterCase(Params::Pmin, 2.1); success *= invalidParameterCase(Params::mva, true); + success *= invalidParameterCase(Params::Tfltr, -0.2); + success *= invalidParameterCase(Params::Tft, -0.1); + success *= invalidParameterCase(Params::Tp, -0.3); + success *= invalidParameterCase(Params::Tlag, -0.4); + const RealT nan = std::numeric_limits::quiet_NaN(); const RealT infinity = std::numeric_limits::infinity(); for (const Params parameter : {Params::mva, Params::Tfltr, Params::Vfrz, Params::Rc, + Params::Xc, Params::Kc, Params::dbdlow, + Params::dbdupper, Params::emax, + Params::emin, Params::Kp, + Params::Ki, Params::Qmax, + Params::Qmin, Params::Tft, + Params::Tfv, + Params::Tp, + Params::fdbd1, + Params::fdbd2, Params::Ddn, + Params::Dup, + Params::femax, + Params::femin, + Params::Kpg, + Params::Kig, Params::Pmax, + Params::Pmin, Params::Tlag}) { success *= invalidParameterCase(parameter, nan); @@ -176,9 +199,9 @@ namespace GridKit success *= unlinkedSignalRejected(); auto floor_data = makeInitializationData(); - floor_data.parameters[Params::Tfltr] = -0.2; + floor_data.parameters[Params::Tfltr] = 0.0; floor_data.parameters[Params::Tp] = 0.0; - floor_data.parameters[Params::Tlag] = -0.4; + floor_data.parameters[Params::Tlag] = 0.0; Fixture floored(floor_data); floored.attachAllInputs(); @@ -241,17 +264,17 @@ namespace GridKit {Vars::PEXT, 0.45}}, "initialization"); - success *= scalarMatches(fixture.input(index(Ext::IBRANCHR)), 0.2, "preserved ibranchr"); - success *= scalarMatches(fixture.input(index(Ext::IBRANCHI)), -0.1, "preserved ibranchi"); - success *= scalarMatches(fixture.input(index(Ext::PBRANCH)), 0.4, "preserved pbranch"); - success *= scalarMatches(fixture.input(index(Ext::QBRANCH)), 0.1, "preserved qbranch"); - success *= scalarMatches(fixture.input(index(Ext::FREQ)), 0.99, "preserved freq"); - success *= scalarMatches(fixture.qext(), 0.25, "preserved qext"); - success *= scalarMatches(fixture.pext(), 0.45, "preserved pext"); - success *= scalarMatches(fixture.input(index(Ext::FREQREF)), 0.99, "published freqref"); - success *= scalarMatches(fixture.input(index(Ext::VREF)), 0.99200050403213, "published vref"); - success *= scalarMatches(fixture.input(index(Ext::QREF)), 0.1, "published qref"); - success *= scalarMatches(fixture.input(index(Ext::PPLANTREF)), + success *= scalarPreserved(fixture.input(Ext::IBRANCHR), 0.2, "preserved ibranchr"); + success *= scalarPreserved(fixture.input(Ext::IBRANCHI), -0.1, "preserved ibranchi"); + success *= scalarPreserved(fixture.input(Ext::PBRANCH), 0.4, "preserved pbranch"); + success *= scalarPreserved(fixture.input(Ext::QBRANCH), 0.1, "preserved qbranch"); + success *= scalarPreserved(fixture.input(Ext::FREQ), 0.99, "preserved freq"); + success *= scalarPreserved(fixture.qext(), 0.25, "preserved qext"); + success *= scalarPreserved(fixture.pext(), 0.45, "preserved pext"); + success *= scalarMatches(fixture.input(Ext::FREQREF), 0.99, "published freqref"); + success *= scalarMatches(fixture.input(Ext::VREF), 0.99200050403213, "published vref"); + success *= scalarMatches(fixture.input(Ext::QREF), 0.1, "published qref"); + success *= scalarMatches(fixture.input(Ext::PPLANTREF), 0.39874213184871782, "published pplantref"); @@ -293,12 +316,9 @@ namespace GridKit exact_commands.attachAllInputs(); setInitializationInputs(exact_commands); success *= exact_commands.initialize(0.25, 0.45); - success *= valueUnchanged(exact_commands.qext(), 0.25, "qext signal", index(Vars::QEXT)); - success *= valueUnchanged(exact_commands.pext(), 0.45, "pext signal", index(Vars::PEXT)); - success *= valueUnchanged(exact_commands.input(index(Ext::QREF)), - 0.1, - "qref signal", - index(Ext::QREF)); + success *= scalarPreserved(exact_commands.qext(), 0.25, "qext signal"); + success *= scalarPreserved(exact_commands.pext(), 0.45, "pext signal"); + success *= scalarPreserved(exact_commands.input(Ext::QREF), 0.1, "qref signal"); success *= (exact_commands.repca.evaluateResidual() == 0); success *= allResidualsZero(exact_commands.repca); } @@ -394,14 +414,17 @@ namespace GridKit RealT pext; }; + const RealT nan = std::numeric_limits::quiet_NaN(); const RealT infinity = std::numeric_limits::infinity(); - const std::array rejection_cases{{ + const std::array rejection_cases{{ {"qext below Qmin", -0.4000000001, 0.45}, {"qext above Qmax", 0.4500000001, 0.45}, {"pext below Pmin", 0.25, -1.0e-10}, {"pext above Pmax", 0.25, 0.6000000001}, {"nonfinite qext", infinity, 0.45}, {"nonfinite pext", 0.25, infinity}, + {"nan qext", nan, 0.45}, + {"nan pext", 0.25, nan}, }}; for (const auto& test_case : rejection_cases) { @@ -410,11 +433,25 @@ namespace GridKit test_case.pext, test_case.label); } - success *= initializationRejectedAtomically(data, - 0.25, - 0.45, - "nonfinite required signal", - NonfiniteTarget::FREQUENCY); + for (const Ext port : {Ext::IBRANCHR, + Ext::IBRANCHI, + Ext::PBRANCH, + Ext::QBRANCH, + Ext::FREQ}) + { + for (const RealT value : {infinity, -infinity, nan}) + { + success *= initializationRejectedAtomically(data, + 0.25, + 0.45, + "nonfinite required signal", + NonfiniteTarget::INPUT, + 0.8, + 0.6, + port, + value); + } + } success *= initializationRejectedAtomically(data, 0.25, 0.45, @@ -477,6 +514,26 @@ namespace GridKit 0.45, "nonfinite derived initialization candidate"); + // An invalid configuration is rejected before any state is written. + { + auto invalid_data = data; + invalid_data.parameters[Params::Tfv] = 0.0; + Fixture invalid_fixture(invalid_data); + invalid_fixture.attachAllInputs(); + setInitializationInputs(invalid_fixture); + success *= (invalid_fixture.repca.allocate() == 0); + poisonState(invalid_fixture, 0.25, 0.45); + const auto invalid_y = copyVector(invalid_fixture.repca.y()); + const auto invalid_yp = copyVector(invalid_fixture.repca.yp()); + if (invalid_fixture.repca.initialize() == 0) + { + std::cout << "Expected REPCA initialization rejection: invalid configuration\n"; + success = false; + } + success *= vectorUnchanged(invalid_fixture.repca.y(), invalid_y, "state"); + success *= vectorUnchanged(invalid_fixture.repca.yp(), invalid_yp, "derivative"); + } + { Fixture qmax_pmin_boundary(data, 0.8, 0.6); qmax_pmin_boundary.attachAllInputs(); @@ -538,11 +595,12 @@ namespace GridKit } /// Check every residual row against an independent numerical answer key. + /// The expected values are literals, not a second implementation of REPCA. TestOutcome residualEquations() { TestStatus success = true; - Fixture fixture(makeDynamicData(), kStateVr, kStateVi); + Fixture fixture(makeResidualData(), kStateVr, kStateVi); fixture.attachAllInputs(); setAnswerKeyInputs(fixture); success *= fixture.prepare(0.0, 0.0); @@ -557,20 +615,20 @@ namespace GridKit {Vars::PMEAS, "PMEAS", 0.92999999999999983}, {Vars::XPPI, "XPPI", 0.082000000000000003}, {Vars::PREF, "PREF", 0.070000000000000104}, - {Vars::V, "V", -0.029999999999999916}, + {Vars::V, "V", -0.02999999999999993}, {Vars::VLDC, "VLDC", -0.015651160000000081}, {Vars::VDROOP, "VDROOP", 0.053999999999999965}, {Vars::VCTRL, "VCTRL", -0.030000000000000027}, {Vars::SFRZ, "SFRZ", 0.19999999999999996}, {Vars::ERQ, "ERQ", 2.7755575615628914e-17}, - {Vars::ERQDB, "ERQDB", -0.017111912348473052}, - {Vars::ERQLIM, "ERQLIM", 1.7347234759768071e-17}, + {Vars::ERQDB, "ERQDB", -0.047111912348473055}, + {Vars::ERQLIM, "ERQLIM", 0.030000000000000044}, {Vars::QPI, "QPI", -0.16000000000000009}, {Vars::QEXT, "QEXT", -0.36399999999999999}, {Vars::EF, "EF", -0.0089371400000009833}, - {Vars::EP, "EP", 0.64036181730064157}, - {Vars::EPLIM, "EPLIM", 3.4694469519536142e-17}, - {Vars::PPI, "PPI", -0.38200000000000001}, + {Vars::EP, "EP", 0.59036181730064152}, + {Vars::EPLIM, "EPLIM", 0.049999999999999968}, + {Vars::PPI, "PPI", -0.38200000000000006}, {Vars::PEXT, "PEXT", -0.62}, }}; @@ -614,15 +672,15 @@ namespace GridKit }}; for (const auto& test_case : flag_cases) { - auto data = makeDynamicData(); + auto data = makeResidualData(); data.parameters[Params::VcompFlag] = test_case.voltage_compensation; data.parameters[Params::RefFlag] = test_case.voltage_reference; Fixture fixture(data); fixture.attachAllInputs(); setAnswerKeyInputs(fixture); - fixture.input(index(Ext::VREF)) = 1.05; - fixture.input(index(Ext::QREF)) = 0.20; + fixture.input(Ext::VREF) = 1.05; + fixture.input(Ext::QREF) = 0.20; success *= fixture.prepare(0.0, 0.0); setState(fixture.repca, @@ -640,7 +698,7 @@ namespace GridKit test_case.label); } - Fixture fixture(makeDynamicData()); + Fixture fixture(makeResidualData()); fixture.attachAllInputs(); setAnswerKeyInputs(fixture); success *= fixture.prepare(0.0, 0.0); @@ -742,6 +800,33 @@ namespace GridKit {Vars::QEXT, -0.36399999999999999}}, "reactive-command lead-lag"); + // The command sits at Qmax with the error at emax driving further + // out, so the antiwindup gate must block the PI state. + { + Fixture blocked(makeResidualData()); + blocked.attachAllInputs(); + setAnswerKeyInputs(blocked); + success *= blocked.prepare(0.0, 0.0); + setState(blocked.repca, + {{Vars::QPI, 0.9}, {Vars::ERQLIM, 0.8}, {Vars::SFRZ, 1.0}}); + setDerivative(blocked.repca, {{Vars::XQPI, 0.0}}); + numberVariables(blocked, 1.0); + success *= (blocked.repca.evaluateResidual() == 0); + + const JacobianRow expected{ + {index(Vars::XQPI), -1.0}, + {index(Vars::SFRZ), 1.2}, + {index(Vars::ERQLIM), 1.5}, + {index(Vars::QPI), -144.0}, + }; + success *= jacobianRowMatches( + blocked.repca.getResidual().getData()[index(Vars::XQPI)].getDependencies(), + expected, + index(Vars::XQPI), + "blocked reactive-power antiwindup", + kBehaviorTol); + } + return success.report(__func__); } @@ -763,7 +848,7 @@ namespace GridKit }}; for (const auto& test_case : flag_cases) { - auto data = makeDynamicData(); + auto data = makeResidualData(); data.parameters[Params::Freqflag] = test_case.frequency_control; Fixture fixture(data); fixture.attachAllInputs(); @@ -776,7 +861,7 @@ namespace GridKit test_case.label); } - Fixture fixture(makeDynamicData()); + Fixture fixture(makeResidualData()); fixture.attachAllInputs(); setAnswerKeyInputs(fixture); success *= fixture.prepare(0.0, 0.0); @@ -790,8 +875,8 @@ namespace GridKit }}; for (const auto& test_case : frequency_deadband_cases) { - fixture.input(index(Ext::FREQ)) = 1.0; - fixture.input(index(Ext::FREQREF)) = 1.0 + test_case.input; + fixture.input(Ext::FREQ) = 1.0; + fixture.input(Ext::FREQREF) = 1.0 + test_case.input; setState(fixture.repca, {{Vars::EF, 0.0}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, @@ -804,7 +889,7 @@ namespace GridKit {0.0, 0.0028881132523331052}, {0.1, 0.2000000000001573}, }}; - fixture.input(index(Ext::PPLANTREF)) = 0.2; + fixture.input(Ext::PPLANTREF) = 0.2; for (const auto& test_case : droop_cases) { setState(fixture.repca, @@ -889,7 +974,7 @@ namespace GridKit { TestStatus success = true; - Fixture fixture(makeDynamicData(), kStateVr, kStateVi); + Fixture fixture(makeResidualData(), kStateVr, kStateVi); fixture.attachAllInputs(); setAnswerKeyInputs(fixture); success *= fixture.prepare(0.0, 0.0); @@ -916,19 +1001,19 @@ namespace GridKit return success.report(__func__); } - /// Check every Jacobian row against an independent numerical answer key. - TestOutcome jacobian() + /// Check every dependency-tracking Jacobian row against an independent + /// numerical answer key, in every selector mode and at a non-unit alpha. + TestOutcome dependencyTracking() { TestStatus success = true; - const auto data = makeDynamicData(); // The fixed state places ERQ exactly at dbdupper, so the key also // covers the CommonMath deadband transition derivative. - const auto expected = expectedJacobian(); + const auto data = makeResidualData(); const auto dependency = dependencyTrackingJacobian(data, success); success *= jacobianMatches(dependency, - expected, + expectedJacobian(), "dependency tracking", kBehaviorTol); @@ -943,35 +1028,49 @@ namespace GridKit "all-flags-off dependency tracking", kBehaviorTol); - constexpr RealT nonunit_alpha = 2.5; - const auto nonunit_alpha_dependency = - dependencyTrackingJacobian(data, success, nonunit_alpha); + const auto nonunit_alpha_dependency = + dependencyTrackingJacobian(data, success, kNonunitAlpha); success *= jacobianMatches(nonunit_alpha_dependency, expectedJacobianNonunitAlpha(), "non-unit-alpha dependency tracking", kBehaviorTol); + return success.report(__func__); + } + #ifdef GRIDKIT_ENABLE_ENZYME - const auto enzyme = enzymeJacobian(data, success); + /// One rich state, every selector mode, and a non-unit alpha drive both + /// sensitivity paths; every Enzyme CSR row must match dependency tracking. + TestOutcome jacobian() + { + TestStatus success = true; - success *= jacobianMatches(enzyme, expected, "Enzyme", kJacobianTol); + const auto data = makeResidualData(); - const auto all_flags_off_enzyme = enzymeJacobian(all_flags_off_data, success); - success *= jacobianMatches(all_flags_off_enzyme, - expectedJacobianAllFlagsOff(), - "all-flags-off Enzyme", + success *= jacobianMatches(enzymeJacobian(data, success), + dependencyTrackingJacobian(data, success), + "Enzyme versus dependency tracking", kJacobianTol); - const auto nonunit_alpha_enzyme = - enzymeJacobian(data, success, nonunit_alpha); - success *= jacobianMatches(nonunit_alpha_enzyme, - expectedJacobianNonunitAlpha(), - "non-unit-alpha Enzyme", - kJacobianTol); -#endif + auto all_flags_off_data = data; + all_flags_off_data.parameters[Params::VcompFlag] = false; + all_flags_off_data.parameters[Params::RefFlag] = false; + all_flags_off_data.parameters[Params::Freqflag] = false; + success *= jacobianMatches( + enzymeJacobian(all_flags_off_data, success), + dependencyTrackingJacobian(all_flags_off_data, success), + "all-flags-off Enzyme versus dependency tracking", + kJacobianTol); + + success *= jacobianMatches( + enzymeJacobian(data, success, kNonunitAlpha), + dependencyTrackingJacobian(data, success, kNonunitAlpha), + "non-unit-alpha Enzyme versus dependency tracking", + kJacobianTol); return success.report(__func__); } +#endif private: using Params = PhasorDynamics::Converter::RepcaParameters; @@ -1030,7 +1129,7 @@ namespace GridKit enum class NonfiniteTarget { NONE, - FREQUENCY, + INPUT, BUS_VOLTAGE }; @@ -1143,22 +1242,23 @@ namespace GridKit return repca.y().getData()[index(Vars::PEXT)]; } - T& input(size_t port) + T& input(Ext port) { - return input_values_[port]; + return input_values_[index(port)]; } - IdxT inputIndex(size_t port) const + IdxT inputIndex(Ext port) const { - return input_indices_[port]; + return input_indices_[index(port)]; } PhasorDynamics::Bus bus; PhasorDynamics::Converter::Repca repca; }; - static constexpr RealT kStateVr = 0.9; - static constexpr RealT kStateVi = 0.4; + static constexpr RealT kStateVr = 0.9; + static constexpr RealT kStateVi = 0.4; + static constexpr RealT kNonunitAlpha = 2.5; static constexpr size_t kBusVrColumn = index(Vars::MAXIMUM); static constexpr size_t kBusViColumn = kBusVrColumn + 1; @@ -1227,7 +1327,7 @@ namespace GridKit return data; } - Data makeDynamicData() const + Data makeResidualData() const { auto data = makeData(); data.parameters[Params::mva] = 50.0; @@ -1261,7 +1361,7 @@ namespace GridKit Data makeInitializationData() const { - auto data = makeDynamicData(); + auto data = makeResidualData(); data.parameters[Params::dbdupper] = 0.02; data.parameters[Params::emin] = -0.8; data.parameters[Params::femin] = -0.6; @@ -1271,25 +1371,25 @@ namespace GridKit template void setInitializationInputs(Fixture& fixture) const { - fixture.input(index(Ext::IBRANCHR)) = static_cast(0.2); - fixture.input(index(Ext::IBRANCHI)) = static_cast(-0.1); - fixture.input(index(Ext::PBRANCH)) = static_cast(0.4); - fixture.input(index(Ext::QBRANCH)) = static_cast(0.1); - fixture.input(index(Ext::FREQ)) = static_cast(0.99); + fixture.input(Ext::IBRANCHR) = static_cast(0.2); + fixture.input(Ext::IBRANCHI) = static_cast(-0.1); + fixture.input(Ext::PBRANCH) = static_cast(0.4); + fixture.input(Ext::QBRANCH) = static_cast(0.1); + fixture.input(Ext::FREQ) = static_cast(0.99); } template void setAnswerKeyInputs(Fixture& fixture) const { - fixture.input(index(Ext::IBRANCHR)) = static_cast(0.08); - fixture.input(index(Ext::IBRANCHI)) = static_cast(-0.02); - fixture.input(index(Ext::PBRANCH)) = static_cast(0.41); - fixture.input(index(Ext::QBRANCH)) = static_cast(0.13); - fixture.input(index(Ext::FREQ)) = static_cast(0.99); - fixture.input(index(Ext::FREQREF)) = static_cast(1.0); - fixture.input(index(Ext::VREF)) = static_cast(1.01); - fixture.input(index(Ext::QREF)) = static_cast(0.12); - fixture.input(index(Ext::PPLANTREF)) = static_cast(0.55); + fixture.input(Ext::IBRANCHR) = static_cast(0.08); + fixture.input(Ext::IBRANCHI) = static_cast(-0.02); + fixture.input(Ext::PBRANCH) = static_cast(0.41); + fixture.input(Ext::QBRANCH) = static_cast(0.13); + fixture.input(Ext::FREQ) = static_cast(0.99); + fixture.input(Ext::FREQREF) = static_cast(1.0); + fixture.input(Ext::VREF) = static_cast(1.01); + fixture.input(Ext::QREF) = static_cast(0.12); + fixture.input(Ext::PPLANTREF) = static_cast(0.55); } template @@ -1309,12 +1409,12 @@ namespace GridKit {Vars::VCTRL, 1.02}, {Vars::SFRZ, 0.8}, {Vars::ERQ, 0.03}, - {Vars::ERQDB, 0.02}, + {Vars::ERQDB, 0.05}, {Vars::ERQLIM, 0.02}, {Vars::QPI, 0.27}, {Vars::QEXT, 0.20}, {Vars::EF, 0.01}, - {Vars::EP, 0.04}, + {Vars::EP, 0.09}, {Vars::EPLIM, 0.04}, {Vars::PPI, 0.66}, {Vars::PEXT, 0.61}}); @@ -1335,12 +1435,12 @@ namespace GridKit implicit_defaults.attachAllInputs(); explicit_defaults.attachAllInputs(); - implicit_defaults.input(index(Ext::PBRANCH)) = 0.2; - implicit_defaults.input(index(Ext::QBRANCH)) = 0.1; - implicit_defaults.input(index(Ext::FREQ)) = 1.0; - explicit_defaults.input(index(Ext::PBRANCH)) = 0.2; - explicit_defaults.input(index(Ext::QBRANCH)) = 0.1; - explicit_defaults.input(index(Ext::FREQ)) = 1.0; + implicit_defaults.input(Ext::PBRANCH) = 0.2; + implicit_defaults.input(Ext::QBRANCH) = 0.1; + implicit_defaults.input(Ext::FREQ) = 1.0; + explicit_defaults.input(Ext::PBRANCH) = 0.2; + explicit_defaults.input(Ext::QBRANCH) = 0.1; + explicit_defaults.input(Ext::FREQ) = 1.0; bool success = implicit_defaults.initialize(0.1, 0.2) && explicit_defaults.initialize(0.1, 0.2); @@ -1378,8 +1478,9 @@ namespace GridKit } for (size_t port = 0; port < index(Ext::MAXIMUM); ++port) { - if (!rowMatches(implicit_defaults.input(port), - explicit_defaults.input(port), + const auto variable = static_cast(port); + if (!rowMatches(implicit_defaults.input(variable), + explicit_defaults.input(variable), "documented-default signal", port, "")) @@ -1429,13 +1530,31 @@ namespace GridKit return fixture.repca.verify() > 0; } + /// Fill state and derivative with a recognizable ramp, restoring the + /// aliased commands, so any write by a rejected initialization shows. + void poisonState(Fixture& fixture, RealT qext, RealT pext) const + { + auto* y = fixture.repca.y().getData(); + auto* yp = fixture.repca.yp().getData(); + for (size_t row = 0; row < index(Vars::MAXIMUM); ++row) + { + y[row] = 0.125 + 0.01 * static_cast(row); + yp[row] = -0.25 - 0.01 * static_cast(row); + } + fixture.setCommands(qext, pext); + fixture.repca.yp().setDataUpdated(); + } + bool initializationRejectedAtomically(const Data& data, RealT qext, RealT pext, const char* label, - NonfiniteTarget target = NonfiniteTarget::NONE, - RealT initial_vr = 0.8, - RealT initial_vi = 0.6) const + NonfiniteTarget target = NonfiniteTarget::NONE, + RealT initial_vr = 0.8, + RealT initial_vi = 0.6, + Ext poisoned_port = Ext::FREQ, + RealT poison_value = + std::numeric_limits::infinity()) const { Fixture fixture(data, initial_vr, initial_vi); fixture.attachAllInputs(77.0); @@ -1445,28 +1564,17 @@ namespace GridKit return false; } - const RealT infinity = std::numeric_limits::infinity(); - if (target == NonfiniteTarget::FREQUENCY) + if (target == NonfiniteTarget::INPUT) { - fixture.input(index(Ext::FREQ)) = infinity; + fixture.input(poisoned_port) = poison_value; } if (target == NonfiniteTarget::BUS_VOLTAGE) { - fixture.bus.Vr() = infinity; + fixture.bus.Vr() = poison_value; fixture.bus.y().setDataUpdated(); } - auto* y = fixture.repca.y().getData(); - auto* yp = fixture.repca.yp().getData(); - for (size_t row = 0; row < index(Vars::MAXIMUM); ++row) - { - y[row] = 0.125 + 0.01 * static_cast(row); - yp[row] = -0.25 - 0.01 * static_cast(row); - } - // Restore assigned commands after filling model storage with sentinels. - fixture.setCommands(qext, pext); - fixture.repca.y().setDataUpdated(); - fixture.repca.yp().setDataUpdated(); + poisonState(fixture, qext, pext); const auto y_before = copyVector(fixture.repca.y()); const auto yp_before = copyVector(fixture.repca.yp()); @@ -1474,7 +1582,7 @@ namespace GridKit std::array inputs_before{}; for (size_t port = 0; port < index(Ext::MAXIMUM); ++port) { - inputs_before[port] = fixture.input(port); + inputs_before[port] = fixture.input(static_cast(port)); } bool success = true; @@ -1484,11 +1592,11 @@ namespace GridKit success = false; } - if (!valueUnchanged(fixture.qext(), qext, "qext signal", index(Vars::QEXT))) + if (!scalarPreserved(fixture.qext(), qext, "rejected qext preservation")) { success = false; } - if (!valueUnchanged(fixture.pext(), pext, "pext signal", index(Vars::PEXT))) + if (!scalarPreserved(fixture.pext(), pext, "rejected pext preservation")) { success = false; } @@ -1506,7 +1614,7 @@ namespace GridKit } for (size_t port = 0; port < index(Ext::MAXIMUM); ++port) { - if (!valueUnchanged(fixture.input(port), + if (!valueUnchanged(fixture.input(static_cast(port)), inputs_before[port], "external signal", port)) @@ -1670,12 +1778,34 @@ namespace GridKit return success; } + /// A value retains exactly what its owner supplied, including signed + /// infinities and NaN. + static bool preserved(RealT actual, RealT expected) + { + if (std::isnan(expected)) + { + return std::isnan(actual); + } + return actual == expected; + } + + bool scalarPreserved(RealT actual, RealT expected, const char* label) const + { + if (preserved(actual, expected)) + { + return true; + } + std::cout << label << " changed: " << std::setprecision(std::numeric_limits::max_digits10) << actual + << " != " << expected << '\n'; + return false; + } + static bool valueUnchanged(RealT actual, RealT expected, const char* what, size_t index) { - if (actual == expected) + if (preserved(actual, expected)) { return true; } @@ -1898,8 +2028,42 @@ namespace GridKit return entry->second; } + bool jacobianRowMatches(const JacobianRow& actual, + const JacobianRow& expected, + size_t row, + const char* source, + RealT tolerance) const + { + constexpr size_t column_count = externalColumn(index(Ext::MAXIMUM)); + bool success = true; + for (size_t column = 0; column < column_count; ++column) + { + const RealT actual_value = mapValue(actual, column); + const RealT expected_value = mapValue(expected, column); + if (!isEqual(actual_value, expected_value, tolerance)) + { + std::cout << "REPCA " << source << " Jacobian row " << row + << " column " << column << " mismatch: " + << std::setprecision(std::numeric_limits::max_digits10) << actual_value << " != " + << expected_value << '\n'; + success = false; + } + } + for (const auto& [column, value] : actual) + { + if (column >= column_count && !isEqual(value, 0.0, tolerance)) + { + std::cout << "REPCA " << source << " Jacobian row " << row + << " has unexpected column " << column << '\n'; + success = false; + } + } + return success; + } + + template bool jacobianMatches(const std::vector& actual, - const JacobianKey& expected, + const ExpectedT& expected, const char* source, RealT tolerance) const { @@ -1909,31 +2073,12 @@ namespace GridKit return false; } - constexpr size_t column_count = externalColumn(index(Ext::MAXIMUM)); - bool success = true; + bool success = true; for (size_t row = 0; row < index(Vars::MAXIMUM); ++row) { - for (size_t column = 0; column < column_count; ++column) + if (!jacobianRowMatches(actual[row], expected[row], row, source, tolerance)) { - const RealT actual_value = mapValue(actual[row], column); - const RealT expected_value = mapValue(expected[row], column); - if (!isEqual(actual_value, expected_value, tolerance)) - { - std::cout << "REPCA " << source << " Jacobian row " << row - << " column " << column << " mismatch: " - << std::setprecision(std::numeric_limits::max_digits10) << actual_value << " != " - << expected_value << '\n'; - success = false; - } - } - for (const auto& [column, value] : actual[row]) - { - if (column >= column_count && !isEqual(value, 0.0, tolerance)) - { - std::cout << "REPCA " << source << " Jacobian row " << row - << " has unexpected column " << column << '\n'; - success = false; - } + success = false; } } return success; @@ -1958,7 +2103,8 @@ namespace GridKit } for (size_t port = 0; port < index(Ext::MAXIMUM); ++port) { - fixture.input(port).setVariableNumber(fixture.inputIndex(port)); + const auto variable = static_cast(port); + fixture.input(variable).setVariableNumber(fixture.inputIndex(variable)); } fixture.repca.y().setDataUpdated(); diff --git a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp index e9d875808..4a28c8476 100644 --- a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp @@ -257,7 +257,6 @@ namespace GridKit /// and prove assigned command outputs alias the assembled signals. TestOutcome repca() { - using Repca = PhasorDynamics::Converter::Repca; using Buses = PhasorDynamics::Converter::RepcaBuses; using Inputs = PhasorDynamics::Converter::RepcaSignalInputs; using Outputs = PhasorDynamics::Converter::RepcaSignalOutputs; @@ -346,10 +345,10 @@ namespace GridKit success *= system.evaluateJacobian() == 0; success *= system.size() == static_cast(Vars::MAXIMUM); - auto* repca = dynamic_cast(system.getComponent(0)); - success *= repca != nullptr; + auto* repca = system.getComponent(0); auto* qext = system.getSignal(qext_id); auto* pext = system.getSignal(pext_id); + success *= repca != nullptr; success *= qext != nullptr; success *= pext != nullptr; if (repca != nullptr && qext != nullptr && pext != nullptr) @@ -358,16 +357,12 @@ namespace GridKit success *= pext->linked(); if (qext->linked() && pext->linked()) { - success *= qext->getVariableIndex() - == repca->getVariableIndex(static_cast(Vars::QEXT)); - success *= pext->getVariableIndex() - == repca->getVariableIndex(static_cast(Vars::PEXT)); - success *= isEqual(qext->read(), - repca->y().getData()[static_cast(Vars::QEXT)], - static_cast(1.0e-12)); - success *= isEqual(pext->read(), - repca->y().getData()[static_cast(Vars::PEXT)], - static_cast(1.0e-12)); + success *= qext->getVariableIndex() == static_cast(Vars::QEXT); + success *= pext->getVariableIndex() == static_cast(Vars::PEXT); + + // Both nodes alias the model-owned command states. + success *= qext->read() == repca->y().getData()[static_cast(Vars::QEXT)]; + success *= pext->read() == repca->y().getData()[static_cast(Vars::PEXT)]; } } diff --git a/tests/UnitTests/PhasorDynamics/runComponentConnectionTests.cpp b/tests/UnitTests/PhasorDynamics/runComponentConnectionTests.cpp index 127b5107b..b8c7f36f5 100644 --- a/tests/UnitTests/PhasorDynamics/runComponentConnectionTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runComponentConnectionTests.cpp @@ -9,6 +9,7 @@ int main() result += test.genrouEsdc1a(); result += test.genrouHygov(); + result += test.regcaRepca(); return result.summary(); } diff --git a/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp b/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp index 06f90e5f4..52e2aee07 100644 --- a/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp @@ -13,7 +13,10 @@ int main() result += test.reactiveControl(); result += test.activePowerControl(); result += test.derivatives(); + result += test.dependencyTracking(); +#ifdef GRIDKIT_ENABLE_ENZYME result += test.jacobian(); +#endif return result.summary(); } diff --git a/tests/UnitTests/Utilities/CaseFormatTests.hpp b/tests/UnitTests/Utilities/CaseFormatTests.hpp index 93b853e29..6f730b57d 100644 --- a/tests/UnitTests/Utilities/CaseFormatTests.hpp +++ b/tests/UnitTests/Utilities/CaseFormatTests.hpp @@ -244,7 +244,7 @@ namespace GridKit "Velm": 0.2, "Gmax": 0.98, "Gmin": 0.02, "Tw": 1.2, "At": 1.1, "Dturb": 0.4, "Qnl": 0.08, "Tn": 0.7, "Tnp": 1.4, "db1": 0.01, "db2": 0.02, "Hdam": 1.05, "Gv0": 0.0, "Gv1": 0.2, "Gv2": 0.4, "Gv3": 0.6, "Gv4": 0.8, "Gv5": 1.0, "Pgv0": 0.0, "Pgv1": 0.15, "Pgv2": 0.42, "Pgv3": 0.66, "Pgv4": 0.85, "Pgv5": 1.0}, "mon": ["pmech", "filter", "desiredgate", "gate", "flow", "head"]}, - { "class": "Repca", "ports": {"bus":1, "ibranchr":11, "ibranchi":12, "pbranch":13, "qbranch":14, "freq":15, "freqref":16, "vref":17, "qref":18, "pplantref":19, "qext":20, "pext":21}, "id": "PC1", "params": {"mva":50, "VcompFlag":false, "Tfltr":0.2}, "mon": ["qext", "pext", "vmeas", "qmeas", "pmeas"] }, + { "class": "Repca", "ports": {"bus":1, "ibranchr":11, "ibranchi":12, "pbranch":13, "qbranch":14, "freq":15, "freqref":16, "vref":17, "qref":18, "pplantref":19, "qext":20, "pext":21}, "id": "PC1", "params": {"mva":50, "VcompFlag":false, "RefFlag":true, "Freqflag":true, "Tfltr":0.2, "Vfrz":0.65, "Rc":0.02, "Xc":0.03, "Kc":0.4, "dbdlow":-0.02, "dbdupper":0.03, "emax":0.8, "emin":-0.7, "Kp":2.0, "Ki":3.0, "Qmax":0.9, "Qmin":-0.8, "Tft":0.2, "Tfv":1.5, "Tp":0.4, "fdbd1":-0.01, "fdbd2":0.015, "Ddn":2.0, "Dup":1.0, "femax":0.6, "femin":-0.5, "Kpg":1.7, "Kig":1.8, "Pmax":1.2, "Pmin":0.1, "Tlag":0.5}, "mon": ["qext", "pext", "vmeas", "qmeas", "pmeas"] }, { "class": "Ieeet1", "ports": {"bus":1, "speed": 1, "efd":3}, "id": "DV3", "params": {"Tr":0.0, "Ka":50.0, "Ta":0.04, "Ke":-0.06, "Te":0.6, "Kf":0.09, "Tf":1.46, "Vrmin":-1.0, "Vrmax":1.0, "E1":2.8, "E2":3.373, "Se1":0.04, "Se2":0.33, "Ispdlim":0.0}}, { "class": "SexsPti", "ports": {"bus":1, "efd":3}, "id": "DV4", "params": {"Ta":0.1, "Tb":0.5, "Te":0.8, "K":10.0, "Efdmax":5.0, "Efdmin":-5.0}}, { "class": "BusFault", "ports": {"bus":1}, "id": "1", "params": {"state0": false, "R":0.0, "X":1e-3} } @@ -436,7 +436,35 @@ namespace GridKit success *= std::get(result.repca[0].parameters[RepcaData::Parameters::mva]) == 50; success *= !std::get(result.repca[0].parameters[RepcaData::Parameters::VcompFlag]); + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::RefFlag]); + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Freqflag]); success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Tfltr]) == 0.2; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Vfrz]) == 0.65; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Rc]) == 0.02; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Xc]) == 0.03; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Kc]) == 0.4; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::dbdlow]) == -0.02; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::dbdupper]) == 0.03; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::emax]) == 0.8; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::emin]) == -0.7; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Kp]) == 2.0; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Ki]) == 3.0; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Qmax]) == 0.9; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Qmin]) == -0.8; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Tft]) == 0.2; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Tfv]) == 1.5; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Tp]) == 0.4; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::fdbd1]) == -0.01; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::fdbd2]) == 0.015; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Ddn]) == 2.0; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Dup]) == 1.0; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::femax]) == 0.6; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::femin]) == -0.5; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Kpg]) == 1.7; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Kig]) == 1.8; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Pmax]) == 1.2; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Pmin]) == 0.1; + success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Tlag]) == 0.5; success *= result.repca[0].buses[RepcaData::Buses::bus] == 1; success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::ibranchr] == 11; success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::ibranchi] == 12; From 16bdc46d30e355f3b888183db4c3caeb851c6967 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Wed, 5 Aug 2026 00:01:06 -0500 Subject: [PATCH 08/19] consistancy in impl, mu-independent tests, cleaner port name --- .../PhasorDynamics/Converter/REPCA/README.md | 26 +- .../PhasorDynamics/Converter/REPCA/Repca.hpp | 35 +- .../Converter/REPCA/RepcaData.hpp | 28 +- .../Converter/REPCA/RepcaImpl.hpp | 159 +++--- .../Model/PhasorDynamics/SystemModelImpl.hpp | 40 +- .../ComponentConnectionTests.hpp | 40 +- .../PhasorDynamics/ConverterRepcaTests.hpp | 514 +++++++++--------- .../SystemSingleComponentTests.hpp | 10 +- tests/UnitTests/Utilities/CaseFormatTests.hpp | 12 +- 9 files changed, 441 insertions(+), 423 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md index 5d2933adf..3f5c70667 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md @@ -163,13 +163,13 @@ $V_\mathrm{r}$ | [p.u.] | Known | Regulated-bus voltage, real c $V_\mathrm{i}$ | [p.u.] | Known | Regulated-bus voltage, imaginary component | Bus input $I_\mathrm{r}$ | [p.u.] | Known | Branch-current real component | Signal port `ibranchr`; system base $I_\mathrm{i}$ | [p.u.] | Known | Branch-current imaginary component | Signal port `ibranchi`; system base -$P^\mathrm{br}$ | [p.u.] | Known | Branch active power | Signal port `pbranch`; system base -$Q^\mathrm{br}$ | [p.u.] | Known | Branch reactive power | Signal port `qbranch`; system base +$P$ | [p.u.] | Known | Branch active power | Signal port `pbranch`; system base +$Q$ | [p.u.] | Known | Branch reactive power | Signal port `qbranch`; system base $f$ | [p.u.] | Known | Frequency input | Signal port `freq` $f^\mathrm{ref}$ | [p.u.] | Unknown | Frequency reference | Optional signal port `freqref` $V^\mathrm{ref}$ | [p.u.] | Unknown | Voltage-control reference | Optional signal port `vref` $Q^\mathrm{ref}$ | [p.u.] | Unknown | Reactive-power reference | Optional signal port `qref`; system base -$P_\mathrm{plant}^\mathrm{ref}$ | [p.u.] | Unknown | Plant active-power reference | Optional signal port `pplantref`; system base +$P^\mathrm{ref}$ | [p.u.] | Unknown | Plant active-power reference | Optional signal port `pplantref`; system base ## Model Equations @@ -180,10 +180,10 @@ $P_\mathrm{plant}^\mathrm{ref}$ | [p.u.] | Unknown | Plant active-power referenc ```math \begin{aligned} 0 &= -\dot{V}^\mathrm{meas} + \dfrac{1}{T_\mathrm{fltr}} (V^\mathrm{ctrl} - V^\mathrm{meas}) \\ - 0 &= -\dot{Q}^\mathrm{meas} + \dfrac{1}{T_\mathrm{fltr}} (k_\mathrm{base}Q^\mathrm{br} - Q^\mathrm{meas}) \\ + 0 &= -\dot{Q}^\mathrm{meas} + \dfrac{1}{T_\mathrm{fltr}} (k_\mathrm{base}Q - Q^\mathrm{meas}) \\ 0 &= -\dot{x}_Q^\mathrm{PI} + s_\mathrm{frz}\, \text{antiwindup}(Q^\mathrm{PI}, K_\mathrm{i}e_\mathrm{RQ}^\mathrm{lim};\,Q^{\min}, Q^{\max}) \\ 0 &= -\dot{x}_Q^\mathrm{lag} + \dfrac{1}{T_\mathrm{fv}} (Q^\mathrm{PI} - x_Q^\mathrm{lag}) \\ - 0 &= -\dot{P}^\mathrm{meas} + \dfrac{1}{T_\mathrm{p}} (k_\mathrm{base}P^\mathrm{br} - P^\mathrm{meas}) \\ + 0 &= -\dot{P}^\mathrm{meas} + \dfrac{1}{T_\mathrm{p}} (k_\mathrm{base}P - P^\mathrm{meas}) \\ 0 &= -\dot{x}_P^\mathrm{PI} + \text{antiwindup}(P^\mathrm{PI}, K_\mathrm{ig}e_P^\mathrm{lim};\,P^{\min}, P^{\max}) \\ 0 &= -\dot{P}^\mathrm{ref} + \dfrac{1}{T_\mathrm{lag}} (P^\mathrm{PI} - P^\mathrm{ref}). \end{aligned} @@ -198,7 +198,7 @@ target and smooth approximation. \begin{aligned} 0 &= -V^2 + V_\mathrm{r}^2 + V_\mathrm{i}^2 \\ 0 &= -(V^\mathrm{ldc})^2 + (V_\mathrm{r} - R_c I_\mathrm{r} + X_c I_\mathrm{i})^2 + (V_\mathrm{i} - R_c I_\mathrm{i} - X_c I_\mathrm{r})^2 \\ - 0 &= -V^\mathrm{droop} + V + K_c k_\mathrm{base}Q^\mathrm{br} \\ + 0 &= -V^\mathrm{droop} + V + K_c k_\mathrm{base}Q \\ 0 &= -V^\mathrm{ctrl} + s_\mathrm{comp}V^\mathrm{ldc} + s_\mathrm{comp}^\mathrm{off}V^\mathrm{droop} \\ 0 &= -s_\mathrm{frz} + \text{above}(V;\,V^\mathrm{frz}) \\ 0 &= -e_\mathrm{RQ} + s_\mathrm{ref}(V^\mathrm{ref} - V^\mathrm{meas}) + s_\mathrm{ref}^\mathrm{off} (k_\mathrm{base}Q^\mathrm{ref} - Q^\mathrm{meas}) \\ @@ -207,7 +207,7 @@ target and smooth approximation. 0 &= -Q^\mathrm{PI} + \text{clamp}(K_\mathrm{p}e_\mathrm{RQ}^\mathrm{lim}+x_Q^\mathrm{PI};\,Q^{\min},Q^{\max}) \\ 0 &= -T_\mathrm{fv} (k_\mathrm{base}Q^\mathrm{ext}-x_Q^\mathrm{lag}) + T_\mathrm{ft} (Q^\mathrm{PI}-x_Q^\mathrm{lag}) \\ 0 &= -e_f + \text{deadband2}(f^\mathrm{ref}-f;\,D_\mathrm{bd1}^{f},D_\mathrm{bd2}^{f}) \\ - 0 &= -e_P + k_\mathrm{base}P_\mathrm{plant}^\mathrm{ref} - P^\mathrm{meas} + D_\mathrm{dn}\text{ramp}(e_f) - D_\mathrm{up}\text{ramp}(-e_f) \\ + 0 &= -e_P + k_\mathrm{base}P^\mathrm{ref} - P^\mathrm{meas} + D_\mathrm{dn}\text{ramp}(e_f) - D_\mathrm{up}\text{ramp}(-e_f) \\ 0 &= -e_P^\mathrm{lim} + \text{clamp}(e_P;\,e_P^{\min},e_P^{\max}) \\ 0 &= -P^\mathrm{PI} + \text{clamp}(K_\mathrm{pg}e_P^\mathrm{lim}+x_P^\mathrm{PI};\,P^{\min},P^{\max}) \\ 0 &= -k_\mathrm{base}P^\mathrm{ext} + s_\mathrm{freq}P^\mathrm{ref}. @@ -231,7 +231,7 @@ REPCA reconstructs a steady operating point; arbitrary-state restart is unsuppor \begin{aligned} V_\mathrm{r}, V_\mathrm{i} &\leftarrow \text{regulated-bus voltage} \\ I_\mathrm{r}, I_\mathrm{i} &\leftarrow \text{branch current} \\ - P^\mathrm{br}, Q^\mathrm{br} &\leftarrow \text{branch power} \\ + P, Q &\leftarrow \text{branch power} \\ f &\leftarrow \text{frequency input} \\ Q^\mathrm{ext} &\leftarrow \text{known reactive-power command on system base} \\ P^\mathrm{ext} &\leftarrow \text{known active-power command on system base}, \quad s_\mathrm{freq}\ \text{enabled}. @@ -248,11 +248,11 @@ $z\in[\ell,u]$ within that tolerance, including the limits. \begin{aligned} V &\leftarrow \sqrt{V_\mathrm{r}^2 + V_\mathrm{i}^2} \\ V^\mathrm{ldc} &\leftarrow \sqrt{(V_\mathrm{r}-R_c I_\mathrm{r}+X_c I_\mathrm{i})^2 + (V_\mathrm{i}-R_c I_\mathrm{i}-X_c I_\mathrm{r})^2} \\ - V^\mathrm{droop} &\leftarrow V + K_c k_\mathrm{base}Q^\mathrm{br} \\ + V^\mathrm{droop} &\leftarrow V + K_c k_\mathrm{base}Q \\ V^\mathrm{ctrl} &\leftarrow s_\mathrm{comp}V^\mathrm{ldc} + s_\mathrm{comp}^\mathrm{off}V^\mathrm{droop} \\ V^\mathrm{meas} &\leftarrow V^\mathrm{ctrl} \\ - Q^\mathrm{meas} &\leftarrow k_\mathrm{base}Q^\mathrm{br} \\ - P^\mathrm{meas} &\leftarrow k_\mathrm{base}P^\mathrm{br} \\ + Q^\mathrm{meas} &\leftarrow k_\mathrm{base}Q \\ + P^\mathrm{meas} &\leftarrow k_\mathrm{base}P \\ s_\mathrm{frz} &\leftarrow \text{above}(V;\,V^\mathrm{frz}) \\ e_\mathrm{RQ} &\leftarrow 0 \\ e_\mathrm{RQ}^\mathrm{db} &\leftarrow \text{deadband2}(e_\mathrm{RQ};\,D_\mathrm{bd1},D_\mathrm{bd2}) \\ @@ -297,8 +297,8 @@ Initialization is atomic; candidates are validated before state or signal writes \begin{aligned} f^\mathrm{ref} &\leftarrow f \\ V^\mathrm{ref} &\leftarrow V^\mathrm{meas} \\ - Q^\mathrm{ref} &\leftarrow Q^\mathrm{br} \\ - P_\mathrm{plant}^\mathrm{ref} &\leftarrow \dfrac{P^\mathrm{meas}-P^\mathrm{freq}}{k_\mathrm{base}}. + Q^\mathrm{ref} &\leftarrow Q \\ + P^\mathrm{ref} &\leftarrow \dfrac{P^\mathrm{meas}-P^\mathrm{freq}}{k_\mathrm{base}}. \end{aligned} ``` diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp index 38d2f92a4..b80dc213b 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp @@ -27,7 +27,7 @@ namespace GridKit namespace Converter { - /// Internal variables and residual rows of a `Repca`. + /// Internal variables of `Repca`. enum class RepcaInternalVariables : size_t { VMEAS, ///< \f$V^\mathrm{meas}\f$ Differential filtered regulated voltage [p.u.] @@ -52,22 +52,22 @@ namespace GridKit EPLIM, ///< \f$e_P^\mathrm{lim}\f$ Algebraic limited active-power control error on component base [p.u.] PPI, ///< \f$P^\mathrm{PI}\f$ Algebraic active-power PI output on component base [p.u.] PEXT, ///< \f$P^\mathrm{ext}\f$ Algebraic active-power command on system base [p.u.] - MAXIMUM ///< Number of REPCA internal variables and residual rows + MAXIMUM ///< Number of internal variables }; - /// External signal variables read or initialized by a `Repca`. + /// External variables of `Repca`. enum class RepcaExternalVariables : size_t { - IBRANCHR, ///< \f$I_\mathrm{r}\f$ Required branch-current real component on system base [p.u.] - IBRANCHI, ///< \f$I_\mathrm{i}\f$ Required branch-current imaginary component on system base [p.u.] - PBRANCH, ///< \f$P^\mathrm{br}\f$ Required branch active power on system base [p.u.] - QBRANCH, ///< \f$Q^\mathrm{br}\f$ Required branch reactive power on system base [p.u.] - FREQ, ///< \f$f\f$ Required frequency input [p.u.] - FREQREF, ///< \f$f^\mathrm{ref}\f$ Optional frequency reference [p.u.] - VREF, ///< \f$V^\mathrm{ref}\f$ Optional voltage-control reference [p.u.] - QREF, ///< \f$Q^\mathrm{ref}\f$ Optional reactive-power reference on system base [p.u.] - PPLANTREF, ///< \f$P_\mathrm{plant}^\mathrm{ref}\f$ Optional plant active-power reference on system base [p.u.] - MAXIMUM ///< Number of REPCA external signal variables + IR, ///< \f$I_\mathrm{r}\f$ Required branch-current real component on system base [p.u.] + II, ///< \f$I_\mathrm{i}\f$ Required branch-current imaginary component on system base [p.u.] + P, ///< \f$P\f$ Required branch active power on system base [p.u.] + Q, ///< \f$Q\f$ Required branch reactive power on system base [p.u.] + FREQ, ///< \f$f\f$ Required frequency input [p.u.] + FREQREF, ///< \f$f^\mathrm{ref}\f$ Optional frequency reference [p.u.] + VREF, ///< \f$V^\mathrm{ref}\f$ Optional voltage-control reference [p.u.] + QREF, ///< \f$Q^\mathrm{ref}\f$ Optional reactive-power reference on system base [p.u.] + PREF, ///< \f$P^\mathrm{ref}\f$ Optional plant active-power reference on system base [p.u.] + MAXIMUM ///< Number of external variables }; template @@ -102,6 +102,10 @@ namespace GridKit using InternalVariablesT = RepcaInternalVariables; using ExternalVariablesT = RepcaExternalVariables; + // A command reconstructed exactly on a limit leaves the widest + // steady residual, 1.6e-13 at the QPI row. + static constexpr RealT INITIALIZATION_TOLERANCE = static_cast(1.0e-12); + Repca(BusT* bus); Repca(BusT* bus, const ModelDataT& data); ~Repca(); @@ -158,8 +162,7 @@ namespace GridKit ScalarT& Vr(); ScalarT& Vi(); - static constexpr RealT TIME_CONSTANT_MINIMUM = static_cast(1.0e-3); - static constexpr RealT INITIALIZATION_TOLERANCE = static_cast(1.0e-10); + static constexpr RealT TIME_CONSTANT_MINIMUM = static_cast(1.0e-3); static constexpr RealT INITIALIZATION_LIMIT_OFFSET = static_cast(0.1); @@ -208,7 +211,7 @@ namespace GridKit ScalarT freqref_set_{ONE}; ScalarT vref_set_{ONE}; ScalarT qref_set_{ZERO}; - ScalarT pplantref_set_{ZERO}; + ScalarT pref_set_{ZERO}; ComponentSignals signals_; std::unique_ptr monitor_; diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp index fed8f163c..72dc09903 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp @@ -14,7 +14,7 @@ namespace GridKit { namespace Converter { - /// Parameter keys for REPCA; all are optional with documented defaults. + /// Parameter keys for `Repca`. enum class RepcaParameters { mva, ///< \f$S^\mathrm{base}\f$ Component power base [MVA] @@ -50,29 +50,29 @@ namespace GridKit Tlag ///< \f$T_\mathrm{lag}\f$ Active-power command lag time constant [sec] }; - /// Buses for the REPCA plant-control model. + /// Buses for `Repca`. enum class RepcaBuses : size_t { bus, ///< \f$V_\mathrm{r},V_\mathrm{i}\f$ Required Known regulated-bus voltage [p.u.] SIZE ///< Number of REPCA bus ports }; - /// Signal inputs for the REPCA plant-control model. + /// Signal inputs for the `Repca`. enum class RepcaSignalInputs : size_t { - ibranchr, ///< \f$I_\mathrm{r}\f$ Required Known branch-current real input on system base [p.u.] - ibranchi, ///< \f$I_\mathrm{i}\f$ Required Known branch-current imaginary input on system base [p.u.] - pbranch, ///< \f$P^\mathrm{br}\f$ Required Known branch active-power input on system base [p.u.] - qbranch, ///< \f$Q^\mathrm{br}\f$ Required Known branch reactive-power input on system base [p.u.] - freq, ///< \f$f\f$ Required Known frequency input [p.u.] - freqref, ///< \f$f^\mathrm{ref}\f$ Optional Unknown frequency-reference input [p.u.] - vref, ///< \f$V^\mathrm{ref}\f$ Optional Unknown voltage-reference input [p.u.] - qref, ///< \f$Q^\mathrm{ref}\f$ Optional Unknown reactive-power reference on system base [p.u.] - pplantref, ///< \f$P_\mathrm{plant}^\mathrm{ref}\f$ Optional Unknown plant active-power reference on system base [p.u.] - SIZE ///< Number of REPCA signal-input ports + ir, ///< \f$I_\mathrm{r}\f$ Required Known branch-current real input on system base [p.u.] + ii, ///< \f$I_\mathrm{i}\f$ Required Known branch-current imaginary input on system base [p.u.] + p, ///< \f$P\f$ Required Known branch active-power input on system base [p.u.] + q, ///< \f$Q\f$ Required Known branch reactive-power input on system base [p.u.] + freq, ///< \f$f\f$ Required Known frequency input [p.u.] + freqref, ///< \f$f^\mathrm{ref}\f$ Optional Unknown frequency-reference input [p.u.] + vref, ///< \f$V^\mathrm{ref}\f$ Optional Unknown voltage-reference input [p.u.] + qref, ///< \f$Q^\mathrm{ref}\f$ Optional Unknown reactive-power reference on system base [p.u.] + pref, ///< \f$P^\mathrm{ref}\f$ Optional Unknown plant active-power reference on system base [p.u.] + SIZE ///< Number of REPCA signal-input ports }; - /// Signal outputs for the REPCA plant-control model. + /// Signal outputs for `Repca`. enum class RepcaSignalOutputs : size_t { qext, ///< \f$Q^\mathrm{ext}\f$ Optional Known reactive-power command output on system base [p.u.] diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp index 30f1a800f..48d17b104 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp @@ -150,7 +150,6 @@ namespace GridKit * system/component bases and both conversion ratios, the regulated * bus, required measurement signals, and attached optional reference * signals. Command-output assignment is optional. - * Operating-point feasibility is checked by initialize(). * * @return Number of configuration errors; zero when valid. */ @@ -216,10 +215,10 @@ namespace GridKit } }; - check_required_signal.template operator()("ibranchr"); - check_required_signal.template operator()("ibranchi"); - check_required_signal.template operator()("pbranch"); - check_required_signal.template operator()("qbranch"); + check_required_signal.template operator()("ir"); + check_required_signal.template operator()("ii"); + check_required_signal.template operator()("p"); + check_required_signal.template operator()("q"); check_required_signal.template operator()("freq"); auto check_optional_signal = [&](const char* name) @@ -235,7 +234,7 @@ namespace GridKit check_optional_signal.template operator()("freqref"); check_optional_signal.template operator()("vref"); check_optional_signal.template operator()("qref"); - check_optional_signal.template operator()("pplantref"); + check_optional_signal.template operator()("pref"); return ret; } @@ -285,23 +284,23 @@ namespace GridKit const ScalarT vr = Vr(); const ScalarT vi = Vi(); - const ScalarT ibranchr = - signals_.template readExternalVariable(); - const ScalarT ibranchi = - signals_.template readExternalVariable(); - const ScalarT pbranch_system = - signals_.template readExternalVariable(); - const ScalarT qbranch_system = - signals_.template readExternalVariable(); + const ScalarT ir = + signals_.template readExternalVariable(); + const ScalarT ii = + signals_.template readExternalVariable(); + const ScalarT p_system = + signals_.template readExternalVariable(); + const ScalarT q_system = + signals_.template readExternalVariable(); const ScalarT freq = signals_.template readExternalVariable(); auto is_finite = [](ScalarT value) { return std::isfinite(static_cast(value)); }; - if (!is_finite(vr) || !is_finite(vi) || !is_finite(ibranchr) - || !is_finite(ibranchi) || !is_finite(pbranch_system) - || !is_finite(qbranch_system) + if (!is_finite(vr) || !is_finite(vi) || !is_finite(ir) + || !is_finite(ii) || !is_finite(p_system) + || !is_finite(q_system) || !is_finite(freq) || !is_finite(qext0) || (Freqflag_ && !is_finite(pext0))) { @@ -309,19 +308,19 @@ namespace GridKit return 1; } - const ScalarT pbranch = toComponentBase(pbranch_system); - const ScalarT qbranch = toComponentBase(qbranch_system); + const ScalarT p = toComponentBase(p_system); + const ScalarT q = toComponentBase(q_system); - const ScalarT vldc_r = vr - Rc_ * ibranchr + Xc_ * ibranchi; - const ScalarT vldc_i = vi - Rc_ * ibranchi - Xc_ * ibranchr; + const ScalarT vldc_r = vr - Rc_ * ir + Xc_ * ii; + const ScalarT vldc_i = vi - Rc_ * ii - Xc_ * ir; const ScalarT v0 = std::sqrt(vr * vr + vi * vi); const ScalarT vldc0 = std::sqrt(vldc_r * vldc_r + vldc_i * vldc_i); - const ScalarT vdroop0 = v0 + Kc_ * qbranch; + const ScalarT vdroop0 = v0 + Kc_ * q; const ScalarT vctrl0 = vcomp_on_ * vldc0 + vcomp_off_ * vdroop0; const ScalarT vmeas0 = vctrl0; - const ScalarT qmeas0 = qbranch; - const ScalarT pmeas0 = pbranch; + const ScalarT qmeas0 = q; + const ScalarT pmeas0 = p; const ScalarT sfrz0 = Math::above(v0, Vfrz_); const ScalarT zero = static_cast(ZERO); @@ -375,19 +374,19 @@ namespace GridKit return 1; } - const ScalarT pext_output0 = Freqflag_ ? pext0_system : zero; - const ScalarT freqref0 = freq; - const ScalarT vref0 = vmeas0; - const ScalarT qref0_system = qbranch_system; - const ScalarT pplantref0_system = toSystemBase(pmeas0 - pfreq0); + const ScalarT pext_output0 = Freqflag_ ? pext0_system : zero; + const ScalarT freqref0 = freq; + const ScalarT vref0 = vmeas0; + const ScalarT qref0_system = q_system; + const ScalarT pref0_system = toSystemBase(pmeas0 - pfreq0); const bool candidates_are_finite = is_finite(qext0_system) && (!Freqflag_ || is_finite(pext0_system)) && is_finite(qext0) && (!Freqflag_ || is_finite(pext0)) - && is_finite(pbranch) - && is_finite(qbranch) + && is_finite(p) + && is_finite(q) && is_finite(vldc_r) && is_finite(vldc_i) && is_finite(v0) @@ -420,7 +419,7 @@ namespace GridKit && is_finite(freqref0) && is_finite(vref0) && is_finite(qref0_system) - && is_finite(pplantref0_system); + && is_finite(pref0_system); if (!candidates_are_finite) { Log::error() << "Repca: derived initial values must be finite\n"; @@ -450,10 +449,10 @@ namespace GridKit y[index(I::PPI)] = ppi0; y[index(I::PEXT)] = pext_output0; - freqref_set_ = freqref0; - vref_set_ = vref0; - qref_set_ = qref0_system; - pplantref_set_ = pplantref0_system; + freqref_set_ = freqref0; + vref_set_ = vref0; + qref_set_ = qref0_system; + pref_set_ = pref0_system; if (signals_.template isAttached()) { @@ -468,10 +467,10 @@ namespace GridKit { signals_.template writeExternalVariable(qref_set_); } - if (signals_.template isAttached()) + if (signals_.template isAttached()) { - signals_.template writeExternalVariable( - pplantref_set_); + signals_.template writeExternalVariable( + pref_set_); } y_.setDataUpdated(); @@ -530,28 +529,28 @@ namespace GridKit { using E = RepcaExternalVariables; - ws_[index(E::FREQREF)] = freqref_set_; - ws_[index(E::VREF)] = vref_set_; - ws_[index(E::QREF)] = qref_set_; - ws_[index(E::PPLANTREF)] = pplantref_set_; + ws_[index(E::FREQREF)] = freqref_set_; + ws_[index(E::VREF)] = vref_set_; + ws_[index(E::QREF)] = qref_set_; + ws_[index(E::PREF)] = pref_set_; std::fill(ws_indices_.begin(), ws_indices_.end(), INVALID_INDEX); - ws_[index(E::IBRANCHR)] = - signals_.template readExternalVariable(); - ws_indices_[index(E::IBRANCHR)] = - signals_.template readExternalVariableIndex(); - ws_[index(E::IBRANCHI)] = - signals_.template readExternalVariable(); - ws_indices_[index(E::IBRANCHI)] = - signals_.template readExternalVariableIndex(); - ws_[index(E::PBRANCH)] = - signals_.template readExternalVariable(); - ws_indices_[index(E::PBRANCH)] = - signals_.template readExternalVariableIndex(); - ws_[index(E::QBRANCH)] = - signals_.template readExternalVariable(); - ws_indices_[index(E::QBRANCH)] = - signals_.template readExternalVariableIndex(); + ws_[index(E::IR)] = + signals_.template readExternalVariable(); + ws_indices_[index(E::IR)] = + signals_.template readExternalVariableIndex(); + ws_[index(E::II)] = + signals_.template readExternalVariable(); + ws_indices_[index(E::II)] = + signals_.template readExternalVariableIndex(); + ws_[index(E::P)] = + signals_.template readExternalVariable(); + ws_indices_[index(E::P)] = + signals_.template readExternalVariableIndex(); + ws_[index(E::Q)] = + signals_.template readExternalVariable(); + ws_indices_[index(E::Q)] = + signals_.template readExternalVariableIndex(); ws_[index(E::FREQ)] = signals_.template readExternalVariable(); ws_indices_[index(E::FREQ)] = @@ -578,12 +577,12 @@ namespace GridKit ws_indices_[index(E::QREF)] = signals_.template readExternalVariableIndex(); } - if (signals_.template isAttached()) + if (signals_.template isAttached()) { - ws_[index(E::PPLANTREF)] = - signals_.template readExternalVariable(); - ws_indices_[index(E::PPLANTREF)] = - signals_.template readExternalVariableIndex(); + ws_[index(E::PREF)] = + signals_.template readExternalVariable(); + ws_indices_[index(E::PREF)] = + signals_.template readExternalVariableIndex(); } wb_[0] = Vr(); @@ -685,31 +684,31 @@ namespace GridKit const ScalarT vr = wb[0]; const ScalarT vi = wb[1]; - const ScalarT ibranchr = ws[index(E::IBRANCHR)]; - const ScalarT ibranchi = ws[index(E::IBRANCHI)]; - const ScalarT pbranch = toComponentBase(ws[index(E::PBRANCH)]); - const ScalarT qbranch = toComponentBase(ws[index(E::QBRANCH)]); - const ScalarT freq = ws[index(E::FREQ)]; - const ScalarT freqref = ws[index(E::FREQREF)]; - const ScalarT vref = ws[index(E::VREF)]; - const ScalarT qref = toComponentBase(ws[index(E::QREF)]); - const ScalarT pplantref = toComponentBase(ws[index(E::PPLANTREF)]); - - const ScalarT vldc_r = vr - Rc_ * ibranchr + Xc_ * ibranchi; - const ScalarT vldc_i = vi - Rc_ * ibranchi - Xc_ * ibranchr; + const ScalarT ir = ws[index(E::IR)]; + const ScalarT ii = ws[index(E::II)]; + const ScalarT p = toComponentBase(ws[index(E::P)]); + const ScalarT q = toComponentBase(ws[index(E::Q)]); + const ScalarT freq = ws[index(E::FREQ)]; + const ScalarT freqref = ws[index(E::FREQREF)]; + const ScalarT vref = ws[index(E::VREF)]; + const ScalarT qref = toComponentBase(ws[index(E::QREF)]); + const ScalarT pref_in = toComponentBase(ws[index(E::PREF)]); + + const ScalarT vldc_r = vr - Rc_ * ir + Xc_ * ii; + const ScalarT vldc_i = vi - Rc_ * ii - Xc_ * ir; const ScalarT pfreq = Ddn_ * Math::ramp(ef) - Dup_ * Math::ramp(-ef); f[index(I::VMEAS)] = -vmeas_dot + (vctrl - vmeas) / Tfltr_; - f[index(I::QMEAS)] = -qmeas_dot + (qbranch - qmeas) / Tfltr_; + f[index(I::QMEAS)] = -qmeas_dot + (q - qmeas) / Tfltr_; f[index(I::XQPI)] = -xqpi_dot + sfrz * Math::antiwindup(qpi, Ki_ * erqlim, Qmin_, Qmax_); f[index(I::XQLAG)] = -xqlag_dot + (qpi - xqlag) / Tfv_; - f[index(I::PMEAS)] = -pmeas_dot + (pbranch - pmeas) / Tp_; + f[index(I::PMEAS)] = -pmeas_dot + (p - pmeas) / Tp_; f[index(I::XPPI)] = -xppi_dot + Math::antiwindup(ppi, Kig_ * eplim, Pmin_, Pmax_); f[index(I::PREF)] = -pref_dot + (ppi - pref) / Tlag_; f[index(I::V)] = -v * v + vr * vr + vi * vi; f[index(I::VLDC)] = -vldc * vldc + vldc_r * vldc_r + vldc_i * vldc_i; - f[index(I::VDROOP)] = -vdroop + v + Kc_ * qbranch; + f[index(I::VDROOP)] = -vdroop + v + Kc_ * q; f[index(I::VCTRL)] = -vctrl + vcomp_on_ * vldc + vcomp_off_ * vdroop; f[index(I::SFRZ)] = -sfrz + Math::above(v, Vfrz_); f[index(I::ERQ)] = -erq + ref_on_ * (vref - vmeas) + ref_off_ * (qref - qmeas); @@ -719,7 +718,7 @@ namespace GridKit f[index(I::QEXT)] = -Tfv_ * (qext - xqlag) + Tft_ * (qpi - xqlag); f[index(I::EF)] = -ef + Math::deadband2(freqref - freq, fdbd1_, fdbd2_); - f[index(I::EP)] = -ep + pplantref - pmeas + pfreq; + f[index(I::EP)] = -ep + pref_in - pmeas + pfreq; f[index(I::EPLIM)] = -eplim + Math::clamp(ep, femin_, femax_); f[index(I::PPI)] = -ppi + Math::clamp(Kpg_ * eplim + xppi, Pmin_, Pmax_); f[index(I::PEXT)] = -pext + freq_on_ * pref; diff --git a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp index 9d9bde040..283e437bb 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp @@ -494,29 +494,29 @@ namespace GridKit auto* repca = new Repca(bus, repcadata); - if (repcadata.signal_inputs.contains(RepcaSignalInputs::ibranchr)) + if (repcadata.signal_inputs.contains(RepcaSignalInputs::ir)) { - const IdxT ibranchr = repcadata.signal_inputs.at(RepcaSignalInputs::ibranchr); - constexpr auto IBRANCHR = RepcaExternalVariables::IBRANCHR; - repca->getSignals().template attachSignalNode(getSignal(ibranchr)); + const IdxT ir = repcadata.signal_inputs.at(RepcaSignalInputs::ir); + constexpr auto IR = RepcaExternalVariables::IR; + repca->getSignals().template attachSignalNode(getSignal(ir)); } - if (repcadata.signal_inputs.contains(RepcaSignalInputs::ibranchi)) + if (repcadata.signal_inputs.contains(RepcaSignalInputs::ii)) { - const IdxT ibranchi = repcadata.signal_inputs.at(RepcaSignalInputs::ibranchi); - constexpr auto IBRANCHI = RepcaExternalVariables::IBRANCHI; - repca->getSignals().template attachSignalNode(getSignal(ibranchi)); + const IdxT ii = repcadata.signal_inputs.at(RepcaSignalInputs::ii); + constexpr auto II = RepcaExternalVariables::II; + repca->getSignals().template attachSignalNode(getSignal(ii)); } - if (repcadata.signal_inputs.contains(RepcaSignalInputs::pbranch)) + if (repcadata.signal_inputs.contains(RepcaSignalInputs::p)) { - const IdxT pbranch = repcadata.signal_inputs.at(RepcaSignalInputs::pbranch); - constexpr auto PBRANCH = RepcaExternalVariables::PBRANCH; - repca->getSignals().template attachSignalNode(getSignal(pbranch)); + const IdxT p = repcadata.signal_inputs.at(RepcaSignalInputs::p); + constexpr auto P = RepcaExternalVariables::P; + repca->getSignals().template attachSignalNode

(getSignal(p)); } - if (repcadata.signal_inputs.contains(RepcaSignalInputs::qbranch)) + if (repcadata.signal_inputs.contains(RepcaSignalInputs::q)) { - const IdxT qbranch = repcadata.signal_inputs.at(RepcaSignalInputs::qbranch); - constexpr auto QBRANCH = RepcaExternalVariables::QBRANCH; - repca->getSignals().template attachSignalNode(getSignal(qbranch)); + const IdxT q = repcadata.signal_inputs.at(RepcaSignalInputs::q); + constexpr auto Q = RepcaExternalVariables::Q; + repca->getSignals().template attachSignalNode(getSignal(q)); } if (repcadata.signal_inputs.contains(RepcaSignalInputs::freq)) { @@ -542,11 +542,11 @@ namespace GridKit constexpr auto QREF = RepcaExternalVariables::QREF; repca->getSignals().template attachSignalNode(getSignal(qref)); } - if (repcadata.signal_inputs.contains(RepcaSignalInputs::pplantref)) + if (repcadata.signal_inputs.contains(RepcaSignalInputs::pref)) { - const IdxT pplantref = repcadata.signal_inputs.at(RepcaSignalInputs::pplantref); - constexpr auto PPLANTREF = RepcaExternalVariables::PPLANTREF; - repca->getSignals().template attachSignalNode(getSignal(pplantref)); + const IdxT pref = repcadata.signal_inputs.at(RepcaSignalInputs::pref); + constexpr auto PREF = RepcaExternalVariables::PREF; + repca->getSignals().template attachSignalNode(getSignal(pref)); } if (repcadata.signal_outputs.contains(RepcaSignalOutputs::qext)) { diff --git a/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp b/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp index de1075c44..dfb7a1a3f 100644 --- a/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp @@ -149,10 +149,10 @@ namespace GridKit PhasorDynamics::BusInfinite bus( static_cast(1.0), static_cast(0.0)); - PhasorDynamics::SignalNode ibranchr; - PhasorDynamics::SignalNode ibranchi; - PhasorDynamics::SignalNode pbranch; - PhasorDynamics::SignalNode qbranch; + PhasorDynamics::SignalNode ir; + PhasorDynamics::SignalNode ii; + PhasorDynamics::SignalNode p; + PhasorDynamics::SignalNode q; ScalarT freq_value = static_cast(1.0); IdxT freq_index = INVALID_INDEX; @@ -184,16 +184,16 @@ namespace GridKit PhasorDynamics::Converter::Repca plant(&bus, plant_data); auto& converter_signals = converter.getSignals(); - converter_signals.template assignSignalNode(&ibranchr); - converter_signals.template assignSignalNode(&ibranchi); - converter_signals.template assignSignalNode(&pbranch); - converter_signals.template assignSignalNode(&qbranch); + converter_signals.template assignSignalNode(&ir); + converter_signals.template assignSignalNode(&ii); + converter_signals.template assignSignalNode(&p); + converter_signals.template assignSignalNode(&q); auto& plant_signals = plant.getSignals(); - plant_signals.template attachSignalNode(&ibranchr); - plant_signals.template attachSignalNode(&ibranchi); - plant_signals.template attachSignalNode(&pbranch); - plant_signals.template attachSignalNode(&qbranch); + plant_signals.template attachSignalNode(&ir); + plant_signals.template attachSignalNode(&ii); + plant_signals.template attachSignalNode(&p); + plant_signals.template attachSignalNode(&q); plant_signals.template attachSignalNode(&freq); system.addBus(&bus); @@ -201,18 +201,18 @@ namespace GridKit system.addComponent(&plant); success *= system.allocate() == 0; - success *= ibranchr.linked(); - success *= ibranchi.linked(); - success *= pbranch.linked(); - success *= qbranch.linked(); + success *= ir.linked(); + success *= ii.linked(); + success *= p.linked(); + success *= q.linked(); success *= system.initialize() == 0; success *= system.evaluateResidual() == 0; // At zero power the converter draws no branch current or power. - success *= isEqual(ibranchr.read(), static_cast(0.0), kTol); - success *= isEqual(ibranchi.read(), static_cast(0.0), kTol); - success *= isEqual(pbranch.read(), static_cast(0.0), kTol); - success *= isEqual(qbranch.read(), static_cast(0.0), kTol); + success *= isEqual(ir.read(), static_cast(0.0), kTol); + success *= isEqual(ii.read(), static_cast(0.0), kTol); + success *= isEqual(p.read(), static_cast(0.0), kTol); + success *= isEqual(q.read(), static_cast(0.0), kTol); const auto* residual = plant.getResidual().getData(); for (IdxT row = 0; row < plant.size(); ++row) diff --git a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp index 4c52253fe..4a331446a 100644 --- a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp @@ -39,13 +39,14 @@ namespace GridKit ConverterRepcaTests() = default; ~ConverterRepcaTests() = default; - // A command initialized exactly at a limit leaves the widest residual - // tail: QPI at 708 eps. - static constexpr RealT kBehaviorTol = - static_cast(1000.0) * std::numeric_limits::epsilon(); + // Every probe state clears its smooth transitions by a margin, so the + // widest gap to an ideal expected value is the floored-lag PREF row at + // 3.2 eps. + static constexpr RealT kTol = + static_cast(10.0) * std::numeric_limits::epsilon(); // The widest Enzyme-versus-dependency-tracking gap is QMEAS at the - // qbranch column, 0.7 eps. + // q column, 0.7 eps. static constexpr RealT kJacobianTol = static_cast(10.0) * std::numeric_limits::epsilon(); @@ -188,15 +189,15 @@ namespace GridKit PhasorDynamics::Converter::Repca busless(nullptr, makeData()); success *= (busless.verify() > 0); - success *= unlinkedSignalRejected(); - success *= unlinkedSignalRejected(); - success *= unlinkedSignalRejected(); - success *= unlinkedSignalRejected(); + success *= unlinkedSignalRejected(); + success *= unlinkedSignalRejected(); + success *= unlinkedSignalRejected(); + success *= unlinkedSignalRejected(); success *= unlinkedSignalRejected(); success *= unlinkedSignalRejected(); success *= unlinkedSignalRejected(); success *= unlinkedSignalRejected(); - success *= unlinkedSignalRejected(); + success *= unlinkedSignalRejected(); auto floor_data = makeInitializationData(); floor_data.parameters[Params::Tfltr] = 0.0; @@ -208,7 +209,7 @@ namespace GridKit setInitializationInputs(floored); success *= floored.initialize(0.25, 0.45); success *= (floored.repca.evaluateResidual() == 0); - success *= allResidualsZero(floored.repca); + success *= allResidualsWithinInitTolerance(floored.repca); auto* y = floored.repca.y().getData(); y[index(Vars::VMEAS)] -= 0.001; @@ -257,26 +258,24 @@ namespace GridKit {Vars::ERQLIM, 0.0}, {Vars::QPI, 0.5}, {Vars::QEXT, 0.25}, - {Vars::EF, -0.00024949607977392432}, + {Vars::EF, 0.0}, {Vars::EP, 0.0}, {Vars::EPLIM, 0.0}, {Vars::PPI, 0.9}, {Vars::PEXT, 0.45}}, "initialization"); - success *= scalarPreserved(fixture.input(Ext::IBRANCHR), 0.2, "preserved ibranchr"); - success *= scalarPreserved(fixture.input(Ext::IBRANCHI), -0.1, "preserved ibranchi"); - success *= scalarPreserved(fixture.input(Ext::PBRANCH), 0.4, "preserved pbranch"); - success *= scalarPreserved(fixture.input(Ext::QBRANCH), 0.1, "preserved qbranch"); + success *= scalarPreserved(fixture.input(Ext::IR), 0.2, "preserved ir"); + success *= scalarPreserved(fixture.input(Ext::II), -0.1, "preserved ii"); + success *= scalarPreserved(fixture.input(Ext::P), 0.4, "preserved p"); + success *= scalarPreserved(fixture.input(Ext::Q), 0.1, "preserved q"); success *= scalarPreserved(fixture.input(Ext::FREQ), 0.99, "preserved freq"); success *= scalarPreserved(fixture.qext(), 0.25, "preserved qext"); success *= scalarPreserved(fixture.pext(), 0.45, "preserved pext"); success *= scalarMatches(fixture.input(Ext::FREQREF), 0.99, "published freqref"); success *= scalarMatches(fixture.input(Ext::VREF), 0.99200050403213, "published vref"); success *= scalarMatches(fixture.input(Ext::QREF), 0.1, "published qref"); - success *= scalarMatches(fixture.input(Ext::PPLANTREF), - 0.39874213184871782, - "published pplantref"); + success *= scalarMatches(fixture.input(Ext::PREF), 0.4, "published pref"); for (size_t row = 0; row < index(Vars::MAXIMUM); ++row) { @@ -305,7 +304,7 @@ namespace GridKit {{0.25, 0.45, 0.99200050403213, 0.2, 0.8}}, "initialization"); - success *= allResidualsZero(fixture.repca); + success *= allResidualsWithinInitTolerance(fixture.repca); { auto exact_data = makeInitializationData(); @@ -320,7 +319,7 @@ namespace GridKit success *= scalarPreserved(exact_commands.pext(), 0.45, "pext signal"); success *= scalarPreserved(exact_commands.input(Ext::QREF), 0.1, "qref signal"); success *= (exact_commands.repca.evaluateResidual() == 0); - success *= allResidualsZero(exact_commands.repca); + success *= allResidualsWithinInitTolerance(exact_commands.repca); } Fixture fallback(makeInitializationData(), 0.8, 0.6); @@ -328,7 +327,7 @@ namespace GridKit setInitializationInputs(fallback); success *= fallback.initialize(0.25, 0.45); success *= (fallback.repca.evaluateResidual() == 0); - success *= allResidualsZero(fallback.repca); + success *= allResidualsWithinInitTolerance(fallback.repca); Fixture outputless(makeInitializationData(), 0.8, @@ -346,7 +345,7 @@ namespace GridKit success *= monitorMatches(outputless.repca, {{0.25, 0.45, 0.99200050403213, 0.2, 0.8}}, "unassigned command outputs"); - success *= allResidualsZero(outputless.repca); + success *= allResidualsWithinInitTolerance(outputless.repca); struct FlagCase { @@ -391,7 +390,7 @@ namespace GridKit test_case.label); success *= scalarMatches(scenario.qext(), 0.25, test_case.label); success *= scalarMatches(scenario.pext(), test_case.pext, test_case.label); - success *= allResidualsZero(scenario.repca); + success *= allResidualsWithinInitTolerance(scenario.repca); } return success.report(__func__); @@ -418,9 +417,9 @@ namespace GridKit const RealT infinity = std::numeric_limits::infinity(); const std::array rejection_cases{{ {"qext below Qmin", -0.4000000001, 0.45}, - {"qext above Qmax", 0.4500000001, 0.45}, + {"qext above Qmax", 0.7500000001, 0.45}, {"pext below Pmin", 0.25, -1.0e-10}, - {"pext above Pmax", 0.25, 0.6000000001}, + {"pext above Pmax", 0.25, 1.0000000001}, {"nonfinite qext", infinity, 0.45}, {"nonfinite pext", 0.25, infinity}, {"nan qext", nan, 0.45}, @@ -433,10 +432,10 @@ namespace GridKit test_case.pext, test_case.label); } - for (const Ext port : {Ext::IBRANCHR, - Ext::IBRANCHI, - Ext::PBRANCH, - Ext::QBRANCH, + for (const Ext port : {Ext::IR, + Ext::II, + Ext::P, + Ext::Q, Ext::FREQ}) { for (const RealT value : {infinity, -infinity, nan}) @@ -487,16 +486,19 @@ namespace GridKit 0.45, "nonzero gated reactive-power PI state rate at the freeze transition", NonfiniteTarget::NONE, - 0.7, + 0.2, 0.0); + auto frozen_data = reactive_aw_data; + frozen_data.parameters[Params::Vfrz] = 0.9; + { - Fixture frozen_reactive_rate(reactive_aw_data, 0.5, 0.0); + Fixture frozen_reactive_rate(frozen_data, 0.05, 0.0); frozen_reactive_rate.attachAllInputs(); setInitializationInputs(frozen_reactive_rate); success *= frozen_reactive_rate.initialize(0.25, 0.45); success *= (frozen_reactive_rate.repca.evaluateResidual() == 0); - success *= allResidualsZero(frozen_reactive_rate.repca); + success *= allResidualsWithinInitTolerance(frozen_reactive_rate.repca); } auto active_aw_data = makeInitializationData(); @@ -534,42 +536,43 @@ namespace GridKit success *= vectorUnchanged(invalid_fixture.repca.yp(), invalid_yp, "derivative"); } + // A command exactly on a limit is reconstructed through the offset + // branch of the limiter inverse, which leaves a smoothing-scaled + // residual, so only the reconstructed state is checked. { Fixture qmax_pmin_boundary(data, 0.8, 0.6); qmax_pmin_boundary.attachAllInputs(); setInitializationInputs(qmax_pmin_boundary); - success *= qmax_pmin_boundary.initialize(0.45, 0.0); + success *= qmax_pmin_boundary.initialize(0.75, 0.0); success *= (qmax_pmin_boundary.repca.evaluateResidual() == 0); success *= stateMatches(qmax_pmin_boundary.repca, - {{Vars::QPI, 0.9}, - {Vars::XQLAG, 0.9}, - {Vars::XQPI, 1.0}, - {Vars::QEXT, 0.45}, + {{Vars::QPI, 1.5}, + {Vars::XQLAG, 1.5}, + {Vars::XQPI, 1.6}, + {Vars::QEXT, 0.75}, {Vars::PREF, 0.0}, {Vars::PPI, 0.0}, {Vars::XPPI, -0.1}, {Vars::PEXT, 0.0}}, "Qmax/Pmin command boundary"); - success *= allResidualsZero(qmax_pmin_boundary.repca); } { Fixture qmin_pmax_boundary(data, 0.8, 0.6); qmin_pmax_boundary.attachAllInputs(); setInitializationInputs(qmin_pmax_boundary); - success *= qmin_pmax_boundary.initialize(-0.4, 0.6); + success *= qmin_pmax_boundary.initialize(-0.4, 1.0); success *= (qmin_pmax_boundary.repca.evaluateResidual() == 0); success *= stateMatches(qmin_pmax_boundary.repca, {{Vars::QPI, -0.8}, {Vars::XQLAG, -0.8}, {Vars::XQPI, -0.9}, {Vars::QEXT, -0.4}, - {Vars::PREF, 1.2}, - {Vars::PPI, 1.2}, - {Vars::XPPI, 1.3}, - {Vars::PEXT, 0.6}}, + {Vars::PREF, 2.0}, + {Vars::PPI, 2.0}, + {Vars::XPPI, 2.1}, + {Vars::PEXT, 1.0}}, "Qmin/Pmax command boundary"); - success *= allResidualsZero(qmin_pmax_boundary.repca); } { @@ -588,7 +591,7 @@ namespace GridKit {Vars::PPI, 0.9}, {Vars::PEXT, 0.45}}, "collapsed Q/P limits"); - success *= allResidualsZero(collapsed_limits.repca); + success *= allResidualsWithinInitTolerance(collapsed_limits.repca); } return success.report(__func__); @@ -608,28 +611,28 @@ namespace GridKit success *= (fixture.repca.evaluateResidual() == 0); const std::array expected{{ - {Vars::VMEAS, "VMEAS", 0.19000000000000017}, - {Vars::QMEAS, "QMEAS", 0.77000000000000013}, - {Vars::XQPI, "XQPI", 0.018000000000000002}, - {Vars::XQLAG, "XQLAG", 0.12666666666666668}, - {Vars::PMEAS, "PMEAS", 0.92999999999999983}, - {Vars::XPPI, "XPPI", 0.082000000000000003}, - {Vars::PREF, "PREF", 0.070000000000000104}, - {Vars::V, "V", -0.02999999999999993}, - {Vars::VLDC, "VLDC", -0.015651160000000081}, - {Vars::VDROOP, "VDROOP", 0.053999999999999965}, - {Vars::VCTRL, "VCTRL", -0.030000000000000027}, - {Vars::SFRZ, "SFRZ", 0.19999999999999996}, - {Vars::ERQ, "ERQ", 2.7755575615628914e-17}, - {Vars::ERQDB, "ERQDB", -0.047111912348473055}, - {Vars::ERQLIM, "ERQLIM", 0.030000000000000044}, - {Vars::QPI, "QPI", -0.16000000000000009}, - {Vars::QEXT, "QEXT", -0.36399999999999999}, - {Vars::EF, "EF", -0.0089371400000009833}, - {Vars::EP, "EP", 0.59036181730064152}, - {Vars::EPLIM, "EPLIM", 0.049999999999999968}, - {Vars::PPI, "PPI", -0.38200000000000006}, - {Vars::PEXT, "PEXT", -0.62}, + {Vars::VMEAS, "VMEAS", 0.4}, + {Vars::QMEAS, "QMEAS", 0.45}, + {Vars::XQPI, "XQPI", 0.3}, + {Vars::XQLAG, "XQLAG", 0.46}, + {Vars::PMEAS, "PMEAS", 0.5}, + {Vars::XPPI, "XPPI", -0.3}, + {Vars::PREF, "PREF", 0.4}, + {Vars::V, "V", -1.28}, + {Vars::VLDC, "VLDC", -0.0075}, + {Vars::VDROOP, "VDROOP", 0.1}, + {Vars::VCTRL, "VCTRL", 0.05}, + {Vars::SFRZ, "SFRZ", 0.5}, + {Vars::ERQ, "ERQ", -0.63}, + {Vars::ERQDB, "ERQDB", 0.75}, + {Vars::ERQLIM, "ERQLIM", -0.35}, + {Vars::QPI, "QPI", -0.05}, + {Vars::QEXT, "QEXT", -1.345}, + {Vars::EF, "EF", -0.015}, + {Vars::EP, "EP", 0.4}, + {Vars::EPLIM, "EPLIM", 1.1}, + {Vars::PPI, "PPI", 0.1}, + {Vars::PEXT, "PEXT", 0.05}, }}; success *= (static_cast(fixture.repca.getResidual().getSize()) == expected.size()); @@ -703,24 +706,37 @@ namespace GridKit setAnswerKeyInputs(fixture); success *= fixture.prepare(0.0, 0.0); - const std::array freeze_cases{{ - {0.6, 3.7751357595539048e-11}, - {0.7, 0.5}, - {0.8, 0.99999999996224864}, - }}; - for (const auto& test_case : freeze_cases) + // A voltage of zero must clear the freeze threshold by the same + // margin the enabled probe clears it, so the threshold is raised. { - setState(fixture.repca, {{Vars::V, test_case.input}, {Vars::SFRZ, 0.0}}); - success *= (fixture.repca.evaluateResidual() == 0); - success *= residualsMatch(fixture.repca, {{Vars::SFRZ, test_case.expected}}, "freeze gate"); + auto freeze_data = makeResidualData(); + freeze_data.parameters[Params::Vfrz] = 0.8; + + Fixture freeze(freeze_data); + freeze.attachAllInputs(); + setAnswerKeyInputs(freeze); + success *= freeze.prepare(0.0, 0.0); + + const std::array freeze_cases{{ + {0.0, 0.0}, + {1.6, 1.0}, + }}; + for (const auto& test_case : freeze_cases) + { + setState(freeze.repca, {{Vars::V, test_case.input}, {Vars::SFRZ, 0.0}}); + success *= (freeze.repca.evaluateResidual() == 0); + success *= residualsMatch(freeze.repca, + {{Vars::SFRZ, test_case.expected}}, + "freeze gate"); + } } - const std::array deadband_cases{{ - {-0.05, -0.030003109594436028}, - {-0.02, -0.002888087651526948}, - {0.0, -3.104066702683552e-5}, - {0.03, 0.002888087651526948}, - {0.06, 0.030003109594436025}, + // The interior probe sits at the midpoint of the band, where the + // smooth deadband cancels exactly. + const std::array deadband_cases{{ + {-0.82, -0.8}, + {0.005, 0.0}, + {0.83, 0.8}, }}; for (const auto& test_case : deadband_cases) { @@ -731,12 +747,10 @@ namespace GridKit "reactive-power deadband"); } - const std::array error_limit_cases{{ - {-1.0, -0.7}, - {-0.7, -0.69711188674766689}, - {0.2, 0.2}, - {0.8, 0.79711188674766698}, - {1.0, 0.8}, + const std::array error_limit_cases{{ + {-1.5, -0.7}, + {0.05, 0.05}, + {1.6, 0.8}, }}; for (const auto& test_case : error_limit_cases) { @@ -747,12 +761,10 @@ namespace GridKit "reactive-power error limit"); } - const std::array command_limit_cases{{ - {-1.0, -0.8}, - {-0.8, -0.79711188674766698}, - {0.2, 0.2}, - {0.9, 0.89711188674766706}, - {1.1, 0.9}, + const std::array command_limit_cases{{ + {-1.6, -0.8}, + {0.05, 0.05}, + {1.7, 0.9}, }}; for (const auto& test_case : command_limit_cases) { @@ -766,15 +778,15 @@ namespace GridKit "reactive-power command limit"); } - const std::array antiwindup_cases{{ - {-0.9, -0.1, -1.1325407278661714e-11}, - {-0.9, 0.1, 0.3}, - {-0.8, -0.1, -0.15}, - {-0.8, 0.1, 0.3}, - {0.9, -0.1, -0.3}, - {0.9, 0.1, 0.15}, - {1.0, -0.1, -0.3}, - {1.0, 0.1, 1.1325407278661714e-11}, + // Saturated probes sit beyond their limit by a margin, so a blocked + // gate contributes nothing and an admitted gate passes the full rate. + const std::array antiwindup_cases{{ + {-1.6, -0.4, 0.0}, + {-1.6, 0.4, 1.2}, + {0.05, -0.4, -1.2}, + {0.05, 0.4, 1.2}, + {1.7, -0.4, -1.2}, + {1.7, 0.4, 0.0}, }}; for (const auto& test_case : antiwindup_cases) { @@ -796,35 +808,36 @@ namespace GridKit setDerivative(fixture.repca, {{Vars::XQLAG, -0.04}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{Vars::XQLAG, 0.12666666666666668}, - {Vars::QEXT, -0.36399999999999999}}, + {{Vars::XQLAG, 0.092}, + {Vars::QEXT, -0.624}}, "reactive-command lead-lag"); - // The command sits at Qmax with the error at emax driving further - // out, so the antiwindup gate must block the PI state. + // The command sits beyond Qmax with the error driving further out, + // so the blocked gate leaves the PI state with no sensitivity to the + // gate, the error, or the command. { Fixture blocked(makeResidualData()); blocked.attachAllInputs(); setAnswerKeyInputs(blocked); success *= blocked.prepare(0.0, 0.0); setState(blocked.repca, - {{Vars::QPI, 0.9}, {Vars::ERQLIM, 0.8}, {Vars::SFRZ, 1.0}}); + {{Vars::QPI, 1.7}, {Vars::ERQLIM, 0.4}, {Vars::SFRZ, 1.0}}); setDerivative(blocked.repca, {{Vars::XQPI, 0.0}}); numberVariables(blocked, 1.0); success *= (blocked.repca.evaluateResidual() == 0); const JacobianRow expected{ {index(Vars::XQPI), -1.0}, - {index(Vars::SFRZ), 1.2}, - {index(Vars::ERQLIM), 1.5}, - {index(Vars::QPI), -144.0}, + {index(Vars::SFRZ), 0.0}, + {index(Vars::ERQLIM), 0.0}, + {index(Vars::QPI), 0.0}, }; success *= jacobianRowMatches( blocked.repca.getResidual().getData()[index(Vars::XQPI)].getDependencies(), expected, index(Vars::XQPI), "blocked reactive-power antiwindup", - kBehaviorTol); + kTol); } return success.report(__func__); @@ -866,12 +879,12 @@ namespace GridKit setAnswerKeyInputs(fixture); success *= fixture.prepare(0.0, 0.0); - const std::array frequency_deadband_cases{{ - {-0.05, -0.040000281494001103}, - {-0.01, -0.0028777978975925616}, - {0.0, -0.00024949607977392432}, - {0.015, 0.0028777978975925616}, - {0.05, 0.035000934519396246}, + // The interior probe sits at the midpoint of the band, where the + // smooth deadband cancels exactly. + const std::array frequency_deadband_cases{{ + {-0.81, -0.8}, + {0.0025, 0.0}, + {0.815, 0.8}, }}; for (const auto& test_case : frequency_deadband_cases) { @@ -884,12 +897,11 @@ namespace GridKit "frequency deadband"); } - const std::array droop_cases{{ - {-0.1, -0.099999999999842701}, - {0.0, 0.0028881132523331052}, - {0.1, 0.2000000000001573}, + const std::array droop_cases{{ + {-0.9, -0.9}, + {0.9, 1.8}, }}; - fixture.input(Ext::PPLANTREF) = 0.2; + fixture.input(Ext::PREF) = 0.2; for (const auto& test_case : droop_cases) { setState(fixture.repca, @@ -902,12 +914,10 @@ namespace GridKit "frequency droop"); } - const std::array error_limit_cases{{ - {-1.0, -0.5}, - {-0.5, -0.49711188674766688}, - {0.2, 0.2}, - {0.6, 0.59711188674766702}, - {1.0, 0.6}, + const std::array error_limit_cases{{ + {-1.3, -0.5}, + {0.05, 0.05}, + {1.4, 0.6}, }}; for (const auto& test_case : error_limit_cases) { @@ -918,12 +928,10 @@ namespace GridKit "active-power error limit"); } - const std::array command_limit_cases{{ - {-0.2, 5.9381836780872301e-24}, - {0.0, 0.0028881132523331052}, - {0.4, 0.4}, - {1.2, 1.1971118867476669}, - {1.4, 1.2}, + const std::array command_limit_cases{{ + {-0.8, 0.0}, + {1.0, 1.0}, + {2.8, 2.0}, }}; for (const auto& test_case : command_limit_cases) { @@ -937,15 +945,15 @@ namespace GridKit "active-power command limit"); } - const std::array antiwindup_cases{{ - {-0.1, -0.1, -6.7952443671970281e-12}, - {-0.1, 0.1, 0.18}, - {0.0, -0.1, -0.09}, - {0.0, 0.1, 0.18}, - {1.2, -0.1, -0.18}, - {1.2, 0.1, 0.09}, - {1.3, -0.1, -0.18}, - {1.3, 0.1, 6.7952443671970281e-12}, + // Saturated probes sit beyond their limit by a margin, so a blocked + // gate contributes nothing and an admitted gate passes the full rate. + const std::array antiwindup_cases{{ + {-0.8, -0.5, 0.0}, + {-0.8, 0.5, 0.9}, + {1.0, -0.5, -0.9}, + {1.0, 0.5, 0.9}, + {2.8, -0.5, -0.9}, + {2.8, 0.5, 0.0}, }}; for (const auto& test_case : antiwindup_cases) { @@ -963,7 +971,7 @@ namespace GridKit setDerivative(fixture.repca, {{Vars::PREF, 0.05}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{Vars::PREF, 0.070000000000000104}}, + {{Vars::PREF, 0.07}}, "active-power command lag"); return success.report(__func__); @@ -980,22 +988,22 @@ namespace GridKit success *= fixture.prepare(0.0, 0.0); setAnswerKeyState(fixture.repca); setDerivative(fixture.repca, - {{Vars::VMEAS, 0.11}, - {Vars::QMEAS, -0.12}, - {Vars::XQPI, 0.13}, - {Vars::XQLAG, -0.14}, - {Vars::PMEAS, 0.15}, - {Vars::XPPI, -0.16}, - {Vars::PREF, 0.17}}); + {{Vars::VMEAS, 0.2}, + {Vars::QMEAS, -0.1}, + {Vars::XQPI, 0.4}, + {Vars::XQLAG, -0.3}, + {Vars::PMEAS, 0.6}, + {Vars::XPPI, -0.5}, + {Vars::PREF, 0.8}}); success *= (fixture.repca.evaluateResidual() == 0); success *= residualsMatch(fixture.repca, - {{Vars::VMEAS, 0.09000000000000018}, - {Vars::QMEAS, 0.8700000000000001}, - {Vars::XQPI, -0.082}, - {Vars::XQLAG, 0.22666666666666668}, - {Vars::PMEAS, 0.7999999999999998}, - {Vars::XPPI, 0.232}, - {Vars::PREF, -0.04999999999999989}}, + {{Vars::VMEAS, 0.3}, + {Vars::QMEAS, 0.35}, + {Vars::XQPI, 0.2}, + {Vars::XQLAG, 0.36}, + {Vars::PMEAS, 0.4}, + {Vars::XPPI, -0.4}, + {Vars::PREF, 0.3}}, "explicit derivatives"); return success.report(__func__); @@ -1007,15 +1015,13 @@ namespace GridKit { TestStatus success = true; - // The fixed state places ERQ exactly at dbdupper, so the key also - // covers the CommonMath deadband transition derivative. const auto data = makeResidualData(); const auto dependency = dependencyTrackingJacobian(data, success); success *= jacobianMatches(dependency, expectedJacobian(), "dependency tracking", - kBehaviorTol); + kTol); auto all_flags_off_data = data; all_flags_off_data.parameters[Params::VcompFlag] = false; @@ -1026,14 +1032,14 @@ namespace GridKit success *= jacobianMatches(all_flags_off, expectedJacobianAllFlagsOff(), "all-flags-off dependency tracking", - kBehaviorTol); + kTol); const auto nonunit_alpha_dependency = dependencyTrackingJacobian(data, success, kNonunitAlpha); success *= jacobianMatches(nonunit_alpha_dependency, expectedJacobianNonunitAlpha(), "non-unit-alpha dependency tracking", - kBehaviorTol); + kTol); return success.report(__func__); } @@ -1178,10 +1184,10 @@ namespace GridKit } auto& signals = repca.getSignals(); - signals.template attachSignalNode(&input_nodes_[index(Ext::IBRANCHR)]); - signals.template attachSignalNode(&input_nodes_[index(Ext::IBRANCHI)]); - signals.template attachSignalNode(&input_nodes_[index(Ext::PBRANCH)]); - signals.template attachSignalNode(&input_nodes_[index(Ext::QBRANCH)]); + signals.template attachSignalNode(&input_nodes_[index(Ext::IR)]); + signals.template attachSignalNode(&input_nodes_[index(Ext::II)]); + signals.template attachSignalNode(&input_nodes_[index(Ext::P)]); + signals.template attachSignalNode(&input_nodes_[index(Ext::Q)]); signals.template attachSignalNode(&input_nodes_[index(Ext::FREQ)]); } @@ -1193,7 +1199,7 @@ namespace GridKit signals.template attachSignalNode(&input_nodes_[index(Ext::FREQREF)]); signals.template attachSignalNode(&input_nodes_[index(Ext::VREF)]); signals.template attachSignalNode(&input_nodes_[index(Ext::QREF)]); - signals.template attachSignalNode(&input_nodes_[index(Ext::PPLANTREF)]); + signals.template attachSignalNode(&input_nodes_[index(Ext::PREF)]); } void setCommands(RealT qext, RealT pext) @@ -1327,6 +1333,9 @@ namespace GridKit return data; } + /// Distinct nonzero values for every parameter. The limiter bands are + /// wide enough, and the lag reciprocals exact enough, for probe states + /// to clear every smooth transition on an exact decimal. Data makeResidualData() const { auto data = makeData(); @@ -1344,7 +1353,7 @@ namespace GridKit data.parameters[Params::Qmax] = 0.9; data.parameters[Params::Qmin] = -0.8; data.parameters[Params::Tft] = 0.2; - data.parameters[Params::Tfv] = 1.5; + data.parameters[Params::Tfv] = 2.5; data.parameters[Params::Tp] = 0.4; data.parameters[Params::fdbd1] = -0.01; data.parameters[Params::fdbd2] = 0.015; @@ -1354,16 +1363,23 @@ namespace GridKit data.parameters[Params::femin] = -0.5; data.parameters[Params::Kpg] = 1.7; data.parameters[Params::Kig] = 1.8; - data.parameters[Params::Pmax] = 1.2; + data.parameters[Params::Pmax] = 2.0; data.parameters[Params::Tlag] = 0.5; return data; } + /// Both deadbands and both error limits are symmetric and the droop + /// gains are equal, so an operating point with no error reconstructs + /// exactly; the commands and the freeze threshold clear their limits. Data makeInitializationData() const { auto data = makeResidualData(); + data.parameters[Params::Vfrz] = 0.2; data.parameters[Params::dbdupper] = 0.02; data.parameters[Params::emin] = -0.8; + data.parameters[Params::Qmax] = 1.5; + data.parameters[Params::fdbd1] = -0.015; + data.parameters[Params::Dup] = 2.0; data.parameters[Params::femin] = -0.6; return data; } @@ -1371,61 +1387,64 @@ namespace GridKit template void setInitializationInputs(Fixture& fixture) const { - fixture.input(Ext::IBRANCHR) = static_cast(0.2); - fixture.input(Ext::IBRANCHI) = static_cast(-0.1); - fixture.input(Ext::PBRANCH) = static_cast(0.4); - fixture.input(Ext::QBRANCH) = static_cast(0.1); - fixture.input(Ext::FREQ) = static_cast(0.99); + fixture.input(Ext::IR) = static_cast(0.2); + fixture.input(Ext::II) = static_cast(-0.1); + fixture.input(Ext::P) = static_cast(0.4); + fixture.input(Ext::Q) = static_cast(0.1); + fixture.input(Ext::FREQ) = static_cast(0.99); } template void setAnswerKeyInputs(Fixture& fixture) const { - fixture.input(Ext::IBRANCHR) = static_cast(0.08); - fixture.input(Ext::IBRANCHI) = static_cast(-0.02); - fixture.input(Ext::PBRANCH) = static_cast(0.41); - fixture.input(Ext::QBRANCH) = static_cast(0.13); - fixture.input(Ext::FREQ) = static_cast(0.99); - fixture.input(Ext::FREQREF) = static_cast(1.0); - fixture.input(Ext::VREF) = static_cast(1.01); - fixture.input(Ext::QREF) = static_cast(0.12); - fixture.input(Ext::PPLANTREF) = static_cast(0.55); + fixture.input(Ext::IR) = static_cast(1.0); + fixture.input(Ext::II) = static_cast(2.0); + fixture.input(Ext::P) = static_cast(0.35); + fixture.input(Ext::Q) = static_cast(0.25); + fixture.input(Ext::FREQ) = static_cast(0.2); + fixture.input(Ext::FREQREF) = static_cast(1.0); + fixture.input(Ext::VREF) = static_cast(1.05); + fixture.input(Ext::QREF) = static_cast(0.3); + fixture.input(Ext::PREF) = static_cast(0.55); } template void setAnswerKeyState(PhasorDynamics::Converter::Repca& repca) const { + // Every smooth-transition argument keeps a saturation margin, and + // every clamp that must pass its input through sits at the midpoint + // of its limits, so each row carries its ideal value. setState(repca, - {{Vars::VMEAS, 0.98}, - {Vars::QMEAS, 0.11}, - {Vars::XQPI, 0.07}, - {Vars::XQLAG, 0.14}, - {Vars::PMEAS, 0.44}, - {Vars::XPPI, 0.21}, - {Vars::PREF, 0.60}, - {Vars::V, 1.0}, - {Vars::VLDC, 0.99}, - {Vars::VDROOP, 1.05}, - {Vars::VCTRL, 1.02}, - {Vars::SFRZ, 0.8}, - {Vars::ERQ, 0.03}, + {{Vars::VMEAS, 0.85}, + {Vars::QMEAS, 0.45}, + {Vars::XQPI, -0.75}, + {Vars::XQLAG, -0.05}, + {Vars::PMEAS, 0.3}, + {Vars::XPPI, 1.85}, + {Vars::PREF, 0.35}, + {Vars::V, 1.5}, + {Vars::VLDC, 1.0}, + {Vars::VDROOP, 1.6}, + {Vars::VCTRL, 0.95}, + {Vars::SFRZ, 0.5}, + {Vars::ERQ, 0.83}, {Vars::ERQDB, 0.05}, - {Vars::ERQLIM, 0.02}, - {Vars::QPI, 0.27}, - {Vars::QEXT, 0.20}, - {Vars::EF, 0.01}, - {Vars::EP, 0.09}, - {Vars::EPLIM, 0.04}, - {Vars::PPI, 0.66}, - {Vars::PEXT, 0.61}}); + {Vars::ERQLIM, 0.4}, + {Vars::QPI, 0.1}, + {Vars::QEXT, 0.25}, + {Vars::EF, 0.8}, + {Vars::EP, 2.0}, + {Vars::EPLIM, -0.5}, + {Vars::PPI, 0.9}, + {Vars::PEXT, 0.15}}); setDerivative(repca, - {{Vars::VMEAS, 0.01}, - {Vars::QMEAS, -0.02}, - {Vars::XQPI, 0.03}, - {Vars::XQLAG, -0.04}, - {Vars::PMEAS, 0.02}, - {Vars::XPPI, -0.01}, - {Vars::PREF, 0.05}}); + {{Vars::VMEAS, 0.1}, + {Vars::QMEAS, -0.2}, + {Vars::XQPI, 0.3}, + {Vars::XQLAG, -0.4}, + {Vars::PMEAS, 0.5}, + {Vars::XPPI, -0.6}, + {Vars::PREF, 0.7}}); } bool defaultsMatchDocumentedValues() const @@ -1435,12 +1454,12 @@ namespace GridKit implicit_defaults.attachAllInputs(); explicit_defaults.attachAllInputs(); - implicit_defaults.input(Ext::PBRANCH) = 0.2; - implicit_defaults.input(Ext::QBRANCH) = 0.1; - implicit_defaults.input(Ext::FREQ) = 1.0; - explicit_defaults.input(Ext::PBRANCH) = 0.2; - explicit_defaults.input(Ext::QBRANCH) = 0.1; - explicit_defaults.input(Ext::FREQ) = 1.0; + implicit_defaults.input(Ext::P) = 0.2; + implicit_defaults.input(Ext::Q) = 0.1; + implicit_defaults.input(Ext::FREQ) = 1.0; + explicit_defaults.input(Ext::P) = 0.2; + explicit_defaults.input(Ext::Q) = 0.1; + explicit_defaults.input(Ext::FREQ) = 1.0; bool success = implicit_defaults.initialize(0.1, 0.2) && explicit_defaults.initialize(0.1, 0.2); @@ -1681,7 +1700,7 @@ namespace GridKit const char* what, Vars variable, const char* context, - RealT tolerance = kBehaviorTol) + RealT tolerance = kTol) { if (isEqual(actual, expected, tolerance)) { @@ -1703,7 +1722,7 @@ namespace GridKit const char* what, size_t row, const char* context, - RealT tolerance = kBehaviorTol) + RealT tolerance = kTol) { if (isEqual(actual, expected, tolerance)) { @@ -1722,7 +1741,7 @@ namespace GridKit bool scalarMatches(RealT actual, RealT expected, const char* label, - RealT tolerance = kBehaviorTol) const + RealT tolerance = kTol) const { if (isEqual(actual, expected, tolerance)) { @@ -1855,7 +1874,7 @@ namespace GridKit return rowsMatch(repca.y(), rows.begin(), rows.size(), "state", context); } - bool allResidualsZero(const RepcaT& repca) const + bool allResidualsWithinInitTolerance(const RepcaT& repca) const { bool success = true; const auto* f = repca.getResidual().getData(); @@ -1867,15 +1886,12 @@ namespace GridKit 0.0, "residual", variable, - "at rest")) + "at rest", + RepcaT::INITIALIZATION_TOLERANCE)) { success = false; } - if (!variableMatches(yp[row], - 0.0, - "derivative", - variable, - "at rest")) + if (!valueUnchanged(yp[row], 0.0, "derivative", row)) { success = false; } @@ -1954,34 +1970,34 @@ namespace GridKit { return {{ {{index(Vars::VMEAS), -6.0}, {index(Vars::VCTRL), 5.0}}, - {{index(Vars::QMEAS), -6.0}, {externalColumn(index(Ext::QBRANCH)), 10.0}}, - {{index(Vars::XQPI), -1.0}, {index(Vars::SFRZ), 0.06}, {index(Vars::ERQLIM), 2.4}}, - {{index(Vars::XQLAG), -1.6666666666666667}, {index(Vars::QPI), 0.6666666666666666}}, - {{index(Vars::PMEAS), -3.5}, {externalColumn(index(Ext::PBRANCH)), 5.0}}, + {{index(Vars::QMEAS), -6.0}, {externalColumn(index(Ext::Q)), 10.0}}, + {{index(Vars::XQPI), -1.0}, {index(Vars::SFRZ), 1.2}, {index(Vars::ERQLIM), 1.5}}, + {{index(Vars::XQLAG), -1.4}, {index(Vars::QPI), 0.4}}, + {{index(Vars::PMEAS), -3.5}, {externalColumn(index(Ext::P)), 5.0}}, {{index(Vars::XPPI), -1.0}, {index(Vars::EPLIM), 1.8}}, {{index(Vars::PREF), -3.0}, {index(Vars::PPI), 2.0}}, - {{index(Vars::V), -2.0}, {kBusVrColumn, 1.8}, {kBusViColumn, 0.8}}, - {{index(Vars::VLDC), -1.98}, - {kBusVrColumn, 1.7956}, - {kBusViColumn, 0.796}, - {externalColumn(index(Ext::IBRANCHR)), -0.059792}, - {externalColumn(index(Ext::IBRANCHI)), 0.037948}}, - {{index(Vars::V), 1.0}, {index(Vars::VDROOP), -1.0}, {externalColumn(index(Ext::QBRANCH)), 0.8}}, + {{index(Vars::V), -3.0}, {kBusVrColumn, 1.8}, {kBusViColumn, 0.8}}, + {{index(Vars::VLDC), -2.0}, + {kBusVrColumn, 1.88}, + {kBusViColumn, 0.66}, + {externalColumn(index(Ext::IR)), -0.0574}, + {externalColumn(index(Ext::II)), 0.0432}}, + {{index(Vars::V), 1.0}, {index(Vars::VDROOP), -1.0}, {externalColumn(index(Ext::Q)), 0.8}}, {{index(Vars::VLDC), 1.0}, {index(Vars::VCTRL), -1.0}}, {{index(Vars::SFRZ), -1.0}}, {{index(Vars::VMEAS), -1.0}, {index(Vars::ERQ), -1.0}, {externalColumn(index(Ext::VREF)), 1.0}}, - {{index(Vars::ERQ), 0.50000614417460221}, {index(Vars::ERQDB), -1.0}}, + {{index(Vars::ERQ), 1.0}, {index(Vars::ERQDB), -1.0}}, {{index(Vars::ERQDB), 1.0}, {index(Vars::ERQLIM), -1.0}}, {{index(Vars::XQPI), 1.0}, {index(Vars::ERQLIM), 2.0}, {index(Vars::QPI), -1.0}}, - {{index(Vars::XQLAG), 1.3}, {index(Vars::QPI), 0.2}, {index(Vars::QEXT), -3.0}}, + {{index(Vars::XQLAG), 2.3}, {index(Vars::QPI), 0.2}, {index(Vars::QEXT), -5.0}}, {{index(Vars::EF), -1.0}, - {externalColumn(index(Ext::FREQ)), -0.23963778765414229}, - {externalColumn(index(Ext::FREQREF)), 0.23963778765414229}}, + {externalColumn(index(Ext::FREQ)), -1.0}, + {externalColumn(index(Ext::FREQREF)), 1.0}}, {{index(Vars::PMEAS), -1.0}, - {index(Vars::EF), 1.9168273035060777}, + {index(Vars::EF), 2.0}, {index(Vars::EP), -1.0}, - {externalColumn(index(Ext::PPLANTREF)), 2.0}}, - {{index(Vars::EP), 1.0}, {index(Vars::EPLIM), -1.0}}, + {externalColumn(index(Ext::PREF)), 2.0}}, + {{index(Vars::EPLIM), -1.0}}, {{index(Vars::XPPI), 1.0}, {index(Vars::EPLIM), 1.7}, {index(Vars::PPI), -1.0}}, {{index(Vars::PREF), 1.0}, {index(Vars::PEXT), -2.0}}, }}; @@ -2011,7 +2027,7 @@ namespace GridKit expected[index(Vars::VMEAS)][index(Vars::VMEAS)] = -7.5; expected[index(Vars::QMEAS)][index(Vars::QMEAS)] = -7.5; expected[index(Vars::XQPI)][index(Vars::XQPI)] = -2.5; - expected[index(Vars::XQLAG)][index(Vars::XQLAG)] = -3.1666666666666665; + expected[index(Vars::XQLAG)][index(Vars::XQLAG)] = -2.9; expected[index(Vars::PMEAS)][index(Vars::PMEAS)] = -5.0; expected[index(Vars::XPPI)][index(Vars::XPPI)] = -2.5; expected[index(Vars::PREF)][index(Vars::PREF)] = -4.5; diff --git a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp index 4a28c8476..f497cda0b 100644 --- a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp @@ -269,15 +269,15 @@ namespace GridKit constexpr IdxT pext_id = static_cast(input_count + 2); constexpr std::array input_ports{{ - Inputs::ibranchr, - Inputs::ibranchi, - Inputs::pbranch, - Inputs::qbranch, + Inputs::ir, + Inputs::ii, + Inputs::p, + Inputs::q, Inputs::freq, Inputs::freqref, Inputs::vref, Inputs::qref, - Inputs::pplantref, + Inputs::pref, }}; constexpr std::array input_names{{ "Branch Current Real", diff --git a/tests/UnitTests/Utilities/CaseFormatTests.hpp b/tests/UnitTests/Utilities/CaseFormatTests.hpp index 6f730b57d..385cea52e 100644 --- a/tests/UnitTests/Utilities/CaseFormatTests.hpp +++ b/tests/UnitTests/Utilities/CaseFormatTests.hpp @@ -244,7 +244,7 @@ namespace GridKit "Velm": 0.2, "Gmax": 0.98, "Gmin": 0.02, "Tw": 1.2, "At": 1.1, "Dturb": 0.4, "Qnl": 0.08, "Tn": 0.7, "Tnp": 1.4, "db1": 0.01, "db2": 0.02, "Hdam": 1.05, "Gv0": 0.0, "Gv1": 0.2, "Gv2": 0.4, "Gv3": 0.6, "Gv4": 0.8, "Gv5": 1.0, "Pgv0": 0.0, "Pgv1": 0.15, "Pgv2": 0.42, "Pgv3": 0.66, "Pgv4": 0.85, "Pgv5": 1.0}, "mon": ["pmech", "filter", "desiredgate", "gate", "flow", "head"]}, - { "class": "Repca", "ports": {"bus":1, "ibranchr":11, "ibranchi":12, "pbranch":13, "qbranch":14, "freq":15, "freqref":16, "vref":17, "qref":18, "pplantref":19, "qext":20, "pext":21}, "id": "PC1", "params": {"mva":50, "VcompFlag":false, "RefFlag":true, "Freqflag":true, "Tfltr":0.2, "Vfrz":0.65, "Rc":0.02, "Xc":0.03, "Kc":0.4, "dbdlow":-0.02, "dbdupper":0.03, "emax":0.8, "emin":-0.7, "Kp":2.0, "Ki":3.0, "Qmax":0.9, "Qmin":-0.8, "Tft":0.2, "Tfv":1.5, "Tp":0.4, "fdbd1":-0.01, "fdbd2":0.015, "Ddn":2.0, "Dup":1.0, "femax":0.6, "femin":-0.5, "Kpg":1.7, "Kig":1.8, "Pmax":1.2, "Pmin":0.1, "Tlag":0.5}, "mon": ["qext", "pext", "vmeas", "qmeas", "pmeas"] }, + { "class": "Repca", "ports": {"bus":1, "ir":11, "ii":12, "p":13, "q":14, "freq":15, "freqref":16, "vref":17, "qref":18, "pref":19, "qext":20, "pext":21}, "id": "PC1", "params": {"mva":50, "VcompFlag":false, "RefFlag":true, "Freqflag":true, "Tfltr":0.2, "Vfrz":0.65, "Rc":0.02, "Xc":0.03, "Kc":0.4, "dbdlow":-0.02, "dbdupper":0.03, "emax":0.8, "emin":-0.7, "Kp":2.0, "Ki":3.0, "Qmax":0.9, "Qmin":-0.8, "Tft":0.2, "Tfv":1.5, "Tp":0.4, "fdbd1":-0.01, "fdbd2":0.015, "Ddn":2.0, "Dup":1.0, "femax":0.6, "femin":-0.5, "Kpg":1.7, "Kig":1.8, "Pmax":1.2, "Pmin":0.1, "Tlag":0.5}, "mon": ["qext", "pext", "vmeas", "qmeas", "pmeas"] }, { "class": "Ieeet1", "ports": {"bus":1, "speed": 1, "efd":3}, "id": "DV3", "params": {"Tr":0.0, "Ka":50.0, "Ta":0.04, "Ke":-0.06, "Te":0.6, "Kf":0.09, "Tf":1.46, "Vrmin":-1.0, "Vrmax":1.0, "E1":2.8, "E2":3.373, "Se1":0.04, "Se2":0.33, "Ispdlim":0.0}}, { "class": "SexsPti", "ports": {"bus":1, "efd":3}, "id": "DV4", "params": {"Ta":0.1, "Tb":0.5, "Te":0.8, "K":10.0, "Efdmax":5.0, "Efdmin":-5.0}}, { "class": "BusFault", "ports": {"bus":1}, "id": "1", "params": {"state0": false, "R":0.0, "X":1e-3} } @@ -466,15 +466,15 @@ namespace GridKit success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Pmin]) == 0.1; success *= std::get(result.repca[0].parameters[RepcaData::Parameters::Tlag]) == 0.5; success *= result.repca[0].buses[RepcaData::Buses::bus] == 1; - success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::ibranchr] == 11; - success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::ibranchi] == 12; - success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::pbranch] == 13; - success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::qbranch] == 14; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::ir] == 11; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::ii] == 12; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::p] == 13; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::q] == 14; success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::freq] == 15; success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::freqref] == 16; success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::vref] == 17; success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::qref] == 18; - success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::pplantref] == 19; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::pref] == 19; success *= result.repca[0].signal_outputs[RepcaData::SignalOutputs::qext] == 20; success *= result.repca[0].signal_outputs[RepcaData::SignalOutputs::pext] == 21; success *= result.repca[0].disambiguation_string == "PC1"; From 23bb6283139d587906688d43dc73b41400585066 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Wed, 5 Aug 2026 00:12:02 -0500 Subject: [PATCH 09/19] clean and consistant ordering --- .../PhasorDynamics/Converter/REPCA/README.md | 28 +++++------ .../PhasorDynamics/Converter/REPCA/Repca.hpp | 4 +- .../Converter/REPCA/RepcaData.hpp | 4 +- .../Converter/REPCA/RepcaImpl.hpp | 48 +++++++++---------- .../Model/PhasorDynamics/SystemModelImpl.hpp | 20 ++++---- .../PhasorDynamics/ConverterRepcaTests.hpp | 16 +++---- .../SystemSingleComponentTests.hpp | 8 ++-- tests/UnitTests/Utilities/CaseFormatTests.hpp | 14 +++--- 8 files changed, 71 insertions(+), 71 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md index 3f5c70667..2d65f28f4 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md @@ -95,15 +95,15 @@ power to component base. Name | Port | Init | Description ------------|--------|---------|------------ `bus` | Bus | Known | Regulated-bus voltage -`ibranchr` | Input | Known | Branch-current real component on system base -`ibranchi` | Input | Known | Branch-current imaginary component on system base -`pbranch` | Input | Known | Branch active power on system base -`qbranch` | Input | Known | Branch reactive power on system base +`ir` | Input | Known | Branch-current real component on system base +`ii` | Input | Known | Branch-current imaginary component on system base +`p` | Input | Known | Branch active power on system base +`q` | Input | Known | Branch reactive power on system base `freq` | Input | Known | Frequency input[^frequency-measurement] Not supported yet. -`freqref` | Input | Unknown | Frequency reference `vref` | Input | Unknown | Voltage-control reference +`pref` | Input | Unknown | Plant active-power reference on system base `qref` | Input | Unknown | Reactive-power reference on system base -`pplantref` | Input | Unknown | Plant active-power reference on system base +`freqref` | Input | Unknown | Frequency reference `qext` | Output | Known | Reactive-power command on system base `pext` | Output | Known | Active-power command on system base @@ -161,15 +161,15 @@ Symbol | Units | Init | Description -------------------------------|--------|---------|-----------------------------------|------ $V_\mathrm{r}$ | [p.u.] | Known | Regulated-bus voltage, real component | Bus input $V_\mathrm{i}$ | [p.u.] | Known | Regulated-bus voltage, imaginary component | Bus input -$I_\mathrm{r}$ | [p.u.] | Known | Branch-current real component | Signal port `ibranchr`; system base -$I_\mathrm{i}$ | [p.u.] | Known | Branch-current imaginary component | Signal port `ibranchi`; system base -$P$ | [p.u.] | Known | Branch active power | Signal port `pbranch`; system base -$Q$ | [p.u.] | Known | Branch reactive power | Signal port `qbranch`; system base +$I_\mathrm{r}$ | [p.u.] | Known | Branch-current real component | Signal port `ir`; system base +$I_\mathrm{i}$ | [p.u.] | Known | Branch-current imaginary component | Signal port `ii`; system base +$P$ | [p.u.] | Known | Branch active power | Signal port `p`; system base +$Q$ | [p.u.] | Known | Branch reactive power | Signal port `q`; system base $f$ | [p.u.] | Known | Frequency input | Signal port `freq` -$f^\mathrm{ref}$ | [p.u.] | Unknown | Frequency reference | Optional signal port `freqref` $V^\mathrm{ref}$ | [p.u.] | Unknown | Voltage-control reference | Optional signal port `vref` +$P^\mathrm{ref}$ | [p.u.] | Unknown | Plant active-power reference | Optional signal port `pref`; system base $Q^\mathrm{ref}$ | [p.u.] | Unknown | Reactive-power reference | Optional signal port `qref`; system base -$P^\mathrm{ref}$ | [p.u.] | Unknown | Plant active-power reference | Optional signal port `pplantref`; system base +$f^\mathrm{ref}$ | [p.u.] | Unknown | Frequency reference | Optional signal port `freqref` ## Model Equations @@ -295,10 +295,10 @@ Initialization is atomic; candidates are validated before state or signal writes ```math \begin{aligned} - f^\mathrm{ref} &\leftarrow f \\ V^\mathrm{ref} &\leftarrow V^\mathrm{meas} \\ + P^\mathrm{ref} &\leftarrow \dfrac{P^\mathrm{meas}-P^\mathrm{freq}}{k_\mathrm{base}} \\ Q^\mathrm{ref} &\leftarrow Q \\ - P^\mathrm{ref} &\leftarrow \dfrac{P^\mathrm{meas}-P^\mathrm{freq}}{k_\mathrm{base}}. + f^\mathrm{ref} &\leftarrow f. \end{aligned} ``` diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp index b80dc213b..84044ee6c 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp @@ -63,10 +63,10 @@ namespace GridKit P, ///< \f$P\f$ Required branch active power on system base [p.u.] Q, ///< \f$Q\f$ Required branch reactive power on system base [p.u.] FREQ, ///< \f$f\f$ Required frequency input [p.u.] - FREQREF, ///< \f$f^\mathrm{ref}\f$ Optional frequency reference [p.u.] VREF, ///< \f$V^\mathrm{ref}\f$ Optional voltage-control reference [p.u.] - QREF, ///< \f$Q^\mathrm{ref}\f$ Optional reactive-power reference on system base [p.u.] PREF, ///< \f$P^\mathrm{ref}\f$ Optional plant active-power reference on system base [p.u.] + QREF, ///< \f$Q^\mathrm{ref}\f$ Optional reactive-power reference on system base [p.u.] + FREQREF, ///< \f$f^\mathrm{ref}\f$ Optional frequency reference [p.u.] MAXIMUM ///< Number of external variables }; diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp index 72dc09903..91185c2cb 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp @@ -65,10 +65,10 @@ namespace GridKit p, ///< \f$P\f$ Required Known branch active-power input on system base [p.u.] q, ///< \f$Q\f$ Required Known branch reactive-power input on system base [p.u.] freq, ///< \f$f\f$ Required Known frequency input [p.u.] - freqref, ///< \f$f^\mathrm{ref}\f$ Optional Unknown frequency-reference input [p.u.] vref, ///< \f$V^\mathrm{ref}\f$ Optional Unknown voltage-reference input [p.u.] - qref, ///< \f$Q^\mathrm{ref}\f$ Optional Unknown reactive-power reference on system base [p.u.] pref, ///< \f$P^\mathrm{ref}\f$ Optional Unknown plant active-power reference on system base [p.u.] + qref, ///< \f$Q^\mathrm{ref}\f$ Optional Unknown reactive-power reference on system base [p.u.] + freqref, ///< \f$f^\mathrm{ref}\f$ Optional Unknown frequency-reference input [p.u.] SIZE ///< Number of REPCA signal-input ports }; diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp index 48d17b104..d9d7fd782 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp @@ -231,10 +231,10 @@ namespace GridKit } }; - check_optional_signal.template operator()("freqref"); check_optional_signal.template operator()("vref"); - check_optional_signal.template operator()("qref"); check_optional_signal.template operator()("pref"); + check_optional_signal.template operator()("qref"); + check_optional_signal.template operator()("freqref"); return ret; } @@ -454,23 +454,23 @@ namespace GridKit qref_set_ = qref0_system; pref_set_ = pref0_system; - if (signals_.template isAttached()) - { - signals_.template writeExternalVariable( - freqref_set_); - } if (signals_.template isAttached()) { signals_.template writeExternalVariable(vref_set_); } + if (signals_.template isAttached()) + { + signals_.template writeExternalVariable( + pref_set_); + } if (signals_.template isAttached()) { signals_.template writeExternalVariable(qref_set_); } - if (signals_.template isAttached()) + if (signals_.template isAttached()) { - signals_.template writeExternalVariable( - pref_set_); + signals_.template writeExternalVariable( + freqref_set_); } y_.setDataUpdated(); @@ -529,10 +529,10 @@ namespace GridKit { using E = RepcaExternalVariables; - ws_[index(E::FREQREF)] = freqref_set_; ws_[index(E::VREF)] = vref_set_; - ws_[index(E::QREF)] = qref_set_; ws_[index(E::PREF)] = pref_set_; + ws_[index(E::QREF)] = qref_set_; + ws_[index(E::FREQREF)] = freqref_set_; std::fill(ws_indices_.begin(), ws_indices_.end(), INVALID_INDEX); ws_[index(E::IR)] = @@ -556,13 +556,6 @@ namespace GridKit ws_indices_[index(E::FREQ)] = signals_.template readExternalVariableIndex(); - if (signals_.template isAttached()) - { - ws_[index(E::FREQREF)] = - signals_.template readExternalVariable(); - ws_indices_[index(E::FREQREF)] = - signals_.template readExternalVariableIndex(); - } if (signals_.template isAttached()) { ws_[index(E::VREF)] = @@ -570,6 +563,13 @@ namespace GridKit ws_indices_[index(E::VREF)] = signals_.template readExternalVariableIndex(); } + if (signals_.template isAttached()) + { + ws_[index(E::PREF)] = + signals_.template readExternalVariable(); + ws_indices_[index(E::PREF)] = + signals_.template readExternalVariableIndex(); + } if (signals_.template isAttached()) { ws_[index(E::QREF)] = @@ -577,12 +577,12 @@ namespace GridKit ws_indices_[index(E::QREF)] = signals_.template readExternalVariableIndex(); } - if (signals_.template isAttached()) + if (signals_.template isAttached()) { - ws_[index(E::PREF)] = - signals_.template readExternalVariable(); - ws_indices_[index(E::PREF)] = - signals_.template readExternalVariableIndex(); + ws_[index(E::FREQREF)] = + signals_.template readExternalVariable(); + ws_indices_[index(E::FREQREF)] = + signals_.template readExternalVariableIndex(); } wb_[0] = Vr(); diff --git a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp index 283e437bb..2fa42881a 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp @@ -524,29 +524,29 @@ namespace GridKit constexpr auto FREQ = RepcaExternalVariables::FREQ; repca->getSignals().template attachSignalNode(getSignal(freq)); } - if (repcadata.signal_inputs.contains(RepcaSignalInputs::freqref)) - { - const IdxT freqref = repcadata.signal_inputs.at(RepcaSignalInputs::freqref); - constexpr auto FREQREF = RepcaExternalVariables::FREQREF; - repca->getSignals().template attachSignalNode(getSignal(freqref)); - } if (repcadata.signal_inputs.contains(RepcaSignalInputs::vref)) { const IdxT vref = repcadata.signal_inputs.at(RepcaSignalInputs::vref); constexpr auto VREF = RepcaExternalVariables::VREF; repca->getSignals().template attachSignalNode(getSignal(vref)); } + if (repcadata.signal_inputs.contains(RepcaSignalInputs::pref)) + { + const IdxT pref = repcadata.signal_inputs.at(RepcaSignalInputs::pref); + constexpr auto PREF = RepcaExternalVariables::PREF; + repca->getSignals().template attachSignalNode(getSignal(pref)); + } if (repcadata.signal_inputs.contains(RepcaSignalInputs::qref)) { const IdxT qref = repcadata.signal_inputs.at(RepcaSignalInputs::qref); constexpr auto QREF = RepcaExternalVariables::QREF; repca->getSignals().template attachSignalNode(getSignal(qref)); } - if (repcadata.signal_inputs.contains(RepcaSignalInputs::pref)) + if (repcadata.signal_inputs.contains(RepcaSignalInputs::freqref)) { - const IdxT pref = repcadata.signal_inputs.at(RepcaSignalInputs::pref); - constexpr auto PREF = RepcaExternalVariables::PREF; - repca->getSignals().template attachSignalNode(getSignal(pref)); + const IdxT freqref = repcadata.signal_inputs.at(RepcaSignalInputs::freqref); + constexpr auto FREQREF = RepcaExternalVariables::FREQREF; + repca->getSignals().template attachSignalNode(getSignal(freqref)); } if (repcadata.signal_outputs.contains(RepcaSignalOutputs::qext)) { diff --git a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp index 4a331446a..8de8290ab 100644 --- a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp @@ -194,10 +194,10 @@ namespace GridKit success *= unlinkedSignalRejected(); success *= unlinkedSignalRejected(); success *= unlinkedSignalRejected(); - success *= unlinkedSignalRejected(); success *= unlinkedSignalRejected(); - success *= unlinkedSignalRejected(); success *= unlinkedSignalRejected(); + success *= unlinkedSignalRejected(); + success *= unlinkedSignalRejected(); auto floor_data = makeInitializationData(); floor_data.parameters[Params::Tfltr] = 0.0; @@ -272,10 +272,10 @@ namespace GridKit success *= scalarPreserved(fixture.input(Ext::FREQ), 0.99, "preserved freq"); success *= scalarPreserved(fixture.qext(), 0.25, "preserved qext"); success *= scalarPreserved(fixture.pext(), 0.45, "preserved pext"); - success *= scalarMatches(fixture.input(Ext::FREQREF), 0.99, "published freqref"); success *= scalarMatches(fixture.input(Ext::VREF), 0.99200050403213, "published vref"); - success *= scalarMatches(fixture.input(Ext::QREF), 0.1, "published qref"); success *= scalarMatches(fixture.input(Ext::PREF), 0.4, "published pref"); + success *= scalarMatches(fixture.input(Ext::QREF), 0.1, "published qref"); + success *= scalarMatches(fixture.input(Ext::FREQREF), 0.99, "published freqref"); for (size_t row = 0; row < index(Vars::MAXIMUM); ++row) { @@ -1196,10 +1196,10 @@ namespace GridKit attachRequiredInputs(initial_value); auto& signals = repca.getSignals(); - signals.template attachSignalNode(&input_nodes_[index(Ext::FREQREF)]); signals.template attachSignalNode(&input_nodes_[index(Ext::VREF)]); - signals.template attachSignalNode(&input_nodes_[index(Ext::QREF)]); signals.template attachSignalNode(&input_nodes_[index(Ext::PREF)]); + signals.template attachSignalNode(&input_nodes_[index(Ext::QREF)]); + signals.template attachSignalNode(&input_nodes_[index(Ext::FREQREF)]); } void setCommands(RealT qext, RealT pext) @@ -1402,10 +1402,10 @@ namespace GridKit fixture.input(Ext::P) = static_cast(0.35); fixture.input(Ext::Q) = static_cast(0.25); fixture.input(Ext::FREQ) = static_cast(0.2); - fixture.input(Ext::FREQREF) = static_cast(1.0); fixture.input(Ext::VREF) = static_cast(1.05); - fixture.input(Ext::QREF) = static_cast(0.3); fixture.input(Ext::PREF) = static_cast(0.55); + fixture.input(Ext::QREF) = static_cast(0.3); + fixture.input(Ext::FREQREF) = static_cast(1.0); } template diff --git a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp index f497cda0b..a2d52bd60 100644 --- a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp @@ -274,10 +274,10 @@ namespace GridKit Inputs::p, Inputs::q, Inputs::freq, - Inputs::freqref, Inputs::vref, - Inputs::qref, Inputs::pref, + Inputs::qref, + Inputs::freqref, }}; constexpr std::array input_names{{ "Branch Current Real", @@ -285,10 +285,10 @@ namespace GridKit "Branch Active Power", "Branch Reactive Power", "Frequency", - "Frequency Reference", "Voltage Reference", - "Reactive Power Reference", "Plant Active Power Reference", + "Reactive Power Reference", + "Frequency Reference", }}; PhasorDynamics::SystemModelData data; diff --git a/tests/UnitTests/Utilities/CaseFormatTests.hpp b/tests/UnitTests/Utilities/CaseFormatTests.hpp index 385cea52e..4ec637c21 100644 --- a/tests/UnitTests/Utilities/CaseFormatTests.hpp +++ b/tests/UnitTests/Utilities/CaseFormatTests.hpp @@ -228,10 +228,10 @@ namespace GridKit { "signal_id": 13, "name": "Branch Active Power"}, { "signal_id": 14, "name": "Branch Reactive Power"}, { "signal_id": 15, "name": "Frequency"}, - { "signal_id": 16, "name": "Frequency Reference"}, - { "signal_id": 17, "name": "Plant Voltage Reference"}, + { "signal_id": 16, "name": "Plant Voltage Reference"}, + { "signal_id": 17, "name": "Plant Active Power Reference"}, { "signal_id": 18, "name": "Reactive Power Reference"}, - { "signal_id": 19, "name": "Plant Active Power Reference"}, + { "signal_id": 19, "name": "Frequency Reference"}, { "signal_id": 20, "name": "Reactive Power Command"}, { "signal_id": 21, "name": "Active Power Command"} ], @@ -244,7 +244,7 @@ namespace GridKit "Velm": 0.2, "Gmax": 0.98, "Gmin": 0.02, "Tw": 1.2, "At": 1.1, "Dturb": 0.4, "Qnl": 0.08, "Tn": 0.7, "Tnp": 1.4, "db1": 0.01, "db2": 0.02, "Hdam": 1.05, "Gv0": 0.0, "Gv1": 0.2, "Gv2": 0.4, "Gv3": 0.6, "Gv4": 0.8, "Gv5": 1.0, "Pgv0": 0.0, "Pgv1": 0.15, "Pgv2": 0.42, "Pgv3": 0.66, "Pgv4": 0.85, "Pgv5": 1.0}, "mon": ["pmech", "filter", "desiredgate", "gate", "flow", "head"]}, - { "class": "Repca", "ports": {"bus":1, "ir":11, "ii":12, "p":13, "q":14, "freq":15, "freqref":16, "vref":17, "qref":18, "pref":19, "qext":20, "pext":21}, "id": "PC1", "params": {"mva":50, "VcompFlag":false, "RefFlag":true, "Freqflag":true, "Tfltr":0.2, "Vfrz":0.65, "Rc":0.02, "Xc":0.03, "Kc":0.4, "dbdlow":-0.02, "dbdupper":0.03, "emax":0.8, "emin":-0.7, "Kp":2.0, "Ki":3.0, "Qmax":0.9, "Qmin":-0.8, "Tft":0.2, "Tfv":1.5, "Tp":0.4, "fdbd1":-0.01, "fdbd2":0.015, "Ddn":2.0, "Dup":1.0, "femax":0.6, "femin":-0.5, "Kpg":1.7, "Kig":1.8, "Pmax":1.2, "Pmin":0.1, "Tlag":0.5}, "mon": ["qext", "pext", "vmeas", "qmeas", "pmeas"] }, + { "class": "Repca", "ports": {"bus":1, "ir":11, "ii":12, "p":13, "q":14, "freq":15, "vref":16, "pref":17, "qref":18, "freqref":19, "qext":20, "pext":21}, "id": "PC1", "params": {"mva":50, "VcompFlag":false, "RefFlag":true, "Freqflag":true, "Tfltr":0.2, "Vfrz":0.65, "Rc":0.02, "Xc":0.03, "Kc":0.4, "dbdlow":-0.02, "dbdupper":0.03, "emax":0.8, "emin":-0.7, "Kp":2.0, "Ki":3.0, "Qmax":0.9, "Qmin":-0.8, "Tft":0.2, "Tfv":1.5, "Tp":0.4, "fdbd1":-0.01, "fdbd2":0.015, "Ddn":2.0, "Dup":1.0, "femax":0.6, "femin":-0.5, "Kpg":1.7, "Kig":1.8, "Pmax":1.2, "Pmin":0.1, "Tlag":0.5}, "mon": ["qext", "pext", "vmeas", "qmeas", "pmeas"] }, { "class": "Ieeet1", "ports": {"bus":1, "speed": 1, "efd":3}, "id": "DV3", "params": {"Tr":0.0, "Ka":50.0, "Ta":0.04, "Ke":-0.06, "Te":0.6, "Kf":0.09, "Tf":1.46, "Vrmin":-1.0, "Vrmax":1.0, "E1":2.8, "E2":3.373, "Se1":0.04, "Se2":0.33, "Ispdlim":0.0}}, { "class": "SexsPti", "ports": {"bus":1, "efd":3}, "id": "DV4", "params": {"Ta":0.1, "Tb":0.5, "Te":0.8, "K":10.0, "Efdmax":5.0, "Efdmin":-5.0}}, { "class": "BusFault", "ports": {"bus":1}, "id": "1", "params": {"state0": false, "R":0.0, "X":1e-3} } @@ -471,10 +471,10 @@ namespace GridKit success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::p] == 13; success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::q] == 14; success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::freq] == 15; - success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::freqref] == 16; - success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::vref] == 17; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::vref] == 16; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::pref] == 17; success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::qref] == 18; - success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::pref] == 19; + success *= result.repca[0].signal_inputs[RepcaData::SignalInputs::freqref] == 19; success *= result.repca[0].signal_outputs[RepcaData::SignalOutputs::qext] == 20; success *= result.repca[0].signal_outputs[RepcaData::SignalOutputs::pext] == 21; success *= result.repca[0].disambiguation_string == "PC1"; From 775f08bd6cd368dab74d6507354408ec4967ab82 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Wed, 5 Aug 2026 11:08:08 -0500 Subject: [PATCH 10/19] Move to Controller --- CHANGELOG.md | 2 +- GridKit/Model/PhasorDynamics/CMakeLists.txt | 1 + .../Model/PhasorDynamics/ComponentLibrary.hpp | 2 +- .../PhasorDynamics/Controller/CMakeLists.txt | 6 +++ .../Model/PhasorDynamics/Controller/README.md | 10 +++++ .../REPCA/CMakeLists.txt | 10 ++--- .../{Converter => Controller}/REPCA/README.md | 0 .../{Converter => Controller}/REPCA/Repca.cpp | 4 +- .../{Converter => Controller}/REPCA/Repca.hpp | 6 +-- .../REPCA/RepcaData.hpp | 4 +- .../REPCA/RepcaDependencyTracking.cpp | 4 +- .../REPCA/RepcaEnzyme.cpp | 6 +-- .../REPCA/RepcaImpl.hpp | 8 ++-- .../PhasorDynamics/Converter/CMakeLists.txt | 1 - .../Model/PhasorDynamics/Converter/README.md | 1 - .../Model/PhasorDynamics/SystemModelData.hpp | 4 +- .../Model/PhasorDynamics/SystemModelImpl.hpp | 1 + .../Model/PhasorDynamics/Controller/README.md | 14 +++++++ .../PhasorDynamics/Controller/REPCA/README.md | 6 +++ .../Model/PhasorDynamics/Converter/README.md | 1 - .../PhasorDynamics/Converter/REPCA/README.md | 6 --- docs/GridKit/Model/PhasorDynamics/README.md | 1 + tests/UnitTests/PhasorDynamics/CMakeLists.txt | 12 +++--- .../ComponentConnectionTests.hpp | 14 ++++--- ...epcaTests.hpp => ControllerRepcaTests.hpp} | 38 +++++++++---------- .../SystemSingleComponentTests.hpp | 10 ++--- ...aTests.cpp => runControllerRepcaTests.cpp} | 4 +- tests/UnitTests/Utilities/CaseFormatTests.hpp | 4 +- 28 files changed, 107 insertions(+), 73 deletions(-) create mode 100644 GridKit/Model/PhasorDynamics/Controller/CMakeLists.txt create mode 100644 GridKit/Model/PhasorDynamics/Controller/README.md rename GridKit/Model/PhasorDynamics/{Converter => Controller}/REPCA/CMakeLists.txt (82%) rename GridKit/Model/PhasorDynamics/{Converter => Controller}/REPCA/README.md (100%) rename GridKit/Model/PhasorDynamics/{Converter => Controller}/REPCA/Repca.cpp (93%) rename GridKit/Model/PhasorDynamics/{Converter => Controller}/REPCA/Repca.hpp (98%) rename GridKit/Model/PhasorDynamics/{Converter => Controller}/REPCA/RepcaData.hpp (99%) rename GridKit/Model/PhasorDynamics/{Converter => Controller}/REPCA/RepcaDependencyTracking.cpp (94%) rename GridKit/Model/PhasorDynamics/{Converter => Controller}/REPCA/RepcaEnzyme.cpp (96%) rename GridKit/Model/PhasorDynamics/{Converter => Controller}/REPCA/RepcaImpl.hpp (99%) create mode 100644 docs/GridKit/Model/PhasorDynamics/Controller/README.md create mode 100644 docs/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md delete mode 100644 docs/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md rename tests/UnitTests/PhasorDynamics/{ConverterRepcaTests.hpp => ControllerRepcaTests.hpp} (98%) rename tests/UnitTests/PhasorDynamics/{runConverterRepcaTests.cpp => runControllerRepcaTests.cpp} (82%) diff --git a/CHANGELOG.md b/CHANGELOG.md index e99716231..e552d2cf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,7 +77,7 @@ - Added `REGCA` converter model implementation for PhasorDynamics. - Remove unnecessary data copying while evaluating `PowerElectronics` models, speeding up large simulations by up to 3x - Added `HYGOV` governor model implementation for PhasorDynamics. -- Added `REPCA` converter model implementation for PhasorDynamics. +- Added `REPCA` controller model implementation for PhasorDynamics. ## v0.1 diff --git a/GridKit/Model/PhasorDynamics/CMakeLists.txt b/GridKit/Model/PhasorDynamics/CMakeLists.txt index fda957537..d2b9a20dc 100644 --- a/GridKit/Model/PhasorDynamics/CMakeLists.txt +++ b/GridKit/Model/PhasorDynamics/CMakeLists.txt @@ -35,6 +35,7 @@ add_subdirectory(Branch) add_subdirectory(Bus) add_subdirectory(BusFault) add_subdirectory(BusToSignalAdapter) +add_subdirectory(Controller) add_subdirectory(Converter) add_subdirectory(Exciter) add_subdirectory(Governor) diff --git a/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp b/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp index 4942f364a..21b7210ff 100644 --- a/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp +++ b/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp @@ -5,8 +5,8 @@ #include #include #include +#include #include -#include #include #include #include diff --git a/GridKit/Model/PhasorDynamics/Controller/CMakeLists.txt b/GridKit/Model/PhasorDynamics/Controller/CMakeLists.txt new file mode 100644 index 000000000..be3287ed4 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Controller/CMakeLists.txt @@ -0,0 +1,6 @@ +# [[ +# Author(s): +# - Luke Lowery +# ]] + +add_subdirectory(REPCA) diff --git a/GridKit/Model/PhasorDynamics/Controller/README.md b/GridKit/Model/PhasorDynamics/Controller/README.md new file mode 100644 index 000000000..ce5f57486 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Controller/README.md @@ -0,0 +1,10 @@ +# **Controller Models** + +## Introduction + +Controller models produce command signals for dynamic device models without +directly contributing to the network equations. + +## Types + +- Renewable Energy Plant Control Model REPCA (See [REPCA](REPCA/README.md)) diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/CMakeLists.txt b/GridKit/Model/PhasorDynamics/Controller/REPCA/CMakeLists.txt similarity index 82% rename from GridKit/Model/PhasorDynamics/Converter/REPCA/CMakeLists.txt rename to GridKit/Model/PhasorDynamics/Controller/REPCA/CMakeLists.txt index c315b9c8d..f0e2b345c 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/CMakeLists.txt +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/CMakeLists.txt @@ -7,7 +7,7 @@ set(_install_headers Repca.hpp RepcaData.hpp) if(GRIDKIT_ENABLE_ENZYME) gridkit_add_library( - phasor_dynamics_converter_repca + phasor_dynamics_controller_repca SOURCES RepcaEnzyme.cpp HEADERS ${_install_headers} INCLUDE_DIRECTORIES PRIVATE ${GRIDKIT_THIRD_PARTY_DIR}/magic-enum/include @@ -25,7 +25,7 @@ if(GRIDKIT_ENABLE_ENZYME) -fno-math-errno) else() gridkit_add_library( - phasor_dynamics_converter_repca + phasor_dynamics_controller_repca SOURCES Repca.cpp HEADERS ${_install_headers} INCLUDE_DIRECTORIES PRIVATE ${GRIDKIT_THIRD_PARTY_DIR}/magic-enum/include @@ -37,7 +37,7 @@ else() endif() gridkit_add_library( - phasor_dynamics_converter_repca_dependency_tracking + phasor_dynamics_controller_repca_dependency_tracking SOURCES RepcaDependencyTracking.cpp INCLUDE_DIRECTORIES PRIVATE ${GRIDKIT_THIRD_PARTY_DIR}/magic-enum/include LINK_LIBRARIES @@ -48,7 +48,7 @@ gridkit_add_library( target_link_libraries( phasor_dynamics_components - INTERFACE GridKit::phasor_dynamics_converter_repca) + INTERFACE GridKit::phasor_dynamics_controller_repca) target_link_libraries( phasor_dynamics_components_dependency_tracking - INTERFACE GridKit::phasor_dynamics_converter_repca_dependency_tracking) + INTERFACE GridKit::phasor_dynamics_controller_repca_dependency_tracking) diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md b/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md similarity index 100% rename from GridKit/Model/PhasorDynamics/Converter/REPCA/README.md rename to GridKit/Model/PhasorDynamics/Controller/REPCA/README.md diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.cpp similarity index 93% rename from GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp rename to GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.cpp index 8705053b3..76fc4ce09 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.cpp @@ -10,7 +10,7 @@ namespace GridKit { namespace PhasorDynamics { - namespace Converter + namespace Controller { /** * @brief Report that a separate Jacobian is unavailable in the plain-real build. @@ -25,6 +25,6 @@ namespace GridKit template class Repca; template class Repca; - } // namespace Converter + } // namespace Controller } // namespace PhasorDynamics } // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp similarity index 98% rename from GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp rename to GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp index 84044ee6c..d58c0a9a0 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp @@ -12,7 +12,7 @@ #include #include -#include +#include #include namespace GridKit @@ -25,7 +25,7 @@ namespace GridKit template class SignalNode; - namespace Converter + namespace Controller { /// Internal variables of `Repca`. enum class RepcaInternalVariables : size_t @@ -219,6 +219,6 @@ namespace GridKit std::vector ws_; std::vector ws_indices_; }; - } // namespace Converter + } // namespace Controller } // namespace PhasorDynamics } // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaData.hpp similarity index 99% rename from GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp rename to GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaData.hpp index 91185c2cb..0897a5f5b 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaData.hpp @@ -12,7 +12,7 @@ namespace GridKit { namespace PhasorDynamics { - namespace Converter + namespace Controller { /// Parameter keys for `Repca`. enum class RepcaParameters @@ -115,6 +115,6 @@ namespace GridKit using SignalOutputs = RepcaSignalOutputs; using MonitorableVariables = RepcaMonitorableVariables; }; - } // namespace Converter + } // namespace Controller } // namespace PhasorDynamics } // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaDependencyTracking.cpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaDependencyTracking.cpp similarity index 94% rename from GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaDependencyTracking.cpp rename to GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaDependencyTracking.cpp index 71e822739..e3d8095a7 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaDependencyTracking.cpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaDependencyTracking.cpp @@ -10,7 +10,7 @@ namespace GridKit { namespace PhasorDynamics { - namespace Converter + namespace Controller { /** * @brief Report that DependencyTracking exposes structure through the @@ -26,6 +26,6 @@ namespace GridKit template class Repca; template class Repca; - } // namespace Converter + } // namespace Controller } // namespace PhasorDynamics } // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaEnzyme.cpp similarity index 96% rename from GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp rename to GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaEnzyme.cpp index 2f5782de2..0f6b1451e 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaEnzyme.cpp @@ -12,7 +12,7 @@ namespace GridKit { namespace PhasorDynamics { - namespace Converter + namespace Controller { /** * @brief Assemble the sparse REPCA Jacobian with Enzyme. @@ -45,7 +45,7 @@ namespace GridKit J_vals_buffer_ = new RealT[buffer_size]; } - using ModelT = GridKit::PhasorDynamics::Converter::Repca; + using ModelT = GridKit::PhasorDynamics::Controller::Repca; using Fn = GridKit::Enzyme::Sparse::MemberFunctions; nnz_ = 0; @@ -117,6 +117,6 @@ namespace GridKit template class Repca; template class Repca; - } // namespace Converter + } // namespace Controller } // namespace PhasorDynamics } // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp similarity index 99% rename from GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp rename to GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp index d9d7fd782..7d5a792eb 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp @@ -10,8 +10,8 @@ #include #include -#include -#include +#include +#include #include #include #include @@ -20,7 +20,7 @@ namespace GridKit { namespace PhasorDynamics { - namespace Converter + namespace Controller { /// Logger used for REPCA diagnostics. using Log = ::GridKit::Utilities::Logger; @@ -1038,6 +1038,6 @@ namespace GridKit return bus_->Vi(); } - } // namespace Converter + } // namespace Controller } // namespace PhasorDynamics } // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/CMakeLists.txt b/GridKit/Model/PhasorDynamics/Converter/CMakeLists.txt index 74372e2d6..cafb2cc36 100644 --- a/GridKit/Model/PhasorDynamics/Converter/CMakeLists.txt +++ b/GridKit/Model/PhasorDynamics/Converter/CMakeLists.txt @@ -4,4 +4,3 @@ # ]] add_subdirectory(REGCA) -add_subdirectory(REPCA) diff --git a/GridKit/Model/PhasorDynamics/Converter/README.md b/GridKit/Model/PhasorDynamics/Converter/README.md index f14960a80..ad38ba19a 100644 --- a/GridKit/Model/PhasorDynamics/Converter/README.md +++ b/GridKit/Model/PhasorDynamics/Converter/README.md @@ -12,4 +12,3 @@ The GridKit converter documentation includes: - Renewable Energy Generator/Converter Model REGCA (See [REGCA](REGCA/README.md)) - Renewable Energy Generator/Converter Model REGCB (See [REGCB](REGCB/README.md)) - Renewable Energy Electrical Control Model REECA (See [REECA](REECA/README.md)) -- Renewable Energy Plant Control Model REPCA (See [REPCA](REPCA/README.md)) diff --git a/GridKit/Model/PhasorDynamics/SystemModelData.hpp b/GridKit/Model/PhasorDynamics/SystemModelData.hpp index e50f8c1c5..224c95e3c 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelData.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelData.hpp @@ -10,8 +10,8 @@ #include #include #include +#include #include -#include #include #include #include @@ -46,7 +46,7 @@ namespace GridKit using BusToSignalAdapterDataT = BusToSignalAdapterData; using BusFaultDataT = BusFaultData; using RegcaDataT = Converter::RegcaData; - using RepcaDataT = Converter::RepcaData; + using RepcaDataT = Controller::RepcaData; using Tgov1DataT = Governor::Tgov1Data; using Esdc1aDataT = Exciter::Esdc1aData; using HygovDataT = Governor::HygovData; diff --git a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp index 2fa42881a..1edc3f62c 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp @@ -42,6 +42,7 @@ namespace GridKit using namespace Governor; using namespace Exciter; using namespace Stabilizer; + using namespace Controller; using namespace Converter; owns_components_ = true; diff --git a/docs/GridKit/Model/PhasorDynamics/Controller/README.md b/docs/GridKit/Model/PhasorDynamics/Controller/README.md new file mode 100644 index 000000000..c7b0dd611 --- /dev/null +++ b/docs/GridKit/Model/PhasorDynamics/Controller/README.md @@ -0,0 +1,14 @@ +# Controller + +```{toctree} +:maxdepth: 4 +:titlesonly: +:hidden: + +REPCA +``` + +```{include} ../../../../../GridKit/Model/PhasorDynamics/Controller/README.md +:start-line: 1 +:relative-images: +``` diff --git a/docs/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md b/docs/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md new file mode 100644 index 000000000..153acf4bc --- /dev/null +++ b/docs/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md @@ -0,0 +1,6 @@ +# REPCA + +```{include} ../../../../../../GridKit/Model/PhasorDynamics/Controller/REPCA/README.md +:start-line: 1 +:relative-images: +``` diff --git a/docs/GridKit/Model/PhasorDynamics/Converter/README.md b/docs/GridKit/Model/PhasorDynamics/Converter/README.md index 857859edf..fa56e8dd8 100644 --- a/docs/GridKit/Model/PhasorDynamics/Converter/README.md +++ b/docs/GridKit/Model/PhasorDynamics/Converter/README.md @@ -8,7 +8,6 @@ REGCA REGCB REECA -REPCA ``` ```{include} ../../../../../GridKit/Model/PhasorDynamics/Converter/README.md diff --git a/docs/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md b/docs/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md deleted file mode 100644 index 855a3ea10..000000000 --- a/docs/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# REPCA - -```{include} ../../../../../../GridKit/Model/PhasorDynamics/Converter/REPCA/README.md -:start-line: 1 -:relative-images: -``` diff --git a/docs/GridKit/Model/PhasorDynamics/README.md b/docs/GridKit/Model/PhasorDynamics/README.md index e5209139a..e867035a4 100644 --- a/docs/GridKit/Model/PhasorDynamics/README.md +++ b/docs/GridKit/Model/PhasorDynamics/README.md @@ -10,6 +10,7 @@ Branch Bus BusFault BusToSignalAdapter +Controller Converter Exciter Governor diff --git a/tests/UnitTests/PhasorDynamics/CMakeLists.txt b/tests/UnitTests/PhasorDynamics/CMakeLists.txt index 8a8de8c9f..6c8721977 100644 --- a/tests/UnitTests/PhasorDynamics/CMakeLists.txt +++ b/tests/UnitTests/PhasorDynamics/CMakeLists.txt @@ -132,12 +132,12 @@ target_link_libraries( GridKit::phasor_dynamics_bus_dependency_tracking GridKit::testing) -add_executable(test_phasor_converter_repca runConverterRepcaTests.cpp) +add_executable(test_phasor_controller_repca runControllerRepcaTests.cpp) target_link_libraries( - test_phasor_converter_repca + test_phasor_controller_repca GridKit::definitions - GridKit::phasor_dynamics_converter_repca - GridKit::phasor_dynamics_converter_repca_dependency_tracking + GridKit::phasor_dynamics_controller_repca + GridKit::phasor_dynamics_controller_repca_dependency_tracking GridKit::phasor_dynamics_bus GridKit::phasor_dynamics_bus_dependency_tracking GridKit::testing) @@ -197,7 +197,7 @@ add_test(NAME PhasorDynamicsExciterEsdc1aTest COMMAND test_phasor_exciter_esdc1a add_test(NAME PhasorDynamicsGensalTest COMMAND test_phasor_gensal) add_test(NAME PhasorDynamicsExciterSexsPtiTest COMMAND test_phasor_exciter_sexspti) add_test(NAME PhasorDynamicsConverterRegcaTest COMMAND test_phasor_converter_regca) -add_test(NAME PhasorDynamicsConverterRepcaTest COMMAND test_phasor_converter_repca) +add_test(NAME PhasorDynamicsControllerRepcaTest COMMAND test_phasor_controller_repca) add_test(NAME PhasorDynamicsStabilizerIeeestTest COMMAND test_phasor_stabilizer_ieeest) add_test(NAME PhasorDynamicsGenClassicalTest COMMAND test_phasor_gen_classical) add_test(NAME PhasorDynamicsLoadZTest COMMAND test_phasor_loadz) @@ -224,7 +224,7 @@ install( test_phasor_gensal test_phasor_exciter_sexspti test_phasor_converter_regca - test_phasor_converter_repca + test_phasor_controller_repca test_phasor_stabilizer_ieeest test_phasor_gen_classical test_phasor_system diff --git a/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp b/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp index dfb7a1a3f..3af821d2f 100644 --- a/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp @@ -4,6 +4,10 @@ #include #include +#include +#include +#include +#include #include #include #include @@ -140,8 +144,8 @@ namespace GridKit { using ConverterInternal = PhasorDynamics::Converter::RegcaInternalVariables; using ConverterParams = PhasorDynamics::Converter::RegcaParameters; - using PlantExternal = PhasorDynamics::Converter::RepcaExternalVariables; - using PlantParams = PhasorDynamics::Converter::RepcaParameters; + using PlantExternal = PhasorDynamics::Controller::RepcaExternalVariables; + using PlantParams = PhasorDynamics::Controller::RepcaParameters; TestStatus success = true; @@ -177,11 +181,11 @@ namespace GridKit converter_data.parameters[ConverterParams::VA1] = static_cast(0.9); converter_data.parameters[ConverterParams::Vhvmax] = static_cast(1.2); - PhasorDynamics::Converter::RepcaData plant_data; + PhasorDynamics::Controller::RepcaData plant_data; plant_data.parameters[PlantParams::Tp] = static_cast(0.05); - PhasorDynamics::Converter::Regca converter(&bus, converter_data); - PhasorDynamics::Converter::Repca plant(&bus, plant_data); + PhasorDynamics::Converter::Regca converter(&bus, converter_data); + PhasorDynamics::Controller::Repca plant(&bus, plant_data); auto& converter_signals = converter.getSignals(); converter_signals.template assignSignalNode(&ir); diff --git a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp similarity index 98% rename from tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp rename to tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp index 8de8290ab..461b8fd8b 100644 --- a/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp @@ -13,8 +13,8 @@ #include #include #include -#include -#include +#include +#include #include #include #include @@ -29,15 +29,15 @@ namespace GridKit using Log = ::GridKit::Utilities::Logger; template - class ConverterRepcaTests + class ControllerRepcaTests { public: using ScalarT = scalar_type; using IdxT = index_type; using RealT = typename PhasorDynamics::Component::RealT; - ConverterRepcaTests() = default; - ~ConverterRepcaTests() = default; + ControllerRepcaTests() = default; + ~ControllerRepcaTests() = default; // Every probe state clears its smooth transitions by a margin, so the // widest gap to an ideal expected value is the floored-lag PREF row at @@ -60,7 +60,7 @@ namespace GridKit PhasorDynamics::Bus bus(1.0, 0.0); - PhasorDynamics::Converter::Repca empty(&bus); + PhasorDynamics::Controller::Repca empty(&bus); success *= (empty.size() == static_cast(index(Vars::MAXIMUM))); success *= (empty.getMonitor() == nullptr); success *= (empty.verify() > 0); @@ -82,7 +82,7 @@ namespace GridKit integer_parameter.attachAllInputs(); success *= (integer_parameter.repca.verify() == 0); - PhasorDynamics::Converter::Repca missing_signals(&bus, makeData()); + PhasorDynamics::Controller::Repca missing_signals(&bus, makeData()); success *= (missing_signals.verify() > 0); success *= invalidParameterCase(Params::mva, 0.0); @@ -186,7 +186,7 @@ namespace GridKit } } - PhasorDynamics::Converter::Repca busless(nullptr, makeData()); + PhasorDynamics::Controller::Repca busless(nullptr, makeData()); success *= (busless.verify() > 0); success *= unlinkedSignalRejected(); @@ -1079,12 +1079,12 @@ namespace GridKit #endif private: - using Params = PhasorDynamics::Converter::RepcaParameters; - using Vars = PhasorDynamics::Converter::RepcaInternalVariables; - using Ext = PhasorDynamics::Converter::RepcaExternalVariables; - using Mon = PhasorDynamics::Converter::RepcaMonitorableVariables; - using Data = PhasorDynamics::Converter::RepcaData; - using RepcaT = PhasorDynamics::Converter::Repca; + using Params = PhasorDynamics::Controller::RepcaParameters; + using Vars = PhasorDynamics::Controller::RepcaInternalVariables; + using Ext = PhasorDynamics::Controller::RepcaExternalVariables; + using Mon = PhasorDynamics::Controller::RepcaMonitorableVariables; + using Data = PhasorDynamics::Controller::RepcaData; + using RepcaT = PhasorDynamics::Controller::Repca; using JacobianRow = DependencyTracking::Variable::DependencyMap; static constexpr size_t index(Vars variable) @@ -1258,8 +1258,8 @@ namespace GridKit return input_indices_[index(port)]; } - PhasorDynamics::Bus bus; - PhasorDynamics::Converter::Repca repca; + PhasorDynamics::Bus bus; + PhasorDynamics::Controller::Repca repca; }; static constexpr RealT kStateVr = 0.9; @@ -1409,7 +1409,7 @@ namespace GridKit } template - void setAnswerKeyState(PhasorDynamics::Converter::Repca& repca) const + void setAnswerKeyState(PhasorDynamics::Controller::Repca& repca) const { // Every smooth-transition argument keeps a saturation margin, and // every clamp that must pass its input through sits at the midpoint @@ -1645,7 +1645,7 @@ namespace GridKit } template - void setState(PhasorDynamics::Converter::Repca& repca, Rows rows) const + void setState(PhasorDynamics::Controller::Repca& repca, Rows rows) const { auto* y = repca.y().getData(); for (const auto& [variable, value] : rows) @@ -1656,7 +1656,7 @@ namespace GridKit } template - void setDerivative(PhasorDynamics::Converter::Repca& repca, Rows rows) const + void setDerivative(PhasorDynamics::Controller::Repca& repca, Rows rows) const { auto* yp = repca.yp().getData(); for (const auto& [variable, value] : rows) diff --git a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp index a2d52bd60..c55743f66 100644 --- a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp @@ -257,11 +257,11 @@ namespace GridKit /// and prove assigned command outputs alias the assembled signals. TestOutcome repca() { - using Buses = PhasorDynamics::Converter::RepcaBuses; - using Inputs = PhasorDynamics::Converter::RepcaSignalInputs; - using Outputs = PhasorDynamics::Converter::RepcaSignalOutputs; - using Params = PhasorDynamics::Converter::RepcaParameters; - using Vars = PhasorDynamics::Converter::RepcaInternalVariables; + using Buses = PhasorDynamics::Controller::RepcaBuses; + using Inputs = PhasorDynamics::Controller::RepcaSignalInputs; + using Outputs = PhasorDynamics::Controller::RepcaSignalOutputs; + using Params = PhasorDynamics::Controller::RepcaParameters; + using Vars = PhasorDynamics::Controller::RepcaInternalVariables; constexpr size_t input_count = static_cast(Inputs::SIZE); constexpr IdxT bus_id = static_cast(1); diff --git a/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp b/tests/UnitTests/PhasorDynamics/runControllerRepcaTests.cpp similarity index 82% rename from tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp rename to tests/UnitTests/PhasorDynamics/runControllerRepcaTests.cpp index 52e2aee07..ab10fca94 100644 --- a/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runControllerRepcaTests.cpp @@ -1,10 +1,10 @@ -#include "ConverterRepcaTests.hpp" +#include "ControllerRepcaTests.hpp" int main() { GridKit::Testing::TestingResults result; - GridKit::Testing::ConverterRepcaTests test; + GridKit::Testing::ControllerRepcaTests test; result += test.validation(); result += test.initializationAndSignals(); diff --git a/tests/UnitTests/Utilities/CaseFormatTests.hpp b/tests/UnitTests/Utilities/CaseFormatTests.hpp index 4ec637c21..e4f46bcc4 100644 --- a/tests/UnitTests/Utilities/CaseFormatTests.hpp +++ b/tests/UnitTests/Utilities/CaseFormatTests.hpp @@ -7,8 +7,8 @@ #include #include #include +#include #include -#include #include #include #include @@ -194,7 +194,7 @@ namespace GridKit using BusType = typename BusData::BusType; using Esdc1aData = Exciter::Esdc1aData; using HygovData = Governor::HygovData; - using RepcaData = Converter::RepcaData; + using RepcaData = Controller::RepcaData; const char data[] = R"({ From 8d14b6253ccb885be56ac648f7e16c076f3264af Mon Sep 17 00:00:00 2001 From: lukelowry Date: Wed, 5 Aug 2026 11:14:41 -0500 Subject: [PATCH 11/19] misc formatting and standardization --- .../PhasorDynamics/Controller/REPCA/Repca.hpp | 10 +- .../Controller/REPCA/RepcaImpl.hpp | 313 ++++++++---------- 2 files changed, 155 insertions(+), 168 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp index d58c0a9a0..9d3896de0 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp @@ -70,6 +70,14 @@ namespace GridKit MAXIMUM ///< Number of external variables }; + /** + * @class Repca + * @brief WECC renewable plant controller with reactive-power and + * active-power control paths. + * + * @tparam scalar_type Plain real or differentiable scalar type. + * @tparam index_type Integer index type. + */ template class Repca : public Component { @@ -201,7 +209,7 @@ namespace GridKit RealT Tlag_{static_cast(3.0)}; IdxT parameter_error_count_{0}; - RealT va_converter_base_{ZERO}; + RealT va_component_base_{ZERO}; RealT vcomp_on_{ONE}; RealT vcomp_off_{ZERO}; RealT ref_on_{ONE}; diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp index 7d5a792eb..13abd072b 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp @@ -25,15 +25,6 @@ namespace GridKit /// Logger used for REPCA diagnostics. using Log = ::GridKit::Utilities::Logger; - /** - * @class Repca - * @brief WECC renewable plant controller with reactive-power and - * active-power control paths. - * - * @tparam scalar_type Plain real or differentiable scalar type. - * @tparam index_type Integer index type. - */ - /** * @brief Construct REPCA with its documented parameter defaults * @@ -98,9 +89,6 @@ namespace GridKit template int Repca::allocate() { - using I = RepcaInternalVariables; - using E = RepcaExternalVariables; - if (!allocated_) { this->allocateVectors(size_); @@ -113,7 +101,7 @@ namespace GridKit wb_.assign(2, ScalarT{0}); - const auto signal_size = index(E::MAXIMUM); + const auto signal_size = index(RepcaExternalVariables::MAXIMUM); ws_.assign(signal_size, ScalarT{0}); ws_indices_.assign(signal_size, INVALID_INDEX); @@ -128,15 +116,15 @@ namespace GridKit if (signals_.template isAssigned()) { signals_.template getSignalNode()->set( - &y[index(I::QEXT)], - &(this->getVariableIndex(static_cast(index(I::QEXT))))); + &y[index(RepcaInternalVariables::QEXT)], + &(this->getVariableIndex(static_cast(index(RepcaInternalVariables::QEXT))))); } if (signals_.template isAssigned()) { signals_.template getSignalNode()->set( - &y[index(I::PEXT)], - &(this->getVariableIndex(static_cast(index(I::PEXT))))); + &y[index(RepcaInternalVariables::PEXT)], + &(this->getVariableIndex(static_cast(index(RepcaInternalVariables::PEXT))))); } allocated_ = true; @@ -169,24 +157,24 @@ namespace GridKit check(bus_ != nullptr, "regulated bus is required"); - const bool valid_converter_base = std::isfinite(mva_base_) + const bool valid_component_base = std::isfinite(mva_base_) && mva_base_ > ZERO - && std::isfinite(va_converter_base_) - && va_converter_base_ > ZERO; + && std::isfinite(va_component_base_) + && va_component_base_ > ZERO; const bool valid_system_base = std::isfinite(va_system_base_) && va_system_base_ > ZERO; - check(valid_converter_base, - "mva must define a finite positive converter power base"); + check(valid_component_base, + "mva must define a finite positive component power base"); check(valid_system_base, "system power base must be finite and positive"); - if (valid_converter_base && valid_system_base) - { - const RealT system_to_converter = va_system_base_ / va_converter_base_; - const RealT converter_to_system = va_converter_base_ / va_system_base_; - check(std::isfinite(system_to_converter) - && system_to_converter > ZERO - && std::isfinite(converter_to_system) - && converter_to_system > ZERO, - "system/converter power-base conversion ratios must be finite and positive"); + if (valid_component_base && valid_system_base) + { + const RealT system_to_component = va_system_base_ / va_component_base_; + const RealT component_to_system = va_component_base_ / va_system_base_; + check(std::isfinite(system_to_component) + && system_to_component > ZERO + && std::isfinite(component_to_system) + && component_to_system > ZERO, + "system/component power-base conversion ratios must be finite and positive"); } check(dbdlow_ <= ZERO && ZERO <= dbdupper_, @@ -261,8 +249,6 @@ namespace GridKit template int Repca::initialize() { - using I = RepcaInternalVariables; - if (!allocated_) { Log::error() << "Repca: allocate must complete before initialize\n"; @@ -277,8 +263,8 @@ namespace GridKit auto* y = y_.getData(); - const ScalarT qext0_system = y[index(I::QEXT)]; - const ScalarT pext0_system = y[index(I::PEXT)]; + const ScalarT qext0_system = y[index(RepcaInternalVariables::QEXT)]; + const ScalarT pext0_system = y[index(RepcaInternalVariables::PEXT)]; const ScalarT qext0 = toComponentBase(qext0_system); const ScalarT pext0 = toComponentBase(pext0_system); @@ -426,28 +412,28 @@ namespace GridKit return 1; } - y[index(I::VMEAS)] = vmeas0; - y[index(I::QMEAS)] = qmeas0; - y[index(I::XQPI)] = xqpi0; - y[index(I::XQLAG)] = xqlag0; - y[index(I::PMEAS)] = pmeas0; - y[index(I::XPPI)] = xppi0; - y[index(I::PREF)] = pref0; - y[index(I::V)] = v0; - y[index(I::VLDC)] = vldc0; - y[index(I::VDROOP)] = vdroop0; - y[index(I::VCTRL)] = vctrl0; - y[index(I::SFRZ)] = sfrz0; - y[index(I::ERQ)] = erq0; - y[index(I::ERQDB)] = erqdb0; - y[index(I::ERQLIM)] = erqlim0; - y[index(I::QPI)] = qpi0; - y[index(I::QEXT)] = qext0_system; - y[index(I::EF)] = ef0; - y[index(I::EP)] = ep0; - y[index(I::EPLIM)] = eplim0; - y[index(I::PPI)] = ppi0; - y[index(I::PEXT)] = pext_output0; + y[index(RepcaInternalVariables::VMEAS)] = vmeas0; + y[index(RepcaInternalVariables::QMEAS)] = qmeas0; + y[index(RepcaInternalVariables::XQPI)] = xqpi0; + y[index(RepcaInternalVariables::XQLAG)] = xqlag0; + y[index(RepcaInternalVariables::PMEAS)] = pmeas0; + y[index(RepcaInternalVariables::XPPI)] = xppi0; + y[index(RepcaInternalVariables::PREF)] = pref0; + y[index(RepcaInternalVariables::V)] = v0; + y[index(RepcaInternalVariables::VLDC)] = vldc0; + y[index(RepcaInternalVariables::VDROOP)] = vdroop0; + y[index(RepcaInternalVariables::VCTRL)] = vctrl0; + y[index(RepcaInternalVariables::SFRZ)] = sfrz0; + y[index(RepcaInternalVariables::ERQ)] = erq0; + y[index(RepcaInternalVariables::ERQDB)] = erqdb0; + y[index(RepcaInternalVariables::ERQLIM)] = erqlim0; + y[index(RepcaInternalVariables::QPI)] = qpi0; + y[index(RepcaInternalVariables::QEXT)] = qext0_system; + y[index(RepcaInternalVariables::EF)] = ef0; + y[index(RepcaInternalVariables::EP)] = ep0; + y[index(RepcaInternalVariables::EPLIM)] = eplim0; + y[index(RepcaInternalVariables::PPI)] = ppi0; + y[index(RepcaInternalVariables::PEXT)] = pext_output0; freqref_set_ = freqref0; vref_set_ = vref0; @@ -488,16 +474,14 @@ namespace GridKit template int Repca::tagDifferentiable() { - using I = RepcaInternalVariables; - std::fill(tag_.begin(), tag_.end(), false); - tag_[index(I::VMEAS)] = true; - tag_[index(I::QMEAS)] = true; - tag_[index(I::XQPI)] = true; - tag_[index(I::XQLAG)] = true; - tag_[index(I::PMEAS)] = true; - tag_[index(I::XPPI)] = true; - tag_[index(I::PREF)] = true; + tag_[index(RepcaInternalVariables::VMEAS)] = true; + tag_[index(RepcaInternalVariables::QMEAS)] = true; + tag_[index(RepcaInternalVariables::XQPI)] = true; + tag_[index(RepcaInternalVariables::XQLAG)] = true; + tag_[index(RepcaInternalVariables::PMEAS)] = true; + tag_[index(RepcaInternalVariables::XPPI)] = true; + tag_[index(RepcaInternalVariables::PREF)] = true; return 0; } @@ -527,61 +511,59 @@ namespace GridKit template int Repca::evaluateResidual() { - using E = RepcaExternalVariables; - - ws_[index(E::VREF)] = vref_set_; - ws_[index(E::PREF)] = pref_set_; - ws_[index(E::QREF)] = qref_set_; - ws_[index(E::FREQREF)] = freqref_set_; + ws_[index(RepcaExternalVariables::VREF)] = vref_set_; + ws_[index(RepcaExternalVariables::PREF)] = pref_set_; + ws_[index(RepcaExternalVariables::QREF)] = qref_set_; + ws_[index(RepcaExternalVariables::FREQREF)] = freqref_set_; std::fill(ws_indices_.begin(), ws_indices_.end(), INVALID_INDEX); - ws_[index(E::IR)] = + ws_[index(RepcaExternalVariables::IR)] = signals_.template readExternalVariable(); - ws_indices_[index(E::IR)] = + ws_indices_[index(RepcaExternalVariables::IR)] = signals_.template readExternalVariableIndex(); - ws_[index(E::II)] = + ws_[index(RepcaExternalVariables::II)] = signals_.template readExternalVariable(); - ws_indices_[index(E::II)] = + ws_indices_[index(RepcaExternalVariables::II)] = signals_.template readExternalVariableIndex(); - ws_[index(E::P)] = + ws_[index(RepcaExternalVariables::P)] = signals_.template readExternalVariable(); - ws_indices_[index(E::P)] = + ws_indices_[index(RepcaExternalVariables::P)] = signals_.template readExternalVariableIndex(); - ws_[index(E::Q)] = + ws_[index(RepcaExternalVariables::Q)] = signals_.template readExternalVariable(); - ws_indices_[index(E::Q)] = + ws_indices_[index(RepcaExternalVariables::Q)] = signals_.template readExternalVariableIndex(); - ws_[index(E::FREQ)] = + ws_[index(RepcaExternalVariables::FREQ)] = signals_.template readExternalVariable(); - ws_indices_[index(E::FREQ)] = + ws_indices_[index(RepcaExternalVariables::FREQ)] = signals_.template readExternalVariableIndex(); if (signals_.template isAttached()) { - ws_[index(E::VREF)] = + ws_[index(RepcaExternalVariables::VREF)] = signals_.template readExternalVariable(); - ws_indices_[index(E::VREF)] = + ws_indices_[index(RepcaExternalVariables::VREF)] = signals_.template readExternalVariableIndex(); } if (signals_.template isAttached()) { - ws_[index(E::PREF)] = + ws_[index(RepcaExternalVariables::PREF)] = signals_.template readExternalVariable(); - ws_indices_[index(E::PREF)] = + ws_indices_[index(RepcaExternalVariables::PREF)] = signals_.template readExternalVariableIndex(); } if (signals_.template isAttached()) { - ws_[index(E::QREF)] = + ws_[index(RepcaExternalVariables::QREF)] = signals_.template readExternalVariable(); - ws_indices_[index(E::QREF)] = + ws_indices_[index(RepcaExternalVariables::QREF)] = signals_.template readExternalVariableIndex(); } if (signals_.template isAttached()) { - ws_[index(E::FREQREF)] = + ws_[index(RepcaExternalVariables::FREQREF)] = signals_.template readExternalVariable(); - ws_indices_[index(E::FREQREF)] = + ws_indices_[index(RepcaExternalVariables::FREQREF)] = signals_.template readExternalVariableIndex(); } @@ -636,7 +618,8 @@ namespace GridKit * @param[in] yp Internal derivatives in the same enum order and bases. * @param[in] wb Regulated-bus real and imaginary voltage components. * @param[in] ws External signals in `RepcaExternalVariables` order. - * @param[out] f Model-owned residuals in `RepcaInternalVariables` order. + * @param[out] f Caller-provided residual output buffer in + * `RepcaInternalVariables` order. */ template [[gnu::always_inline]] inline int @@ -647,81 +630,78 @@ namespace GridKit const ScalarT* ws, ScalarT* f) { - using I = RepcaInternalVariables; - using E = RepcaExternalVariables; - - const ScalarT vmeas = y[index(I::VMEAS)]; - const ScalarT qmeas = y[index(I::QMEAS)]; - const ScalarT xqpi = y[index(I::XQPI)]; - const ScalarT xqlag = y[index(I::XQLAG)]; - const ScalarT pmeas = y[index(I::PMEAS)]; - const ScalarT xppi = y[index(I::XPPI)]; - const ScalarT pref = y[index(I::PREF)]; - const ScalarT v = y[index(I::V)]; - const ScalarT vldc = y[index(I::VLDC)]; - const ScalarT vdroop = y[index(I::VDROOP)]; - const ScalarT vctrl = y[index(I::VCTRL)]; - const ScalarT sfrz = y[index(I::SFRZ)]; - const ScalarT erq = y[index(I::ERQ)]; - const ScalarT erqdb = y[index(I::ERQDB)]; - const ScalarT erqlim = y[index(I::ERQLIM)]; - const ScalarT qpi = y[index(I::QPI)]; - const ScalarT qext = toComponentBase(y[index(I::QEXT)]); - const ScalarT ef = y[index(I::EF)]; - const ScalarT ep = y[index(I::EP)]; - const ScalarT eplim = y[index(I::EPLIM)]; - const ScalarT ppi = y[index(I::PPI)]; - const ScalarT pext = toComponentBase(y[index(I::PEXT)]); - - const ScalarT vmeas_dot = yp[index(I::VMEAS)]; - const ScalarT qmeas_dot = yp[index(I::QMEAS)]; - const ScalarT xqpi_dot = yp[index(I::XQPI)]; - const ScalarT xqlag_dot = yp[index(I::XQLAG)]; - const ScalarT pmeas_dot = yp[index(I::PMEAS)]; - const ScalarT xppi_dot = yp[index(I::XPPI)]; - const ScalarT pref_dot = yp[index(I::PREF)]; + const ScalarT vmeas = y[index(RepcaInternalVariables::VMEAS)]; + const ScalarT qmeas = y[index(RepcaInternalVariables::QMEAS)]; + const ScalarT xqpi = y[index(RepcaInternalVariables::XQPI)]; + const ScalarT xqlag = y[index(RepcaInternalVariables::XQLAG)]; + const ScalarT pmeas = y[index(RepcaInternalVariables::PMEAS)]; + const ScalarT xppi = y[index(RepcaInternalVariables::XPPI)]; + const ScalarT pref = y[index(RepcaInternalVariables::PREF)]; + const ScalarT v = y[index(RepcaInternalVariables::V)]; + const ScalarT vldc = y[index(RepcaInternalVariables::VLDC)]; + const ScalarT vdroop = y[index(RepcaInternalVariables::VDROOP)]; + const ScalarT vctrl = y[index(RepcaInternalVariables::VCTRL)]; + const ScalarT sfrz = y[index(RepcaInternalVariables::SFRZ)]; + const ScalarT erq = y[index(RepcaInternalVariables::ERQ)]; + const ScalarT erqdb = y[index(RepcaInternalVariables::ERQDB)]; + const ScalarT erqlim = y[index(RepcaInternalVariables::ERQLIM)]; + const ScalarT qpi = y[index(RepcaInternalVariables::QPI)]; + const ScalarT qext = toComponentBase(y[index(RepcaInternalVariables::QEXT)]); + const ScalarT ef = y[index(RepcaInternalVariables::EF)]; + const ScalarT ep = y[index(RepcaInternalVariables::EP)]; + const ScalarT eplim = y[index(RepcaInternalVariables::EPLIM)]; + const ScalarT ppi = y[index(RepcaInternalVariables::PPI)]; + const ScalarT pext = toComponentBase(y[index(RepcaInternalVariables::PEXT)]); + + const ScalarT vmeas_dot = yp[index(RepcaInternalVariables::VMEAS)]; + const ScalarT qmeas_dot = yp[index(RepcaInternalVariables::QMEAS)]; + const ScalarT xqpi_dot = yp[index(RepcaInternalVariables::XQPI)]; + const ScalarT xqlag_dot = yp[index(RepcaInternalVariables::XQLAG)]; + const ScalarT pmeas_dot = yp[index(RepcaInternalVariables::PMEAS)]; + const ScalarT xppi_dot = yp[index(RepcaInternalVariables::XPPI)]; + const ScalarT pref_dot = yp[index(RepcaInternalVariables::PREF)]; const ScalarT vr = wb[0]; const ScalarT vi = wb[1]; - const ScalarT ir = ws[index(E::IR)]; - const ScalarT ii = ws[index(E::II)]; - const ScalarT p = toComponentBase(ws[index(E::P)]); - const ScalarT q = toComponentBase(ws[index(E::Q)]); - const ScalarT freq = ws[index(E::FREQ)]; - const ScalarT freqref = ws[index(E::FREQREF)]; - const ScalarT vref = ws[index(E::VREF)]; - const ScalarT qref = toComponentBase(ws[index(E::QREF)]); - const ScalarT pref_in = toComponentBase(ws[index(E::PREF)]); + const ScalarT ir = ws[index(RepcaExternalVariables::IR)]; + const ScalarT ii = ws[index(RepcaExternalVariables::II)]; + const ScalarT p = toComponentBase(ws[index(RepcaExternalVariables::P)]); + const ScalarT q = toComponentBase(ws[index(RepcaExternalVariables::Q)]); + const ScalarT freq = ws[index(RepcaExternalVariables::FREQ)]; + const ScalarT freqref = ws[index(RepcaExternalVariables::FREQREF)]; + const ScalarT vref = ws[index(RepcaExternalVariables::VREF)]; + const ScalarT qref = toComponentBase(ws[index(RepcaExternalVariables::QREF)]); + const ScalarT pref_in = toComponentBase(ws[index(RepcaExternalVariables::PREF)]); const ScalarT vldc_r = vr - Rc_ * ir + Xc_ * ii; const ScalarT vldc_i = vi - Rc_ * ii - Xc_ * ir; const ScalarT pfreq = Ddn_ * Math::ramp(ef) - Dup_ * Math::ramp(-ef); - f[index(I::VMEAS)] = -vmeas_dot + (vctrl - vmeas) / Tfltr_; - f[index(I::QMEAS)] = -qmeas_dot + (q - qmeas) / Tfltr_; - f[index(I::XQPI)] = -xqpi_dot + sfrz * Math::antiwindup(qpi, Ki_ * erqlim, Qmin_, Qmax_); - f[index(I::XQLAG)] = -xqlag_dot + (qpi - xqlag) / Tfv_; - f[index(I::PMEAS)] = -pmeas_dot + (p - pmeas) / Tp_; - f[index(I::XPPI)] = -xppi_dot + Math::antiwindup(ppi, Kig_ * eplim, Pmin_, Pmax_); - f[index(I::PREF)] = -pref_dot + (ppi - pref) / Tlag_; - - f[index(I::V)] = -v * v + vr * vr + vi * vi; - f[index(I::VLDC)] = -vldc * vldc + vldc_r * vldc_r + vldc_i * vldc_i; - f[index(I::VDROOP)] = -vdroop + v + Kc_ * q; - f[index(I::VCTRL)] = -vctrl + vcomp_on_ * vldc + vcomp_off_ * vdroop; - f[index(I::SFRZ)] = -sfrz + Math::above(v, Vfrz_); - f[index(I::ERQ)] = -erq + ref_on_ * (vref - vmeas) + ref_off_ * (qref - qmeas); - f[index(I::ERQDB)] = -erqdb + Math::deadband2(erq, dbdlow_, dbdupper_); - f[index(I::ERQLIM)] = -erqlim + Math::clamp(erqdb, emin_, emax_); - f[index(I::QPI)] = -qpi + Math::clamp(Kp_ * erqlim + xqpi, Qmin_, Qmax_); - f[index(I::QEXT)] = -Tfv_ * (qext - xqlag) + Tft_ * (qpi - xqlag); - - f[index(I::EF)] = -ef + Math::deadband2(freqref - freq, fdbd1_, fdbd2_); - f[index(I::EP)] = -ep + pref_in - pmeas + pfreq; - f[index(I::EPLIM)] = -eplim + Math::clamp(ep, femin_, femax_); - f[index(I::PPI)] = -ppi + Math::clamp(Kpg_ * eplim + xppi, Pmin_, Pmax_); - f[index(I::PEXT)] = -pext + freq_on_ * pref; + f[index(RepcaInternalVariables::VMEAS)] = -vmeas_dot + (vctrl - vmeas) / Tfltr_; + f[index(RepcaInternalVariables::QMEAS)] = -qmeas_dot + (q - qmeas) / Tfltr_; + f[index(RepcaInternalVariables::XQPI)] = -xqpi_dot + sfrz * Math::antiwindup(qpi, Ki_ * erqlim, Qmin_, Qmax_); + f[index(RepcaInternalVariables::XQLAG)] = -xqlag_dot + (qpi - xqlag) / Tfv_; + f[index(RepcaInternalVariables::PMEAS)] = -pmeas_dot + (p - pmeas) / Tp_; + f[index(RepcaInternalVariables::XPPI)] = -xppi_dot + Math::antiwindup(ppi, Kig_ * eplim, Pmin_, Pmax_); + f[index(RepcaInternalVariables::PREF)] = -pref_dot + (ppi - pref) / Tlag_; + + f[index(RepcaInternalVariables::V)] = -v * v + vr * vr + vi * vi; + f[index(RepcaInternalVariables::VLDC)] = -vldc * vldc + vldc_r * vldc_r + vldc_i * vldc_i; + f[index(RepcaInternalVariables::VDROOP)] = -vdroop + v + Kc_ * q; + f[index(RepcaInternalVariables::VCTRL)] = -vctrl + vcomp_on_ * vldc + vcomp_off_ * vdroop; + f[index(RepcaInternalVariables::SFRZ)] = -sfrz + Math::above(v, Vfrz_); + f[index(RepcaInternalVariables::ERQ)] = -erq + ref_on_ * (vref - vmeas) + ref_off_ * (qref - qmeas); + f[index(RepcaInternalVariables::ERQDB)] = -erqdb + Math::deadband2(erq, dbdlow_, dbdupper_); + f[index(RepcaInternalVariables::ERQLIM)] = -erqlim + Math::clamp(erqdb, emin_, emax_); + f[index(RepcaInternalVariables::QPI)] = -qpi + Math::clamp(Kp_ * erqlim + xqpi, Qmin_, Qmax_); + f[index(RepcaInternalVariables::QEXT)] = -Tfv_ * (qext - xqlag) + Tft_ * (qpi - xqlag); + + f[index(RepcaInternalVariables::EF)] = -ef + Math::deadband2(freqref - freq, fdbd1_, fdbd2_); + f[index(RepcaInternalVariables::EP)] = -ep + pref_in - pmeas + pfreq; + f[index(RepcaInternalVariables::EPLIM)] = -eplim + Math::clamp(ep, femin_, femax_); + f[index(RepcaInternalVariables::PPI)] = -ppi + Math::clamp(Kpg_ * eplim + xppi, Pmin_, Pmax_); + f[index(RepcaInternalVariables::PEXT)] = -pext + freq_on_ * pref; return 0; } @@ -841,25 +821,24 @@ namespace GridKit template void Repca::initializeMonitor() { - using I = RepcaInternalVariables; using Variable = typename ModelDataT::MonitorableVariables; monitor_->set(Variable::qext, [this] - { return y_.getData()[index(I::QEXT)]; }); + { return y_.getData()[index(RepcaInternalVariables::QEXT)]; }); monitor_->set(Variable::pext, [this] - { return y_.getData()[index(I::PEXT)]; }); + { return y_.getData()[index(RepcaInternalVariables::PEXT)]; }); monitor_->set(Variable::vmeas, [this] - { return y_.getData()[index(I::VMEAS)]; }); + { return y_.getData()[index(RepcaInternalVariables::VMEAS)]; }); monitor_->set(Variable::qmeas, [this] - { return y_.getData()[index(I::QMEAS)]; }); + { return y_.getData()[index(RepcaInternalVariables::QMEAS)]; }); monitor_->set(Variable::pmeas, [this] - { return y_.getData()[index(I::PMEAS)]; }); + { return y_.getData()[index(RepcaInternalVariables::PMEAS)]; }); } /** * @brief Resolve parameter-derived constants * - * Raises the explicit controller lags in place, computes the converter + * Raises the explicit controller lags in place, computes the component * power base, and resolves selector masks. */ template @@ -893,7 +872,7 @@ namespace GridKit Tp_ = std::max(Tp_, TIME_CONSTANT_MINIMUM); Tlag_ = std::max(Tlag_, TIME_CONSTANT_MINIMUM); - va_converter_base_ = mva_base_ * static_cast(1.0e6); + va_component_base_ = mva_base_ * static_cast(1.0e6); vcomp_on_ = VcompFlag_ ? ONE : ZERO; vcomp_off_ = ONE - vcomp_on_; @@ -1001,7 +980,7 @@ namespace GridKit [[gnu::always_inline]] inline scalar_type Repca::toComponentBase(scalar_type value) const { - return value * (va_system_base_ / va_converter_base_); + return value * (va_system_base_ / va_component_base_); } /** @@ -1013,7 +992,7 @@ namespace GridKit template scalar_type Repca::toSystemBase(scalar_type value) const { - return value * (va_converter_base_ / va_system_base_); + return value * (va_component_base_ / va_system_base_); } /** From a40326bfca635cbf893429149d3134331119f7dc Mon Sep 17 00:00:00 2001 From: lukelowry Date: Wed, 5 Aug 2026 11:21:19 -0500 Subject: [PATCH 12/19] final tweaking of parameter validation --- .../PhasorDynamics/Controller/REPCA/README.md | 22 ++++++++++--------- .../Controller/REPCA/RepcaImpl.hpp | 9 ++++---- .../PhasorDynamics/ControllerRepcaTests.hpp | 4 ++-- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md b/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md index 2d65f28f4..0000d5f98 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md @@ -7,8 +7,11 @@ resources. - Fig. 1 hard nonlinearities use the linked CommonMath smooth approximations; transition-point values may differ. -- The PowerWorld Governor Response Limits `Down Only` and `Fixed` modes are not - implemented; $P^{\min}$ and $P^{\max}$ remain static parameters. + +> [!WARNING] +> GridKit does not yet apply the associated generator's Governor Response Limits +> modes `Down Only` and `Fixed` to REPCA. The model always uses its configured +> $P^{\min}$ and $P^{\max}$. ## Block Diagram @@ -60,7 +63,6 @@ All real parameters must be finite. Invalid parameter sets are rejected by: ```math \begin{aligned} S^\mathrm{base} &> 0 \\ - T_\mathrm{fv} &> 0 \\ D_\mathrm{bd1} &\le 0 \le D_\mathrm{bd2} \\ e^{\min} &\le 0 \le e^{\max} \\ Q^{\min} &\le Q^{\max} \\ @@ -80,7 +82,7 @@ floor with a warning: ```math \begin{aligned} - T_x &\leftarrow \max(T_x,\epsilon_T), \quad x\in\{\mathrm{fltr},\mathrm{p},\mathrm{lag}\} \\ + T_x &\leftarrow \max(T_x,\epsilon_T), \quad x\in\{\mathrm{fltr},\mathrm{fv},\mathrm{p},\mathrm{lag}\} \\ s_\mathrm{comp}^\mathrm{off} &= 1 - s_\mathrm{comp} \\ s_\mathrm{ref}^\mathrm{off} &= 1 - s_\mathrm{ref} \\ k_\mathrm{base} &= \dfrac{S^\mathrm{sys}}{S^\mathrm{base}} @@ -234,7 +236,7 @@ REPCA reconstructs a steady operating point; arbitrary-state restart is unsuppor P, Q &\leftarrow \text{branch power} \\ f &\leftarrow \text{frequency input} \\ Q^\mathrm{ext} &\leftarrow \text{known reactive-power command on system base} \\ - P^\mathrm{ext} &\leftarrow \text{known active-power command on system base}, \quad s_\mathrm{freq}\ \text{enabled}. + P^\mathrm{ext} &\leftarrow \text{known active-power command on system base}, \quad s_\mathrm{freq}=1 \end{aligned} ``` @@ -266,13 +268,13 @@ $z\in[\ell,u]$ within that tolerance, including the limits. e_P &\leftarrow 0 \\ e_P^\mathrm{lim} &\leftarrow \text{clamp}(e_P;\,e_P^{\min},e_P^{\max}) \\ P^\mathrm{ref} &\leftarrow \begin{cases} - k_\mathrm{base}P^\mathrm{ext} & s_\mathrm{freq}\ \text{enabled} \\ - \text{clamp}(P^\mathrm{meas};\,P^{\min},P^{\max}) & s_\mathrm{freq}\ \text{disabled} + k_\mathrm{base}P^\mathrm{ext} & s_\mathrm{freq}=1 \\ + \text{clamp}(P^\mathrm{meas};\,P^{\min},P^{\max}) & s_\mathrm{freq}=0 \end{cases} \\ P^\mathrm{PI} &\leftarrow P^\mathrm{ref} \\ u_P^\mathrm{PI} &\leftarrow \begin{cases} - \text{clamp}^{-1}(P^\mathrm{PI};\,P^{\min},P^{\max}) & s_\mathrm{freq}\ \text{enabled} \\ - P^\mathrm{meas} & s_\mathrm{freq}\ \text{disabled} + \text{clamp}^{-1}(P^\mathrm{PI};\,P^{\min},P^{\max}) & s_\mathrm{freq}=1 \\ + P^\mathrm{meas} & s_\mathrm{freq}=0 \end{cases} \\ x_P^\mathrm{PI} &\leftarrow u_P^\mathrm{PI} - K_\mathrm{pg}e_P^\mathrm{lim} \\ P^\mathrm{ext} &\leftarrow \dfrac{s_\mathrm{freq}}{k_\mathrm{base}}P^\mathrm{ref} \\ @@ -284,7 +286,7 @@ Initialization rejects an operating point if: - a required input or derived value is not finite; - $k_\mathrm{base}Q^\mathrm{ext}\notin[Q^{\min},Q^{\max}]$, or - $s_\mathrm{freq}$ is enabled and + $s_\mathrm{freq}=1$ and $k_\mathrm{base}P^\mathrm{ext}\notin[P^{\min},P^{\max}]$; or - the gated reactive-power or active-power PI state rate is nonfinite or exceeds $\epsilon_\mathrm{init}$ in magnitude. diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp index 13abd072b..28be4cf29 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp @@ -182,7 +182,6 @@ namespace GridKit check(emin_ <= ZERO && ZERO <= emax_, "emin <= 0 <= emax is required"); check(Qmin_ <= Qmax_, "Qmin must be less than or equal to Qmax"); - check(Tfv_ > ZERO, "Tfv must be positive"); check(fdbd1_ <= ZERO && ZERO <= fdbd2_, "fdbd1 <= 0 <= fdbd2 is required"); check(femin_ <= ZERO && ZERO <= femax_, @@ -857,18 +856,20 @@ namespace GridKit check_non_negative(Tfltr_, "Tfltr"); check_non_negative(Tft_, "Tft"); + check_non_negative(Tfv_, "Tfv"); check_non_negative(Tp_, "Tp"); check_non_negative(Tlag_, "Tlag"); - if (Tfltr_ < TIME_CONSTANT_MINIMUM || Tp_ < TIME_CONSTANT_MINIMUM - || Tlag_ < TIME_CONSTANT_MINIMUM) + if (Tfltr_ < TIME_CONSTANT_MINIMUM || Tfv_ < TIME_CONSTANT_MINIMUM + || Tp_ < TIME_CONSTANT_MINIMUM || Tlag_ < TIME_CONSTANT_MINIMUM) { - Log::warning() << "Repca: Tfltr, Tp, and Tlag below " + Log::warning() << "Repca: Tfltr, Tfv, Tp, and Tlag below " << TIME_CONSTANT_MINIMUM << " s are raised to that floor to keep the controller lags well posed\n"; } Tfltr_ = std::max(Tfltr_, TIME_CONSTANT_MINIMUM); + Tfv_ = std::max(Tfv_, TIME_CONSTANT_MINIMUM); Tp_ = std::max(Tp_, TIME_CONSTANT_MINIMUM); Tlag_ = std::max(Tlag_, TIME_CONSTANT_MINIMUM); diff --git a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp index 461b8fd8b..348fcf271 100644 --- a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp @@ -86,7 +86,6 @@ namespace GridKit success *= (missing_signals.verify() > 0); success *= invalidParameterCase(Params::mva, 0.0); - success *= invalidParameterCase(Params::Tfv, 0.0); success *= invalidParameterCase(Params::Tfv, -0.1); success *= invalidParameterCase(Params::dbdlow, 0.1); success *= invalidParameterCase(Params::dbdupper, -0.1); @@ -201,6 +200,7 @@ namespace GridKit auto floor_data = makeInitializationData(); floor_data.parameters[Params::Tfltr] = 0.0; + floor_data.parameters[Params::Tfv] = 0.0; floor_data.parameters[Params::Tp] = 0.0; floor_data.parameters[Params::Tlag] = 0.0; @@ -519,7 +519,7 @@ namespace GridKit // An invalid configuration is rejected before any state is written. { auto invalid_data = data; - invalid_data.parameters[Params::Tfv] = 0.0; + invalid_data.parameters[Params::Tfv] = -0.1; Fixture invalid_fixture(invalid_data); invalid_fixture.attachAllInputs(); setInitializationInputs(invalid_fixture); From 96c5e612bc55293483b4cb617f780c26cd541a9b Mon Sep 17 00:00:00 2001 From: lukelowry Date: Wed, 5 Aug 2026 11:58:56 -0500 Subject: [PATCH 13/19] make freq port not required --- .../PhasorDynamics/Controller/REPCA/README.md | 6 +++++- .../PhasorDynamics/Controller/REPCA/Repca.hpp | 2 +- .../Controller/REPCA/RepcaData.hpp | 2 +- .../Controller/REPCA/RepcaImpl.hpp | 20 +++++++++++++------ .../ComponentConnectionTests.hpp | 10 +--------- .../PhasorDynamics/ControllerRepcaTests.hpp | 11 +++++++--- 6 files changed, 30 insertions(+), 21 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md b/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md index 0000d5f98..5e4cce55c 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md @@ -8,6 +8,10 @@ resources. - Fig. 1 hard nonlinearities use the linked CommonMath smooth approximations; transition-point values may differ. +> [!NOTE] +> `freq` is optional because the regulated bus does not yet expose a frequency +> signal. + > [!WARNING] > GridKit does not yet apply the associated generator's Governor Response Limits > modes `Down Only` and `Fixed` to REPCA. The model always uses its configured @@ -101,7 +105,7 @@ Name | Port | Init | Description `ii` | Input | Known | Branch-current imaginary component on system base `p` | Input | Known | Branch active power on system base `q` | Input | Known | Branch reactive power on system base -`freq` | Input | Known | Frequency input[^frequency-measurement] Not supported yet. +`freq` | Input | Known | Optional frequency input; defaults to 1.0 p.u.[^frequency-measurement] `vref` | Input | Unknown | Voltage-control reference `pref` | Input | Unknown | Plant active-power reference on system base `qref` | Input | Unknown | Reactive-power reference on system base diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp index 9d3896de0..ce84c6c3a 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp @@ -62,7 +62,7 @@ namespace GridKit II, ///< \f$I_\mathrm{i}\f$ Required branch-current imaginary component on system base [p.u.] P, ///< \f$P\f$ Required branch active power on system base [p.u.] Q, ///< \f$Q\f$ Required branch reactive power on system base [p.u.] - FREQ, ///< \f$f\f$ Required frequency input [p.u.] + FREQ, ///< \f$f\f$ Optional frequency input [p.u.] VREF, ///< \f$V^\mathrm{ref}\f$ Optional voltage-control reference [p.u.] PREF, ///< \f$P^\mathrm{ref}\f$ Optional plant active-power reference on system base [p.u.] QREF, ///< \f$Q^\mathrm{ref}\f$ Optional reactive-power reference on system base [p.u.] diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaData.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaData.hpp index 0897a5f5b..cd4b7805a 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaData.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaData.hpp @@ -64,7 +64,7 @@ namespace GridKit ii, ///< \f$I_\mathrm{i}\f$ Required Known branch-current imaginary input on system base [p.u.] p, ///< \f$P\f$ Required Known branch active-power input on system base [p.u.] q, ///< \f$Q\f$ Required Known branch reactive-power input on system base [p.u.] - freq, ///< \f$f\f$ Required Known frequency input [p.u.] + freq, ///< \f$f\f$ Optional Known frequency input [p.u.] vref, ///< \f$V^\mathrm{ref}\f$ Optional Unknown voltage-reference input [p.u.] pref, ///< \f$P^\mathrm{ref}\f$ Optional Unknown plant active-power reference on system base [p.u.] qref, ///< \f$Q^\mathrm{ref}\f$ Optional Unknown reactive-power reference on system base [p.u.] diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp index 28be4cf29..b3f419b39 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp @@ -206,7 +206,6 @@ namespace GridKit check_required_signal.template operator()("ii"); check_required_signal.template operator()("p"); check_required_signal.template operator()("q"); - check_required_signal.template operator()("freq"); auto check_optional_signal = [&](const char* name) { @@ -221,6 +220,7 @@ namespace GridKit check_optional_signal.template operator()("vref"); check_optional_signal.template operator()("pref"); check_optional_signal.template operator()("qref"); + check_optional_signal.template operator()("freq"); check_optional_signal.template operator()("freqref"); return ret; @@ -277,7 +277,11 @@ namespace GridKit signals_.template readExternalVariable(); const ScalarT q_system = signals_.template readExternalVariable(); - const ScalarT freq = signals_.template readExternalVariable(); + ScalarT freq = static_cast(ONE); + if (signals_.template isAttached()) + { + freq = signals_.template readExternalVariable(); + } auto is_finite = [](ScalarT value) { @@ -513,6 +517,7 @@ namespace GridKit ws_[index(RepcaExternalVariables::VREF)] = vref_set_; ws_[index(RepcaExternalVariables::PREF)] = pref_set_; ws_[index(RepcaExternalVariables::QREF)] = qref_set_; + ws_[index(RepcaExternalVariables::FREQ)] = static_cast(ONE); ws_[index(RepcaExternalVariables::FREQREF)] = freqref_set_; std::fill(ws_indices_.begin(), ws_indices_.end(), INVALID_INDEX); @@ -532,10 +537,13 @@ namespace GridKit signals_.template readExternalVariable(); ws_indices_[index(RepcaExternalVariables::Q)] = signals_.template readExternalVariableIndex(); - ws_[index(RepcaExternalVariables::FREQ)] = - signals_.template readExternalVariable(); - ws_indices_[index(RepcaExternalVariables::FREQ)] = - signals_.template readExternalVariableIndex(); + if (signals_.template isAttached()) + { + ws_[index(RepcaExternalVariables::FREQ)] = + signals_.template readExternalVariable(); + ws_indices_[index(RepcaExternalVariables::FREQ)] = + signals_.template readExternalVariableIndex(); + } if (signals_.template isAttached()) { diff --git a/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp b/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp index 3af821d2f..5e7dd9b04 100644 --- a/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp @@ -138,8 +138,7 @@ namespace GridKit /// REGCA initializes first and publishes its branch current and power /// to the four shared nodes. REPCA then initializes around those - /// measurements and must hold a steady state. REGCA has no frequency - /// output, so that required input keeps a locally owned node. + /// measurements and must hold a steady state without a frequency input. TestOutcome regcaRepca() { using ConverterInternal = PhasorDynamics::Converter::RegcaInternalVariables; @@ -158,12 +157,6 @@ namespace GridKit PhasorDynamics::SignalNode p; PhasorDynamics::SignalNode q; - ScalarT freq_value = static_cast(1.0); - IdxT freq_index = INVALID_INDEX; - - PhasorDynamics::SignalNode freq; - freq.set(&freq_value, &freq_index); - PhasorDynamics::Converter::RegcaData converter_data; converter_data.parameters[ConverterParams::p0] = static_cast(0.0); converter_data.parameters[ConverterParams::q0] = static_cast(0.0); @@ -198,7 +191,6 @@ namespace GridKit plant_signals.template attachSignalNode(&ii); plant_signals.template attachSignalNode(&p); plant_signals.template attachSignalNode(&q); - plant_signals.template attachSignalNode(&freq); system.addBus(&bus); system.addComponent(&converter); diff --git a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp index 348fcf271..14bad5c76 100644 --- a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp @@ -323,9 +323,10 @@ namespace GridKit } Fixture fallback(makeInitializationData(), 0.8, 0.6); - fallback.attachRequiredInputs(); + fallback.attachAllInputs(0.0, false); setInitializationInputs(fallback); success *= fallback.initialize(0.25, 0.45); + success *= scalarMatches(fallback.input(Ext::FREQREF), 1.0, "default frequency"); success *= (fallback.repca.evaluateResidual() == 0); success *= allResidualsWithinInitTolerance(fallback.repca); @@ -1188,14 +1189,18 @@ namespace GridKit signals.template attachSignalNode(&input_nodes_[index(Ext::II)]); signals.template attachSignalNode(&input_nodes_[index(Ext::P)]); signals.template attachSignalNode(&input_nodes_[index(Ext::Q)]); - signals.template attachSignalNode(&input_nodes_[index(Ext::FREQ)]); } - void attachAllInputs(RealT initial_value = 0.0) + void attachAllInputs(RealT initial_value = 0.0, + bool attach_frequency = true) { attachRequiredInputs(initial_value); auto& signals = repca.getSignals(); + if (attach_frequency) + { + signals.template attachSignalNode(&input_nodes_[index(Ext::FREQ)]); + } signals.template attachSignalNode(&input_nodes_[index(Ext::VREF)]); signals.template attachSignalNode(&input_nodes_[index(Ext::PREF)]); signals.template attachSignalNode(&input_nodes_[index(Ext::QREF)]); From ebc33869ee42ee977610468bd650a079e4935c4a Mon Sep 17 00:00:00 2001 From: lukelowry Date: Wed, 5 Aug 2026 12:19:42 -0500 Subject: [PATCH 14/19] Make sure current is on component base. Complete --- .../PhasorDynamics/Controller/REPCA/README.md | 61 +++++--- .../PhasorDynamics/Controller/REPCA/Repca.hpp | 23 ++- .../Controller/REPCA/RepcaData.hpp | 6 +- .../Controller/REPCA/RepcaImpl.hpp | 146 +++++++++++++++--- .../PhasorDynamics/ControllerRepcaTests.hpp | 114 +++++++++----- 5 files changed, 264 insertions(+), 86 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md b/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md index 5e4cce55c..ffe2e8214 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md @@ -34,8 +34,8 @@ $s_\mathrm{ref}$ | [boolean] | `RefFlag` | Reactive-loop ref $s_\mathrm{freq}$ | [boolean] | `Freqflag` | Active-power output selector | `false` | `true` = command enabled, `false` = zero output $T_\mathrm{fltr}$ | [sec] | `Tfltr` | Voltage and reactive-power filter time constant | 0.05 | $V^\mathrm{frz}$ | [p.u.] | `Vfrz` | Reactive-power PI freeze-voltage threshold | 0.7 | -$R_c$ | [p.u.] | `Rc` | Line-drop compensation resistance | 0.0 | System base -$X_c$ | [p.u.] | `Xc` | Line-drop compensation reactance | 0.0 | System base +$R_c$ | [p.u.] | `Rc` | Line-drop compensation resistance | 0.0 | Component base +$X_c$ | [p.u.] | `Xc` | Line-drop compensation reactance | 0.0 | Component base $K_c$ | [p.u.] | `Kc` | Reactive-current compensation gain | 1.0 | $D_\mathrm{bd1}$ | [p.u.] | `dbdlow` | Lower reactive-loop deadband threshold | 0.0 | $D_\mathrm{bd2}$ | [p.u.] | `dbdupper` | Upper reactive-loop deadband threshold | 0.0 | @@ -89,12 +89,14 @@ floor with a warning: T_x &\leftarrow \max(T_x,\epsilon_T), \quad x\in\{\mathrm{fltr},\mathrm{fv},\mathrm{p},\mathrm{lag}\} \\ s_\mathrm{comp}^\mathrm{off} &= 1 - s_\mathrm{comp} \\ s_\mathrm{ref}^\mathrm{off} &= 1 - s_\mathrm{ref} \\ - k_\mathrm{base} &= \dfrac{S^\mathrm{sys}}{S^\mathrm{base}} + k_\mathrm{base} &= \dfrac{S^\mathrm{sys}}{S^\mathrm{base}} \\ + I_\mathrm{r}^\mathrm{comp} &= k_\mathrm{base}I_\mathrm{r}, \quad + I_\mathrm{i}^\mathrm{comp} = k_\mathrm{base}I_\mathrm{i} \end{aligned} ``` $S^\mathrm{sys}$ is the system power base; $k_\mathrm{base}$ converts system-base -power to component base. +power and current signals to component base. ## Model Ports @@ -173,7 +175,7 @@ $P$ | [p.u.] | Known | Branch active power $Q$ | [p.u.] | Known | Branch reactive power | Signal port `q`; system base $f$ | [p.u.] | Known | Frequency input | Signal port `freq` $V^\mathrm{ref}$ | [p.u.] | Unknown | Voltage-control reference | Optional signal port `vref` -$P^\mathrm{ref}$ | [p.u.] | Unknown | Plant active-power reference | Optional signal port `pref`; system base +$P_\mathrm{plant}^\mathrm{ref}$ | [p.u.] | Unknown | Plant active-power reference | Optional signal port `pref`; system base $Q^\mathrm{ref}$ | [p.u.] | Unknown | Reactive-power reference | Optional signal port `qref`; system base $f^\mathrm{ref}$ | [p.u.] | Unknown | Frequency reference | Optional signal port `freqref` @@ -203,7 +205,7 @@ target and smooth approximation. ```math \begin{aligned} 0 &= -V^2 + V_\mathrm{r}^2 + V_\mathrm{i}^2 \\ - 0 &= -(V^\mathrm{ldc})^2 + (V_\mathrm{r} - R_c I_\mathrm{r} + X_c I_\mathrm{i})^2 + (V_\mathrm{i} - R_c I_\mathrm{i} - X_c I_\mathrm{r})^2 \\ + 0 &= -(V^\mathrm{ldc})^2 + (V_\mathrm{r} - R_c I_\mathrm{r}^\mathrm{comp} + X_c I_\mathrm{i}^\mathrm{comp})^2 + (V_\mathrm{i} - R_c I_\mathrm{i}^\mathrm{comp} - X_c I_\mathrm{r}^\mathrm{comp})^2 \\ 0 &= -V^\mathrm{droop} + V + K_c k_\mathrm{base}Q \\ 0 &= -V^\mathrm{ctrl} + s_\mathrm{comp}V^\mathrm{ldc} + s_\mathrm{comp}^\mathrm{off}V^\mathrm{droop} \\ 0 &= -s_\mathrm{frz} + \text{above}(V;\,V^\mathrm{frz}) \\ @@ -213,7 +215,7 @@ target and smooth approximation. 0 &= -Q^\mathrm{PI} + \text{clamp}(K_\mathrm{p}e_\mathrm{RQ}^\mathrm{lim}+x_Q^\mathrm{PI};\,Q^{\min},Q^{\max}) \\ 0 &= -T_\mathrm{fv} (k_\mathrm{base}Q^\mathrm{ext}-x_Q^\mathrm{lag}) + T_\mathrm{ft} (Q^\mathrm{PI}-x_Q^\mathrm{lag}) \\ 0 &= -e_f + \text{deadband2}(f^\mathrm{ref}-f;\,D_\mathrm{bd1}^{f},D_\mathrm{bd2}^{f}) \\ - 0 &= -e_P + k_\mathrm{base}P^\mathrm{ref} - P^\mathrm{meas} + D_\mathrm{dn}\text{ramp}(e_f) - D_\mathrm{up}\text{ramp}(-e_f) \\ + 0 &= -e_P + k_\mathrm{base}P_\mathrm{plant}^\mathrm{ref} - P^\mathrm{meas} + \text{droop}(e_f;D_\mathrm{dn},D_\mathrm{up}) \\ 0 &= -e_P^\mathrm{lim} + \text{clamp}(e_P;\,e_P^{\min},e_P^{\max}) \\ 0 &= -P^\mathrm{PI} + \text{clamp}(K_\mathrm{pg}e_P^\mathrm{lim}+x_P^\mathrm{PI};\,P^{\min},P^{\max}) \\ 0 &= -k_\mathrm{base}P^\mathrm{ext} + s_\mathrm{freq}P^\mathrm{ref}. @@ -221,7 +223,7 @@ target and smooth approximation. ``` CommonMath defines the [derived limiter functions](../../../../CommonMath.md#derived-functions) -and [ramp primitive](../../../../CommonMath.md#primitives) used above. +used above; Appendix A defines `droop`. ### External Equations @@ -246,31 +248,31 @@ REPCA reconstructs a steady operating point; arbitrary-state restart is unsuppor ### Internal Initialization -For initialization tolerance $\epsilon_\mathrm{init}$ of $10^{-10}$, -$\text{clamp}^{-1}(z;\ell,u)$ returns a finite input matching -$z\in[\ell,u]$ within that tolerance, including the limits. +Initialization-only $\text{clamp}^{-1}$ and $\text{deadband2}^{-1}$ recover +finite inputs that reproduce the requested smooth-block outputs. ```math \begin{aligned} V &\leftarrow \sqrt{V_\mathrm{r}^2 + V_\mathrm{i}^2} \\ - V^\mathrm{ldc} &\leftarrow \sqrt{(V_\mathrm{r}-R_c I_\mathrm{r}+X_c I_\mathrm{i})^2 + (V_\mathrm{i}-R_c I_\mathrm{i}-X_c I_\mathrm{r})^2} \\ + V^\mathrm{ldc} &\leftarrow \sqrt{(V_\mathrm{r}-R_c I_\mathrm{r}^\mathrm{comp}+X_c I_\mathrm{i}^\mathrm{comp})^2 + (V_\mathrm{i}-R_c I_\mathrm{i}^\mathrm{comp}-X_c I_\mathrm{r}^\mathrm{comp})^2} \\ V^\mathrm{droop} &\leftarrow V + K_c k_\mathrm{base}Q \\ V^\mathrm{ctrl} &\leftarrow s_\mathrm{comp}V^\mathrm{ldc} + s_\mathrm{comp}^\mathrm{off}V^\mathrm{droop} \\ V^\mathrm{meas} &\leftarrow V^\mathrm{ctrl} \\ Q^\mathrm{meas} &\leftarrow k_\mathrm{base}Q \\ P^\mathrm{meas} &\leftarrow k_\mathrm{base}P \\ s_\mathrm{frz} &\leftarrow \text{above}(V;\,V^\mathrm{frz}) \\ - e_\mathrm{RQ} &\leftarrow 0 \\ - e_\mathrm{RQ}^\mathrm{db} &\leftarrow \text{deadband2}(e_\mathrm{RQ};\,D_\mathrm{bd1},D_\mathrm{bd2}) \\ - e_\mathrm{RQ}^\mathrm{lim} &\leftarrow \text{clamp}(e_\mathrm{RQ}^\mathrm{db};\,e^{\min},e^{\max}) \\ + e_\mathrm{RQ}^\mathrm{db} &\leftarrow \text{clamp}^{-1}(0;\,e^{\min},e^{\max}) \\ + e_\mathrm{RQ} &\leftarrow \text{deadband2}^{-1}(e_\mathrm{RQ}^\mathrm{db};\,D_\mathrm{bd1},D_\mathrm{bd2}) \\ + e_\mathrm{RQ}^\mathrm{lim} &\leftarrow 0 \\ Q^\mathrm{PI} &\leftarrow k_\mathrm{base}Q^\mathrm{ext} \\ x_Q^\mathrm{lag} &\leftarrow Q^\mathrm{PI} \\ u_Q^\mathrm{PI} &\leftarrow \text{clamp}^{-1}(Q^\mathrm{PI};\,Q^{\min},Q^{\max}) \\ x_Q^\mathrm{PI} &\leftarrow u_Q^\mathrm{PI} - K_\mathrm{p}e_\mathrm{RQ}^\mathrm{lim} \\ - e_f &\leftarrow \text{deadband2}(0;\,D_\mathrm{bd1}^{f},D_\mathrm{bd2}^{f}) \\ - P^\mathrm{freq} &\leftarrow D_\mathrm{dn}\text{ramp}(e_f) - D_\mathrm{up}\text{ramp}(-e_f) \\ - e_P &\leftarrow 0 \\ - e_P^\mathrm{lim} &\leftarrow \text{clamp}(e_P;\,e_P^{\min},e_P^{\max}) \\ + \Delta f_0 &\leftarrow \text{deadband2}^{-1}(0;\,D_\mathrm{bd1}^{f},D_\mathrm{bd2}^{f}) \\ + e_f &\leftarrow 0 \\ + P^\mathrm{freq} &\leftarrow \text{droop}(e_f;D_\mathrm{dn},D_\mathrm{up}) = 0 \\ + e_P &\leftarrow \text{clamp}^{-1}(0;\,e_P^{\min},e_P^{\max}) \\ + e_P^\mathrm{lim} &\leftarrow 0 \\ P^\mathrm{ref} &\leftarrow \begin{cases} k_\mathrm{base}P^\mathrm{ext} & s_\mathrm{freq}=1 \\ \text{clamp}(P^\mathrm{meas};\,P^{\min},P^{\max}) & s_\mathrm{freq}=0 @@ -301,10 +303,10 @@ Initialization is atomic; candidates are validated before state or signal writes ```math \begin{aligned} - V^\mathrm{ref} &\leftarrow V^\mathrm{meas} \\ - P^\mathrm{ref} &\leftarrow \dfrac{P^\mathrm{meas}-P^\mathrm{freq}}{k_\mathrm{base}} \\ - Q^\mathrm{ref} &\leftarrow Q \\ - f^\mathrm{ref} &\leftarrow f. + V^\mathrm{ref} &\leftarrow V^\mathrm{meas} + s_\mathrm{ref}e_\mathrm{RQ} \\ + Q^\mathrm{ref} &\leftarrow Q + \dfrac{s_\mathrm{ref}^\mathrm{off}e_\mathrm{RQ}}{k_\mathrm{base}} \\ + P_\mathrm{plant}^\mathrm{ref} &\leftarrow P + \dfrac{e_P-P^\mathrm{freq}}{k_\mathrm{base}} \\ + f^\mathrm{ref} &\leftarrow f + \Delta f_0. \end{aligned} ``` @@ -323,7 +325,7 @@ Output | Units | Description | Note - `validation()` checks defaults, parameter domains, signal contracts, and time floors. - `initializationAndSignals()` checks reconstruction, bases, signals, monitors, tags, and selectors. -- `initializationDomain()` checks rejection, exact and collapsed limits, +- `initializationDomain()` checks rejection, asymmetric and collapsed limits, nonfinite values, and atomicity. - `residualEquations()` checks every residual against a fixed answer key. - `reactiveControl()` checks compensation and reference modes, voltage freeze, @@ -334,6 +336,17 @@ Output | Units | Description | Note - `jacobian()` checks fixed numerical and structural oracles, plus Enzyme agreement to $10^{-9}$ when enabled. +## Appendix A: `droop` + +```math +\text{droop}(e;D_\mathrm{dn},D_\mathrm{up}) +=e\left[D_\mathrm{up}+(D_\mathrm{dn}-D_\mathrm{up})\sigma(e)\right], +``` + +where $\sigma$ is GridKit's smooth +[`sigmoid`](../../../../CommonMath.md#primitives). The response preserves +$\text{droop}(0;D_\mathrm{dn},D_\mathrm{up})=0$. + [^frequency-measurement]: Background for phase-derived, filtered frequency measurement: [PSCAD Frequency/Phase/Magnitude Meter](https://www.pscad.com/webhelp-pscad-v5.1.0-ol/Master_Library_Models/Meters/Frequency_Phase_Magnitude_Meter.htm) and [Ting et al., *Evaluating Methods for Measuring Grid Frequency in Low-Inertia Power Systems*](https://research-hub.nrel.gov/en/publications/evaluating-methods-for-measuring-grid-frequency-in-low-inertia-po-3/). diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp index ce84c6c3a..6c1444ad7 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp @@ -64,7 +64,7 @@ namespace GridKit Q, ///< \f$Q\f$ Required branch reactive power on system base [p.u.] FREQ, ///< \f$f\f$ Optional frequency input [p.u.] VREF, ///< \f$V^\mathrm{ref}\f$ Optional voltage-control reference [p.u.] - PREF, ///< \f$P^\mathrm{ref}\f$ Optional plant active-power reference on system base [p.u.] + PREF, ///< \f$P_\mathrm{plant}^\mathrm{ref}\f$ Optional plant active-power reference on system base [p.u.] QREF, ///< \f$Q^\mathrm{ref}\f$ Optional reactive-power reference on system base [p.u.] FREQREF, ///< \f$f^\mathrm{ref}\f$ Optional frequency reference [p.u.] MAXIMUM ///< Number of external variables @@ -162,11 +162,32 @@ namespace GridKit RealT upper_limit, ScalarT& limiter_input) const; + bool solveDeadbandInput(ScalarT requested_output, + RealT lower_limit, + RealT upper_limit, + ScalarT& deadband_input) const; + RealT logOneMinusExp(RealT x) const; [[gnu::always_inline]] inline ScalarT toComponentBase(ScalarT value) const; ScalarT toSystemBase(ScalarT value) const; + /** + * @brief Smooth asymmetric frequency-droop response. + * + * @param[in] error Deadbanded frequency error. + * @param[in] gain_down Down-frequency response gain. + * @param[in] gain_up Up-frequency response gain. + * @return Active-power frequency response. + */ + static __attribute__((always_inline)) inline ScalarT droop( + const ScalarT error, + const RealT gain_down, + const RealT gain_up) + { + return error * (gain_up + (gain_down - gain_up) * Math::sigmoid(error)); + } + ScalarT& Vr(); ScalarT& Vi(); diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaData.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaData.hpp index cd4b7805a..d6dd79f14 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaData.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaData.hpp @@ -23,8 +23,8 @@ namespace GridKit Freqflag, ///< \f$s_\mathrm{freq}\f$ Active-power output selector [boolean] Tfltr, ///< \f$T_\mathrm{fltr}\f$ Voltage and reactive-power filter time constant [sec] Vfrz, ///< \f$V^\mathrm{frz}\f$ Reactive-power PI freeze threshold [p.u.] - Rc, ///< \f$R_c\f$ Line-drop resistance on system base [p.u.] - Xc, ///< \f$X_c\f$ Line-drop reactance on system base [p.u.] + Rc, ///< \f$R_c\f$ Line-drop resistance on component base [p.u.] + Xc, ///< \f$X_c\f$ Line-drop reactance on component base [p.u.] Kc, ///< \f$K_c\f$ Reactive-current compensation gain [p.u.] dbdlow, ///< \f$D_\mathrm{bd1}\f$ Lower reactive-loop deadband threshold [p.u.] dbdupper, ///< \f$D_\mathrm{bd2}\f$ Upper reactive-loop deadband threshold [p.u.] @@ -66,7 +66,7 @@ namespace GridKit q, ///< \f$Q\f$ Required Known branch reactive-power input on system base [p.u.] freq, ///< \f$f\f$ Optional Known frequency input [p.u.] vref, ///< \f$V^\mathrm{ref}\f$ Optional Unknown voltage-reference input [p.u.] - pref, ///< \f$P^\mathrm{ref}\f$ Optional Unknown plant active-power reference on system base [p.u.] + pref, ///< \f$P_\mathrm{plant}^\mathrm{ref}\f$ Optional Unknown plant active-power reference on system base [p.u.] qref, ///< \f$Q^\mathrm{ref}\f$ Optional Unknown reactive-power reference on system base [p.u.] freqref, ///< \f$f^\mathrm{ref}\f$ Optional Unknown frequency-reference input [p.u.] SIZE ///< Number of REPCA signal-input ports diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp index b3f419b39..eaf7ea418 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp @@ -269,15 +269,17 @@ namespace GridKit const ScalarT vr = Vr(); const ScalarT vi = Vi(); - const ScalarT ir = + const ScalarT ir_system = signals_.template readExternalVariable(); - const ScalarT ii = + const ScalarT ii_system = signals_.template readExternalVariable(); const ScalarT p_system = signals_.template readExternalVariable(); const ScalarT q_system = signals_.template readExternalVariable(); - ScalarT freq = static_cast(ONE); + const ScalarT ir = toComponentBase(ir_system); + const ScalarT ii = toComponentBase(ii_system); + ScalarT freq = static_cast(ONE); if (signals_.template isAttached()) { freq = signals_.template readExternalVariable(); @@ -287,10 +289,9 @@ namespace GridKit { return std::isfinite(static_cast(value)); }; - if (!is_finite(vr) || !is_finite(vi) || !is_finite(ir) - || !is_finite(ii) || !is_finite(p_system) - || !is_finite(q_system) - || !is_finite(freq) || !is_finite(qext0) + if (!is_finite(vr) || !is_finite(vi) || !is_finite(ir_system) + || !is_finite(ii_system) || !is_finite(p_system) + || !is_finite(q_system) || !is_finite(freq) || !is_finite(qext0) || (Freqflag_ && !is_finite(pext0))) { Log::error() << "Repca: initial bus, signal, and command values must be finite\n"; @@ -312,10 +313,16 @@ namespace GridKit const ScalarT pmeas0 = p; const ScalarT sfrz0 = Math::above(v0, Vfrz_); - const ScalarT zero = static_cast(ZERO); - const ScalarT erq0 = zero; - const ScalarT erqdb0 = Math::deadband2(erq0, dbdlow_, dbdupper_); - const ScalarT erqlim0 = Math::clamp(erqdb0, emin_, emax_); + const ScalarT zero = static_cast(ZERO); + ScalarT erq0{}; + ScalarT erqdb0{}; + if (!solveLimiterInput(zero, emin_, emax_, erqdb0) + || !solveDeadbandInput(erqdb0, dbdlow_, dbdupper_, erq0)) + { + Log::error() << "Repca: reactive error blocks have no finite steady input\n"; + return 1; + } + const ScalarT erqlim0 = zero; const ScalarT qpi0 = qext0; const ScalarT xqlag0 = qpi0; @@ -335,10 +342,17 @@ namespace GridKit return 1; } - const ScalarT ef0 = Math::deadband2(zero, fdbd1_, fdbd2_); - const ScalarT pfreq0 = Ddn_ * Math::ramp(ef0) - Dup_ * Math::ramp(-ef0); - const ScalarT ep0 = zero; - const ScalarT eplim0 = Math::clamp(ep0, femin_, femax_); + ScalarT frequency_error0{}; + ScalarT ep0{}; + if (!solveDeadbandInput(zero, fdbd1_, fdbd2_, frequency_error0) + || !solveLimiterInput(zero, femin_, femax_, ep0)) + { + Log::error() << "Repca: active error blocks have no finite steady input\n"; + return 1; + } + const ScalarT ef0 = zero; + const ScalarT pfreq0 = droop(ef0, Ddn_, Dup_); + const ScalarT eplim0 = zero; const ScalarT pref0 = Freqflag_ ? pext0 : Math::clamp(pmeas0, Pmin_, Pmax_); const ScalarT ppi0 = pref0; ScalarT ppi_input0{}; @@ -364,10 +378,18 @@ namespace GridKit } const ScalarT pext_output0 = Freqflag_ ? pext0_system : zero; - const ScalarT freqref0 = freq; - const ScalarT vref0 = vmeas0; - const ScalarT qref0_system = q_system; - const ScalarT pref0_system = toSystemBase(pmeas0 - pfreq0); + const ScalarT freqref0 = freq + frequency_error0; + ScalarT vref0 = vmeas0; + ScalarT qref0_system = q_system; + if (RefFlag_) + { + vref0 += erq0; + } + else + { + qref0_system = toSystemBase(qmeas0 + erq0); + } + const ScalarT pref0_system = p_system + toSystemBase(ep0 - pfreq0); const bool candidates_are_finite = is_finite(qext0_system) @@ -671,8 +693,8 @@ namespace GridKit const ScalarT vr = wb[0]; const ScalarT vi = wb[1]; - const ScalarT ir = ws[index(RepcaExternalVariables::IR)]; - const ScalarT ii = ws[index(RepcaExternalVariables::II)]; + const ScalarT ir = toComponentBase(ws[index(RepcaExternalVariables::IR)]); + const ScalarT ii = toComponentBase(ws[index(RepcaExternalVariables::II)]); const ScalarT p = toComponentBase(ws[index(RepcaExternalVariables::P)]); const ScalarT q = toComponentBase(ws[index(RepcaExternalVariables::Q)]); const ScalarT freq = ws[index(RepcaExternalVariables::FREQ)]; @@ -683,7 +705,7 @@ namespace GridKit const ScalarT vldc_r = vr - Rc_ * ir + Xc_ * ii; const ScalarT vldc_i = vi - Rc_ * ii - Xc_ * ir; - const ScalarT pfreq = Ddn_ * Math::ramp(ef) - Dup_ * Math::ramp(-ef); + const ScalarT pfreq = droop(ef, Ddn_, Dup_); f[index(RepcaInternalVariables::VMEAS)] = -vmeas_dot + (vctrl - vmeas) / Tfltr_; f[index(RepcaInternalVariables::QMEAS)] = -qmeas_dot + (q - qmeas) / Tfltr_; @@ -955,6 +977,84 @@ namespace GridKit return std::isfinite(static_cast(limiter_input)); } + /** + * @brief Recover an input for a requested smooth deadband output + * + * @param[in] requested_output Requested deadband output. + * @param[in] lower_limit Lower deadband threshold. + * @param[in] upper_limit Upper deadband threshold. + * @param[out] deadband_input Recovered deadband input on success. + * @return true when a finite input was recovered. + */ + template + bool Repca::solveDeadbandInput( + ScalarT requested_output, + RealT lower_limit, + RealT upper_limit, + ScalarT& deadband_input) const + { + const RealT output_value = static_cast(requested_output); + + if (!std::isfinite(output_value) + || !std::isfinite(lower_limit) + || !std::isfinite(upper_limit) + || lower_limit > upper_limit) + { + return false; + } + + const RealT midpoint = lower_limit + HALF * (upper_limit - lower_limit); + if (std::abs(output_value) <= INITIALIZATION_TOLERANCE) + { + deadband_input = static_cast(midpoint); + return true; + } + + RealT lower_input = midpoint; + RealT upper_input = midpoint; + if (output_value < ZERO) + { + lower_input = lower_limit + output_value; + } + else + { + upper_input = upper_limit + output_value; + } + + const RealT lower_output = Math::deadband2(lower_input, + lower_limit, + upper_limit); + const RealT upper_output = Math::deadband2(upper_input, + lower_limit, + upper_limit); + if (lower_output > output_value || output_value > upper_output) + { + return false; + } + + for (std::size_t iteration = 0; iteration < 128; ++iteration) + { + const RealT input = lower_input + + HALF * (upper_input - lower_input); + if (Math::deadband2(input, lower_limit, upper_limit) < output_value) + { + lower_input = input; + } + else + { + upper_input = input; + } + } + + const RealT input = lower_input + + HALF * (upper_input - lower_input); + deadband_input = static_cast(input); + return std::isfinite(input) + && std::abs(Math::deadband2(input, lower_limit, upper_limit) + - output_value) + <= INITIALIZATION_TOLERANCE; + } + /** * @brief Evaluate log(1 - exp(-x)) accurately for positive x * @@ -980,7 +1080,7 @@ namespace GridKit } /** - * @brief Convert a system-base power quantity to REPCA component base + * @brief Convert a system-base power or current quantity to REPCA component base * * @param[in] value Quantity on the system base. * @return The same quantity on the REPCA component base. diff --git a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp index 14bad5c76..e67591e80 100644 --- a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp @@ -241,7 +241,7 @@ namespace GridKit success *= (fixture.repca.evaluateResidual() == 0); success *= stateMatches(fixture.repca, - {{Vars::VMEAS, 0.99200050403213}, + {{Vars::VMEAS, 0.984002032518226}, {Vars::QMEAS, 0.2}, {Vars::XQPI, 0.5}, {Vars::XQLAG, 0.5}, @@ -249,9 +249,9 @@ namespace GridKit {Vars::XPPI, 0.9}, {Vars::PREF, 0.9}, {Vars::V, 1.0}, - {Vars::VLDC, 0.99200050403213}, + {Vars::VLDC, 0.984002032518226}, {Vars::VDROOP, 1.08}, - {Vars::VCTRL, 0.99200050403213}, + {Vars::VCTRL, 0.984002032518226}, {Vars::SFRZ, 1.0}, {Vars::ERQ, 0.0}, {Vars::ERQDB, 0.0}, @@ -272,7 +272,7 @@ namespace GridKit success *= scalarPreserved(fixture.input(Ext::FREQ), 0.99, "preserved freq"); success *= scalarPreserved(fixture.qext(), 0.25, "preserved qext"); success *= scalarPreserved(fixture.pext(), 0.45, "preserved pext"); - success *= scalarMatches(fixture.input(Ext::VREF), 0.99200050403213, "published vref"); + success *= scalarMatches(fixture.input(Ext::VREF), 0.984002032518226, "published vref"); success *= scalarMatches(fixture.input(Ext::PREF), 0.4, "published pref"); success *= scalarMatches(fixture.input(Ext::QREF), 0.1, "published qref"); success *= scalarMatches(fixture.input(Ext::FREQREF), 0.99, "published freqref"); @@ -301,7 +301,7 @@ namespace GridKit } success *= monitorMatches(fixture.repca, - {{0.25, 0.45, 0.99200050403213, 0.2, 0.8}}, + {{0.25, 0.45, 0.984002032518226, 0.2, 0.8}}, "initialization"); success *= allResidualsWithinInitTolerance(fixture.repca); @@ -344,7 +344,7 @@ namespace GridKit {Vars::PEXT, 0.45}}, "unassigned command outputs"); success *= monitorMatches(outputless.repca, - {{0.25, 0.45, 0.99200050403213, 0.2, 0.8}}, + {{0.25, 0.45, 0.984002032518226, 0.2, 0.8}}, "unassigned command outputs"); success *= allResidualsWithinInitTolerance(outputless.repca); @@ -364,10 +364,10 @@ namespace GridKit {"droop/reactive-reference/enabled-frequency", false, false, true, 1.08, 0.9, 0.45}, {"droop/voltage-reference/disabled-frequency", false, true, false, 1.08, 0.8, 0.0}, {"droop/voltage-reference/enabled-frequency", false, true, true, 1.08, 0.9, 0.45}, - {"line-drop/reactive-reference/disabled-frequency", true, false, false, 0.99200050403213, 0.8, 0.0}, - {"line-drop/reactive-reference/enabled-frequency", true, false, true, 0.99200050403213, 0.9, 0.45}, - {"line-drop/voltage-reference/disabled-frequency", true, true, false, 0.99200050403213, 0.8, 0.0}, - {"line-drop/voltage-reference/enabled-frequency", true, true, true, 0.99200050403213, 0.9, 0.45}, + {"line-drop/reactive-reference/disabled-frequency", true, false, false, 0.984002032518226, 0.8, 0.0}, + {"line-drop/reactive-reference/enabled-frequency", true, false, true, 0.984002032518226, 0.9, 0.45}, + {"line-drop/voltage-reference/disabled-frequency", true, true, false, 0.984002032518226, 0.8, 0.0}, + {"line-drop/voltage-reference/enabled-frequency", true, true, true, 0.984002032518226, 0.9, 0.45}, }}; for (const auto& test_case : flag_cases) { @@ -476,19 +476,24 @@ namespace GridKit auto reactive_aw_data = makeInitializationData(); reactive_aw_data.parameters[Params::dbdupper] = 0.03; + reactive_aw_data.parameters[Params::emin] = 0.0; - success *= initializationRejectedAtomically(reactive_aw_data, - 0.25, - 0.45, - "nonzero reactive-power PI antiwindup rate"); - success *= initializationRejectedAtomically( - reactive_aw_data, - 0.25, - 0.45, - "nonzero gated reactive-power PI state rate at the freeze transition", - NonfiniteTarget::NONE, - 0.2, - 0.0); + for (const auto& voltage : {std::pair{0.8, 0.6}, + std::pair{0.2, 0.0}}) + { + Fixture asymmetric_reactive(reactive_aw_data, + voltage.first, + voltage.second); + asymmetric_reactive.attachAllInputs(); + setInitializationInputs(asymmetric_reactive); + success *= asymmetric_reactive.initialize(0.25, 0.45); + success *= (asymmetric_reactive.repca.evaluateResidual() == 0); + success *= stateMatches(asymmetric_reactive.repca, + {{Vars::ERQDB, -0.1}, + {Vars::ERQLIM, 0.0}}, + "asymmetric reactive initialization"); + success *= allResidualsWithinInitTolerance(asymmetric_reactive.repca); + } auto frozen_data = reactive_aw_data; frozen_data.parameters[Params::Vfrz] = 0.9; @@ -502,13 +507,51 @@ namespace GridKit success *= allResidualsWithinInitTolerance(frozen_reactive_rate.repca); } - auto active_aw_data = makeInitializationData(); - active_aw_data.parameters[Params::femin] = 0.0; - - success *= initializationRejectedAtomically(active_aw_data, - 0.25, - 0.45, - "nonzero active-power PI antiwindup rate"); + { + auto active_aw_data = makeInitializationData(); + active_aw_data.parameters[Params::fdbd2] = 0.025; + active_aw_data.parameters[Params::femin] = 0.0; + Fixture asymmetric_active(active_aw_data, 0.8, 0.6); + asymmetric_active.attachAllInputs(); + setInitializationInputs(asymmetric_active); + success *= asymmetric_active.initialize(0.25, 0.45); + success *= (asymmetric_active.repca.evaluateResidual() == 0); + success *= stateMatches(asymmetric_active.repca, + {{Vars::EF, 0.0}, + {Vars::EP, -0.1}, + {Vars::EPLIM, 0.0}}, + "lower active-error boundary"); + success *= scalarMatches(asymmetric_active.input(Ext::FREQREF), + 0.995, + "asymmetric frequency reference"); + success *= scalarMatches(asymmetric_active.input(Ext::PREF), + 0.35, + "lower-bound plant reference"); + success *= allResidualsWithinInitTolerance(asymmetric_active.repca); + } + + { + auto active_aw_data = makeInitializationData(); + active_aw_data.parameters[Params::fdbd1] = -0.025; + active_aw_data.parameters[Params::femax] = 0.0; + Fixture asymmetric_active(active_aw_data, 0.8, 0.6); + asymmetric_active.attachAllInputs(); + setInitializationInputs(asymmetric_active); + success *= asymmetric_active.initialize(0.25, 0.45); + success *= (asymmetric_active.repca.evaluateResidual() == 0); + success *= stateMatches(asymmetric_active.repca, + {{Vars::EF, 0.0}, + {Vars::EP, 0.1}, + {Vars::EPLIM, 0.0}}, + "upper active-error boundary"); + success *= scalarMatches(asymmetric_active.input(Ext::FREQREF), + 0.985, + "asymmetric frequency reference"); + success *= scalarMatches(asymmetric_active.input(Ext::PREF), + 0.45, + "upper-bound plant reference"); + success *= allResidualsWithinInitTolerance(asymmetric_active.repca); + } auto overflow_data = makeInitializationData(); overflow_data.parameters[Params::Rc] = std::numeric_limits::max(); @@ -620,7 +663,7 @@ namespace GridKit {Vars::XPPI, "XPPI", -0.3}, {Vars::PREF, "PREF", 0.4}, {Vars::V, "V", -1.28}, - {Vars::VLDC, "VLDC", -0.0075}, + {Vars::VLDC, "VLDC", 0.028}, {Vars::VDROOP, "VDROOP", 0.1}, {Vars::VCTRL, "VCTRL", 0.05}, {Vars::SFRZ, "SFRZ", 0.5}, @@ -898,8 +941,9 @@ namespace GridKit "frequency deadband"); } - const std::array droop_cases{{ + const std::array droop_cases{{ {-0.9, -0.9}, + {0.0, 0.0}, {0.9, 1.8}, }}; fixture.input(Ext::PREF) = 0.2; @@ -1983,10 +2027,10 @@ namespace GridKit {{index(Vars::PREF), -3.0}, {index(Vars::PPI), 2.0}}, {{index(Vars::V), -3.0}, {kBusVrColumn, 1.8}, {kBusViColumn, 0.8}}, {{index(Vars::VLDC), -2.0}, - {kBusVrColumn, 1.88}, - {kBusViColumn, 0.66}, - {externalColumn(index(Ext::IR)), -0.0574}, - {externalColumn(index(Ext::II)), 0.0432}}, + {kBusVrColumn, 1.96}, + {kBusViColumn, 0.52}, + {externalColumn(index(Ext::IR)), -0.1096}, + {externalColumn(index(Ext::II)), 0.0968}}, {{index(Vars::V), 1.0}, {index(Vars::VDROOP), -1.0}, {externalColumn(index(Ext::Q)), 0.8}}, {{index(Vars::VLDC), 1.0}, {index(Vars::VCTRL), -1.0}}, {{index(Vars::SFRZ), -1.0}}, From 959d9abd3a03b5dc04807dda86fca88edfd20f85 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Wed, 5 Aug 2026 12:23:52 -0500 Subject: [PATCH 15/19] numeric safety check just in case --- .../PhasorDynamics/Controller/REPCA/README.md | 4 +-- .../Controller/REPCA/RepcaImpl.hpp | 8 +++-- .../PhasorDynamics/ControllerRepcaTests.hpp | 35 +++++++++++-------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md b/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md index ffe2e8214..d926b9da6 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md @@ -294,8 +294,8 @@ Initialization rejects an operating point if: - $k_\mathrm{base}Q^\mathrm{ext}\notin[Q^{\min},Q^{\max}]$, or $s_\mathrm{freq}=1$ and $k_\mathrm{base}P^\mathrm{ext}\notin[P^{\min},P^{\max}]$; or -- the gated reactive-power or active-power PI state rate is nonfinite or - exceeds $\epsilon_\mathrm{init}$ in magnitude. +- the gated reactive-power or active-power PI state rate is nonfinite or does + not vanish within the implementation tolerance. Initialization is atomic; candidates are validated before state or signal writes. diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp index eaf7ea418..de81b2139 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp @@ -1003,7 +1003,8 @@ namespace GridKit return false; } - const RealT midpoint = lower_limit + HALF * (upper_limit - lower_limit); + const RealT midpoint = HALF * lower_limit + + HALF * upper_limit; if (std::abs(output_value) <= INITIALIZATION_TOLERANCE) { deadband_input = static_cast(midpoint); @@ -1027,7 +1028,10 @@ namespace GridKit const RealT upper_output = Math::deadband2(upper_input, lower_limit, upper_limit); - if (lower_output > output_value || output_value > upper_output) + if (!std::isfinite(lower_output) + || !std::isfinite(upper_output) + || lower_output - output_value > INITIALIZATION_TOLERANCE + || output_value - upper_output > INITIALIZATION_TOLERANCE) { return false; } diff --git a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp index e67591e80..b3db1cd78 100644 --- a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp @@ -478,21 +478,26 @@ namespace GridKit reactive_aw_data.parameters[Params::dbdupper] = 0.03; reactive_aw_data.parameters[Params::emin] = 0.0; - for (const auto& voltage : {std::pair{0.8, 0.6}, - std::pair{0.2, 0.0}}) - { - Fixture asymmetric_reactive(reactive_aw_data, - voltage.first, - voltage.second); - asymmetric_reactive.attachAllInputs(); - setInitializationInputs(asymmetric_reactive); - success *= asymmetric_reactive.initialize(0.25, 0.45); - success *= (asymmetric_reactive.repca.evaluateResidual() == 0); - success *= stateMatches(asymmetric_reactive.repca, - {{Vars::ERQDB, -0.1}, - {Vars::ERQLIM, 0.0}}, - "asymmetric reactive initialization"); - success *= allResidualsWithinInitTolerance(asymmetric_reactive.repca); + for (const bool voltage_reference : {false, true}) + { + auto data = reactive_aw_data; + data.parameters[Params::RefFlag] = voltage_reference; + for (const auto& voltage : {std::pair{0.8, 0.6}, + std::pair{0.2, 0.0}}) + { + Fixture asymmetric_reactive(data, + voltage.first, + voltage.second); + asymmetric_reactive.attachAllInputs(); + setInitializationInputs(asymmetric_reactive); + success *= asymmetric_reactive.initialize(0.25, 0.45); + success *= (asymmetric_reactive.repca.evaluateResidual() == 0); + success *= stateMatches(asymmetric_reactive.repca, + {{Vars::ERQDB, -0.1}, + {Vars::ERQLIM, 0.0}}, + "asymmetric reactive initialization"); + success *= allResidualsWithinInitTolerance(asymmetric_reactive.repca); + } } auto frozen_data = reactive_aw_data; From a6ed9015a3795f3fd3d18dce7d97429b4b29b4eb Mon Sep 17 00:00:00 2001 From: lukelowry Date: Wed, 5 Aug 2026 13:40:56 -0500 Subject: [PATCH 16/19] Cleaned and aligned with hygov --- .../PhasorDynamics/Controller/REPCA/Repca.hpp | 26 +- .../Controller/REPCA/RepcaImpl.hpp | 355 +++++++----- .../PhasorDynamics/ControllerRepcaTests.hpp | 548 ++++++++++-------- .../SystemSingleComponentTests.hpp | 104 +--- 4 files changed, 548 insertions(+), 485 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp index 6c1444ad7..3685f0d6a 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp @@ -110,8 +110,6 @@ namespace GridKit using InternalVariablesT = RepcaInternalVariables; using ExternalVariablesT = RepcaExternalVariables; - // A command reconstructed exactly on a limit leaves the widest - // steady residual, 1.6e-13 at the QPI row. static constexpr RealT INITIALIZATION_TOLERANCE = static_cast(1.0e-12); Repca(BusT* bus); @@ -143,16 +141,6 @@ namespace GridKit ScalarT* f); private: - static constexpr size_t index(RepcaInternalVariables variable) - { - return static_cast(variable); - } - - static constexpr size_t index(RepcaExternalVariables variable) - { - return static_cast(variable); - } - void initializeParameters(const ModelDataT& data); void initializeMonitor(); void setDerivedParameters(); @@ -172,21 +160,11 @@ namespace GridKit [[gnu::always_inline]] inline ScalarT toComponentBase(ScalarT value) const; ScalarT toSystemBase(ScalarT value) const; - /** - * @brief Smooth asymmetric frequency-droop response. - * - * @param[in] error Deadbanded frequency error. - * @param[in] gain_down Down-frequency response gain. - * @param[in] gain_up Up-frequency response gain. - * @return Active-power frequency response. - */ + /// Smooth asymmetric frequency-droop response. static __attribute__((always_inline)) inline ScalarT droop( const ScalarT error, const RealT gain_down, - const RealT gain_up) - { - return error * (gain_up + (gain_down - gain_up) * Math::sigmoid(error)); - } + const RealT gain_up); ScalarT& Vr(); ScalarT& Vi(); diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp index de81b2139..b5e304c7c 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp @@ -38,7 +38,7 @@ namespace GridKit Repca::Repca(BusT* bus) : bus_(bus) { - size_ = static_cast(index(RepcaInternalVariables::MAXIMUM)); + size_ = static_cast(RepcaInternalVariables::MAXIMUM); setDerivedParameters(); } @@ -55,7 +55,7 @@ namespace GridKit { initializeParameters(data); initializeMonitor(); - size_ = static_cast(index(RepcaInternalVariables::MAXIMUM)); + size_ = static_cast(RepcaInternalVariables::MAXIMUM); } /** @@ -89,6 +89,9 @@ namespace GridKit template int Repca::allocate() { + const auto QEXT = static_cast(RepcaInternalVariables::QEXT); + const auto PEXT = static_cast(RepcaInternalVariables::PEXT); + if (!allocated_) { this->allocateVectors(size_); @@ -101,7 +104,7 @@ namespace GridKit wb_.assign(2, ScalarT{0}); - const auto signal_size = index(RepcaExternalVariables::MAXIMUM); + const auto signal_size = static_cast(RepcaExternalVariables::MAXIMUM); ws_.assign(signal_size, ScalarT{0}); ws_indices_.assign(signal_size, INVALID_INDEX); @@ -116,15 +119,15 @@ namespace GridKit if (signals_.template isAssigned()) { signals_.template getSignalNode()->set( - &y[index(RepcaInternalVariables::QEXT)], - &(this->getVariableIndex(static_cast(index(RepcaInternalVariables::QEXT))))); + &y[QEXT], + &(this->getVariableIndex(static_cast(QEXT)))); } if (signals_.template isAssigned()) { signals_.template getSignalNode()->set( - &y[index(RepcaInternalVariables::PEXT)], - &(this->getVariableIndex(static_cast(index(RepcaInternalVariables::PEXT))))); + &y[PEXT], + &(this->getVariableIndex(static_cast(PEXT)))); } allocated_ = true; @@ -248,6 +251,29 @@ namespace GridKit template int Repca::initialize() { + const auto VMEAS = static_cast(RepcaInternalVariables::VMEAS); + const auto QMEAS = static_cast(RepcaInternalVariables::QMEAS); + const auto XQPI = static_cast(RepcaInternalVariables::XQPI); + const auto XQLAG = static_cast(RepcaInternalVariables::XQLAG); + const auto PMEAS = static_cast(RepcaInternalVariables::PMEAS); + const auto XPPI = static_cast(RepcaInternalVariables::XPPI); + const auto PREF = static_cast(RepcaInternalVariables::PREF); + const auto V = static_cast(RepcaInternalVariables::V); + const auto VLDC = static_cast(RepcaInternalVariables::VLDC); + const auto VDROOP = static_cast(RepcaInternalVariables::VDROOP); + const auto VCTRL = static_cast(RepcaInternalVariables::VCTRL); + const auto SFRZ = static_cast(RepcaInternalVariables::SFRZ); + const auto ERQ = static_cast(RepcaInternalVariables::ERQ); + const auto ERQDB = static_cast(RepcaInternalVariables::ERQDB); + const auto ERQLIM = static_cast(RepcaInternalVariables::ERQLIM); + const auto QPI = static_cast(RepcaInternalVariables::QPI); + const auto QEXT = static_cast(RepcaInternalVariables::QEXT); + const auto EF = static_cast(RepcaInternalVariables::EF); + const auto EP = static_cast(RepcaInternalVariables::EP); + const auto EPLIM = static_cast(RepcaInternalVariables::EPLIM); + const auto PPI = static_cast(RepcaInternalVariables::PPI); + const auto PEXT = static_cast(RepcaInternalVariables::PEXT); + if (!allocated_) { Log::error() << "Repca: allocate must complete before initialize\n"; @@ -262,8 +288,8 @@ namespace GridKit auto* y = y_.getData(); - const ScalarT qext0_system = y[index(RepcaInternalVariables::QEXT)]; - const ScalarT pext0_system = y[index(RepcaInternalVariables::PEXT)]; + const ScalarT qext0_system = y[QEXT]; + const ScalarT pext0_system = y[PEXT]; const ScalarT qext0 = toComponentBase(qext0_system); const ScalarT pext0 = toComponentBase(pext0_system); @@ -437,28 +463,28 @@ namespace GridKit return 1; } - y[index(RepcaInternalVariables::VMEAS)] = vmeas0; - y[index(RepcaInternalVariables::QMEAS)] = qmeas0; - y[index(RepcaInternalVariables::XQPI)] = xqpi0; - y[index(RepcaInternalVariables::XQLAG)] = xqlag0; - y[index(RepcaInternalVariables::PMEAS)] = pmeas0; - y[index(RepcaInternalVariables::XPPI)] = xppi0; - y[index(RepcaInternalVariables::PREF)] = pref0; - y[index(RepcaInternalVariables::V)] = v0; - y[index(RepcaInternalVariables::VLDC)] = vldc0; - y[index(RepcaInternalVariables::VDROOP)] = vdroop0; - y[index(RepcaInternalVariables::VCTRL)] = vctrl0; - y[index(RepcaInternalVariables::SFRZ)] = sfrz0; - y[index(RepcaInternalVariables::ERQ)] = erq0; - y[index(RepcaInternalVariables::ERQDB)] = erqdb0; - y[index(RepcaInternalVariables::ERQLIM)] = erqlim0; - y[index(RepcaInternalVariables::QPI)] = qpi0; - y[index(RepcaInternalVariables::QEXT)] = qext0_system; - y[index(RepcaInternalVariables::EF)] = ef0; - y[index(RepcaInternalVariables::EP)] = ep0; - y[index(RepcaInternalVariables::EPLIM)] = eplim0; - y[index(RepcaInternalVariables::PPI)] = ppi0; - y[index(RepcaInternalVariables::PEXT)] = pext_output0; + y[VMEAS] = vmeas0; + y[QMEAS] = qmeas0; + y[XQPI] = xqpi0; + y[XQLAG] = xqlag0; + y[PMEAS] = pmeas0; + y[XPPI] = xppi0; + y[PREF] = pref0; + y[V] = v0; + y[VLDC] = vldc0; + y[VDROOP] = vdroop0; + y[VCTRL] = vctrl0; + y[SFRZ] = sfrz0; + y[ERQ] = erq0; + y[ERQDB] = erqdb0; + y[ERQLIM] = erqlim0; + y[QPI] = qpi0; + y[QEXT] = qext0_system; + y[EF] = ef0; + y[EP] = ep0; + y[EPLIM] = eplim0; + y[PPI] = ppi0; + y[PEXT] = pext_output0; freqref_set_ = freqref0; vref_set_ = vref0; @@ -499,14 +525,22 @@ namespace GridKit template int Repca::tagDifferentiable() { + const auto VMEAS = static_cast(RepcaInternalVariables::VMEAS); + const auto QMEAS = static_cast(RepcaInternalVariables::QMEAS); + const auto XQPI = static_cast(RepcaInternalVariables::XQPI); + const auto XQLAG = static_cast(RepcaInternalVariables::XQLAG); + const auto PMEAS = static_cast(RepcaInternalVariables::PMEAS); + const auto XPPI = static_cast(RepcaInternalVariables::XPPI); + const auto PREF = static_cast(RepcaInternalVariables::PREF); + std::fill(tag_.begin(), tag_.end(), false); - tag_[index(RepcaInternalVariables::VMEAS)] = true; - tag_[index(RepcaInternalVariables::QMEAS)] = true; - tag_[index(RepcaInternalVariables::XQPI)] = true; - tag_[index(RepcaInternalVariables::XQLAG)] = true; - tag_[index(RepcaInternalVariables::PMEAS)] = true; - tag_[index(RepcaInternalVariables::XPPI)] = true; - tag_[index(RepcaInternalVariables::PREF)] = true; + tag_[VMEAS] = true; + tag_[QMEAS] = true; + tag_[XQPI] = true; + tag_[XQLAG] = true; + tag_[PMEAS] = true; + tag_[XPPI] = true; + tag_[PREF] = true; return 0; } @@ -536,63 +570,73 @@ namespace GridKit template int Repca::evaluateResidual() { - ws_[index(RepcaExternalVariables::VREF)] = vref_set_; - ws_[index(RepcaExternalVariables::PREF)] = pref_set_; - ws_[index(RepcaExternalVariables::QREF)] = qref_set_; - ws_[index(RepcaExternalVariables::FREQ)] = static_cast(ONE); - ws_[index(RepcaExternalVariables::FREQREF)] = freqref_set_; + const auto IR = static_cast(RepcaExternalVariables::IR); + const auto II = static_cast(RepcaExternalVariables::II); + const auto P = static_cast(RepcaExternalVariables::P); + const auto Q = static_cast(RepcaExternalVariables::Q); + const auto FREQ = static_cast(RepcaExternalVariables::FREQ); + const auto VREF = static_cast(RepcaExternalVariables::VREF); + const auto PREF = static_cast(RepcaExternalVariables::PREF); + const auto QREF = static_cast(RepcaExternalVariables::QREF); + const auto FREQREF = static_cast(RepcaExternalVariables::FREQREF); + + ws_[VREF] = vref_set_; + ws_[PREF] = pref_set_; + ws_[QREF] = qref_set_; + ws_[FREQ] = static_cast(ONE); + ws_[FREQREF] = freqref_set_; std::fill(ws_indices_.begin(), ws_indices_.end(), INVALID_INDEX); - ws_[index(RepcaExternalVariables::IR)] = + ws_[IR] = signals_.template readExternalVariable(); - ws_indices_[index(RepcaExternalVariables::IR)] = + ws_indices_[IR] = signals_.template readExternalVariableIndex(); - ws_[index(RepcaExternalVariables::II)] = + ws_[II] = signals_.template readExternalVariable(); - ws_indices_[index(RepcaExternalVariables::II)] = + ws_indices_[II] = signals_.template readExternalVariableIndex(); - ws_[index(RepcaExternalVariables::P)] = + ws_[P] = signals_.template readExternalVariable(); - ws_indices_[index(RepcaExternalVariables::P)] = + ws_indices_[P] = signals_.template readExternalVariableIndex(); - ws_[index(RepcaExternalVariables::Q)] = + ws_[Q] = signals_.template readExternalVariable(); - ws_indices_[index(RepcaExternalVariables::Q)] = + ws_indices_[Q] = signals_.template readExternalVariableIndex(); if (signals_.template isAttached()) { - ws_[index(RepcaExternalVariables::FREQ)] = + ws_[FREQ] = signals_.template readExternalVariable(); - ws_indices_[index(RepcaExternalVariables::FREQ)] = + ws_indices_[FREQ] = signals_.template readExternalVariableIndex(); } if (signals_.template isAttached()) { - ws_[index(RepcaExternalVariables::VREF)] = + ws_[VREF] = signals_.template readExternalVariable(); - ws_indices_[index(RepcaExternalVariables::VREF)] = + ws_indices_[VREF] = signals_.template readExternalVariableIndex(); } if (signals_.template isAttached()) { - ws_[index(RepcaExternalVariables::PREF)] = + ws_[PREF] = signals_.template readExternalVariable(); - ws_indices_[index(RepcaExternalVariables::PREF)] = + ws_indices_[PREF] = signals_.template readExternalVariableIndex(); } if (signals_.template isAttached()) { - ws_[index(RepcaExternalVariables::QREF)] = + ws_[QREF] = signals_.template readExternalVariable(); - ws_indices_[index(RepcaExternalVariables::QREF)] = + ws_indices_[QREF] = signals_.template readExternalVariableIndex(); } if (signals_.template isAttached()) { - ws_[index(RepcaExternalVariables::FREQREF)] = + ws_[FREQREF] = signals_.template readExternalVariable(); - ws_indices_[index(RepcaExternalVariables::FREQREF)] = + ws_indices_[FREQREF] = signals_.template readExternalVariableIndex(); } @@ -659,78 +703,111 @@ namespace GridKit const ScalarT* ws, ScalarT* f) { - const ScalarT vmeas = y[index(RepcaInternalVariables::VMEAS)]; - const ScalarT qmeas = y[index(RepcaInternalVariables::QMEAS)]; - const ScalarT xqpi = y[index(RepcaInternalVariables::XQPI)]; - const ScalarT xqlag = y[index(RepcaInternalVariables::XQLAG)]; - const ScalarT pmeas = y[index(RepcaInternalVariables::PMEAS)]; - const ScalarT xppi = y[index(RepcaInternalVariables::XPPI)]; - const ScalarT pref = y[index(RepcaInternalVariables::PREF)]; - const ScalarT v = y[index(RepcaInternalVariables::V)]; - const ScalarT vldc = y[index(RepcaInternalVariables::VLDC)]; - const ScalarT vdroop = y[index(RepcaInternalVariables::VDROOP)]; - const ScalarT vctrl = y[index(RepcaInternalVariables::VCTRL)]; - const ScalarT sfrz = y[index(RepcaInternalVariables::SFRZ)]; - const ScalarT erq = y[index(RepcaInternalVariables::ERQ)]; - const ScalarT erqdb = y[index(RepcaInternalVariables::ERQDB)]; - const ScalarT erqlim = y[index(RepcaInternalVariables::ERQLIM)]; - const ScalarT qpi = y[index(RepcaInternalVariables::QPI)]; - const ScalarT qext = toComponentBase(y[index(RepcaInternalVariables::QEXT)]); - const ScalarT ef = y[index(RepcaInternalVariables::EF)]; - const ScalarT ep = y[index(RepcaInternalVariables::EP)]; - const ScalarT eplim = y[index(RepcaInternalVariables::EPLIM)]; - const ScalarT ppi = y[index(RepcaInternalVariables::PPI)]; - const ScalarT pext = toComponentBase(y[index(RepcaInternalVariables::PEXT)]); - - const ScalarT vmeas_dot = yp[index(RepcaInternalVariables::VMEAS)]; - const ScalarT qmeas_dot = yp[index(RepcaInternalVariables::QMEAS)]; - const ScalarT xqpi_dot = yp[index(RepcaInternalVariables::XQPI)]; - const ScalarT xqlag_dot = yp[index(RepcaInternalVariables::XQLAG)]; - const ScalarT pmeas_dot = yp[index(RepcaInternalVariables::PMEAS)]; - const ScalarT xppi_dot = yp[index(RepcaInternalVariables::XPPI)]; - const ScalarT pref_dot = yp[index(RepcaInternalVariables::PREF)]; + const auto VMEAS = static_cast(RepcaInternalVariables::VMEAS); + const auto QMEAS = static_cast(RepcaInternalVariables::QMEAS); + const auto XQPI = static_cast(RepcaInternalVariables::XQPI); + const auto XQLAG = static_cast(RepcaInternalVariables::XQLAG); + const auto PMEAS = static_cast(RepcaInternalVariables::PMEAS); + const auto XPPI = static_cast(RepcaInternalVariables::XPPI); + const auto PREF_STATE = static_cast(RepcaInternalVariables::PREF); + const auto V = static_cast(RepcaInternalVariables::V); + const auto VLDC = static_cast(RepcaInternalVariables::VLDC); + const auto VDROOP = static_cast(RepcaInternalVariables::VDROOP); + const auto VCTRL = static_cast(RepcaInternalVariables::VCTRL); + const auto SFRZ = static_cast(RepcaInternalVariables::SFRZ); + const auto ERQ = static_cast(RepcaInternalVariables::ERQ); + const auto ERQDB = static_cast(RepcaInternalVariables::ERQDB); + const auto ERQLIM = static_cast(RepcaInternalVariables::ERQLIM); + const auto QPI = static_cast(RepcaInternalVariables::QPI); + const auto QEXT = static_cast(RepcaInternalVariables::QEXT); + const auto EF = static_cast(RepcaInternalVariables::EF); + const auto EP = static_cast(RepcaInternalVariables::EP); + const auto EPLIM = static_cast(RepcaInternalVariables::EPLIM); + const auto PPI = static_cast(RepcaInternalVariables::PPI); + const auto PEXT = static_cast(RepcaInternalVariables::PEXT); + + const auto IR = static_cast(RepcaExternalVariables::IR); + const auto II = static_cast(RepcaExternalVariables::II); + const auto P = static_cast(RepcaExternalVariables::P); + const auto Q = static_cast(RepcaExternalVariables::Q); + const auto FREQ = static_cast(RepcaExternalVariables::FREQ); + const auto VREF = static_cast(RepcaExternalVariables::VREF); + const auto PREF_INPUT = static_cast(RepcaExternalVariables::PREF); + const auto QREF = static_cast(RepcaExternalVariables::QREF); + const auto FREQREF = static_cast(RepcaExternalVariables::FREQREF); + + const ScalarT vmeas = y[VMEAS]; + const ScalarT qmeas = y[QMEAS]; + const ScalarT xqpi = y[XQPI]; + const ScalarT xqlag = y[XQLAG]; + const ScalarT pmeas = y[PMEAS]; + const ScalarT xppi = y[XPPI]; + const ScalarT pref = y[PREF_STATE]; + const ScalarT v = y[V]; + const ScalarT vldc = y[VLDC]; + const ScalarT vdroop = y[VDROOP]; + const ScalarT vctrl = y[VCTRL]; + const ScalarT sfrz = y[SFRZ]; + const ScalarT erq = y[ERQ]; + const ScalarT erqdb = y[ERQDB]; + const ScalarT erqlim = y[ERQLIM]; + const ScalarT qpi = y[QPI]; + const ScalarT qext = toComponentBase(y[QEXT]); + const ScalarT ef = y[EF]; + const ScalarT ep = y[EP]; + const ScalarT eplim = y[EPLIM]; + const ScalarT ppi = y[PPI]; + const ScalarT pext = toComponentBase(y[PEXT]); + + const ScalarT vmeas_dot = yp[VMEAS]; + const ScalarT qmeas_dot = yp[QMEAS]; + const ScalarT xqpi_dot = yp[XQPI]; + const ScalarT xqlag_dot = yp[XQLAG]; + const ScalarT pmeas_dot = yp[PMEAS]; + const ScalarT xppi_dot = yp[XPPI]; + const ScalarT pref_dot = yp[PREF_STATE]; const ScalarT vr = wb[0]; const ScalarT vi = wb[1]; - const ScalarT ir = toComponentBase(ws[index(RepcaExternalVariables::IR)]); - const ScalarT ii = toComponentBase(ws[index(RepcaExternalVariables::II)]); - const ScalarT p = toComponentBase(ws[index(RepcaExternalVariables::P)]); - const ScalarT q = toComponentBase(ws[index(RepcaExternalVariables::Q)]); - const ScalarT freq = ws[index(RepcaExternalVariables::FREQ)]; - const ScalarT freqref = ws[index(RepcaExternalVariables::FREQREF)]; - const ScalarT vref = ws[index(RepcaExternalVariables::VREF)]; - const ScalarT qref = toComponentBase(ws[index(RepcaExternalVariables::QREF)]); - const ScalarT pref_in = toComponentBase(ws[index(RepcaExternalVariables::PREF)]); + const ScalarT ir = toComponentBase(ws[IR]); + const ScalarT ii = toComponentBase(ws[II]); + const ScalarT p = toComponentBase(ws[P]); + const ScalarT q = toComponentBase(ws[Q]); + const ScalarT freq = ws[FREQ]; + const ScalarT freqref = ws[FREQREF]; + const ScalarT vref = ws[VREF]; + const ScalarT qref = toComponentBase(ws[QREF]); + const ScalarT pref_in = toComponentBase(ws[PREF_INPUT]); const ScalarT vldc_r = vr - Rc_ * ir + Xc_ * ii; const ScalarT vldc_i = vi - Rc_ * ii - Xc_ * ir; const ScalarT pfreq = droop(ef, Ddn_, Dup_); - f[index(RepcaInternalVariables::VMEAS)] = -vmeas_dot + (vctrl - vmeas) / Tfltr_; - f[index(RepcaInternalVariables::QMEAS)] = -qmeas_dot + (q - qmeas) / Tfltr_; - f[index(RepcaInternalVariables::XQPI)] = -xqpi_dot + sfrz * Math::antiwindup(qpi, Ki_ * erqlim, Qmin_, Qmax_); - f[index(RepcaInternalVariables::XQLAG)] = -xqlag_dot + (qpi - xqlag) / Tfv_; - f[index(RepcaInternalVariables::PMEAS)] = -pmeas_dot + (p - pmeas) / Tp_; - f[index(RepcaInternalVariables::XPPI)] = -xppi_dot + Math::antiwindup(ppi, Kig_ * eplim, Pmin_, Pmax_); - f[index(RepcaInternalVariables::PREF)] = -pref_dot + (ppi - pref) / Tlag_; - - f[index(RepcaInternalVariables::V)] = -v * v + vr * vr + vi * vi; - f[index(RepcaInternalVariables::VLDC)] = -vldc * vldc + vldc_r * vldc_r + vldc_i * vldc_i; - f[index(RepcaInternalVariables::VDROOP)] = -vdroop + v + Kc_ * q; - f[index(RepcaInternalVariables::VCTRL)] = -vctrl + vcomp_on_ * vldc + vcomp_off_ * vdroop; - f[index(RepcaInternalVariables::SFRZ)] = -sfrz + Math::above(v, Vfrz_); - f[index(RepcaInternalVariables::ERQ)] = -erq + ref_on_ * (vref - vmeas) + ref_off_ * (qref - qmeas); - f[index(RepcaInternalVariables::ERQDB)] = -erqdb + Math::deadband2(erq, dbdlow_, dbdupper_); - f[index(RepcaInternalVariables::ERQLIM)] = -erqlim + Math::clamp(erqdb, emin_, emax_); - f[index(RepcaInternalVariables::QPI)] = -qpi + Math::clamp(Kp_ * erqlim + xqpi, Qmin_, Qmax_); - f[index(RepcaInternalVariables::QEXT)] = -Tfv_ * (qext - xqlag) + Tft_ * (qpi - xqlag); - - f[index(RepcaInternalVariables::EF)] = -ef + Math::deadband2(freqref - freq, fdbd1_, fdbd2_); - f[index(RepcaInternalVariables::EP)] = -ep + pref_in - pmeas + pfreq; - f[index(RepcaInternalVariables::EPLIM)] = -eplim + Math::clamp(ep, femin_, femax_); - f[index(RepcaInternalVariables::PPI)] = -ppi + Math::clamp(Kpg_ * eplim + xppi, Pmin_, Pmax_); - f[index(RepcaInternalVariables::PEXT)] = -pext + freq_on_ * pref; + f[VMEAS] = -vmeas_dot + (vctrl - vmeas) / Tfltr_; + f[QMEAS] = -qmeas_dot + (q - qmeas) / Tfltr_; + f[XQPI] = -xqpi_dot + sfrz * Math::antiwindup(qpi, Ki_ * erqlim, Qmin_, Qmax_); + f[XQLAG] = -xqlag_dot + (qpi - xqlag) / Tfv_; + f[PMEAS] = -pmeas_dot + (p - pmeas) / Tp_; + f[XPPI] = -xppi_dot + Math::antiwindup(ppi, Kig_ * eplim, Pmin_, Pmax_); + f[PREF_STATE] = -pref_dot + (ppi - pref) / Tlag_; + + f[V] = -v * v + vr * vr + vi * vi; + f[VLDC] = -vldc * vldc + vldc_r * vldc_r + vldc_i * vldc_i; + f[VDROOP] = -vdroop + v + Kc_ * q; + f[VCTRL] = -vctrl + vcomp_on_ * vldc + vcomp_off_ * vdroop; + f[SFRZ] = -sfrz + Math::above(v, Vfrz_); + f[ERQ] = -erq + ref_on_ * (vref - vmeas) + ref_off_ * (qref - qmeas); + f[ERQDB] = -erqdb + Math::deadband2(erq, dbdlow_, dbdupper_); + f[ERQLIM] = -erqlim + Math::clamp(erqdb, emin_, emax_); + f[QPI] = -qpi + Math::clamp(Kp_ * erqlim + xqpi, Qmin_, Qmax_); + f[QEXT] = -Tfv_ * (qext - xqlag) + Tft_ * (qpi - xqlag); + + f[EF] = -ef + Math::deadband2(freqref - freq, fdbd1_, fdbd2_); + f[EP] = -ep + pref_in - pmeas + pfreq; + f[EPLIM] = -eplim + Math::clamp(ep, femin_, femax_); + f[PPI] = -ppi + Math::clamp(Kpg_ * eplim + xppi, Pmin_, Pmax_); + f[PEXT] = -pext + freq_on_ * pref; return 0; } @@ -739,6 +816,24 @@ namespace GridKit // Private methods // + /** + * @brief Smooth asymmetric frequency-droop response + * + * @param[in] error Deadbanded frequency error. + * @param[in] gain_down Down-frequency response gain. + * @param[in] gain_up Up-frequency response gain. + * @return Active-power frequency response. + */ + template + __attribute__((always_inline)) inline scalar_type + Repca::droop( + const ScalarT error, + const RealT gain_down, + const RealT gain_up) + { + return error * (gain_up + (gain_down - gain_up) * Math::sigmoid(error)); + } + /** * @brief Read optional parameters from model data * @@ -853,15 +948,15 @@ namespace GridKit using Variable = typename ModelDataT::MonitorableVariables; monitor_->set(Variable::qext, [this] - { return y_.getData()[index(RepcaInternalVariables::QEXT)]; }); + { return y_.getData()[static_cast(RepcaInternalVariables::QEXT)]; }); monitor_->set(Variable::pext, [this] - { return y_.getData()[index(RepcaInternalVariables::PEXT)]; }); + { return y_.getData()[static_cast(RepcaInternalVariables::PEXT)]; }); monitor_->set(Variable::vmeas, [this] - { return y_.getData()[index(RepcaInternalVariables::VMEAS)]; }); + { return y_.getData()[static_cast(RepcaInternalVariables::VMEAS)]; }); monitor_->set(Variable::qmeas, [this] - { return y_.getData()[index(RepcaInternalVariables::QMEAS)]; }); + { return y_.getData()[static_cast(RepcaInternalVariables::QMEAS)]; }); monitor_->set(Variable::pmeas, [this] - { return y_.getData()[index(RepcaInternalVariables::PMEAS)]; }); + { return y_.getData()[static_cast(RepcaInternalVariables::PMEAS)]; }); } /** @@ -1073,7 +1168,7 @@ namespace GridKit typename Repca::RealT Repca::logOneMinusExp(RealT x) const { - const RealT log_two = std::log(static_cast(2.0)); + static constexpr auto log_two = std::numbers::ln2_v; if (x < log_two) { diff --git a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp index b3db1cd78..8d943fa8b 100644 --- a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp @@ -39,16 +39,8 @@ namespace GridKit ControllerRepcaTests() = default; ~ControllerRepcaTests() = default; - // Every probe state clears its smooth transitions by a margin, so the - // widest gap to an ideal expected value is the floored-lag PREF row at - // 3.2 eps. static constexpr RealT kTol = - static_cast(10.0) * std::numeric_limits::epsilon(); - - // The widest Enzyme-versus-dependency-tracking gap is QMEAS at the - // q column, 0.7 eps. - static constexpr RealT kJacobianTol = - static_cast(10.0) * std::numeric_limits::epsilon(); + static_cast(100.0) * std::numeric_limits::epsilon(); /// Validate construction, defaults, parameters, signals, and time floors. TestOutcome validation() @@ -104,36 +96,39 @@ namespace GridKit success *= invalidParameterCase(Params::Tp, -0.3); success *= invalidParameterCase(Params::Tlag, -0.4); - const RealT nan = std::numeric_limits::quiet_NaN(); - const RealT infinity = std::numeric_limits::infinity(); - for (const Params parameter : {Params::mva, - Params::Tfltr, - Params::Vfrz, - Params::Rc, - Params::Xc, - Params::Kc, - Params::dbdlow, - Params::dbdupper, - Params::emax, - Params::emin, - Params::Kp, - Params::Ki, - Params::Qmax, - Params::Qmin, - Params::Tft, - Params::Tfv, - Params::Tp, - Params::fdbd1, - Params::fdbd2, - Params::Ddn, - Params::Dup, - Params::femax, - Params::femin, - Params::Kpg, - Params::Kig, - Params::Pmax, - Params::Pmin, - Params::Tlag}) + const RealT nan = std::numeric_limits::quiet_NaN(); + const RealT infinity = std::numeric_limits::infinity(); + const std::array real_parameters{{ + Params::mva, + Params::Tfltr, + Params::Vfrz, + Params::Rc, + Params::Xc, + Params::Kc, + Params::dbdlow, + Params::dbdupper, + Params::emax, + Params::emin, + Params::Kp, + Params::Ki, + Params::Qmax, + Params::Qmin, + Params::Tft, + Params::Tfv, + Params::Tp, + Params::fdbd1, + Params::fdbd2, + Params::Ddn, + Params::Dup, + Params::femax, + Params::femin, + Params::Kpg, + Params::Kig, + Params::Pmax, + Params::Pmin, + Params::Tlag, + }}; + for (const Params parameter : real_parameters) { success *= invalidParameterCase(parameter, nan); success *= invalidParameterCase(parameter, infinity); @@ -157,9 +152,27 @@ namespace GridKit success *= (overflowing_base_ratio.repca.verify() > 0); } - for (const Params flag : {Params::VcompFlag, Params::RefFlag, Params::Freqflag}) + const std::array flag_parameters{{ + Params::VcompFlag, + Params::RefFlag, + Params::Freqflag, + }}; + const std::array valid_flag_values{{false, true}}; + const std::array invalid_integral_flag_values{{ + static_cast(0), + static_cast(1), + static_cast(2), + }}; + const std::array invalid_real_flag_values{{ + static_cast(0.0), + static_cast(0.5), + static_cast(1.0), + nan, + infinity, + }}; + for (const Params flag : flag_parameters) { - for (const bool value : {false, true}) + for (const bool value : valid_flag_values) { auto data = makeData(); data.parameters[flag] = value; @@ -168,18 +181,12 @@ namespace GridKit success *= (model.repca.verify() == 0); } - for (const IdxT value : {static_cast(0), - static_cast(1), - static_cast(2)}) + for (const IdxT value : invalid_integral_flag_values) { success *= invalidParameterCase(flag, value); } - for (const RealT value : {static_cast(0.0), - static_cast(0.5), - static_cast(1.0), - nan, - infinity}) + for (const RealT value : invalid_real_flag_values) { success *= invalidParameterCase(flag, value); } @@ -218,11 +225,14 @@ namespace GridKit y[index(Vars::PREF)] -= 0.004; floored.repca.y().setDataUpdated(); success *= (floored.repca.evaluateResidual() == 0); + const std::array floored_residuals{{ + {Vars::VMEAS, 1.0}, + {Vars::QMEAS, 2.0}, + {Vars::PMEAS, 3.0}, + {Vars::PREF, 4.0}, + }}; success *= residualsMatch(floored.repca, - {{Vars::VMEAS, 1.0}, - {Vars::QMEAS, 2.0}, - {Vars::PMEAS, 3.0}, - {Vars::PREF, 4.0}}, + floored_residuals, "floored time constants"); return success.report(__func__); @@ -240,30 +250,31 @@ namespace GridKit success *= (fixture.repca.tagDifferentiable() == 0); success *= (fixture.repca.evaluateResidual() == 0); - success *= stateMatches(fixture.repca, - {{Vars::VMEAS, 0.984002032518226}, - {Vars::QMEAS, 0.2}, - {Vars::XQPI, 0.5}, - {Vars::XQLAG, 0.5}, - {Vars::PMEAS, 0.8}, - {Vars::XPPI, 0.9}, - {Vars::PREF, 0.9}, - {Vars::V, 1.0}, - {Vars::VLDC, 0.984002032518226}, - {Vars::VDROOP, 1.08}, - {Vars::VCTRL, 0.984002032518226}, - {Vars::SFRZ, 1.0}, - {Vars::ERQ, 0.0}, - {Vars::ERQDB, 0.0}, - {Vars::ERQLIM, 0.0}, - {Vars::QPI, 0.5}, - {Vars::QEXT, 0.25}, - {Vars::EF, 0.0}, - {Vars::EP, 0.0}, - {Vars::EPLIM, 0.0}, - {Vars::PPI, 0.9}, - {Vars::PEXT, 0.45}}, - "initialization"); + const std::array initial_state{{ + {Vars::VMEAS, 0.984002032518226}, + {Vars::QMEAS, 0.2}, + {Vars::XQPI, 0.5}, + {Vars::XQLAG, 0.5}, + {Vars::PMEAS, 0.8}, + {Vars::XPPI, 0.9}, + {Vars::PREF, 0.9}, + {Vars::V, 1.0}, + {Vars::VLDC, 0.984002032518226}, + {Vars::VDROOP, 1.08}, + {Vars::VCTRL, 0.984002032518226}, + {Vars::SFRZ, 1.0}, + {Vars::ERQ, 0.0}, + {Vars::ERQDB, 0.0}, + {Vars::ERQLIM, 0.0}, + {Vars::QPI, 0.5}, + {Vars::QEXT, 0.25}, + {Vars::EF, 0.0}, + {Vars::EP, 0.0}, + {Vars::EPLIM, 0.0}, + {Vars::PPI, 0.9}, + {Vars::PEXT, 0.45}, + }}; + success *= stateMatches(fixture.repca, initial_state, "initialization"); success *= scalarPreserved(fixture.input(Ext::IR), 0.2, "preserved ir"); success *= scalarPreserved(fixture.input(Ext::II), -0.1, "preserved ii"); @@ -300,9 +311,14 @@ namespace GridKit row); } - success *= monitorMatches(fixture.repca, - {{0.25, 0.45, 0.984002032518226, 0.2, 0.8}}, - "initialization"); + const std::array initial_monitors{{ + 0.25, + 0.45, + 0.984002032518226, + 0.2, + 0.8, + }}; + success *= monitorMatches(fixture.repca, initial_monitors, "initialization"); success *= allResidualsWithinInitTolerance(fixture.repca); @@ -339,12 +355,15 @@ namespace GridKit setInitializationInputs(outputless); success *= outputless.initialize(0.25, 0.45); success *= (outputless.repca.evaluateResidual() == 0); + const std::array outputless_state{{ + {Vars::QEXT, 0.25}, + {Vars::PEXT, 0.45}, + }}; success *= stateMatches(outputless.repca, - {{Vars::QEXT, 0.25}, - {Vars::PEXT, 0.45}}, + outputless_state, "unassigned command outputs"); success *= monitorMatches(outputless.repca, - {{0.25, 0.45, 0.984002032518226, 0.2, 0.8}}, + initial_monitors, "unassigned command outputs"); success *= allResidualsWithinInitTolerance(outputless.repca); @@ -382,12 +401,15 @@ namespace GridKit success *= scenario.initialize(0.25, 0.45); success *= (scenario.repca.evaluateResidual() == 0); + const std::array expected_state{{ + {Vars::VMEAS, test_case.voltage}, + {Vars::VCTRL, test_case.voltage}, + {Vars::PREF, test_case.pref}, + {Vars::PPI, test_case.pref}, + {Vars::PEXT, test_case.pext}, + }}; success *= stateMatches(scenario.repca, - {{Vars::VMEAS, test_case.voltage}, - {Vars::VCTRL, test_case.voltage}, - {Vars::PREF, test_case.pref}, - {Vars::PPI, test_case.pref}, - {Vars::PEXT, test_case.pext}}, + expected_state, test_case.label); success *= scalarMatches(scenario.qext(), 0.25, test_case.label); success *= scalarMatches(scenario.pext(), test_case.pext, test_case.label); @@ -433,13 +455,17 @@ namespace GridKit test_case.pext, test_case.label); } - for (const Ext port : {Ext::IR, - Ext::II, - Ext::P, - Ext::Q, - Ext::FREQ}) + const std::array required_ports{{ + Ext::IR, + Ext::II, + Ext::P, + Ext::Q, + Ext::FREQ, + }}; + const std::array nonfinite_values{{infinity, -infinity, nan}}; + for (const Ext port : required_ports) { - for (const RealT value : {infinity, -infinity, nan}) + for (const RealT value : nonfinite_values) { success *= initializationRejectedAtomically(data, 0.25, @@ -478,12 +504,20 @@ namespace GridKit reactive_aw_data.parameters[Params::dbdupper] = 0.03; reactive_aw_data.parameters[Params::emin] = 0.0; - for (const bool voltage_reference : {false, true}) + const std::array voltage_reference_values{{false, true}}; + const std::array, 2> voltage_cases{{ + {0.8, 0.6}, + {0.2, 0.0}, + }}; + const std::array asymmetric_reactive_state{{ + {Vars::ERQDB, -0.1}, + {Vars::ERQLIM, 0.0}, + }}; + for (const bool voltage_reference : voltage_reference_values) { auto data = reactive_aw_data; data.parameters[Params::RefFlag] = voltage_reference; - for (const auto& voltage : {std::pair{0.8, 0.6}, - std::pair{0.2, 0.0}}) + for (const auto& voltage : voltage_cases) { Fixture asymmetric_reactive(data, voltage.first, @@ -493,8 +527,7 @@ namespace GridKit success *= asymmetric_reactive.initialize(0.25, 0.45); success *= (asymmetric_reactive.repca.evaluateResidual() == 0); success *= stateMatches(asymmetric_reactive.repca, - {{Vars::ERQDB, -0.1}, - {Vars::ERQLIM, 0.0}}, + asymmetric_reactive_state, "asymmetric reactive initialization"); success *= allResidualsWithinInitTolerance(asymmetric_reactive.repca); } @@ -521,10 +554,13 @@ namespace GridKit setInitializationInputs(asymmetric_active); success *= asymmetric_active.initialize(0.25, 0.45); success *= (asymmetric_active.repca.evaluateResidual() == 0); + const std::array lower_active_state{{ + {Vars::EF, 0.0}, + {Vars::EP, -0.1}, + {Vars::EPLIM, 0.0}, + }}; success *= stateMatches(asymmetric_active.repca, - {{Vars::EF, 0.0}, - {Vars::EP, -0.1}, - {Vars::EPLIM, 0.0}}, + lower_active_state, "lower active-error boundary"); success *= scalarMatches(asymmetric_active.input(Ext::FREQREF), 0.995, @@ -544,10 +580,13 @@ namespace GridKit setInitializationInputs(asymmetric_active); success *= asymmetric_active.initialize(0.25, 0.45); success *= (asymmetric_active.repca.evaluateResidual() == 0); + const std::array upper_active_state{{ + {Vars::EF, 0.0}, + {Vars::EP, 0.1}, + {Vars::EPLIM, 0.0}, + }}; success *= stateMatches(asymmetric_active.repca, - {{Vars::EF, 0.0}, - {Vars::EP, 0.1}, - {Vars::EPLIM, 0.0}}, + upper_active_state, "upper active-error boundary"); success *= scalarMatches(asymmetric_active.input(Ext::FREQREF), 0.985, @@ -594,15 +633,18 @@ namespace GridKit setInitializationInputs(qmax_pmin_boundary); success *= qmax_pmin_boundary.initialize(0.75, 0.0); success *= (qmax_pmin_boundary.repca.evaluateResidual() == 0); + const std::array qmax_pmin_state{{ + {Vars::QPI, 1.5}, + {Vars::XQLAG, 1.5}, + {Vars::XQPI, 1.6}, + {Vars::QEXT, 0.75}, + {Vars::PREF, 0.0}, + {Vars::PPI, 0.0}, + {Vars::XPPI, -0.1}, + {Vars::PEXT, 0.0}, + }}; success *= stateMatches(qmax_pmin_boundary.repca, - {{Vars::QPI, 1.5}, - {Vars::XQLAG, 1.5}, - {Vars::XQPI, 1.6}, - {Vars::QEXT, 0.75}, - {Vars::PREF, 0.0}, - {Vars::PPI, 0.0}, - {Vars::XPPI, -0.1}, - {Vars::PEXT, 0.0}}, + qmax_pmin_state, "Qmax/Pmin command boundary"); } @@ -612,15 +654,18 @@ namespace GridKit setInitializationInputs(qmin_pmax_boundary); success *= qmin_pmax_boundary.initialize(-0.4, 1.0); success *= (qmin_pmax_boundary.repca.evaluateResidual() == 0); + const std::array qmin_pmax_state{{ + {Vars::QPI, -0.8}, + {Vars::XQLAG, -0.8}, + {Vars::XQPI, -0.9}, + {Vars::QEXT, -0.4}, + {Vars::PREF, 2.0}, + {Vars::PPI, 2.0}, + {Vars::XPPI, 2.1}, + {Vars::PEXT, 1.0}, + }}; success *= stateMatches(qmin_pmax_boundary.repca, - {{Vars::QPI, -0.8}, - {Vars::XQLAG, -0.8}, - {Vars::XQPI, -0.9}, - {Vars::QEXT, -0.4}, - {Vars::PREF, 2.0}, - {Vars::PPI, 2.0}, - {Vars::XPPI, 2.1}, - {Vars::PEXT, 1.0}}, + qmin_pmax_state, "Qmin/Pmax command boundary"); } @@ -630,15 +675,18 @@ namespace GridKit setInitializationInputs(collapsed_limits); success *= collapsed_limits.initialize(0.25, 0.45); success *= (collapsed_limits.repca.evaluateResidual() == 0); + const std::array collapsed_state{{ + {Vars::XQPI, 0.5}, + {Vars::XQLAG, 0.5}, + {Vars::QPI, 0.5}, + {Vars::QEXT, 0.25}, + {Vars::XPPI, 0.9}, + {Vars::PREF, 0.9}, + {Vars::PPI, 0.9}, + {Vars::PEXT, 0.45}, + }}; success *= stateMatches(collapsed_limits.repca, - {{Vars::XQPI, 0.5}, - {Vars::XQLAG, 0.5}, - {Vars::QPI, 0.5}, - {Vars::QEXT, 0.25}, - {Vars::XPPI, 0.9}, - {Vars::PREF, 0.9}, - {Vars::PPI, 0.9}, - {Vars::PEXT, 0.45}}, + collapsed_state, "collapsed Q/P limits"); success *= allResidualsWithinInitTolerance(collapsed_limits.repca); } @@ -659,44 +707,46 @@ namespace GridKit setAnswerKeyState(fixture.repca); success *= (fixture.repca.evaluateResidual() == 0); - const std::array expected{{ - {Vars::VMEAS, "VMEAS", 0.4}, - {Vars::QMEAS, "QMEAS", 0.45}, - {Vars::XQPI, "XQPI", 0.3}, - {Vars::XQLAG, "XQLAG", 0.46}, - {Vars::PMEAS, "PMEAS", 0.5}, - {Vars::XPPI, "XPPI", -0.3}, - {Vars::PREF, "PREF", 0.4}, - {Vars::V, "V", -1.28}, - {Vars::VLDC, "VLDC", 0.028}, - {Vars::VDROOP, "VDROOP", 0.1}, - {Vars::VCTRL, "VCTRL", 0.05}, - {Vars::SFRZ, "SFRZ", 0.5}, - {Vars::ERQ, "ERQ", -0.63}, - {Vars::ERQDB, "ERQDB", 0.75}, - {Vars::ERQLIM, "ERQLIM", -0.35}, - {Vars::QPI, "QPI", -0.05}, - {Vars::QEXT, "QEXT", -1.345}, - {Vars::EF, "EF", -0.015}, - {Vars::EP, "EP", 0.4}, - {Vars::EPLIM, "EPLIM", 1.1}, - {Vars::PPI, "PPI", 0.1}, - {Vars::PEXT, "PEXT", 0.05}, + const std::array expected_residuals{{ + {Vars::VMEAS, 0.4}, + {Vars::QMEAS, 0.45}, + {Vars::XQPI, 0.3}, + {Vars::XQLAG, 0.46}, + {Vars::PMEAS, 0.5}, + {Vars::XPPI, -0.3}, + {Vars::PREF, 0.4}, + {Vars::V, -1.28}, + {Vars::VLDC, 0.028}, + {Vars::VDROOP, 0.1}, + {Vars::VCTRL, 0.05}, + {Vars::SFRZ, 0.5}, + {Vars::ERQ, -0.63}, + {Vars::ERQDB, 0.75}, + {Vars::ERQLIM, -0.35}, + {Vars::QPI, -0.05}, + {Vars::QEXT, -1.345}, + {Vars::EF, -0.015}, + {Vars::EP, 0.4}, + {Vars::EPLIM, 1.1}, + {Vars::PPI, 0.1}, + {Vars::PEXT, 0.05}, }}; - success *= (static_cast(fixture.repca.getResidual().getSize()) == expected.size()); + success *= (static_cast(fixture.repca.getResidual().getSize()) + == expected_residuals.size()); const auto* residual = fixture.repca.getResidual().getData(); - for (size_t row = 0; row < expected.size(); ++row) + for (size_t row = 0; row < expected_residuals.size(); ++row) { - if (index(expected[row].variable) != row) + const auto variable = expected_residuals[row].variable; + if (index(variable) != row) { std::cout << "REPCA residual key position " << row << " names row " - << expected[row].name << '\n'; + << variableName(variable) << '\n'; success = false; } - success *= scalarMatches(residual[index(expected[row].variable)], - expected[row].value, - expected[row].name); + success *= scalarMatches(residual[index(variable)], + expected_residuals[row].value, + variableName(variable)); } return success.report(__func__); @@ -744,9 +794,12 @@ namespace GridKit {Vars::ERQ, 0.0}}); success *= (fixture.repca.evaluateResidual() == 0); + const std::array expected_residuals{{ + {Vars::VCTRL, test_case.vctrl}, + {Vars::ERQ, test_case.erq}, + }}; success *= residualsMatch(fixture.repca, - {{Vars::VCTRL, test_case.vctrl}, - {Vars::ERQ, test_case.erq}}, + expected_residuals, test_case.label); } @@ -856,9 +909,12 @@ namespace GridKit {Vars::QEXT, 0.20}}); setDerivative(fixture.repca, {{Vars::XQLAG, -0.04}}); success *= (fixture.repca.evaluateResidual() == 0); + const std::array lead_lag_residuals{{ + {Vars::XQLAG, 0.092}, + {Vars::QEXT, -0.624}, + }}; success *= residualsMatch(fixture.repca, - {{Vars::XQLAG, 0.092}, - {Vars::QEXT, -0.624}}, + lead_lag_residuals, "reactive-command lead-lag"); // The command sits beyond Qmax with the error driving further out, @@ -875,7 +931,7 @@ namespace GridKit numberVariables(blocked, 1.0); success *= (blocked.repca.evaluateResidual() == 0); - const JacobianRow expected{ + const DependencyTracking::Variable::DependencyMap expected{ {index(Vars::XQPI), -1.0}, {index(Vars::SFRZ), 0.0}, {index(Vars::ERQLIM), 0.0}, @@ -1046,14 +1102,17 @@ namespace GridKit {Vars::XPPI, -0.5}, {Vars::PREF, 0.8}}); success *= (fixture.repca.evaluateResidual() == 0); + const std::array expected_residuals{{ + {Vars::VMEAS, 0.3}, + {Vars::QMEAS, 0.35}, + {Vars::XQPI, 0.2}, + {Vars::XQLAG, 0.36}, + {Vars::PMEAS, 0.4}, + {Vars::XPPI, -0.4}, + {Vars::PREF, 0.3}, + }}; success *= residualsMatch(fixture.repca, - {{Vars::VMEAS, 0.3}, - {Vars::QMEAS, 0.35}, - {Vars::XQPI, 0.2}, - {Vars::XQLAG, 0.36}, - {Vars::PMEAS, 0.4}, - {Vars::XPPI, -0.4}, - {Vars::PREF, 0.3}}, + expected_residuals, "explicit derivatives"); return success.report(__func__); @@ -1106,7 +1165,7 @@ namespace GridKit success *= jacobianMatches(enzymeJacobian(data, success), dependencyTrackingJacobian(data, success), "Enzyme versus dependency tracking", - kJacobianTol); + kTol); auto all_flags_off_data = data; all_flags_off_data.parameters[Params::VcompFlag] = false; @@ -1116,26 +1175,25 @@ namespace GridKit enzymeJacobian(all_flags_off_data, success), dependencyTrackingJacobian(all_flags_off_data, success), "all-flags-off Enzyme versus dependency tracking", - kJacobianTol); + kTol); success *= jacobianMatches( enzymeJacobian(data, success, kNonunitAlpha), dependencyTrackingJacobian(data, success, kNonunitAlpha), "non-unit-alpha Enzyme versus dependency tracking", - kJacobianTol); + kTol); return success.report(__func__); } #endif private: - using Params = PhasorDynamics::Controller::RepcaParameters; - using Vars = PhasorDynamics::Controller::RepcaInternalVariables; - using Ext = PhasorDynamics::Controller::RepcaExternalVariables; - using Mon = PhasorDynamics::Controller::RepcaMonitorableVariables; - using Data = PhasorDynamics::Controller::RepcaData; - using RepcaT = PhasorDynamics::Controller::Repca; - using JacobianRow = DependencyTracking::Variable::DependencyMap; + using Params = PhasorDynamics::Controller::RepcaParameters; + using Vars = PhasorDynamics::Controller::RepcaInternalVariables; + using Ext = PhasorDynamics::Controller::RepcaExternalVariables; + using Mon = PhasorDynamics::Controller::RepcaMonitorableVariables; + using Data = PhasorDynamics::Controller::RepcaData; + using RepcaT = PhasorDynamics::Controller::Repca; static constexpr size_t index(Vars variable) { @@ -1147,28 +1205,12 @@ namespace GridKit return static_cast(variable); } - struct Row + struct VariableValue { - constexpr Row(Vars row, RealT expected_value) - : variable(row), - value(expected_value) - { - } - Vars variable; RealT value; }; - struct ExpectedResidual - { - Vars variable; - const char* name; - RealT value; - }; - - using Rows = std::initializer_list; - using JacobianKey = std::array; - struct DrivenCase { RealT input; @@ -1699,10 +1741,11 @@ namespace GridKit } template - void setState(PhasorDynamics::Controller::Repca& repca, Rows rows) const + void setState(PhasorDynamics::Controller::Repca& repca, + std::initializer_list values) const { auto* y = repca.y().getData(); - for (const auto& [variable, value] : rows) + for (const auto& [variable, value] : values) { y[index(variable)] = static_cast(value); } @@ -1710,10 +1753,11 @@ namespace GridKit } template - void setDerivative(PhasorDynamics::Controller::Repca& repca, Rows rows) const + void setDerivative(PhasorDynamics::Controller::Repca& repca, + std::initializer_list values) const { auto* yp = repca.yp().getData(); - for (const auto& [variable, value] : rows) + for (const auto& [variable, value] : values) { yp[index(variable)] = static_cast(value); } @@ -1888,21 +1932,19 @@ namespace GridKit return false; } - template + template bool rowsMatch(const VectorT& vector, - const Row* rows, - size_t count, + const ValuesT& values, const char* what, const char* context) const { - bool success = true; - const auto* values = vector.getData(); - for (size_t i = 0; i < count; ++i) + bool success = true; + const auto* vector_values = vector.getData(); + for (const auto& [variable, expected] : values) { - const auto& [variable, expected] = rows[i]; - const size_t row = index(variable); + const size_t row = index(variable); - if (!variableMatches(static_cast(values[row]), + if (!variableMatches(static_cast(vector_values[row]), expected, what, variable, @@ -1914,18 +1956,34 @@ namespace GridKit return success; } - bool residualsMatch(const RepcaT& repca, Rows rows, const char* context = "") const + bool residualsMatch(const RepcaT& repca, + std::initializer_list values, + const char* context = "") const + { + return rowsMatch(repca.getResidual(), values, "residual", context); + } + + template + bool residualsMatch(const RepcaT& repca, + const std::array& values, + const char* context = "") const { - return rowsMatch(repca.getResidual(), - rows.begin(), - rows.size(), - "residual", - context); + return rowsMatch(repca.getResidual(), values, "residual", context); } - bool stateMatches(const RepcaT& repca, Rows rows, const char* context = "") const + bool stateMatches(const RepcaT& repca, + std::initializer_list values, + const char* context = "") const { - return rowsMatch(repca.y(), rows.begin(), rows.size(), "state", context); + return rowsMatch(repca.y(), values, "state", context); + } + + template + bool stateMatches(const RepcaT& repca, + const std::array& values, + const char* context = "") const + { + return rowsMatch(repca.y(), values, "state", context); } bool allResidualsWithinInitTolerance(const RepcaT& repca) const @@ -2020,9 +2078,9 @@ namespace GridKit Log::setVerbosity(previous_verbosity); } - JacobianKey expectedJacobian() const + std::vector expectedJacobian() const { - return {{ + return { {{index(Vars::VMEAS), -6.0}, {index(Vars::VCTRL), 5.0}}, {{index(Vars::QMEAS), -6.0}, {externalColumn(index(Ext::Q)), 10.0}}, {{index(Vars::XQPI), -1.0}, {index(Vars::SFRZ), 1.2}, {index(Vars::ERQLIM), 1.5}}, @@ -2054,10 +2112,10 @@ namespace GridKit {{index(Vars::EPLIM), -1.0}}, {{index(Vars::XPPI), 1.0}, {index(Vars::EPLIM), 1.7}, {index(Vars::PPI), -1.0}}, {{index(Vars::PREF), 1.0}, {index(Vars::PEXT), -2.0}}, - }}; + }; } - JacobianKey expectedJacobianAllFlagsOff() const + std::vector expectedJacobianAllFlagsOff() const { auto expected = expectedJacobian(); expected[index(Vars::VCTRL)] = { @@ -2075,7 +2133,7 @@ namespace GridKit return expected; } - JacobianKey expectedJacobianNonunitAlpha() const + std::vector expectedJacobianNonunitAlpha() const { auto expected = expectedJacobian(); expected[index(Vars::VMEAS)][index(Vars::VMEAS)] = -7.5; @@ -2088,7 +2146,8 @@ namespace GridKit return expected; } - static RealT mapValue(const JacobianRow& row, size_t column) + static RealT mapValue(const DependencyTracking::Variable::DependencyMap& row, + size_t column) { const auto entry = row.find(column); if (entry == row.end()) @@ -2098,11 +2157,12 @@ namespace GridKit return entry->second; } - bool jacobianRowMatches(const JacobianRow& actual, - const JacobianRow& expected, - size_t row, - const char* source, - RealT tolerance) const + bool jacobianRowMatches( + const DependencyTracking::Variable::DependencyMap& actual, + const DependencyTracking::Variable::DependencyMap& expected, + size_t row, + const char* source, + RealT tolerance) const { constexpr size_t column_count = externalColumn(index(Ext::MAXIMUM)); bool success = true; @@ -2131,11 +2191,11 @@ namespace GridKit return success; } - template - bool jacobianMatches(const std::vector& actual, - const ExpectedT& expected, - const char* source, - RealT tolerance) const + bool jacobianMatches( + const std::vector& actual, + const std::vector& expected, + const char* source, + RealT tolerance) const { if (actual.size() != expected.size()) { @@ -2182,9 +2242,10 @@ namespace GridKit fixture.bus.y().setDataUpdated(); } - std::vector dependencyTrackingJacobian(const Data& data, - TestStatus& success, - RealT alpha = 1.0) const + std::vector dependencyTrackingJacobian( + const Data& data, + TestStatus& success, + RealT alpha = 1.0) const { using DepVar = DependencyTracking::Variable; @@ -2196,8 +2257,8 @@ namespace GridKit numberVariables(fixture, alpha); success *= (fixture.repca.evaluateResidual() == 0); - std::vector rows(index(Vars::MAXIMUM)); - const auto* f = fixture.repca.getResidual().getData(); + std::vector rows(index(Vars::MAXIMUM)); + const auto* f = fixture.repca.getResidual().getData(); for (size_t row = 0; row < index(Vars::MAXIMUM); ++row) { rows[row] = f[row].getDependencies(); @@ -2206,9 +2267,10 @@ namespace GridKit } #ifdef GRIDKIT_ENABLE_ENZYME - std::vector enzymeJacobian(const Data& data, - TestStatus& success, - RealT alpha = 1.0) const + std::vector enzymeJacobian( + const Data& data, + TestStatus& success, + RealT alpha = 1.0) const { Fixture fixture(data, kStateVr, kStateVi); fixture.attachAllInputs(); diff --git a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp index c55743f66..6834d0158 100644 --- a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp @@ -1,5 +1,4 @@ #include -#include #include #include @@ -253,43 +252,22 @@ namespace GridKit return success.report(__func__); } - /// Construct REPCA through SystemModel, check its required bus mapping, - /// and prove assigned command outputs alias the assembled signals. TestOutcome repca() { - using Buses = PhasorDynamics::Controller::RepcaBuses; - using Inputs = PhasorDynamics::Controller::RepcaSignalInputs; - using Outputs = PhasorDynamics::Controller::RepcaSignalOutputs; - using Params = PhasorDynamics::Controller::RepcaParameters; - using Vars = PhasorDynamics::Controller::RepcaInternalVariables; - - constexpr size_t input_count = static_cast(Inputs::SIZE); - constexpr IdxT bus_id = static_cast(1); - constexpr IdxT qext_id = static_cast(input_count + 1); - constexpr IdxT pext_id = static_cast(input_count + 2); - - constexpr std::array input_ports{{ + using Buses = PhasorDynamics::Controller::RepcaBuses; + using Inputs = PhasorDynamics::Controller::RepcaSignalInputs; + using Vars = PhasorDynamics::Controller::RepcaInternalVariables; + + constexpr IdxT bus_id = static_cast(1); + constexpr std::array required_inputs{{ Inputs::ir, Inputs::ii, Inputs::p, Inputs::q, - Inputs::freq, - Inputs::vref, - Inputs::pref, - Inputs::qref, - Inputs::freqref, - }}; - constexpr std::array input_names{{ - "Branch Current Real", - "Branch Current Imaginary", - "Branch Active Power", - "Branch Reactive Power", - "Frequency", - "Voltage Reference", - "Plant Active Power Reference", - "Reactive Power Reference", - "Frequency Reference", }}; + constexpr size_t input_count = required_inputs.size(); + + TestStatus success = true; PhasorDynamics::SystemModelData data; data.bus.resize(1); @@ -298,44 +276,27 @@ namespace GridKit data.bus[0].Vr0 = static_cast(1.0); data.bus[0].Vi0 = static_cast(0.0); - data.signal.resize(input_count + 2); - for (size_t port = 0; port < input_count; ++port) - { - data.signal[port].signal_id = static_cast(port + 1); - data.signal[port].name = input_names[port]; - } - data.signal[input_count].signal_id = qext_id; - data.signal[input_count].name = "Reactive Power Command"; - data.signal[input_count + 1].signal_id = pext_id; - data.signal[input_count + 1].name = "Active Power Command"; + data.signal.resize(input_count); typename PhasorDynamics::SystemModelData::RepcaDataT repca_data; - repca_data.device_class = "Repca"; - repca_data.disambiguation_string = "repca_system"; - repca_data.buses[Buses::bus] = bus_id; - repca_data.parameters[Params::Tp] = static_cast(0.05); + repca_data.buses[Buses::bus] = bus_id; for (size_t port = 0; port < input_count; ++port) { - repca_data.signal_inputs[input_ports[port]] = static_cast(port + 1); + const auto signal_id = static_cast(port + 1); + data.signal[port].signal_id = signal_id; + repca_data.signal_inputs[required_inputs[port]] = signal_id; } - repca_data.signal_outputs[Outputs::qext] = qext_id; - repca_data.signal_outputs[Outputs::pext] = pext_id; data.repca.push_back(repca_data); std::array input_values{}; std::array input_indices{}; - input_values[static_cast(Inputs::freq)] = static_cast(1.0); - input_values[static_cast(Inputs::freqref)] = static_cast(1.0); - input_values[static_cast(Inputs::vref)] = static_cast(1.0); input_indices.fill(INVALID_INDEX); PhasorDynamics::SystemModel system(data); - TestStatus success = true; for (size_t port = 0; port < input_count; ++port) { - auto* signal = system.getSignal(static_cast(port + 1)); - signal->set(&input_values[port], &input_indices[port]); - success *= signal->linked(); + system.getSignal(static_cast(port + 1)) + ->set(&input_values[port], &input_indices[port]); } success *= system.allocate() == 0; @@ -345,39 +306,6 @@ namespace GridKit success *= system.evaluateJacobian() == 0; success *= system.size() == static_cast(Vars::MAXIMUM); - auto* repca = system.getComponent(0); - auto* qext = system.getSignal(qext_id); - auto* pext = system.getSignal(pext_id); - success *= repca != nullptr; - success *= qext != nullptr; - success *= pext != nullptr; - if (repca != nullptr && qext != nullptr && pext != nullptr) - { - success *= qext->linked(); - success *= pext->linked(); - if (qext->linked() && pext->linked()) - { - success *= qext->getVariableIndex() == static_cast(Vars::QEXT); - success *= pext->getVariableIndex() == static_cast(Vars::PEXT); - - // Both nodes alias the model-owned command states. - success *= qext->read() == repca->y().getData()[static_cast(Vars::QEXT)]; - success *= pext->read() == repca->y().getData()[static_cast(Vars::PEXT)]; - } - } - - auto missing_bus_data = data; - missing_bus_data.bus[0].bus_id = static_cast(0); - missing_bus_data.repca[0].buses.clear(); - PhasorDynamics::SystemModel missing_bus_system(missing_bus_data); - for (size_t port = 0; port < input_count; ++port) - { - missing_bus_system.getSignal(static_cast(port + 1)) - ->set(&input_values[port], &input_indices[port]); - } - std::cout << "Testing expected REPCA missing-bus configuration error.\n"; - success *= missing_bus_system.verify() > 0; - return success.report(__func__); } From a10a39ea0b2b9f273fc4b951deea11dd4c1e6752 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Thu, 6 Aug 2026 13:21:11 -0500 Subject: [PATCH 17/19] readability and added 1 test --- .../PhasorDynamics/Controller/REPCA/README.md | 47 ++-- .../PhasorDynamics/Controller/REPCA/Repca.hpp | 25 +-- .../Controller/REPCA/RepcaData.hpp | 4 +- .../Controller/REPCA/RepcaImpl.hpp | 210 +++++++++--------- .../ComponentConnectionTests.hpp | 26 ++- .../PhasorDynamics/ControllerRepcaTests.hpp | 124 +++++++++-- 6 files changed, 265 insertions(+), 171 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md b/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md index d926b9da6..0e5df068a 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/README.md @@ -10,12 +10,13 @@ resources. > [!NOTE] > `freq` is optional because the regulated bus does not yet expose a frequency -> signal. +> signal. With frequency control and nonzero droop enabled, omitting it holds +> frequency at 1.0 p.u. and logs a warning. `freq` and `freqref` use absolute +> per-unit frequency. > [!WARNING] > GridKit does not yet apply the associated generator's Governor Response Limits -> modes `Down Only` and `Fixed` to REPCA. The model always uses its configured -> $P^{\min}$ and $P^{\max}$. +> modes `Down Only` and `Fixed` to REPCA. ## Block Diagram @@ -26,9 +27,11 @@ Figure 1: REPCA plant-control model. Figure courtesy of ## Model Parameters -Symbol | Units | JSON | Description | Typical Value | Note +All parameters are optional; omitted keys use the defaults below. + +Symbol | Units | JSON | Description | Default | Note ------------------------------------|-----------|-------------|---------------------------------------------------------|---------------|------ -$S^\mathrm{base}$ | [MVA] | `mva` | REPCA component power base | 100.0 | Component base +$S^\mathrm{base}$ | [MVA] | `mva` | REPCA component power base | 100.0 | Set to the associated converter base $s_\mathrm{comp}$ | [boolean] | `VcompFlag` | Voltage-compensation selector | `true` | `true` = line-drop compensation, `false` = reactive droop $s_\mathrm{ref}$ | [boolean] | `RefFlag` | Reactive-loop reference selector | `true` | `true` = voltage control, `false` = reactive-power control $s_\mathrm{freq}$ | [boolean] | `Freqflag` | Active-power output selector | `false` | `true` = command enabled, `false` = zero output @@ -50,8 +53,8 @@ $T_\mathrm{fv}$ | [sec] | `Tfv` | Reactive-command $T_\mathrm{p}$ | [sec] | `Tp` | Active-power measurement filter time constant | 0.0 | $D_\mathrm{bd1}^{f}$ | [p.u.] | `fdbd1` | Lower frequency-error deadband threshold | 0.0 | $D_\mathrm{bd2}^{f}$ | [p.u.] | `fdbd2` | Upper frequency-error deadband threshold | 0.0 | -$D_\mathrm{dn}$ | [p.u.] | `Ddn` | Down-frequency droop response gain | 20.0 | -$D_\mathrm{up}$ | [p.u.] | `Dup` | Up-frequency droop response gain | 0.0 | +$D_\mathrm{dn}$ | [p.u./p.u.] | `Ddn` | Down-regulation (overfrequency) gain | 20.0 | +$D_\mathrm{up}$ | [p.u./p.u.] | `Dup` | Up-regulation (underfrequency) gain | 0.0 | $e_P^{\max}$ | [p.u.] | `femax` | Maximum active-power error limit | 1.0 | $e_P^{\min}$ | [p.u.] | `femin` | Minimum active-power error limit | -1.0 | $K_\mathrm{pg}$ | [p.u.] | `Kpg` | Active-power controller proportional gain | 10.0 | @@ -71,6 +74,7 @@ All real parameters must be finite. Invalid parameter sets are rejected by: e^{\min} &\le 0 \le e^{\max} \\ Q^{\min} &\le Q^{\max} \\ D_\mathrm{bd1}^{f} &\le 0 \le D_\mathrm{bd2}^{f} \\ + D_\mathrm{dn},D_\mathrm{up} &\ge 0 \\ e_P^{\min} &\le 0 \le e_P^{\max} \\ P^{\min} &\le P^{\max} \end{aligned} @@ -96,7 +100,8 @@ floor with a warning: ``` $S^\mathrm{sys}$ is the system power base; $k_\mathrm{base}$ converts system-base -power and current signals to component base. +power and current signals to component base. GridKit does not expose `PUflag`; +these ports are always on system base. ## Model Ports @@ -107,11 +112,11 @@ Name | Port | Init | Description `ii` | Input | Known | Branch-current imaginary component on system base `p` | Input | Known | Branch active power on system base `q` | Input | Known | Branch reactive power on system base -`freq` | Input | Known | Optional frequency input; defaults to 1.0 p.u.[^frequency-measurement] +`freq` | Input | Known | Optional absolute frequency; defaults to 1.0 p.u.[^frequency-measurement] `vref` | Input | Unknown | Voltage-control reference `pref` | Input | Unknown | Plant active-power reference on system base `qref` | Input | Unknown | Reactive-power reference on system base -`freqref` | Input | Unknown | Frequency reference +`freqref` | Input | Unknown | Absolute per-unit frequency reference `qext` | Output | Known | Reactive-power command on system base `pext` | Output | Known | Active-power command on system base @@ -265,6 +270,7 @@ finite inputs that reproduce the requested smooth-block outputs. e_\mathrm{RQ} &\leftarrow \text{deadband2}^{-1}(e_\mathrm{RQ}^\mathrm{db};\,D_\mathrm{bd1},D_\mathrm{bd2}) \\ e_\mathrm{RQ}^\mathrm{lim} &\leftarrow 0 \\ Q^\mathrm{PI} &\leftarrow k_\mathrm{base}Q^\mathrm{ext} \\ + Q^{\min} &\leftarrow \min(Q^{\min},Q^\mathrm{PI}),\quad Q^{\max}\leftarrow \max(Q^{\max},Q^\mathrm{PI}) \\ x_Q^\mathrm{lag} &\leftarrow Q^\mathrm{PI} \\ u_Q^\mathrm{PI} &\leftarrow \text{clamp}^{-1}(Q^\mathrm{PI};\,Q^{\min},Q^{\max}) \\ x_Q^\mathrm{PI} &\leftarrow u_Q^\mathrm{PI} - K_\mathrm{p}e_\mathrm{RQ}^\mathrm{lim} \\ @@ -275,25 +281,23 @@ finite inputs that reproduce the requested smooth-block outputs. e_P^\mathrm{lim} &\leftarrow 0 \\ P^\mathrm{ref} &\leftarrow \begin{cases} k_\mathrm{base}P^\mathrm{ext} & s_\mathrm{freq}=1 \\ - \text{clamp}(P^\mathrm{meas};\,P^{\min},P^{\max}) & s_\mathrm{freq}=0 - \end{cases} \\ - P^\mathrm{PI} &\leftarrow P^\mathrm{ref} \\ - u_P^\mathrm{PI} &\leftarrow \begin{cases} - \text{clamp}^{-1}(P^\mathrm{PI};\,P^{\min},P^{\max}) & s_\mathrm{freq}=1 \\ P^\mathrm{meas} & s_\mathrm{freq}=0 \end{cases} \\ + P^\mathrm{PI} &\leftarrow P^\mathrm{ref} \\ + P^{\min} &\leftarrow \min(P^{\min},P^\mathrm{PI}),\quad P^{\max}\leftarrow \max(P^{\max},P^\mathrm{PI}) \\ + u_P^\mathrm{PI} &\leftarrow \text{clamp}^{-1}(P^\mathrm{PI};\,P^{\min},P^{\max}) \\ x_P^\mathrm{PI} &\leftarrow u_P^\mathrm{PI} - K_\mathrm{pg}e_P^\mathrm{lim} \\ P^\mathrm{ext} &\leftarrow \dfrac{s_\mathrm{freq}}{k_\mathrm{base}}P^\mathrm{ref} \\ \dot{V}^\mathrm{meas},\dot{Q}^\mathrm{meas}, \dot{x}_Q^\mathrm{PI},\dot{x}_Q^\mathrm{lag}, \dot{P}^\mathrm{meas},\dot{x}_P^\mathrm{PI},\dot{P}^\mathrm{ref} &\leftarrow 0. \end{aligned} ``` +If an initial PI output falls outside its configured limits, REPCA expands the +limits to include it and logs a warning. + Initialization rejects an operating point if: - a required input or derived value is not finite; -- $k_\mathrm{base}Q^\mathrm{ext}\notin[Q^{\min},Q^{\max}]$, or - $s_\mathrm{freq}=1$ and - $k_\mathrm{base}P^\mathrm{ext}\notin[P^{\min},P^{\max}]$; or - the gated reactive-power or active-power PI state rate is nonfinite or does not vanish within the implementation tolerance. @@ -325,14 +329,15 @@ Output | Units | Description | Note - `validation()` checks defaults, parameter domains, signal contracts, and time floors. - `initializationAndSignals()` checks reconstruction, bases, signals, monitors, tags, and selectors. -- `initializationDomain()` checks rejection, asymmetric and collapsed limits, - nonfinite values, and atomicity. +- `initializationDomain()` checks adjusted and collapsed limits, nonfinite + values, and atomicity. - `residualEquations()` checks every residual against a fixed answer key. - `reactiveControl()` checks compensation and reference modes, voltage freeze, deadbands, smooth limits, anti-windup, and lead-lag behavior. - `activePowerControl()` checks frequency selection, deadband, droop, smooth limits, anti-windup, and the output lag. - `derivatives()` checks differential-row derivative signs. +- `dependencyTracking()` checks fixed numerical and structural Jacobian oracles. - `jacobian()` checks fixed numerical and structural oracles, plus Enzyme agreement to $10^{-9}$ when enabled. @@ -340,7 +345,7 @@ Output | Units | Description | Note ```math \text{droop}(e;D_\mathrm{dn},D_\mathrm{up}) -=e\left[D_\mathrm{up}+(D_\mathrm{dn}-D_\mathrm{up})\sigma(e)\right], +=e\left[D_\mathrm{dn}+(D_\mathrm{up}-D_\mathrm{dn})\sigma(e)\right], ``` where $\sigma$ is GridKit's smooth diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp index 3685f0d6a..73213d15d 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp @@ -62,11 +62,11 @@ namespace GridKit II, ///< \f$I_\mathrm{i}\f$ Required branch-current imaginary component on system base [p.u.] P, ///< \f$P\f$ Required branch active power on system base [p.u.] Q, ///< \f$Q\f$ Required branch reactive power on system base [p.u.] - FREQ, ///< \f$f\f$ Optional frequency input [p.u.] + FREQ, ///< \f$f\f$ Optional absolute frequency input [p.u.] VREF, ///< \f$V^\mathrm{ref}\f$ Optional voltage-control reference [p.u.] PREF, ///< \f$P_\mathrm{plant}^\mathrm{ref}\f$ Optional plant active-power reference on system base [p.u.] QREF, ///< \f$Q^\mathrm{ref}\f$ Optional reactive-power reference on system base [p.u.] - FREQREF, ///< \f$f^\mathrm{ref}\f$ Optional frequency reference [p.u.] + FREQREF, ///< \f$f^\mathrm{ref}\f$ Optional absolute frequency reference [p.u.] MAXIMUM ///< Number of external variables }; @@ -141,31 +141,22 @@ namespace GridKit ScalarT* f); private: + /// Smooth asymmetric frequency-droop response. + static __attribute__((always_inline)) inline ScalarT droop(ScalarT error, RealT down, RealT up); + void initializeParameters(const ModelDataT& data); void initializeMonitor(); void setDerivedParameters(); - bool solveLimiterInput(ScalarT requested_output, - RealT lower_limit, - RealT upper_limit, - ScalarT& limiter_input) const; + bool invertClamp(ScalarT output, RealT lower, RealT upper, ScalarT& input) const; - bool solveDeadbandInput(ScalarT requested_output, - RealT lower_limit, - RealT upper_limit, - ScalarT& deadband_input) const; + bool invertDeadband(ScalarT output, RealT lower, RealT upper, ScalarT& input) const; - RealT logOneMinusExp(RealT x) const; + static RealT logOneMinusExp(RealT x); [[gnu::always_inline]] inline ScalarT toComponentBase(ScalarT value) const; ScalarT toSystemBase(ScalarT value) const; - /// Smooth asymmetric frequency-droop response. - static __attribute__((always_inline)) inline ScalarT droop( - const ScalarT error, - const RealT gain_down, - const RealT gain_up); - ScalarT& Vr(); ScalarT& Vi(); diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaData.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaData.hpp index d6dd79f14..90e37bdf0 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaData.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaData.hpp @@ -39,8 +39,8 @@ namespace GridKit Tp, ///< \f$T_\mathrm{p}\f$ Active-power measurement filter time constant [sec] fdbd1, ///< \f$D_\mathrm{bd1}^{f}\f$ Lower frequency-error deadband threshold [p.u.] fdbd2, ///< \f$D_\mathrm{bd2}^{f}\f$ Upper frequency-error deadband threshold [p.u.] - Ddn, ///< \f$D_\mathrm{dn}\f$ Down-frequency droop response gain [p.u.] - Dup, ///< \f$D_\mathrm{up}\f$ Up-frequency droop response gain [p.u.] + Ddn, ///< \f$D_\mathrm{dn}\f$ Down-regulation (overfrequency) gain [p.u./p.u.] + Dup, ///< \f$D_\mathrm{up}\f$ Up-regulation (underfrequency) gain [p.u./p.u.] femax, ///< \f$e_P^{\max}\f$ Maximum active-power error [p.u.] femin, ///< \f$e_P^{\min}\f$ Minimum active-power error [p.u.] Kpg, ///< \f$K_\mathrm{pg}\f$ Active-power proportional gain [p.u.] diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp index b5e304c7c..74a939ee1 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp @@ -187,6 +187,8 @@ namespace GridKit check(Qmin_ <= Qmax_, "Qmin must be less than or equal to Qmax"); check(fdbd1_ <= ZERO && ZERO <= fdbd2_, "fdbd1 <= 0 <= fdbd2 is required"); + check(Ddn_ >= ZERO, "Ddn must be non-negative"); + check(Dup_ >= ZERO, "Dup must be non-negative"); check(femin_ <= ZERO && ZERO <= femax_, "femin <= 0 <= femax is required"); check(Pmin_ <= Pmax_, "Pmin must be less than or equal to Pmax"); @@ -242,11 +244,11 @@ namespace GridKit * @pre `qext` and, when frequency control is enabled, `pext` contain the * initial plant commands. * - * @post On failure no state, derivative, latch, or signal storage is - * modified. + * @post On failure, states, derivatives, limits, latches, and signal + * values are unchanged. * * @return 0 on success; nonzero when allocation, configuration, initial- - * value, candidate, command-limit, or steady-state checks fail. + * value, candidate, or steady-state checks fail. */ template int Repca::initialize() @@ -342,8 +344,8 @@ namespace GridKit const ScalarT zero = static_cast(ZERO); ScalarT erq0{}; ScalarT erqdb0{}; - if (!solveLimiterInput(zero, emin_, emax_, erqdb0) - || !solveDeadbandInput(erqdb0, dbdlow_, dbdupper_, erq0)) + if (!invertClamp(zero, emin_, emax_, erqdb0) + || !invertDeadband(erqdb0, dbdlow_, dbdupper_, erq0)) { Log::error() << "Repca: reactive error blocks have no finite steady input\n"; return 1; @@ -352,14 +354,17 @@ namespace GridKit const ScalarT qpi0 = qext0; const ScalarT xqlag0 = qpi0; + const RealT qmin = std::min(Qmin_, static_cast(qpi0)); + const RealT qmax = std::max(Qmax_, static_cast(qpi0)); + ScalarT qpi_input0{}; - if (!solveLimiterInput(qpi0, Qmin_, Qmax_, qpi_input0)) + if (!invertClamp(qpi0, qmin, qmax, qpi_input0)) { - Log::error() << "Repca: initial reactive-power command is outside Qmin/Qmax\n"; + Log::error() << "Repca: reactive-power PI limiter has no finite steady input\n"; return 1; } const ScalarT xqpi0 = qpi_input0 - Kp_ * erqlim0; - const ScalarT q_aw_rate0 = Math::antiwindup(qpi0, Ki_ * erqlim0, Qmin_, Qmax_); + const ScalarT q_aw_rate0 = Math::antiwindup(qpi0, Ki_ * erqlim0, qmin, qmax); const ScalarT xqpi_rate0 = sfrz0 * q_aw_rate0; if (!is_finite(q_aw_rate0) || !is_finite(xqpi_rate0) || std::abs(static_cast(xqpi_rate0)) > INITIALIZATION_TOLERANCE) @@ -370,8 +375,8 @@ namespace GridKit ScalarT frequency_error0{}; ScalarT ep0{}; - if (!solveDeadbandInput(zero, fdbd1_, fdbd2_, frequency_error0) - || !solveLimiterInput(zero, femin_, femax_, ep0)) + if (!invertDeadband(zero, fdbd1_, fdbd2_, frequency_error0) + || !invertClamp(zero, femin_, femax_, ep0)) { Log::error() << "Repca: active error blocks have no finite steady input\n"; return 1; @@ -379,23 +384,20 @@ namespace GridKit const ScalarT ef0 = zero; const ScalarT pfreq0 = droop(ef0, Ddn_, Dup_); const ScalarT eplim0 = zero; - const ScalarT pref0 = Freqflag_ ? pext0 : Math::clamp(pmeas0, Pmin_, Pmax_); + const ScalarT pref0 = Freqflag_ ? pext0 : pmeas0; const ScalarT ppi0 = pref0; - ScalarT ppi_input0{}; - if (Freqflag_) - { - if (!solveLimiterInput(ppi0, Pmin_, Pmax_, ppi_input0)) - { - Log::error() << "Repca: initial active-power command is outside Pmin/Pmax\n"; - return 1; - } - } - else + + const RealT pmin = std::min(Pmin_, static_cast(ppi0)); + const RealT pmax = std::max(Pmax_, static_cast(ppi0)); + + ScalarT ppi_input0{}; + if (!invertClamp(ppi0, pmin, pmax, ppi_input0)) { - ppi_input0 = pmeas0; + Log::error() << "Repca: active-power PI limiter has no finite steady input\n"; + return 1; } const ScalarT xppi0 = ppi_input0 - Kpg_ * eplim0; - const ScalarT p_aw_rate0 = Math::antiwindup(ppi0, Kig_ * eplim0, Pmin_, Pmax_); + const ScalarT p_aw_rate0 = Math::antiwindup(ppi0, Kig_ * eplim0, pmin, pmax); if (!is_finite(p_aw_rate0) || std::abs(static_cast(p_aw_rate0)) > INITIALIZATION_TOLERANCE) { @@ -486,6 +488,13 @@ namespace GridKit y[PPI] = ppi0; y[PEXT] = pext_output0; + const bool q_adjusted = qmin != Qmin_ || qmax != Qmax_; + const bool p_adjusted = pmin != Pmin_ || pmax != Pmax_; + Qmin_ = qmin; + Qmax_ = qmax; + Pmin_ = pmin; + Pmax_ = pmax; + freqref_set_ = freqref0; vref_set_ = vref0; qref_set_ = qref0_system; @@ -510,6 +519,24 @@ namespace GridKit freqref_set_); } + if (q_adjusted) + { + Log::warning() << "Repca: initial reactive PI output is outside [Qmin, Qmax]; " + "the limits are adjusted to include the initialized value\n"; + } + if (p_adjusted) + { + Log::warning() << "Repca: initial active PI output is outside [Pmin, Pmax]; " + "the limits are adjusted to include the initialized value\n"; + } + if (Freqflag_ + && (Ddn_ != ZERO || Dup_ != ZERO) + && !signals_.template isAttached()) + { + Log::warning() << "Repca: Freqflag is enabled without a freq signal; " + "frequency remains nominal and cannot track bus-frequency deviations\n"; + } + y_.setDataUpdated(); yp_.setToConst(static_cast(ZERO)); return 0; @@ -820,18 +847,15 @@ namespace GridKit * @brief Smooth asymmetric frequency-droop response * * @param[in] error Deadbanded frequency error. - * @param[in] gain_down Down-frequency response gain. - * @param[in] gain_up Up-frequency response gain. + * @param[in] down Down-regulation (overfrequency) gain. + * @param[in] up Up-regulation (underfrequency) gain. * @return Active-power frequency response. */ template __attribute__((always_inline)) inline scalar_type - Repca::droop( - const ScalarT error, - const RealT gain_down, - const RealT gain_up) + Repca::droop(ScalarT error, RealT down, RealT up) { - return error * (gain_up + (gain_down - gain_up) * Math::sigmoid(error)); + return error * (down + (up - down) * Math::sigmoid(error)); } /** @@ -1012,51 +1036,48 @@ namespace GridKit * * This initialization-only helper inverts the CommonMath smooth clamp, * including collapsed limits. Exact-bound requests use a 0.1 offset, - * leaving less than 2e-13 at CommonMath MU = 240. It must not be called - * from residual evaluation. + * leaving less than 2e-13 at CommonMath MU = 240. * - * @param[in] requested_output Requested smooth-clamp output. - * @param[in] lower_limit Lower clamp limit. - * @param[in] upper_limit Upper clamp limit. - * @param[out] limiter_input Recovered clamp input on success. + * @param[in] output Requested smooth-clamp output. + * @param[in] lower Lower clamp limit. + * @param[in] upper Upper clamp limit. + * @param[out] input Recovered clamp input on success. * @return true when a finite admissible input was recovered. + * @warning Contains conditional branching and is for initialization only; + * do not use it during residual evaluation. */ template - bool Repca::solveLimiterInput( - ScalarT requested_output, - RealT lower_limit, - RealT upper_limit, - ScalarT& limiter_input) const + bool Repca::invertClamp(ScalarT output, RealT lower, RealT upper, ScalarT& input) const { - const RealT output_value = static_cast(requested_output); - - if (!std::isfinite(output_value) - || !std::isfinite(lower_limit) - || !std::isfinite(upper_limit) - || lower_limit > upper_limit - || output_value < lower_limit - || output_value > upper_limit) + const RealT value = static_cast(output); + + if (!std::isfinite(value) + || !std::isfinite(lower) + || !std::isfinite(upper) + || lower > upper + || value < lower + || value > upper) { return false; } - const RealT width = upper_limit - lower_limit; + const RealT width = upper - lower; if (width <= INITIALIZATION_TOLERANCE) { - limiter_input = static_cast(lower_limit); + input = static_cast(lower); return true; } - const RealT distance_from_lower = output_value - lower_limit; - const RealT distance_from_upper = upper_limit - output_value; + const RealT distance_from_lower = value - lower; + const RealT distance_from_upper = upper - value; if (distance_from_lower <= INITIALIZATION_TOLERANCE) { - limiter_input = static_cast(lower_limit - INITIALIZATION_LIMIT_OFFSET); + input = static_cast(lower - INITIALIZATION_LIMIT_OFFSET); return true; } if (distance_from_upper <= INITIALIZATION_TOLERANCE) { - limiter_input = static_cast(upper_limit + INITIALIZATION_LIMIT_OFFSET); + input = static_cast(upper + INITIALIZATION_LIMIT_OFFSET); return true; } @@ -1065,92 +1086,81 @@ namespace GridKit const RealT scaled_upper_distance = mu * distance_from_upper; const RealT log_lower = logOneMinusExp(scaled_lower_distance); const RealT log_upper = logOneMinusExp(scaled_upper_distance); - const RealT correction = - (scaled_lower_distance + log_lower - log_upper) / mu; + const RealT correction = (scaled_lower_distance + log_lower - log_upper) / mu; - limiter_input = static_cast(lower_limit + correction); - return std::isfinite(static_cast(limiter_input)); + input = static_cast(lower + correction); + return std::isfinite(static_cast(input)); } /** * @brief Recover an input for a requested smooth deadband output * - * @param[in] requested_output Requested deadband output. - * @param[in] lower_limit Lower deadband threshold. - * @param[in] upper_limit Upper deadband threshold. - * @param[out] deadband_input Recovered deadband input on success. + * @param[in] output Requested deadband output. + * @param[in] lower Lower deadband threshold. + * @param[in] upper Upper deadband threshold. + * @param[out] input Recovered deadband input on success. * @return true when a finite input was recovered. + * @warning Contains conditional branching and is for initialization only; + * do not use it during residual evaluation. */ template - bool Repca::solveDeadbandInput( - ScalarT requested_output, - RealT lower_limit, - RealT upper_limit, - ScalarT& deadband_input) const + bool Repca::invertDeadband(ScalarT output, RealT lower, RealT upper, ScalarT& input) const { - const RealT output_value = static_cast(requested_output); + const RealT value = static_cast(output); - if (!std::isfinite(output_value) - || !std::isfinite(lower_limit) - || !std::isfinite(upper_limit) - || lower_limit > upper_limit) + if (!std::isfinite(value) + || !std::isfinite(lower) + || !std::isfinite(upper) + || lower > upper) { return false; } - const RealT midpoint = HALF * lower_limit - + HALF * upper_limit; - if (std::abs(output_value) <= INITIALIZATION_TOLERANCE) + const RealT midpoint = HALF * lower + HALF * upper; + if (std::abs(value) <= INITIALIZATION_TOLERANCE) { - deadband_input = static_cast(midpoint); + input = static_cast(midpoint); return true; } RealT lower_input = midpoint; RealT upper_input = midpoint; - if (output_value < ZERO) + if (value < ZERO) { - lower_input = lower_limit + output_value; + lower_input = lower + value; } else { - upper_input = upper_limit + output_value; + upper_input = upper + value; } - const RealT lower_output = Math::deadband2(lower_input, - lower_limit, - upper_limit); - const RealT upper_output = Math::deadband2(upper_input, - lower_limit, - upper_limit); + const RealT lower_output = Math::deadband2(lower_input, lower, upper); + const RealT upper_output = Math::deadband2(upper_input, lower, upper); if (!std::isfinite(lower_output) || !std::isfinite(upper_output) - || lower_output - output_value > INITIALIZATION_TOLERANCE - || output_value - upper_output > INITIALIZATION_TOLERANCE) + || lower_output - value > INITIALIZATION_TOLERANCE + || value - upper_output > INITIALIZATION_TOLERANCE) { return false; } for (std::size_t iteration = 0; iteration < 128; ++iteration) { - const RealT input = lower_input - + HALF * (upper_input - lower_input); - if (Math::deadband2(input, lower_limit, upper_limit) < output_value) + const RealT mid = lower_input + HALF * (upper_input - lower_input); + if (Math::deadband2(mid, lower, upper) < value) { - lower_input = input; + lower_input = mid; } else { - upper_input = input; + upper_input = mid; } } - const RealT input = lower_input - + HALF * (upper_input - lower_input); - deadband_input = static_cast(input); - return std::isfinite(input) - && std::abs(Math::deadband2(input, lower_limit, upper_limit) - - output_value) + const RealT result = lower_input + HALF * (upper_input - lower_input); + input = static_cast(result); + return std::isfinite(result) + && std::abs(Math::deadband2(result, lower, upper) - value) <= INITIALIZATION_TOLERANCE; } @@ -1166,7 +1176,7 @@ namespace GridKit */ template typename Repca::RealT - Repca::logOneMinusExp(RealT x) const + Repca::logOneMinusExp(RealT x) { static constexpr auto log_two = std::numbers::ln2_v; diff --git a/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp b/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp index 5e7dd9b04..a849c5c4e 100644 --- a/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ComponentConnectionTests.hpp @@ -143,6 +143,7 @@ namespace GridKit { using ConverterInternal = PhasorDynamics::Converter::RegcaInternalVariables; using ConverterParams = PhasorDynamics::Converter::RegcaParameters; + using PlantInternal = PhasorDynamics::Controller::RepcaInternalVariables; using PlantExternal = PhasorDynamics::Controller::RepcaExternalVariables; using PlantParams = PhasorDynamics::Controller::RepcaParameters; @@ -158,8 +159,8 @@ namespace GridKit PhasorDynamics::SignalNode q; PhasorDynamics::Converter::RegcaData converter_data; - converter_data.parameters[ConverterParams::p0] = static_cast(0.0); - converter_data.parameters[ConverterParams::q0] = static_cast(0.0); + converter_data.parameters[ConverterParams::p0] = static_cast(0.4); + converter_data.parameters[ConverterParams::q0] = static_cast(0.1); converter_data.parameters[ConverterParams::mva] = static_cast(100.0); converter_data.parameters[ConverterParams::Tg] = static_cast(0.02); converter_data.parameters[ConverterParams::TM] = static_cast(0.02); @@ -175,7 +176,8 @@ namespace GridKit converter_data.parameters[ConverterParams::Vhvmax] = static_cast(1.2); PhasorDynamics::Controller::RepcaData plant_data; - plant_data.parameters[PlantParams::Tp] = static_cast(0.05); + plant_data.parameters[PlantParams::mva] = static_cast(50.0); + plant_data.parameters[PlantParams::Tp] = static_cast(0.05); PhasorDynamics::Converter::Regca converter(&bus, converter_data); PhasorDynamics::Controller::Repca plant(&bus, plant_data); @@ -204,11 +206,19 @@ namespace GridKit success *= system.initialize() == 0; success *= system.evaluateResidual() == 0; - // At zero power the converter draws no branch current or power. - success *= isEqual(ir.read(), static_cast(0.0), kTol); - success *= isEqual(ii.read(), static_cast(0.0), kTol); - success *= isEqual(p.read(), static_cast(0.0), kTol); - success *= isEqual(q.read(), static_cast(0.0), kTol); + success *= isEqual(ir.read(), static_cast(0.4), kTol); + success *= isEqual(ii.read(), static_cast(-0.1), kTol); + success *= isEqual(p.read(), static_cast(0.4), kTol); + success *= isEqual(q.read(), static_cast(0.1), kTol); + + const auto* state = plant.y().getData(); + + success *= isEqual(state[static_cast(PlantInternal::PMEAS)], + static_cast(0.8), + kTol); + success *= isEqual(state[static_cast(PlantInternal::QMEAS)], + static_cast(0.2), + kTol); const auto* residual = plant.getResidual().getData(); for (IdxT row = 0; row < plant.size(); ++row) diff --git a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp index 8d943fa8b..8bd4249e2 100644 --- a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp @@ -86,6 +86,8 @@ namespace GridKit success *= invalidParameterCase(Params::Qmin, 1.1); success *= invalidParameterCase(Params::fdbd1, 0.1); success *= invalidParameterCase(Params::fdbd2, -0.1); + success *= invalidParameterCase(Params::Ddn, -0.1); + success *= invalidParameterCase(Params::Dup, -0.1); success *= invalidParameterCase(Params::femin, 0.1); success *= invalidParameterCase(Params::femax, -0.1); success *= invalidParameterCase(Params::Pmin, 2.1); @@ -243,6 +245,9 @@ namespace GridKit { TestStatus success = true; + noteExpectedLogs("Testing REPCA initialization without an attached frequency " + "signal. A warning is expected."); + Fixture fixture(makeInitializationData(), 0.8, 0.6); fixture.attachAllInputs(99.0); setInitializationInputs(fixture); @@ -419,13 +424,13 @@ namespace GridKit return success.report(__func__); } - /// Check initialization rejection, atomicity, and exact command boundaries. + /// Check initialization domains, adjusted limits, and atomicity. TestOutcome initializationDomain() { TestStatus success = true; - noteExpectedLogs("Testing inadmissible REPCA command and controller " - "initialization points. Logged errors are expected."); + noteExpectedLogs("Testing REPCA adjusted limits and inadmissible " + "initialization points. Logged warnings and errors are expected."); const auto data = makeInitializationData(); @@ -438,11 +443,7 @@ namespace GridKit const RealT nan = std::numeric_limits::quiet_NaN(); const RealT infinity = std::numeric_limits::infinity(); - const std::array rejection_cases{{ - {"qext below Qmin", -0.4000000001, 0.45}, - {"qext above Qmax", 0.7500000001, 0.45}, - {"pext below Pmin", 0.25, -1.0e-10}, - {"pext above Pmax", 0.25, 1.0000000001}, + const std::array rejection_cases{{ {"nonfinite qext", infinity, 0.45}, {"nonfinite pext", 0.25, infinity}, {"nan qext", nan, 0.45}, @@ -491,15 +492,6 @@ namespace GridKit collapsed_data.parameters[Params::Pmin] = 0.9; collapsed_data.parameters[Params::Pmax] = 0.9; - success *= initializationRejectedAtomically(collapsed_data, - 0.2500000001, - 0.45, - "qext above collapsed Q limit"); - success *= initializationRejectedAtomically(collapsed_data, - 0.25, - 0.4500000001, - "pext above collapsed P limit"); - auto reactive_aw_data = makeInitializationData(); reactive_aw_data.parameters[Params::dbdupper] = 0.03; reactive_aw_data.parameters[Params::emin] = 0.0; @@ -691,6 +683,92 @@ namespace GridKit success *= allResidualsWithinInitTolerance(collapsed_limits.repca); } + struct LimitCase + { + const char* label; + RealT qext; + RealT pext; + Vars output; + RealT expected; + }; + + const std::array limit_cases{{ + {"adjusted Qmin", -0.5, 0.45, Vars::QPI, -1.0}, + {"adjusted Qmax", 0.9, 0.45, Vars::QPI, 1.8}, + {"adjusted Pmin", 0.25, -0.1, Vars::PPI, -0.2}, + {"adjusted Pmax", 0.25, 1.1, Vars::PPI, 2.2}, + }}; + + for (const auto& test_case : limit_cases) + { + Fixture adjusted(data, 0.8, 0.6); + adjusted.attachAllInputs(); + setInitializationInputs(adjusted); + success *= adjusted.initialize(test_case.qext, test_case.pext); + success *= stateMatches(adjusted.repca, + {{test_case.output, test_case.expected}}, + test_case.label); + success *= (adjusted.repca.evaluateResidual() == 0); + success *= allResidualsWithinInitTolerance(adjusted.repca); + } + + { + auto disabled_data = data; + disabled_data.parameters[Params::Freqflag] = false; + disabled_data.parameters[Params::Pmax] = 0.5; + + Fixture disabled_frequency(disabled_data, 0.8, 0.6); + disabled_frequency.attachAllInputs(); + setInitializationInputs(disabled_frequency); + success *= disabled_frequency.initialize(0.25, 0.45); + success *= stateMatches(disabled_frequency.repca, + {{Vars::PREF, 0.8}, + {Vars::PPI, 0.8}, + {Vars::PEXT, 0.0}}, + "measured-power limit"); + success *= (disabled_frequency.repca.evaluateResidual() == 0); + success *= allResidualsWithinInitTolerance(disabled_frequency.repca); + + setState(disabled_frequency.repca, + {{Vars::PPI, 0.65}, {Vars::EPLIM, 0.1}}); + setDerivative(disabled_frequency.repca, {{Vars::XPPI, 0.0}}); + success *= (disabled_frequency.repca.evaluateResidual() == 0); + success *= residualsMatch(disabled_frequency.repca, + {{Vars::XPPI, 0.18}}, + "measured-power limit"); + } + + { + Fixture adjusted(data, 0.8, 0.6); + adjusted.attachAllInputs(); + setInitializationInputs(adjusted); + success *= adjusted.initialize(1.0, 1.25); + + setState(adjusted.repca, + {{Vars::QPI, 1.75}, + {Vars::ERQLIM, 0.1}, + {Vars::SFRZ, 1.0}, + {Vars::PPI, 2.25}, + {Vars::EPLIM, 0.1}}); + setDerivative(adjusted.repca, {{Vars::XQPI, 0.0}, {Vars::XPPI, 0.0}}); + success *= (adjusted.repca.evaluateResidual() == 0); + success *= residualsMatch(adjusted.repca, + {{Vars::XQPI, 0.3}, {Vars::XPPI, 0.18}}, + "adjusted antiwindup limits"); + + setState(adjusted.repca, + {{Vars::QPI, 0.0}, + {Vars::ERQLIM, 0.0}, + {Vars::XQPI, 1.75}, + {Vars::PPI, 0.0}, + {Vars::EPLIM, 0.0}, + {Vars::XPPI, 2.25}}); + success *= (adjusted.repca.evaluateResidual() == 0); + success *= residualsMatch(adjusted.repca, + {{Vars::QPI, 1.75}, {Vars::PPI, 2.25}}, + "adjusted command limits"); + } + return success.report(__func__); } @@ -726,7 +804,7 @@ namespace GridKit {Vars::QPI, -0.05}, {Vars::QEXT, -1.345}, {Vars::EF, -0.015}, - {Vars::EP, 0.4}, + {Vars::EP, -0.4}, {Vars::EPLIM, 1.1}, {Vars::PPI, 0.1}, {Vars::PEXT, 0.05}, @@ -1003,9 +1081,9 @@ namespace GridKit } const std::array droop_cases{{ - {-0.9, -0.9}, + {-0.9, -1.8}, {0.0, 0.0}, - {0.9, 1.8}, + {0.9, 0.9}, }}; fixture.input(Ext::PREF) = 0.2; for (const auto& test_case : droop_cases) @@ -1119,7 +1197,7 @@ namespace GridKit } /// Check every dependency-tracking Jacobian row against an independent - /// numerical answer key, in every selector mode and at a non-unit alpha. + /// numerical answer key, with both selector settings and a non-unit alpha. TestOutcome dependencyTracking() { TestStatus success = true; @@ -1154,7 +1232,7 @@ namespace GridKit } #ifdef GRIDKIT_ENABLE_ENZYME - /// One rich state, every selector mode, and a non-unit alpha drive both + /// One rich state, both selector settings, and a non-unit alpha drive both /// sensitivity paths; every Enzyme CSR row must match dependency tracking. TestOutcome jacobian() { @@ -2106,7 +2184,7 @@ namespace GridKit {externalColumn(index(Ext::FREQ)), -1.0}, {externalColumn(index(Ext::FREQREF)), 1.0}}, {{index(Vars::PMEAS), -1.0}, - {index(Vars::EF), 2.0}, + {index(Vars::EF), 1.0}, {index(Vars::EP), -1.0}, {externalColumn(index(Ext::PREF)), 2.0}}, {{index(Vars::EPLIM), -1.0}}, From 0386b4dfaf792d9b36e76ef2ca49e5dc1af45cab Mon Sep 17 00:00:00 2001 From: lukelowry Date: Thu, 6 Aug 2026 15:46:50 -0500 Subject: [PATCH 18/19] minor suggestions --- .../PhasorDynamics/ControllerRepcaTests.hpp | 2 + .../SystemSingleComponentTests.hpp | 37 ++++++------------- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp index 8bd4249e2..5f68c7c60 100644 --- a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp @@ -2224,6 +2224,8 @@ namespace GridKit return expected; } + // A missing Jacobian entry means zero. Treat it the same as an explicit zero + // so rows with the same values compare equal. static RealT mapValue(const DependencyTracking::Variable::DependencyMap& row, size_t column) { diff --git a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp index 6834d0158..0feb2a8c7 100644 --- a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp @@ -1,4 +1,3 @@ -#include #include #include @@ -258,14 +257,8 @@ namespace GridKit using Inputs = PhasorDynamics::Controller::RepcaSignalInputs; using Vars = PhasorDynamics::Controller::RepcaInternalVariables; - constexpr IdxT bus_id = static_cast(1); - constexpr std::array required_inputs{{ - Inputs::ir, - Inputs::ii, - Inputs::p, - Inputs::q, - }}; - constexpr size_t input_count = required_inputs.size(); + constexpr IdxT bus_id = static_cast(1); + constexpr IdxT input_id = static_cast(1); TestStatus success = true; @@ -276,28 +269,22 @@ namespace GridKit data.bus[0].Vr0 = static_cast(1.0); data.bus[0].Vi0 = static_cast(0.0); - data.signal.resize(input_count); + data.signal.resize(1); + data.signal[0].signal_id = input_id; typename PhasorDynamics::SystemModelData::RepcaDataT repca_data; - repca_data.buses[Buses::bus] = bus_id; - for (size_t port = 0; port < input_count; ++port) - { - const auto signal_id = static_cast(port + 1); - data.signal[port].signal_id = signal_id; - repca_data.signal_inputs[required_inputs[port]] = signal_id; - } + repca_data.buses[Buses::bus] = bus_id; + repca_data.signal_inputs[Inputs::ir] = input_id; + repca_data.signal_inputs[Inputs::ii] = input_id; + repca_data.signal_inputs[Inputs::p] = input_id; + repca_data.signal_inputs[Inputs::q] = input_id; data.repca.push_back(repca_data); - std::array input_values{}; - std::array input_indices{}; - input_indices.fill(INVALID_INDEX); + ScalarT input_value{}; + IdxT input_index = INVALID_INDEX; PhasorDynamics::SystemModel system(data); - for (size_t port = 0; port < input_count; ++port) - { - system.getSignal(static_cast(port + 1)) - ->set(&input_values[port], &input_indices[port]); - } + system.getSignal(input_id)->set(&input_value, &input_index); success *= system.allocate() == 0; success *= system.initialize() == 0; From e223c346181569ace0420c38db9fb4d75223868f Mon Sep 17 00:00:00 2001 From: lukelowry Date: Thu, 6 Aug 2026 17:31:14 -0500 Subject: [PATCH 19/19] full structural answer key --- .../PhasorDynamics/ControllerRepcaTests.hpp | 72 ++++++++----------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp index 5f68c7c60..8728a23eb 100644 --- a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp @@ -1197,7 +1197,8 @@ namespace GridKit } /// Check every dependency-tracking Jacobian row against an independent - /// numerical answer key, with both selector settings and a non-unit alpha. + /// numerical and structural answer key, with both selector settings and + /// a non-unit alpha. TestOutcome dependencyTracking() { TestStatus success = true; @@ -2161,10 +2162,15 @@ namespace GridKit return { {{index(Vars::VMEAS), -6.0}, {index(Vars::VCTRL), 5.0}}, {{index(Vars::QMEAS), -6.0}, {externalColumn(index(Ext::Q)), 10.0}}, - {{index(Vars::XQPI), -1.0}, {index(Vars::SFRZ), 1.2}, {index(Vars::ERQLIM), 1.5}}, + {{index(Vars::XQPI), -1.0}, + {index(Vars::SFRZ), 1.2}, + {index(Vars::ERQLIM), 1.5}, + {index(Vars::QPI), 0.0}}, {{index(Vars::XQLAG), -1.4}, {index(Vars::QPI), 0.4}}, {{index(Vars::PMEAS), -3.5}, {externalColumn(index(Ext::P)), 5.0}}, - {{index(Vars::XPPI), -1.0}, {index(Vars::EPLIM), 1.8}}, + {{index(Vars::XPPI), -1.0}, + {index(Vars::EPLIM), 1.8}, + {index(Vars::PPI), 0.0}}, {{index(Vars::PREF), -3.0}, {index(Vars::PPI), 2.0}}, {{index(Vars::V), -3.0}, {kBusVrColumn, 1.8}, {kBusViColumn, 0.8}}, {{index(Vars::VLDC), -2.0}, @@ -2173,9 +2179,15 @@ namespace GridKit {externalColumn(index(Ext::IR)), -0.1096}, {externalColumn(index(Ext::II)), 0.0968}}, {{index(Vars::V), 1.0}, {index(Vars::VDROOP), -1.0}, {externalColumn(index(Ext::Q)), 0.8}}, - {{index(Vars::VLDC), 1.0}, {index(Vars::VCTRL), -1.0}}, - {{index(Vars::SFRZ), -1.0}}, - {{index(Vars::VMEAS), -1.0}, {index(Vars::ERQ), -1.0}, {externalColumn(index(Ext::VREF)), 1.0}}, + {{index(Vars::VLDC), 1.0}, + {index(Vars::VDROOP), 0.0}, + {index(Vars::VCTRL), -1.0}}, + {{index(Vars::V), 0.0}, {index(Vars::SFRZ), -1.0}}, + {{index(Vars::VMEAS), -1.0}, + {index(Vars::QMEAS), 0.0}, + {index(Vars::ERQ), -1.0}, + {externalColumn(index(Ext::VREF)), 1.0}, + {externalColumn(index(Ext::QREF)), 0.0}}, {{index(Vars::ERQ), 1.0}, {index(Vars::ERQDB), -1.0}}, {{index(Vars::ERQDB), 1.0}, {index(Vars::ERQLIM), -1.0}}, {{index(Vars::XQPI), 1.0}, {index(Vars::ERQLIM), 2.0}, {index(Vars::QPI), -1.0}}, @@ -2187,7 +2199,7 @@ namespace GridKit {index(Vars::EF), 1.0}, {index(Vars::EP), -1.0}, {externalColumn(index(Ext::PREF)), 2.0}}, - {{index(Vars::EPLIM), -1.0}}, + {{index(Vars::EP), 0.0}, {index(Vars::EPLIM), -1.0}}, {{index(Vars::XPPI), 1.0}, {index(Vars::EPLIM), 1.7}, {index(Vars::PPI), -1.0}}, {{index(Vars::PREF), 1.0}, {index(Vars::PEXT), -2.0}}, }; @@ -2197,15 +2209,19 @@ namespace GridKit { auto expected = expectedJacobian(); expected[index(Vars::VCTRL)] = { + {index(Vars::VLDC), 0.0}, {index(Vars::VDROOP), 1.0}, {index(Vars::VCTRL), -1.0}, }; expected[index(Vars::ERQ)] = { + {index(Vars::VMEAS), 0.0}, {index(Vars::QMEAS), -1.0}, {index(Vars::ERQ), -1.0}, + {externalColumn(index(Ext::VREF)), 0.0}, {externalColumn(index(Ext::QREF)), 2.0}, }; expected[index(Vars::PEXT)] = { + {index(Vars::PREF), 0.0}, {index(Vars::PEXT), -2.0}, }; return expected; @@ -2224,19 +2240,6 @@ namespace GridKit return expected; } - // A missing Jacobian entry means zero. Treat it the same as an explicit zero - // so rows with the same values compare equal. - static RealT mapValue(const DependencyTracking::Variable::DependencyMap& row, - size_t column) - { - const auto entry = row.find(column); - if (entry == row.end()) - { - return 0.0; - } - return entry->second; - } - bool jacobianRowMatches( const DependencyTracking::Variable::DependencyMap& actual, const DependencyTracking::Variable::DependencyMap& expected, @@ -2244,31 +2247,14 @@ namespace GridKit const char* source, RealT tolerance) const { - constexpr size_t column_count = externalColumn(index(Ext::MAXIMUM)); - bool success = true; - for (size_t column = 0; column < column_count; ++column) - { - const RealT actual_value = mapValue(actual, column); - const RealT expected_value = mapValue(expected, column); - if (!isEqual(actual_value, expected_value, tolerance)) - { - std::cout << "REPCA " << source << " Jacobian row " << row - << " column " << column << " mismatch: " - << std::setprecision(std::numeric_limits::max_digits10) << actual_value << " != " - << expected_value << '\n'; - success = false; - } - } - for (const auto& [column, value] : actual) + if (isEqual(actual, expected, tolerance)) { - if (column >= column_count && !isEqual(value, 0.0, tolerance)) - { - std::cout << "REPCA " << source << " Jacobian row " << row - << " has unexpected column " << column << '\n'; - success = false; - } + return true; } - return success; + + std::cout << "REPCA " << source << " Jacobian row " << row + << " mismatch\n"; + return false; } bool jacobianMatches(