diff --git a/CHANGELOG.md b/CHANGELOG.md index 71751c60a..6750dfcf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ - Added cmake-format hooks, including in pre-commit. - Added off-nominal tap ratio and phase shift support to the PhasorDynamics `Branch` model. - Added `REGCA` converter 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 e30067d20..f146a5c5f 100644 --- a/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp +++ b/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp @@ -5,6 +5,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..d690496f6 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/README.md @@ -0,0 +1,368 @@ +# **Renewable Energy Plant Control Model (REPCA)** + +REPCA is a WECC renewable energy plant control model for inverter-coupled +resources. In GridKit it is represented as a plant-level signal-control model +that computes active- and reactive-power commands for downstream electrical +control models. + +## Block Diagram + +Standard REPCA block diagram. + +
+ + + 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 | 0.0 | Block name: `MVABase`; zero uses system base +$s_{\mathrm{comp}}$ | [binary] | `VcompFlag` | Voltage-compensation mode flag | 1.0 | 1 = line-drop compensation, 0 = reactive-droop compensation +$s_{\mathrm{ref}}$ | [binary] | `RefFlag` | Reactive-loop reference flag | 1.0 | 1 = voltage control, 0 = reactive-power control +$s_{\mathrm{freq}}$ | [binary] | `Freqflag` | Active-power control output flag | 0.0 | 1 = use active-power loop output, 0 = output zero +$T_{\mathrm{fltr}}$ | [sec] | `Tfltr` | Voltage and reactive-power measurement filter time constant | 0.05 | If zero, $V_{\mathrm{meas}}$ and $Q_{\mathrm{meas}}$ are algebraic +$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 | If zero, $P_{\mathrm{meas}}$ is algebraic +$T_{\mathrm{lag}}$ | [sec] | `Tlag` | Active-power command lag time constant | 3.0 | If zero, $P_{\mathrm{ref}}$ is algebraic +$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 | REPCA1/REPCTA1 key; negative of `dbd` for symmetric REPC_A data +$D_{\mathrm{bd2}}$ | [p.u.] | `dbdupper` | Upper reactive-loop deadband threshold | 0.0 | REPCA1/REPCTA1 key; `dbd` for symmetric REPC_A data +$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 | +$D_{f,\mathrm{bd1}}$ | [p.u.] | `fdbd1` | Lower frequency-error deadband threshold | 0.0 | +$D_{f,\mathrm{bd2}}$ | [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 | + +### Parameter Validation + +Invalid REPCA parameter sets are rejected by the following checks. + +The required checks are: + +```math +\begin{aligned} + &S^{\mathrm{base}} \ge 0 \\ + &s_{\mathrm{comp}}, s_{\mathrm{ref}}, s_{\mathrm{freq}} \in \{0,1\} \\ + &T_{\mathrm{fltr}}, T_{\mathrm{p}}, T_{\mathrm{lag}} \ge 0 \\ + &T_{\mathrm{ft}} \ge 0,\quad T_{\mathrm{fv}} > 0 \\ + &V_{\mathrm{frz}} \ge 0 \\ + &D_{\mathrm{bd1}} \le 0 \le D_{\mathrm{bd2}} \\ + &e^{\min} \le 0 \le e^{\max} \\ + &Q^{\min} \le Q^{\max} \\ + &D_{f,\mathrm{bd1}} \le 0 \le D_{f,\mathrm{bd2}} \\ + &D_{\mathrm{dn}}, D_{\mathrm{up}} \ge 0 \\ + &e_P^{\min} \le 0 \le e_P^{\max} \\ + &P^{\min} \le P^{\max} +\end{aligned} +``` + +### Model Derived Parameters + +The off-mode flag complements are: + +```math +\begin{aligned} + s_{\mathrm{comp}}^{\mathrm{off}} &= 1 - s_{\mathrm{comp}} \\ + s_{\mathrm{ref}}^{\mathrm{off}} &= 1 - s_{\mathrm{ref}} +\end{aligned} +``` + +The branch-power base conversion factor is: + +```math +\begin{aligned} + k_{\mathrm{base}} &= + \begin{cases} + 1, & S^{\mathrm{base}} = 0 \\ + \dfrac{S^{\mathrm{sys}}}{S^{\mathrm{base}}}, & S^{\mathrm{base}} > 0 + \end{cases} +\end{aligned} +``` + +Here $S^{\mathrm{sys}}$ is the network system power base. Branch power inputs +are on system base and are converted to component base with +$k_{\mathrm{base}}$. Branch current inputs are already on component base. + +## 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`; algebraic when $T_{\mathrm{fltr}} = 0$ +$Q_{\mathrm{meas}}$ | [p.u.] | Filtered reactive-power signal | State 2 in Fig. 1; source label: `Qmeas`; algebraic when $T_{\mathrm{fltr}} = 0$ +$x_Q$ | [p.u.] | Reactive PI controller state | State 3 in Fig. 1; source label: `Reactive PI` +$x_{\mathrm{Qext}}$ | [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`; algebraic when $T_{\mathrm{p}} = 0$ +$x_P$ | [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 when $T_{\mathrm{lag}} = 0$ + +#### Algebraic + +Symbol | Units | Description | Note +--------------------------------|--------|-------------------------------------|------ +$V_{\mathrm{reg}}$ | [p.u.] | Regulated-bus voltage magnitude | From regulated-bus phasor +$V_{\mathrm{ldc}}$ | [p.u.] | Line-drop compensated voltage magnitude | Selected when $s_{\mathrm{comp}}=1$ +$V_{\mathrm{droop}}$ | [p.u.] | Reactive-droop-compensated voltage | Selected when $s_{\mathrm{comp}}=0$ +$V_{\mathrm{ctrl}}$ | [p.u.] | Selected voltage-measurement input | Input to $V_{\mathrm{meas}}$ filter +$s_{\mathrm{frz}}$ | [binary] | Reactive-PI voltage-enable indicator | 1 when $V_{\mathrm{reg}} > V_{\mathrm{frz}}$ +$e_{\mathrm{RQ}}$ | [p.u.] | Selected reactive-loop error | Chosen by $s_{\mathrm{ref}}$ +$e_{\mathrm{RQ}}^{\mathrm{db}}$ | [p.u.] | Deadbanded reactive-loop error | Defined by CommonMath `deadband2` +$e_{\mathrm{RQ}}^{\mathrm{lim}}$ | [p.u.] | Limited reactive-loop error | Feeds reactive PI +$Q_{\mathrm{PI}}$ | [p.u.] | Reactive PI output | Limited by $Q^{\min}$ and $Q^{\max}$ +$Q_{\mathrm{ext}}$ | [p.u.] | Reactive-power command output | Sent to downstream electrical control +$e_f$ | [p.u.] | Frequency error after deadband | From $f_{\mathrm{ref}} - f$ +$e_P$ | [p.u.] | Active-power control error | Plant reference minus $P_{\mathrm{meas}}$ plus frequency droop correction +$e_P^{\mathrm{lim}}$ | [p.u.] | Limited active-power control error | Feeds active-power PI +$P_{\mathrm{PI}}$ | [p.u.] | Active-power PI output | Limited by $P^{\min}$ and $P^{\max}$ +$P_{\mathrm{ext}}$ | [p.u.] | Active-power command output | Sent to downstream electrical control when $s_{\mathrm{freq}}=1$ + +### External Variables + +#### Differential +None. + +#### Algebraic + +Symbol | Units | Description | Note +-------------------------------------|--------|-----------------------------------|------ +$V_{\mathrm{reg,r}}$ | [p.u.] | Regulated-bus voltage, real component | Source label: `Vreg` +$V_{\mathrm{reg,i}}$ | [p.u.] | Regulated-bus voltage, imaginary component | Source label: `Vreg` +$I_{\mathrm{br,r}}$ | [p.u.] | Branch current real component | Component base; source label: `Ibranch`; required +$I_{\mathrm{br,i}}$ | [p.u.] | Branch current imaginary component | Component base; source label: `Ibranch`; required +$P_{\mathrm{br}}$ | [p.u.] | Branch active power | System base; required +$Q_{\mathrm{br}}$ | [p.u.] | Branch reactive power | System base; required +$V_{\mathrm{ref}}$ | [p.u.] | Voltage-control reference | Optional, defaults to initialized constant +$Q_{\mathrm{ref}}$ | [p.u.] | Reactive-power reference | Component base; optional, defaults to initialized constant +$P_{\mathrm{plant}}^{\mathrm{ref}}$ | [p.u.] | Plant active-power reference | Component base; optional, defaults to initialized constant +$f$ | [p.u.] | Frequency input | Source label: `Freq`; optional, defaults to zero +$f_{\mathrm{ref}}$ | [p.u.] | Frequency reference | Source label: `Freq_ref`; optional, defaults to zero + +## Model Equations + +### Differential Equations + +The measurement filters and active-power output lag are written in descriptor form; when $T_{\mathrm{fltr}} = 0$, $T_{\mathrm{p}} = 0$, or $T_{\mathrm{lag}} = 0$, the corresponding residual becomes algebraic. The reactive-command lead-lag denominator requires $T_{\mathrm{fv}} > 0$. + +```math +\begin{aligned} + 0 &= -T_{\mathrm{fltr}}\dot V_{\mathrm{meas}} - V_{\mathrm{meas}} + V_{\mathrm{ctrl}} \\ + 0 &= -T_{\mathrm{fltr}}\dot Q_{\mathrm{meas}} - Q_{\mathrm{meas}} + k_{\mathrm{base}}Q_{\mathrm{br}} \\ + 0 &= + -\dot x_Q + + s_{\mathrm{frz}} + \text{antiwindup}\!\left( + Q_{\mathrm{PI}}, + K_{\mathrm{i}}e_{\mathrm{RQ}}^{\mathrm{lim}}, + Q^{\min}, + Q^{\max} + \right) \\ + 0 &= -T_{\mathrm{fv}}\dot x_{\mathrm{Qext}} - x_{\mathrm{Qext}} + Q_{\mathrm{PI}} \\ + 0 &= -T_{\mathrm{p}}\dot P_{\mathrm{meas}} - P_{\mathrm{meas}} + k_{\mathrm{base}}P_{\mathrm{br}} \\ + 0 &= + -\dot x_P + + \text{antiwindup}\!\left( + P_{\mathrm{PI}}, + K_{\mathrm{ig}}e_P^{\mathrm{lim}}, + P^{\min}, + P^{\max} + \right) \\ + 0 &= -T_{\mathrm{lag}}\dot P_{\mathrm{ref}} - P_{\mathrm{ref}} + P_{\mathrm{PI}} +\end{aligned} +``` + +CommonMath defines the [Anti-Windup](../../../../CommonMath.md#anti-windup-indicator) target and smooth approximation. + +### Algebraic Equations + +The algebraic targets use CommonMath helper notation where applicable: + +```math +\begin{aligned} + 0 &= -V_{\mathrm{reg}}^2 + V_{\mathrm{reg,r}}^2 + V_{\mathrm{reg,i}}^2 \\ + 0 &= + -V_{\mathrm{ldc}}^2 + + (V_{\mathrm{reg,r}} - R_c I_{\mathrm{br,r}} + X_c I_{\mathrm{br,i}})^2 + + (V_{\mathrm{reg,i}} - R_c I_{\mathrm{br,i}} - X_c I_{\mathrm{br,r}})^2 \\ + 0 &= -V_{\mathrm{droop}} + V_{\mathrm{reg}} + 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_{\mathrm{reg}}, 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(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, Q^{\min}, Q^{\max}) \\ + 0 &= -T_{\mathrm{fv}}(Q_{\mathrm{ext}} - x_{\mathrm{Qext}}) + + T_{\mathrm{ft}}(Q_{\mathrm{PI}} - x_{\mathrm{Qext}}) \\[0.5ex] + 0 &= -e_f + \text{deadband2}(f_{\mathrm{ref}} - f, D_{f,\mathrm{bd1}}, D_{f,\mathrm{bd2}}) \\ + 0 &= -e_P + + 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, P^{\min}, P^{\max}) \\ + 0 &= -P_{\mathrm{ext}} + s_{\mathrm{freq}}P_{\mathrm{ref}} +\end{aligned} +``` + +The $V_{\mathrm{reg}}$ and $V_{\mathrm{ldc}}$ variables use nonnegative branches of squared algebraic residuals. The exact frequency-droop target is $D_{\mathrm{dn}}e_f$ for positive deadbanded frequency error, $D_{\mathrm{up}}e_f$ for negative deadbanded frequency error, and zero in the deadband. The $\rho$ form above is the smooth CommonMath representation of that split. + +CommonMath defines the helper targets and smooth approximations for [above, clamp, and deadband2](../../../../CommonMath.md#derived-functions). The frequency split uses the primitive [ramp](../../../../CommonMath.md#primitives) $\rho$. + +## Initialization + +Initialization is performed by evaluating the steady-state residuals in dependency order. Let subscript $0$ denote initial values and set all internal derivatives to zero. REPCA reads branch power and branch current from connected signal ports: + +```math +\begin{aligned} + P_{\mathrm{br},0} &= pbranch_0 \\ + Q_{\mathrm{br},0} &= qbranch_0 \\ + I_{\mathrm{br,r},0} &= ibranchr_0 \\ + I_{\mathrm{br,i},0} &= ibranchi_0 +\end{aligned} +``` + +Then convert branch power to component base and evaluate the voltage-compensation signals: + +```math +\begin{aligned} + V_{\mathrm{reg},0} &= \sqrt{V_{\mathrm{reg,r},0}^2 + V_{\mathrm{reg,i},0}^2} \\ + V_{\mathrm{ldc},0} + &= \sqrt{ + (V_{\mathrm{reg,r},0} - R_c I_{\mathrm{br,r},0} + X_c I_{\mathrm{br,i},0})^2 + + (V_{\mathrm{reg,i},0} - R_c I_{\mathrm{br,i},0} - X_c I_{\mathrm{br,r},0})^2 + } \\ + V_{\mathrm{droop},0} &= V_{\mathrm{reg},0} + K_c k_{\mathrm{base}}Q_{\mathrm{br},0} \\ + V_{\mathrm{ctrl},0} &= s_{\mathrm{comp}}V_{\mathrm{ldc},0} + s_{\mathrm{comp}}^{\mathrm{off}}V_{\mathrm{droop},0} +\end{aligned} +``` + +If optional reference inputs are not connected, use steady-state constants that +make the selected control errors zero. Omitted frequency inputs default to zero: + +```math +\begin{aligned} + f_0 &= 0 \\ + f_{\mathrm{ref},0} &= 0 \\ + e_{f,0} &= \text{deadband2}(f_{\mathrm{ref},0} - f_0, D_{f,\mathrm{bd1}}, D_{f,\mathrm{bd2}}) \\ + P_{\mathrm{freq},0} &= D_{\mathrm{dn}}\rho(e_{f,0}) - D_{\mathrm{up}}\rho(-e_{f,0}) \\ + V_{\mathrm{ref},0} &= V_{\mathrm{ctrl},0} \\ + Q_{\mathrm{ref},0} &= k_{\mathrm{base}}Q_{\mathrm{br},0} \\ + P_{\mathrm{plant},0}^{\mathrm{ref}} &= k_{\mathrm{base}}P_{\mathrm{br},0} - P_{\mathrm{freq},0} +\end{aligned} +``` + +Connected optional references use their supplied initial values and must satisfy the same steady-state residuals. + +Initialize the measurement variables from the descriptor-form filter residuals: + +```math +\begin{aligned} + V_{\mathrm{meas},0} &= V_{\mathrm{ctrl},0} \\ + Q_{\mathrm{meas},0} &= k_{\mathrm{base}}Q_{\mathrm{br},0} \\ + P_{\mathrm{meas},0} &= k_{\mathrm{base}}P_{\mathrm{br},0} +\end{aligned} +``` + +Then evaluate the reactive-control algebraic chain: + +```math +\begin{aligned} + s_{\mathrm{frz},0} &= \text{above}(V_{\mathrm{reg},0}, V_{\mathrm{frz}}) \\ + e_{\mathrm{RQ},0} + &= s_{\mathrm{ref}}\left(V_{\mathrm{ref},0} - V_{\mathrm{meas},0}\right) + + s_{\mathrm{ref}}^{\mathrm{off}}\left(Q_{\mathrm{ref},0} - Q_{\mathrm{meas},0}\right) \\ + e_{\mathrm{RQ},0}^{\mathrm{db}} + &= \text{deadband2}(e_{\mathrm{RQ},0}, D_{\mathrm{bd1}}, D_{\mathrm{bd2}}) \\ + e_{\mathrm{RQ},0}^{\mathrm{lim}} + &= \text{clamp}(e_{\mathrm{RQ},0}^{\mathrm{db}}, e^{\min}, e^{\max}) +\end{aligned} +``` + +Choose the initial reactive-command output and PI states as: + +```math +\begin{aligned} + Q_{\mathrm{ext},0} &= \text{clamp}(k_{\mathrm{base}}Q_{\mathrm{br},0}, Q^{\min}, Q^{\max}) \\ + Q_{\mathrm{PI},0} &= Q_{\mathrm{ext},0} \\ + x_{\mathrm{Qext},0} &= Q_{\mathrm{PI},0} \\ + x_{Q,0} &= Q_{\mathrm{ext},0} - K_{\mathrm{p}}e_{\mathrm{RQ},0}^{\mathrm{lim}} +\end{aligned} +``` + +This satisfies the lead-lag algebraic output and produces a zero lead-lag +state derivative. The reactive PI state derivative must also be zero after +antiwindup is applied. + +If the `qext` output signal already carries a seeded value, REPCA uses that +value for $Q_{\mathrm{ext},0}$ and back-solves the same PI states from it. + +Evaluate the active-power control chain: + +```math +\begin{aligned} + e_{P,0} + &= P_{\mathrm{plant},0}^{\mathrm{ref}} + - P_{\mathrm{meas},0} + + P_{\mathrm{freq},0} \\ + e_{P,0}^{\mathrm{lim}} &= \text{clamp}(e_{P,0}, e_P^{\min}, e_P^{\max}) +\end{aligned} +``` + +Choose: + +```math +\begin{aligned} + P_{\mathrm{ref},0} &= \text{clamp}(k_{\mathrm{base}}P_{\mathrm{br},0}, P^{\min}, P^{\max}) \\ + P_{\mathrm{PI},0} &= P_{\mathrm{ref},0} \\ + x_{P,0} &= P_{\mathrm{ref},0} - K_{\mathrm{pg}}e_{P,0}^{\mathrm{lim}} \\ + P_{\mathrm{ext},0} &= s_{\mathrm{freq}}P_{\mathrm{ref},0} +\end{aligned} +``` + +If the `pext` output signal already carries a seeded value and +$s_{\mathrm{freq}} \ne 0$, REPCA uses +$P_{\mathrm{ref},0}=P_{\mathrm{ext},0}/s_{\mathrm{freq}}$. + +The initialized derivative vector is zero. Initialization rejects the case if +the reactive-power or active-power PI antiwindup rate is nonzero. + +## Model Outputs + +Output | Units | Description | Note +----------------|--------|-------------------------------------|------ +`qext` | [p.u.] | Reactive-power command output | Component base; sent to downstream electrical control +`pext` | [p.u.] | Active-power command output | Component base; sent to downstream electrical control when `Freqflag = 1` +`vmeas` | [p.u.] | Filtered regulated voltage | +`qmeas` | [p.u.] | Filtered reactive-power signal | +`pmeas` | [p.u.] | Filtered active-power signal | +`pref` | [p.u.] | Active-power command lag state | +`vctrl` | [p.u.] | Selected voltage-measurement input | +`sfrz` | [binary] | Reactive-PI voltage-enable indicator | +`qpi` | [p.u.] | Reactive PI output | +`pfreq` | [p.u.] | Computed frequency droop active-power correction | +`ppi` | [p.u.] | Active-power PI output | diff --git a/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp new file mode 100644 index 000000000..995cfd075 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.cpp @@ -0,0 +1,27 @@ +/** + * @file Repca.cpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Non-Enzyme instantiation 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..99927507c --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/Repca.hpp @@ -0,0 +1,190 @@ +/** + * @file Repca.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Declaration of the REPCA 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 + XQ, ///< Reactive PI state + XQEXT, ///< Reactive-command lead-lag state + PMEAS, ///< Filtered active-power signal + XP, ///< Active-power PI state + PREF, ///< Active-power command lag state + VREG, ///< 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 on component base + IBRANCHI, ///< Branch current imaginary component on component base + QBRANCH, ///< Branch reactive-power signal on system base + PBRANCH, ///< Branch active-power signal on system base + VREF, ///< Voltage reference + QREF, ///< Reactive-power reference + PPLANTREF, ///< Plant active-power reference + FREQ, ///< Optional frequency input + FREQREF, ///< Optional frequency reference + MAXIMUM, + }; + + template + class Repca : public Component + { + using Component::alpha_; + using Component::f_; + using Component::gridkit_component_id_; + using Component::J_; + using Component::J_cols_buffer_; + using Component::J_rows_buffer_; + using Component::J_vals_buffer_; + 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 RealT = typename Component::RealT; + using bus_type = BusBase; + using signal_type = SignalNode; + using model_data_type = RepcaData; + using MonitorT = Model::VariableMonitor; + + Repca(bus_type* bus); + Repca(bus_type* bus, const model_data_type& 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 evaluateResidual() override final; + int evaluateJacobian() override final; + + auto getSignals() + -> ComponentSignals& + { + return signals_; + } + + const Model::VariableMonitorBase* getMonitor() const override; + + __attribute__((always_inline)) inline int evaluateInternalResidual( + ScalarT*, ScalarT*, ScalarT*, ScalarT*, ScalarT*); + + private: + void initModelParams(const model_data_type& data); + void initializeMonitor(); + + int verifyBranchSignalPorts() const; + ScalarT readExternalOrDefault(RepcaExternalVariables variable, ScalarT default_value) const; + void setDerivedParameters(); + ScalarT toComponentBase(ScalarT value) const; + + ScalarT& Vr(); + ScalarT& Vi(); + + bus_type* bus_{nullptr}; + + RealT mva_base_{0}; + RealT VcompFlag_{0}; + RealT RefFlag_{0}; + RealT Freqflag_{0}; + RealT Tfltr_{0}; + RealT Tft_{0}; + RealT Tfv_{0}; + RealT Tp_{0}; + RealT Tlag_{0}; + RealT Vfrz_{0}; + RealT Rc_{0}; + RealT Xc_{0}; + RealT Kc_{0}; + RealT dbdlow_{0}; + RealT dbdupper_{0}; + RealT emax_{0}; + RealT emin_{0}; + RealT Kp_{0}; + RealT Ki_{0}; + RealT Qmax_{0}; + RealT Qmin_{0}; + RealT fdbd1_{0}; + RealT fdbd2_{0}; + RealT Ddn_{0}; + RealT Dup_{0}; + RealT femax_{0}; + RealT femin_{0}; + RealT Kpg_{0}; + RealT Kig_{0}; + RealT Pmax_{0}; + RealT Pmin_{0}; + + IdxT parameter_error_count_{0}; + RealT system_to_component_base_{1}; + RealT vcomp_off_{1}; + RealT ref_off_{1}; + + ScalarT vref_set_{0}; + ScalarT qref_set_{0}; + ScalarT pplantref_set_{0}; + ScalarT freq_set_{0}; + ScalarT freqref_set_{0}; + + 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..b16dcb363 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaData.hpp @@ -0,0 +1,101 @@ +/** + * @file RepcaData.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Modeling data for the REPCA plant-control model. + */ + +#pragma once + +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Converter + { + /// Parameter keys for the REPCA plant-control model. + enum class RepcaParameters + { + mva, ///< Component MVA base; zero uses system base + VcompFlag, ///< Voltage-compensation mode flag + RefFlag, ///< Reactive-loop reference flag + Freqflag, ///< Active-power output flag + Tfltr, ///< Voltage and reactive-power filter time constant + Tft, ///< Reactive-command lead time constant + Tfv, ///< Reactive-command lag time constant + Tp, ///< Active-power measurement filter time constant + Tlag, ///< Active-power command lag 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 controller proportional gain + Ki, ///< Reactive controller integral gain + Qmax, ///< Maximum reactive-power command + Qmin, ///< Minimum reactive-power command + 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 proportional gain + Kig, ///< Active-power integral gain + Pmax, ///< Maximum active-power command + Pmin ///< Minimum active-power command + }; + + /// Ports for the REPCA plant-control model. + enum class RepcaPorts + { + bus, ///< Regulated bus ID + ibranchr, ///< Branch current real signal ID + ibranchi, ///< Branch current imaginary signal ID + qbranch, ///< Branch reactive-power signal ID + pbranch, ///< Branch active-power signal ID + vref, ///< Optional voltage reference signal ID + qref, ///< Optional reactive-power reference signal ID + pplantref, ///< Optional plant active-power reference signal ID + freq, ///< Optional frequency input signal ID + freqref, ///< Optional frequency reference signal ID + qext, ///< Reactive-power command output signal ID + pext ///< Active-power command output signal ID + }; + + /// 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 + pref, ///< Active-power command lag state + vctrl, ///< Selected voltage-measurement input + sfrz, ///< Reactive PI voltage-enable indicator + qpi, ///< Reactive PI output + pfreq, ///< Frequency droop active-power correction + ppi ///< Active-power PI output + }; + + template + struct RepcaData : public ComponentData + { + RepcaData() = default; + + using Parameters = RepcaParameters; + using Ports = RepcaPorts; + 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..d76462f30 --- /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..14ec178a0 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaEnzyme.cpp @@ -0,0 +1,92 @@ +/** + * @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; + + J_.zeroMatrix(); + if (J_rows_buffer_ == nullptr) + { + J_rows_buffer_ = new IdxT[static_cast(size_) * static_cast(size_)]; + J_cols_buffer_ = new IdxT[static_cast(size_) * static_cast(size_)]; + J_vals_buffer_ = new RealT[static_cast(size_) * static_cast(size_)]; + } + + using ModelT = GridKit::PhasorDynamics::Converter::Repca; + using Fn = GridKit::Enzyme::Sparse::MemberFunctions; + + GridKit::Enzyme::Sparse::DfDy::eval(this, + f_.size(), + y_.size(), + (this->getResidualIndices()).data(), + (this->getVariableIndices()).data(), + y_.data(), + yp_.data(), + wb_.data(), + ws_.data(), + alpha_, + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + J_); + + GridKit::Enzyme::Sparse::DfDwb::eval(this, + f_.size(), + static_cast(bus_->size()), + (this->getResidualIndices()).data(), + (bus_->getVariableIndices()).data(), + y_.data(), + yp_.data(), + wb_.data(), + ws_.data(), + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + J_); + + GridKit::Enzyme::Sparse::DfDws::eval(this, + f_.size(), + ws_.size(), + (this->getResidualIndices()).data(), + ws_indices_.data(), + y_.data(), + yp_.data(), + wb_.data(), + ws_.data(), + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + J_); + 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..6f7b71c04 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REPCA/RepcaImpl.hpp @@ -0,0 +1,767 @@ +/** + * @file RepcaImpl.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Definition of the REPCA 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(bus_type* bus) + : bus_(bus) + { + size_ = static_cast(RepcaInternalVariables::MAXIMUM); + } + + template + Repca::Repca(bus_type* bus, const model_data_type& data) + : bus_(bus), + monitor_(std::make_unique(data)) + { + initModelParams(data); + initializeMonitor(); + size_ = static_cast(RepcaInternalVariables::MAXIMUM); + } + + template + Repca::~Repca() + { + } + + template + ScalarT& Repca::Vr() + { + return bus_->Vr(); + } + + template + ScalarT& Repca::Vi() + { + return bus_->Vi(); + } + + template + void Repca::setDerivedParameters() + { + system_to_component_base_ = ONE; + if (mva_base_ > ZERO) + { + const RealT va_component_base = mva_base_ * static_cast(1.0e6); + system_to_component_base_ = va_system_base_ / va_component_base; + } + vcomp_off_ = ONE - VcompFlag_; + ref_off_ = ONE - RefFlag_; + } + + template + ScalarT Repca::toComponentBase(ScalarT value) const + { + return value * system_to_component_base_; + } + + template + int Repca::verifyBranchSignalPorts() const + { + int ret = 0; + + const bool has_pbranch = signals_.template isAttached(); + const bool has_qbranch = signals_.template isAttached(); + const bool has_ibranchr = signals_.template isAttached(); + const bool has_ibranchi = signals_.template isAttached(); + + if (!has_pbranch || !has_qbranch) + { + Log::error() << "Repca: pbranch and qbranch must be connected\n"; + ret += 1; + } + + if (!has_ibranchr || !has_ibranchi) + { + Log::error() << "Repca: ibranchr and ibranchi must be connected\n"; + ret += 1; + } + + return ret; + } + + template + void Repca::initModelParams(const model_data_type& data) + { + using Params = typename model_data_type::Parameters; + + parameter_error_count_ = 0; + + auto loadRequiredReal = [&](auto key, RealT& target, const char* name) + { + if (!data.parameters.contains(key)) + { + Log::error() << "Repca: missing required parameter '" << name << "'\n"; + ++parameter_error_count_; + 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 loadRequiredSwitch = [&](auto key, RealT& target, const char* name) + { + if (!data.parameters.contains(key)) + { + Log::error() << "Repca: missing required parameter '" << name << "'\n"; + ++parameter_error_count_; + return; + } + + const auto& value = data.parameters.at(key); + if (const auto* bool_value = std::get_if(&value)) + { + target = ZERO; + if (*bool_value) + { + target = ONE; + } + } + else if (const auto* index_value = std::get_if(&value); + index_value && (*index_value == 0 || *index_value == 1)) + { + target = static_cast(*index_value); + } + else if (const auto* real_value = std::get_if(&value); + real_value && (*real_value == ZERO || *real_value == ONE) ) + { + target = *real_value; + } + else + { + Log::error() << "Repca: parameter '" << name << "' must be bool or 0/1\n"; + ++parameter_error_count_; + } + }; + + loadRequiredReal(Params::mva, mva_base_, "mva"); + loadRequiredSwitch(Params::VcompFlag, VcompFlag_, "VcompFlag"); + loadRequiredSwitch(Params::RefFlag, RefFlag_, "RefFlag"); + loadRequiredSwitch(Params::Freqflag, Freqflag_, "Freqflag"); + loadRequiredReal(Params::Tfltr, Tfltr_, "Tfltr"); + loadRequiredReal(Params::Tft, Tft_, "Tft"); + loadRequiredReal(Params::Tfv, Tfv_, "Tfv"); + loadRequiredReal(Params::Tp, Tp_, "Tp"); + loadRequiredReal(Params::Tlag, Tlag_, "Tlag"); + loadRequiredReal(Params::Vfrz, Vfrz_, "Vfrz"); + loadRequiredReal(Params::Rc, Rc_, "Rc"); + loadRequiredReal(Params::Xc, Xc_, "Xc"); + loadRequiredReal(Params::Kc, Kc_, "Kc"); + loadRequiredReal(Params::dbdlow, dbdlow_, "dbdlow"); + loadRequiredReal(Params::dbdupper, dbdupper_, "dbdupper"); + loadRequiredReal(Params::emax, emax_, "emax"); + loadRequiredReal(Params::emin, emin_, "emin"); + loadRequiredReal(Params::Kp, Kp_, "Kp"); + loadRequiredReal(Params::Ki, Ki_, "Ki"); + loadRequiredReal(Params::Qmax, Qmax_, "Qmax"); + loadRequiredReal(Params::Qmin, Qmin_, "Qmin"); + loadRequiredReal(Params::fdbd1, fdbd1_, "fdbd1"); + loadRequiredReal(Params::fdbd2, fdbd2_, "fdbd2"); + loadRequiredReal(Params::Ddn, Ddn_, "Ddn"); + loadRequiredReal(Params::Dup, Dup_, "Dup"); + loadRequiredReal(Params::femax, femax_, "femax"); + loadRequiredReal(Params::femin, femin_, "femin"); + loadRequiredReal(Params::Kpg, Kpg_, "Kpg"); + loadRequiredReal(Params::Kig, Kig_, "Kig"); + loadRequiredReal(Params::Pmax, Pmax_, "Pmax"); + loadRequiredReal(Params::Pmin, Pmin_, "Pmin"); + + setDerivedParameters(); + } + + template + const Model::VariableMonitorBase* Repca::getMonitor() const + { + return monitor_.get(); + } + + template + void Repca::initializeMonitor() + { + using Variable = typename model_data_type::MonitorableVariables; + auto index = [](RepcaInternalVariables variable) + { + return static_cast(variable); + }; + + monitor_->set(Variable::qext, [this, index] + { return y_[index(RepcaInternalVariables::QEXT)]; }); + monitor_->set(Variable::pext, [this, index] + { return y_[index(RepcaInternalVariables::PEXT)]; }); + monitor_->set(Variable::vmeas, [this, index] + { return y_[index(RepcaInternalVariables::VMEAS)]; }); + monitor_->set(Variable::qmeas, [this, index] + { return y_[index(RepcaInternalVariables::QMEAS)]; }); + monitor_->set(Variable::pmeas, [this, index] + { return y_[index(RepcaInternalVariables::PMEAS)]; }); + monitor_->set(Variable::pref, [this, index] + { return y_[index(RepcaInternalVariables::PREF)]; }); + monitor_->set(Variable::vctrl, [this, index] + { return y_[index(RepcaInternalVariables::VCTRL)]; }); + monitor_->set(Variable::sfrz, [this, index] + { return y_[index(RepcaInternalVariables::SFRZ)]; }); + monitor_->set(Variable::qpi, [this, index] + { return y_[index(RepcaInternalVariables::QPI)]; }); + monitor_->set(Variable::pfreq, [this, index] + { + const auto EF = index(RepcaInternalVariables::EF); + return Ddn_ * Math::ramp(y_[EF]) - Dup_ * Math::ramp(-y_[EF]); }); + monitor_->set(Variable::ppi, [this, index] + { return y_[index(RepcaInternalVariables::PPI)]; }); + } + + template + int Repca::setGridKitComponentID(IdxT component_id) + { + gridkit_component_id_ = component_id; + return 0; + } + + template + int Repca::allocate() + { + setDerivedParameters(); + + size_ = static_cast(RepcaInternalVariables::MAXIMUM); + auto size = static_cast(size_); + + f_.assign(size, ScalarT{0}); + y_.assign(size, ScalarT{0}); + yp_.assign(size, ScalarT{0}); + 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()) + { + signals_.template getSignalNode()->set( + &y_[static_cast(RepcaInternalVariables::QEXT)], + &(this->getVariableIndex(static_cast(RepcaInternalVariables::QEXT)))); + } + + if (signals_.template isAssigned()) + { + signals_.template getSignalNode()->set( + &y_[static_cast(RepcaInternalVariables::PEXT)], + &(this->getVariableIndex(static_cast(RepcaInternalVariables::PEXT)))); + } + + 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_base_ >= ZERO, "mva must be non-negative"); + check(VcompFlag_ == ZERO || VcompFlag_ == ONE, "VcompFlag must be 0 or 1"); + check(RefFlag_ == ZERO || RefFlag_ == ONE, "RefFlag must be 0 or 1"); + check(Freqflag_ == ZERO || Freqflag_ == ONE, "Freqflag must be 0 or 1"); + check(Tfltr_ >= ZERO, "Tfltr must be non-negative"); + check(Tp_ >= ZERO, "Tp must be non-negative"); + check(Tlag_ >= ZERO, "Tlag must be non-negative"); + check(Tft_ >= ZERO, "Tft must be non-negative"); + check(Tfv_ > ZERO, "Tfv must be positive"); + check(Vfrz_ >= ZERO, "Vfrz must be non-negative"); + 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(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"); + ret += verifyBranchSignalPorts(); + + if (signals_.template isAttached() + && !signals_.template isLinked()) + { + Log::error() << "Repca: ibranchr signal attached with no linked source\n"; + ret += 1; + } + if (signals_.template isAttached() + && !signals_.template isLinked()) + { + Log::error() << "Repca: ibranchi signal attached with no linked source\n"; + ret += 1; + } + if (signals_.template isAttached() + && !signals_.template isLinked()) + { + Log::error() << "Repca: qbranch signal attached with no linked source\n"; + ret += 1; + } + if (signals_.template isAttached() + && !signals_.template isLinked()) + { + Log::error() << "Repca: pbranch signal attached with no linked source\n"; + ret += 1; + } + if (signals_.template isAttached() + && !signals_.template isLinked()) + { + Log::error() << "Repca: vref signal attached with no linked source\n"; + ret += 1; + } + if (signals_.template isAttached() + && !signals_.template isLinked()) + { + Log::error() << "Repca: qref signal attached with no linked source\n"; + ret += 1; + } + if (signals_.template isAttached() + && !signals_.template isLinked()) + { + Log::error() << "Repca: pplantref signal attached with no linked source\n"; + ret += 1; + } + if (signals_.template isAttached() + && !signals_.template isLinked()) + { + Log::error() << "Repca: freq signal attached with no linked source\n"; + ret += 1; + } + if (signals_.template isAttached() + && !signals_.template isLinked()) + { + Log::error() << "Repca: freqref signal attached with no linked source\n"; + ret += 1; + } + + return ret; + } + + template + ScalarT Repca::readExternalOrDefault(RepcaExternalVariables variable, + ScalarT default_value) const + { + switch (variable) + { + case RepcaExternalVariables::IBRANCHR: + if (signals_.template isAttached()) + return signals_.template readExternalVariable(); + break; + case RepcaExternalVariables::IBRANCHI: + if (signals_.template isAttached()) + return signals_.template readExternalVariable(); + break; + case RepcaExternalVariables::QBRANCH: + if (signals_.template isAttached()) + return signals_.template readExternalVariable(); + break; + case RepcaExternalVariables::PBRANCH: + if (signals_.template isAttached()) + return signals_.template readExternalVariable(); + break; + case RepcaExternalVariables::VREF: + if (signals_.template isAttached()) + return signals_.template readExternalVariable(); + break; + case RepcaExternalVariables::QREF: + if (signals_.template isAttached()) + return signals_.template readExternalVariable(); + break; + case RepcaExternalVariables::PPLANTREF: + if (signals_.template isAttached()) + return signals_.template readExternalVariable(); + break; + case RepcaExternalVariables::FREQ: + if (signals_.template isAttached()) + return signals_.template readExternalVariable(); + break; + case RepcaExternalVariables::FREQREF: + if (signals_.template isAttached()) + return signals_.template readExternalVariable(); + break; + case RepcaExternalVariables::MAXIMUM: + break; + } + return default_value; + } + + template + int Repca::initialize() + { + if (parameter_error_count_ > 0 || verify() > 0) + { + Log::error() << "Repca: cannot initialize with invalid configuration\n"; + return 1; + } + + setDerivedParameters(); + + const auto VMEAS = static_cast(RepcaInternalVariables::VMEAS); + const auto QMEAS = static_cast(RepcaInternalVariables::QMEAS); + const auto XQ = static_cast(RepcaInternalVariables::XQ); + const auto XQEXT = static_cast(RepcaInternalVariables::XQEXT); + const auto PMEAS = static_cast(RepcaInternalVariables::PMEAS); + const auto XP = static_cast(RepcaInternalVariables::XP); + const auto PREF = static_cast(RepcaInternalVariables::PREF); + const auto VREG = static_cast(RepcaInternalVariables::VREG); + 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 vt_sq = vr * vr + vi * vi; + + if (static_cast(vt_sq) <= ZERO) + { + Log::error() << "Repca: regulated-bus voltage magnitude must be positive at initialization\n"; + return 1; + } + + const ScalarT pbr0_system = + signals_.template readExternalVariable(); + const ScalarT qbr0_system = + signals_.template readExternalVariable(); + const ScalarT pbr0 = toComponentBase(pbr0_system); + const ScalarT qbr0 = toComponentBase(qbr0_system); + const ScalarT ibrr0 = + signals_.template readExternalVariable(); + const ScalarT ibri0 = + signals_.template readExternalVariable(); + + y_[VREG] = std::sqrt(vr * vr + vi * vi); + y_[VLDC] = std::sqrt((vr - Rc_ * ibrr0 + Xc_ * ibri0) + * (vr - Rc_ * ibrr0 + Xc_ * ibri0) + + (vi - Rc_ * ibri0 - Xc_ * ibrr0) + * (vi - Rc_ * ibri0 - Xc_ * ibrr0)); + y_[VDROOP] = y_[VREG] + Kc_ * qbr0; + y_[VCTRL] = VcompFlag_ * y_[VLDC] + vcomp_off_ * y_[VDROOP]; + + y_[VMEAS] = y_[VCTRL]; + y_[QMEAS] = qbr0; + y_[PMEAS] = pbr0; + + freq_set_ = readExternalOrDefault(RepcaExternalVariables::FREQ, ScalarT{ZERO}); + freqref_set_ = readExternalOrDefault(RepcaExternalVariables::FREQREF, ScalarT{ZERO}); + + y_[EF] = Math::deadband2(freqref_set_ - freq_set_, fdbd1_, fdbd2_); + const ScalarT pfreq0 = Ddn_ * Math::ramp(y_[EF]) - Dup_ * Math::ramp(-y_[EF]); + + vref_set_ = readExternalOrDefault(RepcaExternalVariables::VREF, y_[VMEAS]); + qref_set_ = readExternalOrDefault(RepcaExternalVariables::QREF, y_[QMEAS]); + pplantref_set_ = readExternalOrDefault(RepcaExternalVariables::PPLANTREF, y_[PMEAS] - pfreq0); + + y_[SFRZ] = Math::above(y_[VREG], Vfrz_); + y_[ERQ] = RefFlag_ * (vref_set_ - y_[VMEAS]) + ref_off_ * (qref_set_ - y_[QMEAS]); + y_[ERQDB] = Math::deadband2(y_[ERQ], dbdlow_, dbdupper_); + y_[ERQLIM] = Math::clamp(y_[ERQDB], emin_, emax_); + + ScalarT qext0 = Math::clamp(qbr0, Qmin_, Qmax_); + if (signals_.template isAssigned()) + { + qext0 = y_[QEXT]; + } + + y_[QEXT] = qext0; + y_[QPI] = y_[QEXT]; + y_[XQEXT] = y_[QEXT]; + y_[XQ] = y_[QEXT] - Kp_ * y_[ERQLIM]; + + y_[EP] = pplantref_set_ - y_[PMEAS] + pfreq0; + y_[EPLIM] = Math::clamp(y_[EP], femin_, femax_); + + ScalarT pref0 = Math::clamp(pbr0, Pmin_, Pmax_); + if (signals_.template isAssigned() && Freqflag_ != ZERO) + { + pref0 = y_[PEXT] / Freqflag_; + } + + y_[PREF] = pref0; + y_[PPI] = y_[PREF]; + y_[XP] = y_[PREF] - Kpg_ * y_[EPLIM]; + y_[PEXT] = Freqflag_ * y_[PREF]; + + static constexpr RealT init_tol = static_cast(1.0e-10); + + std::fill(yp_.begin(), yp_.end(), ZERO); + + const ScalarT q_rate = + y_[SFRZ] * Math::antiwindup(y_[QPI], Ki_ * y_[ERQLIM], Qmin_, Qmax_); + + const ScalarT p_rate = + Math::antiwindup(y_[PPI], Kig_ * y_[EPLIM], Pmin_, Pmax_); + + if (std::abs(static_cast(q_rate)) > init_tol) + { + Log::error() << "Repca: reactive controller initial condition is not steady state\n"; + return 1; + } + + if (std::abs(static_cast(p_rate)) > init_tol) + { + Log::error() << "Repca: active-power controller initial condition is not steady state\n"; + return 1; + } + + return 0; + } + + template + int Repca::tagDifferentiable() + { + std::fill(tag_.begin(), tag_.end(), false); + tag_[static_cast(RepcaInternalVariables::VMEAS)] = (Tfltr_ > ZERO); + tag_[static_cast(RepcaInternalVariables::QMEAS)] = (Tfltr_ > ZERO); + tag_[static_cast(RepcaInternalVariables::XQ)] = true; + tag_[static_cast(RepcaInternalVariables::XQEXT)] = true; + tag_[static_cast(RepcaInternalVariables::PMEAS)] = (Tp_ > ZERO); + tag_[static_cast(RepcaInternalVariables::XP)] = true; + tag_[static_cast(RepcaInternalVariables::PREF)] = (Tlag_ > ZERO); + return 0; + } + + template + __attribute__((always_inline)) inline int Repca::evaluateInternalResidual( + ScalarT* y, + ScalarT* yp, + ScalarT* wb, + ScalarT* ws, + ScalarT* f) + { + const auto VMEAS = static_cast(RepcaInternalVariables::VMEAS); + const auto QMEAS = static_cast(RepcaInternalVariables::QMEAS); + const auto XQ = static_cast(RepcaInternalVariables::XQ); + const auto XQEXT = static_cast(RepcaInternalVariables::XQEXT); + const auto PMEAS = static_cast(RepcaInternalVariables::PMEAS); + const auto XP = static_cast(RepcaInternalVariables::XP); + const auto PREF = static_cast(RepcaInternalVariables::PREF); + const auto VREG = static_cast(RepcaInternalVariables::VREG); + 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 QBRANCH = static_cast(RepcaExternalVariables::QBRANCH); + const auto PBRANCH = static_cast(RepcaExternalVariables::PBRANCH); + const auto VREF = static_cast(RepcaExternalVariables::VREF); + const auto QREF = static_cast(RepcaExternalVariables::QREF); + const auto PPLANTREF = static_cast(RepcaExternalVariables::PPLANTREF); + const auto FREQ = static_cast(RepcaExternalVariables::FREQ); + const auto FREQREF = static_cast(RepcaExternalVariables::FREQREF); + + const ScalarT vr = wb[0]; + const ScalarT vi = wb[1]; + const ScalarT pbr = toComponentBase(ws[PBRANCH]); + const ScalarT qbr = toComponentBase(ws[QBRANCH]); + const ScalarT ibrr = ws[IBRANCHR]; + const ScalarT ibri = ws[IBRANCHI]; + + f[VMEAS] = -Tfltr_ * yp[VMEAS] - y[VMEAS] + y[VCTRL]; + f[QMEAS] = -Tfltr_ * yp[QMEAS] - y[QMEAS] + qbr; + f[XQ] = -yp[XQ] + + y[SFRZ] * Math::antiwindup(y[QPI], Ki_ * y[ERQLIM], Qmin_, Qmax_); + f[XQEXT] = -Tfv_ * yp[XQEXT] - y[XQEXT] + y[QPI]; + f[PMEAS] = -Tp_ * yp[PMEAS] - y[PMEAS] + pbr; + f[XP] = -yp[XP] + Math::antiwindup(y[PPI], Kig_ * y[EPLIM], Pmin_, Pmax_); + f[PREF] = -Tlag_ * yp[PREF] - y[PREF] + y[PPI]; + + f[VREG] = -y[VREG] * y[VREG] + vr * vr + vi * vi; + f[VLDC] = -y[VLDC] * y[VLDC] + + (vr - Rc_ * ibrr + Xc_ * ibri) + * (vr - Rc_ * ibrr + Xc_ * ibri) + + (vi - Rc_ * ibri - Xc_ * ibrr) + * (vi - Rc_ * ibri - Xc_ * ibrr); + f[VDROOP] = -y[VDROOP] + y[VREG] + Kc_ * qbr; + f[VCTRL] = -y[VCTRL] + VcompFlag_ * y[VLDC] + vcomp_off_ * y[VDROOP]; + f[SFRZ] = -y[SFRZ] + Math::above(y[VREG], Vfrz_); + f[ERQ] = -y[ERQ] + RefFlag_ * (ws[VREF] - y[VMEAS]) + + ref_off_ * (ws[QREF] - y[QMEAS]); + f[ERQDB] = -y[ERQDB] + Math::deadband2(y[ERQ], dbdlow_, dbdupper_); + f[ERQLIM] = -y[ERQLIM] + Math::clamp(y[ERQDB], emin_, emax_); + f[QPI] = -y[QPI] + Math::clamp(Kp_ * y[ERQLIM] + y[XQ], Qmin_, Qmax_); + f[QEXT] = -Tfv_ * (y[QEXT] - y[XQEXT]) + Tft_ * (y[QPI] - y[XQEXT]); + f[EF] = -y[EF] + Math::deadband2(ws[FREQREF] - ws[FREQ], fdbd1_, fdbd2_); + const ScalarT pfreq = Ddn_ * Math::ramp(y[EF]) - Dup_ * Math::ramp(-y[EF]); + f[EP] = -y[EP] + ws[PPLANTREF] - y[PMEAS] + pfreq; + f[EPLIM] = -y[EPLIM] + Math::clamp(y[EP], femin_, femax_); + f[PPI] = -y[PPI] + Math::clamp(Kpg_ * y[EPLIM] + y[XP], Pmin_, Pmax_); + f[PEXT] = -y[PEXT] + Freqflag_ * y[PREF]; + + return 0; + } + + template + int Repca::evaluateResidual() + { + setDerivedParameters(); + + const auto IBRANCHR = static_cast(RepcaExternalVariables::IBRANCHR); + const auto IBRANCHI = static_cast(RepcaExternalVariables::IBRANCHI); + const auto QBRANCH = static_cast(RepcaExternalVariables::QBRANCH); + const auto PBRANCH = static_cast(RepcaExternalVariables::PBRANCH); + const auto VREF = static_cast(RepcaExternalVariables::VREF); + const auto QREF = static_cast(RepcaExternalVariables::QREF); + const auto PPLANTREF = static_cast(RepcaExternalVariables::PPLANTREF); + const auto FREQ = static_cast(RepcaExternalVariables::FREQ); + const auto FREQREF = static_cast(RepcaExternalVariables::FREQREF); + + const ScalarT vr = Vr(); + const ScalarT vi = Vi(); + + ws_[IBRANCHR] = ZERO; + ws_[IBRANCHI] = ZERO; + ws_[QBRANCH] = ZERO; + ws_[PBRANCH] = ZERO; + ws_[VREF] = vref_set_; + ws_[QREF] = qref_set_; + ws_[PPLANTREF] = pplantref_set_; + ws_[FREQ] = freq_set_; + ws_[FREQREF] = freqref_set_; + std::fill(ws_indices_.begin(), ws_indices_.end(), INVALID_INDEX); + + 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_[QBRANCH] = signals_.template readExternalVariable(); + ws_indices_[QBRANCH] = + signals_.template readExternalVariableIndex(); + } + if (signals_.template isAttached()) + { + ws_[PBRANCH] = signals_.template readExternalVariable(); + ws_indices_[PBRANCH] = + 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(); + } + 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(); + } + + wb_[0] = vr; + wb_[1] = vi; + + evaluateInternalResidual(y_.data(), yp_.data(), wb_.data(), ws_.data(), f_.data()); + 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 b33849140..a299c5baf 100644 --- a/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md +++ b/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md @@ -145,6 +145,7 @@ are specified: `Gensal` | 5th order salient-pole machine model | `bus`, `pmech`\*, `speed`\*, `efd`\* | `p0`, `q0`, `H`, `D`, `Ra`, `Tdop`, `Tdopp`, `Tqopp`, `Xd`, `Xdp`, `Xdpp`, `Xq`, `Xl`, `S10`, `S12`, `mva` | `ir`, `ii`, `p`, `q`, `delta`, `omega`, `speed`, `Eqp`, `psidp`, `psiqpp`, `psidpp`, `vd`, `vq`, `te`, `id`, `iq` `GenClassical` | the classical machine model | `bus`, `pmech`\*, `speed`\*, `efd`\* | `p0`, `q0`, `H`, `D`, `Ra`, `Xdp`, `mva` | `ir`, `ii`, `p`, `q`, `delta`, `omega` `Regca` | WECC REGCA renewable generator/converter model | `bus`, `ipcmd`\*, `iqcmd`\*, `ibranchr`\*, `ibranchi`\*, `pbranch`\*, `qbranch`\* | `P0`, `Q0`, `mva`, `Tg`, `TM`, `Rqmax`, `Rqmin`, `Rpmax`, `sL`, `IL1`, `VL0`, `VL1`, `VA0`, `VA1`, `Vhvmax` | `ir`, `ii`, `p`, `q`, `vt`, `vm`, `ip`, `iq`, `iqextra`, `il`, `lp`, `up` + `Repca` | the REPCA renewable plant-control model | `bus`, `ibranchr`, `ibranchi`, `qbranch`, `pbranch`, `vref`\*, `qref`\*, `pplantref`\*, `freq`\*, `freqref`\*, `qext`, `pext` | `mva`, `VcompFlag`, `RefFlag`, `Freqflag`, `Tfltr`, `Tft`, `Tfv`, `Tp`, `Tlag`, `Vfrz`, `Rc`, `Xc`, `Kc`, `dbdlow`, `dbdupper`, `emax`, `emin`, `Kp`, `Ki`, `Qmax`, `Qmin`, `fdbd1`, `fdbd2`, `Ddn`, `Dup`, `femax`, `femin`, `Kpg`, `Kig`, `Pmax`, `Pmin` | `qext`, `pext`, `vmeas`, `qmeas`, `pmeas`, `pref`, `vctrl`, `sfrz`, `qpi`, `pfreq`, `ppi` `Tgov1 ` | the TGOV1 governor model | `pmech`, `speed` | `R`, `T1`, `T2`, `T3`, `Pvmax`, `Pvmin`, `Dt` | `none` `Ieeet1` | the IEEET1 exciter model | `bus`, `speed`, `efd`, `vs`\* | `Tr`, `Ka`, `Ta`, `Ke`, `Te`, `Kf`, `Tf`, `Vrmin`, `Vrmax`, `E1`, `E2`, `Se1`, `Se2`, `Ispdlim` | `efd`, `ksat` `SexsPti` | the SEXS-PTI simplified exciter model | `bus`, `efd`, `vs`\* | `Ta`, `Tb`, `Te`, `K`, `Efdmax`, `Efdmin` | `efd` @@ -153,7 +154,8 @@ are specified: `BusToSignalAdapter` | signal adapter component for a bus | `bus`, `vr`, `vi`, `ir`, `ii` | | Ports marked with \* are optional and, if missing, will be assumed to be -connected to a constant value. This list is subject to change. +connected to a constant value. +This list is subject to change. For `Branch`, `tap` and `phase` are optional parameters. If omitted, `tap` defaults to `1.0` and `phase` defaults to `0.0` radians. Bus `bus1` is the tap diff --git a/GridKit/Model/PhasorDynamics/SystemModel.hpp b/GridKit/Model/PhasorDynamics/SystemModel.hpp index 00b0e73ab..2f7e4faa8 100644 --- a/GridKit/Model/PhasorDynamics/SystemModel.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModel.hpp @@ -210,6 +210,89 @@ namespace GridKit addComponent(regca); } + // Add REPCA plant controllers + for (const auto& repcadata : data.repca) + { + using DataT = typename SystemModelData::RepcaDataT; + + IdxT bus_index = 0; + if (repcadata.ports.contains(DataT::Ports::bus)) + { + bus_index = repcadata.ports.at(DataT::Ports::bus); + } + + auto* repca = new Repca(getBus(bus_index), repcadata); + + if (repcadata.ports.contains(DataT::Ports::ibranchr)) + { + const IdxT signal = repcadata.ports.at(DataT::Ports::ibranchr); + repca->getSignals().template attachSignalNode( + getSignal(signal)); + } + if (repcadata.ports.contains(DataT::Ports::ibranchi)) + { + const IdxT signal = repcadata.ports.at(DataT::Ports::ibranchi); + repca->getSignals().template attachSignalNode( + getSignal(signal)); + } + if (repcadata.ports.contains(DataT::Ports::qbranch)) + { + const IdxT signal = repcadata.ports.at(DataT::Ports::qbranch); + repca->getSignals().template attachSignalNode( + getSignal(signal)); + } + if (repcadata.ports.contains(DataT::Ports::pbranch)) + { + const IdxT signal = repcadata.ports.at(DataT::Ports::pbranch); + repca->getSignals().template attachSignalNode( + getSignal(signal)); + } + if (repcadata.ports.contains(DataT::Ports::vref)) + { + const IdxT signal = repcadata.ports.at(DataT::Ports::vref); + repca->getSignals().template attachSignalNode( + getSignal(signal)); + } + if (repcadata.ports.contains(DataT::Ports::qref)) + { + const IdxT signal = repcadata.ports.at(DataT::Ports::qref); + repca->getSignals().template attachSignalNode( + getSignal(signal)); + } + if (repcadata.ports.contains(DataT::Ports::pplantref)) + { + const IdxT signal = repcadata.ports.at(DataT::Ports::pplantref); + repca->getSignals().template attachSignalNode( + getSignal(signal)); + } + if (repcadata.ports.contains(DataT::Ports::freq)) + { + const IdxT signal = repcadata.ports.at(DataT::Ports::freq); + repca->getSignals().template attachSignalNode( + getSignal(signal)); + } + if (repcadata.ports.contains(DataT::Ports::freqref)) + { + const IdxT signal = repcadata.ports.at(DataT::Ports::freqref); + repca->getSignals().template attachSignalNode( + getSignal(signal)); + } + if (repcadata.ports.contains(DataT::Ports::qext)) + { + const IdxT signal = repcadata.ports.at(DataT::Ports::qext); + repca->getSignals().template assignSignalNode( + getSignal(signal)); + } + if (repcadata.ports.contains(DataT::Ports::pext)) + { + const IdxT signal = repcadata.ports.at(DataT::Ports::pext); + repca->getSignals().template assignSignalNode( + getSignal(signal)); + } + + addComponent(repca); + } + // Add branches for (const auto& branchdata : data.branch) { diff --git a/GridKit/Model/PhasorDynamics/SystemModelData.hpp b/GridKit/Model/PhasorDynamics/SystemModelData.hpp index 20501d169..a00c52e6b 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelData.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelData.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,7 @@ namespace GridKit using BusToSignalAdapterDataT = BusToSignalAdapterData; using BusFaultDataT = BusFaultData; using RegcaDataT = Converter::RegcaData; + using RepcaDataT = Converter::RepcaData; using Tgov1DataT = Governor::Tgov1Data; using Ieeet1DataT = Exciter::Ieeet1Data; using SexsPtiDataT = Exciter::SexsPtiData; @@ -94,6 +96,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 b215c0c86..8ce6907c6 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelDataJSONParser.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelDataJSONParser.hpp @@ -142,6 +142,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/docs/Figures/PhasorDynamics_REPCA_Diagram.png b/docs/Figures/PhasorDynamics_REPCA_Diagram.png new file mode 100644 index 000000000..08fef3c15 Binary files /dev/null and b/docs/Figures/PhasorDynamics_REPCA_Diagram.png differ diff --git a/tests/UnitTests/PhasorDynamics/CMakeLists.txt b/tests/UnitTests/PhasorDynamics/CMakeLists.txt index 515c93954..ead93c57d 100644 --- a/tests/UnitTests/PhasorDynamics/CMakeLists.txt +++ b/tests/UnitTests/PhasorDynamics/CMakeLists.txt @@ -102,6 +102,14 @@ target_link_libraries( GridKit::phasor_dynamics_components_dependency_tracking GridKit::testing) +add_executable(test_phasor_converter_repca runConverterRepcaTests.cpp) +target_link_libraries( + test_phasor_converter_repca + GridKit::definitions + GridKit::phasor_dynamics_components + GridKit::phasor_dynamics_components_dependency_tracking + GridKit::testing) + add_executable(test_phasor_stabilizer_ieeest runStabilizerIeeestTests.cpp) target_link_libraries( test_phasor_stabilizer_ieeest @@ -147,6 +155,7 @@ add_test(NAME PhasorDynamicsExciterIeeet1Test COMMAND test_phasor_exciter_ieeet1 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 PhasorDynamicsLoadTest COMMAND test_phasor_load) @@ -169,6 +178,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..2c891d953 --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/ConverterRepcaTests.hpp @@ -0,0 +1,590 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + namespace Testing + { + template + class ConverterRepcaTests + { + public: + 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); + + PhasorDynamics::Converter::Repca repca(&bus, makeRepcaData()); + success *= (repca.size() + == static_cast(PhasorDynamics::Converter::RepcaInternalVariables::MAXIMUM)); + success *= (repca.getMonitor() != nullptr); + BranchSignalSet branch_signals; + branch_signals.attachTo(repca); + success *= (repca.verify() == 0); + + auto bad_repca = makeRepcaData(); + bad_repca.parameters[PhasorDynamics::Converter::RepcaParameters::Tfv] = static_cast(0.0); + PhasorDynamics::Converter::Repca bad_repca_model(&bus, bad_repca); + branch_signals.attachTo(bad_repca_model); + success *= (bad_repca_model.verify() > 0); + + return success.report(__func__); + } + + TestOutcome repcaSignalsInitializationAndResidual() + { + using Var = PhasorDynamics::Converter::RepcaInternalVariables; + using Ext = PhasorDynamics::Converter::RepcaExternalVariables; + + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + bus.allocate(); + bus.initialize(); + + ScalarT ibranchr_value{0.6}; + ScalarT ibranchi_value{-0.2}; + ScalarT pbranch_value{0.6}; + ScalarT qbranch_value{0.2}; + ScalarT qext_value{0.0}; + ScalarT pext_value{0.0}; + IdxT ibranchr_index = 20; + IdxT ibranchi_index = 21; + IdxT pbranch_index = 22; + IdxT qbranch_index = 23; + IdxT qext_index = 24; + IdxT pext_index = 25; + + PhasorDynamics::SignalNode ibranchr_node; + PhasorDynamics::SignalNode ibranchi_node; + PhasorDynamics::SignalNode pbranch_node; + PhasorDynamics::SignalNode qbranch_node; + PhasorDynamics::SignalNode qext_node; + PhasorDynamics::SignalNode pext_node; + ibranchr_node.set(&ibranchr_value, &ibranchr_index); + ibranchi_node.set(&ibranchi_value, &ibranchi_index); + pbranch_node.set(&pbranch_value, &pbranch_index); + qbranch_node.set(&qbranch_value, &qbranch_index); + qext_node.set(&qext_value, &qext_index); + pext_node.set(&pext_value, &pext_index); + + auto data = makeRepcaData(); + PhasorDynamics::Converter::Repca repca(&bus, data); + repca.getSignals().template attachSignalNode(&ibranchr_node); + repca.getSignals().template attachSignalNode(&ibranchi_node); + repca.getSignals().template attachSignalNode(&pbranch_node); + repca.getSignals().template attachSignalNode(&qbranch_node); + repca.getSignals().template assignSignalNode(&qext_node); + repca.getSignals().template assignSignalNode(&pext_node); + + success *= (repca.allocate() == 0); + qext_node.init(static_cast(0.2)); + pext_node.init(static_cast(0.6)); + success *= (repca.verify() == 0); + success *= (repca.initialize() == 0); + success *= (repca.tagDifferentiable() == 0); + success *= (repca.evaluateResidual() == 0); + + success *= isEqual(repca.y()[index(Var::PMEAS)], static_cast(0.6), kTol); + success *= isEqual(repca.y()[index(Var::QMEAS)], static_cast(0.2), kTol); + success *= isEqual(repca.y()[index(Var::QEXT)], static_cast(0.2), kTol); + success *= isEqual(repca.y()[index(Var::PEXT)], static_cast(0.6), kTol); + success *= isEqual(qext_node.read(), repca.y()[index(Var::QEXT)], kTol); + success *= isEqual(pext_node.read(), repca.y()[index(Var::PEXT)], kTol); + success *= (repca.tag()[index(Var::VMEAS)] == true); + success *= (repca.tag()[index(Var::PREF)] == false); + + for (size_t i = 0; i < repca.getResidual().size(); ++i) + { + success *= isEqual(repca.getResidual()[i], static_cast(0.0), kTol); + success *= isEqual(repca.yp()[i], static_cast(0.0), kTol); + } + + return success.report(__func__); + } + + TestOutcome rejectsHalfConnectedBranchSignals() + { + using Ext = PhasorDynamics::Converter::RepcaExternalVariables; + + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + + ScalarT signal_value{0.6}; + IdxT signal_index = 30; + + PhasorDynamics::SignalNode signal_node; + signal_node.set(&signal_value, &signal_index); + + { + PhasorDynamics::Converter::Repca repca(&bus, makeRepcaData()); + repca.getSignals().template attachSignalNode(&signal_node); + success *= (repca.verify() > 0); + } + + { + PhasorDynamics::Converter::Repca repca(&bus, makeRepcaData()); + repca.getSignals().template attachSignalNode(&signal_node); + success *= (repca.verify() > 0); + } + + { + PhasorDynamics::Converter::Repca repca(&bus, makeRepcaData()); + repca.getSignals().template attachSignalNode(&signal_node); + success *= (repca.verify() > 0); + } + + { + PhasorDynamics::Converter::Repca repca(&bus, makeRepcaData()); + repca.getSignals().template attachSignalNode(&signal_node); + success *= (repca.verify() > 0); + } + + return success.report(__func__); + } + + TestOutcome rejectsLineDropCompensationWithoutCurrentSignals() + { + using Params = PhasorDynamics::Converter::RepcaParameters; + + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + + auto data = makeRepcaData(); + data.parameters[Params::VcompFlag] = static_cast(1); + PhasorDynamics::Converter::Repca repca(&bus, data); + BranchSignalSet branch_signals; + branch_signals.attachPowerTo(repca); + + success *= (repca.verify() > 0); + + return success.report(__func__); + } + + TestOutcome convertsSystemBaseBranchPowerToComponentBase() + { + using Var = PhasorDynamics::Converter::RepcaInternalVariables; + using Params = PhasorDynamics::Converter::RepcaParameters; + + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + bus.allocate(); + bus.initialize(); + + BranchSignalSet branch_signals(static_cast(0.3), + static_cast(0.1), + static_cast(0.3), + static_cast(-0.1), + 31); + + auto data = makeRepcaData(); + data.parameters[Params::mva] = static_cast(50.0); + PhasorDynamics::Converter::Repca repca(&bus, data); + repca.setSystemBase(static_cast(60.0), static_cast(100.0e6)); + branch_signals.attachTo(repca); + + success *= (repca.allocate() == 0); + success *= (repca.verify() == 0); + success *= (repca.initialize() == 0); + success *= isEqual(repca.y()[index(Var::PMEAS)], static_cast(0.6), kTol); + success *= isEqual(repca.y()[index(Var::QMEAS)], static_cast(0.2), kTol); + success *= isEqual(repca.y()[index(Var::PEXT)], static_cast(0.6), kTol); + success *= isEqual(repca.y()[index(Var::QEXT)], static_cast(0.2), kTol); + + return success.report(__func__); + } + + TestOutcome mvaZeroUsesSystemBase() + { + using Var = PhasorDynamics::Converter::RepcaInternalVariables; + using Params = PhasorDynamics::Converter::RepcaParameters; + + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + bus.allocate(); + bus.initialize(); + + BranchSignalSet branch_signals(static_cast(0.3), + static_cast(0.1), + static_cast(0.3), + static_cast(-0.1), + 33); + + auto data = makeRepcaData(); + data.parameters[Params::mva] = static_cast(0.0); + + PhasorDynamics::Converter::Repca repca(&bus, data); + repca.setSystemBase(static_cast(60.0), static_cast(100.0e6)); + branch_signals.attachTo(repca); + + success *= (repca.allocate() == 0); + success *= (repca.verify() == 0); + success *= (repca.initialize() == 0); + success *= isEqual(repca.y()[index(Var::PMEAS)], static_cast(0.3), kTol); + success *= isEqual(repca.y()[index(Var::QMEAS)], static_cast(0.1), kTol); + success *= isEqual(repca.y()[index(Var::PEXT)], static_cast(0.3), kTol); + success *= isEqual(repca.y()[index(Var::QEXT)], static_cast(0.1), kTol); + + return success.report(__func__); + } + + TestOutcome rejectsNonSteadyConnectedReferences() + { + using Params = PhasorDynamics::Converter::RepcaParameters; + using Ext = PhasorDynamics::Converter::RepcaExternalVariables; + + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + bus.allocate(); + bus.initialize(); + + { + auto data = makeRepcaData(); + data.parameters[Params::Ki] = static_cast(1.0); + + ScalarT qref_value{0.5}; + IdxT qref_index = 35; + + PhasorDynamics::SignalNode qref_node; + qref_node.set(&qref_value, &qref_index); + + PhasorDynamics::Converter::Repca repca(&bus, data); + BranchSignalSet branch_signals; + branch_signals.attachTo(repca); + repca.getSignals().template attachSignalNode(&qref_node); + + success *= (repca.allocate() == 0); + success *= (repca.verify() == 0); + success *= (repca.initialize() > 0); + } + + { + auto data = makeRepcaData(); + data.parameters[Params::Kig] = static_cast(1.0); + + ScalarT pplantref_value{0.5}; + IdxT pplantref_index = 36; + + PhasorDynamics::SignalNode pplantref_node; + pplantref_node.set(&pplantref_value, &pplantref_index); + + PhasorDynamics::Converter::Repca repca(&bus, data); + BranchSignalSet branch_signals; + branch_signals.attachTo(repca); + repca.getSignals().template attachSignalNode(&pplantref_node); + + success *= (repca.allocate() == 0); + success *= (repca.verify() == 0); + success *= (repca.initialize() > 0); + } + + return success.report(__func__); + } + + TestOutcome initializationUsesBranchSignals() + { + using Var = PhasorDynamics::Converter::RepcaInternalVariables; + + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + bus.allocate(); + bus.initialize(); + + BranchSignalSet branch_signals(static_cast(0.1), + static_cast(-0.4), + static_cast(0.1), + static_cast(0.4), + 37); + + PhasorDynamics::Converter::Repca repca(&bus, makeRepcaData()); + branch_signals.attachTo(repca); + + success *= (repca.allocate() == 0); + success *= (repca.verify() == 0); + success *= (repca.initialize() == 0); + + success *= isEqual(repca.y()[index(Var::PMEAS)], static_cast(0.1), kTol); + success *= isEqual(repca.y()[index(Var::QMEAS)], static_cast(-0.4), kTol); + success *= isEqual(repca.y()[index(Var::PEXT)], static_cast(0.1), kTol); + success *= isEqual(repca.y()[index(Var::QEXT)], static_cast(-0.4), kTol); + + success *= (repca.evaluateResidual() == 0); + + for (size_t i = 0; i < repca.getResidual().size(); ++i) + { + success *= isEqual(repca.getResidual()[i], static_cast(0.0), kTol); + success *= isEqual(repca.yp()[i], static_cast(0.0), kTol); + } + + return success.report(__func__); + } + + TestOutcome omittedFrequencyPortsDefaultToZero() + { + using Var = PhasorDynamics::Converter::RepcaInternalVariables; + using Params = PhasorDynamics::Converter::RepcaParameters; + + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + bus.allocate(); + bus.initialize(); + + auto data = makeRepcaData(); + data.parameters[Params::Ddn] = static_cast(5.0); + data.parameters[Params::Dup] = static_cast(5.0); + data.parameters[Params::Kig] = static_cast(1.0); + + PhasorDynamics::Converter::Repca repca(&bus, data); + BranchSignalSet branch_signals; + branch_signals.attachTo(repca); + + success *= (repca.allocate() == 0); + success *= (repca.verify() == 0); + success *= (repca.initialize() == 0); + success *= (repca.evaluateResidual() == 0); + + const ScalarT pfreq = static_cast(5.0) * Math::ramp(repca.y()[index(Var::EF)]) + - static_cast(5.0) * Math::ramp(-repca.y()[index(Var::EF)]); + + success *= isEqual(repca.y()[index(Var::EF)], static_cast(0.0), kTol); + success *= isEqual(pfreq, static_cast(0.0), kTol); + + for (size_t i = 0; i < repca.getResidual().size(); ++i) + { + success *= isEqual(repca.getResidual()[i], static_cast(0.0), kTol); + success *= isEqual(repca.yp()[i], static_cast(0.0), kTol); + } + + return success.report(__func__); + } + + TestOutcome jsonParseAndSystemAssembly() + { + TestStatus success = true; + + std::istringstream input(R"json( +{ + "header": { + "format_version": 0, + "format_revision": 1, + "case_name": "renewable plant control", + "case_description": "REPCA parser 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 }, "v_base": 1.0 } + ], + "signals": [ + { "signal_id": 10, "name": "Qext" }, + { "signal_id": 11, "name": "Pext" }, + { "signal_id": 12, "name": "Ibranchr" }, + { "signal_id": 13, "name": "Ibranchi" }, + { "signal_id": 14, "name": "Pbranch" }, + { "signal_id": 15, "name": "Qbranch" } + ], + "devices": [ + { + "class": "Regca", + "ports": { "bus": 1, "ibranchr": 12, "ibranchi": 13, "pbranch": 14, "qbranch": 15 }, + "id": "RG1", + "params": { + "P0": 0.0, + "Q0": 0.0, + "mva": 100.0, + "Tg": 0.02, + "TM": 0.02, + "Rqmax": 999.0, + "Rqmin": -999.0, + "Rpmax": 999.0, + "sL": true, + "IL1": 1.1, + "VL0": 0.4, + "VL1": 0.9, + "VA0": 0.4, + "VA1": 0.9, + "Vhvmax": 1.2 + } + }, + { + "class": "Repca", + "ports": { + "bus": 1, + "ibranchr": 12, + "ibranchi": 13, + "pbranch": 14, + "qbranch": 15, + "qext": 10, + "pext": 11 + }, + "id": "REP1", + "params": { + "mva": 100.0, "VcompFlag": 0, "RefFlag": 0, "Freqflag": 1, + "Tfltr": 0.02, "Tft": 0.0, "Tfv": 0.02, "Tp": 0.02, "Tlag": 0.0, + "Vfrz": 0.7, "Rc": 0.0, "Xc": 0.0, "Kc": 0.0, + "dbdlow": -0.01, "dbdupper": 0.01, "emax": 0.1, "emin": -0.1, + "Kp": 1.0, "Ki": 0.0, "Qmax": 1.0, "Qmin": -1.0, + "fdbd1": -0.01, "fdbd2": 0.01, "Ddn": 0.0, "Dup": 0.0, + "femax": 0.1, "femin": -0.1, "Kpg": 1.0, "Kig": 0.0, + "Pmax": 1.0, "Pmin": 0.0 + } + } + ] +} +)json"); + + auto data = PhasorDynamics::parseSystemModelData(input); + success *= (data.regca.size() == 1); + success *= (data.repca.size() == 1); + success *= (data.repca[0].ports.at(PhasorDynamics::Converter::RepcaPorts::pbranch) == 14); + success *= (data.repca[0].ports.at(PhasorDynamics::Converter::RepcaPorts::qbranch) == 15); + success *= (std::get( + data.repca[0].parameters.at(PhasorDynamics::Converter::RepcaParameters::mva)) + == static_cast(100.0)); + + PhasorDynamics::SystemModel system(data); + success *= (system.allocate() == 0); + success *= (system.initialize() == 0); + success *= (system.evaluateResidual() == 0); + success *= (system.size() > 0); + + return success.report(__func__); + } + + private: + static size_t index(PhasorDynamics::Converter::RepcaInternalVariables variable) + { + return static_cast(variable); + } + + struct BranchSignalSet + { + ScalarT ibranchr_value; + ScalarT ibranchi_value; + ScalarT pbranch_value; + ScalarT qbranch_value; + IdxT ibranchr_index; + IdxT ibranchi_index; + IdxT pbranch_index; + IdxT qbranch_index; + + PhasorDynamics::SignalNode ibranchr_node; + PhasorDynamics::SignalNode ibranchi_node; + PhasorDynamics::SignalNode pbranch_node; + PhasorDynamics::SignalNode qbranch_node; + + BranchSignalSet(ScalarT pbranch = static_cast(0.6), + ScalarT qbranch = static_cast(0.2), + ScalarT ibranchr = static_cast(0.6), + ScalarT ibranchi = static_cast(-0.2), + IdxT first_index = 20) + : ibranchr_value(ibranchr), + ibranchi_value(ibranchi), + pbranch_value(pbranch), + qbranch_value(qbranch), + ibranchr_index(first_index), + ibranchi_index(first_index + 1), + pbranch_index(first_index + 2), + qbranch_index(first_index + 3) + { + ibranchr_node.set(&ibranchr_value, &ibranchr_index); + ibranchi_node.set(&ibranchi_value, &ibranchi_index); + pbranch_node.set(&pbranch_value, &pbranch_index); + qbranch_node.set(&qbranch_value, &qbranch_index); + } + + template + void attachTo(RepcaT& repca) + { + using Ext = PhasorDynamics::Converter::RepcaExternalVariables; + + repca.getSignals().template attachSignalNode(&ibranchr_node); + repca.getSignals().template attachSignalNode(&ibranchi_node); + repca.getSignals().template attachSignalNode(&pbranch_node); + repca.getSignals().template attachSignalNode(&qbranch_node); + } + + template + void attachPowerTo(RepcaT& repca) + { + using Ext = PhasorDynamics::Converter::RepcaExternalVariables; + + repca.getSignals().template attachSignalNode(&pbranch_node); + repca.getSignals().template attachSignalNode(&qbranch_node); + } + }; + + auto makeRepcaData() -> PhasorDynamics::Converter::RepcaData + { + using Params = PhasorDynamics::Converter::RepcaParameters; + 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.parameters[Params::mva] = static_cast(100.0); + data.parameters[Params::VcompFlag] = static_cast(0); + data.parameters[Params::RefFlag] = static_cast(0); + data.parameters[Params::Freqflag] = static_cast(1); + data.parameters[Params::Tfltr] = static_cast(0.02); + data.parameters[Params::Tft] = static_cast(0.0); + data.parameters[Params::Tfv] = static_cast(0.02); + data.parameters[Params::Tp] = static_cast(0.02); + data.parameters[Params::Tlag] = static_cast(0.0); + data.parameters[Params::Vfrz] = static_cast(0.7); + data.parameters[Params::Rc] = static_cast(0.0); + data.parameters[Params::Xc] = static_cast(0.0); + data.parameters[Params::Kc] = static_cast(0.0); + data.parameters[Params::dbdlow] = static_cast(-0.01); + data.parameters[Params::dbdupper] = static_cast(0.01); + data.parameters[Params::emax] = static_cast(0.1); + data.parameters[Params::emin] = static_cast(-0.1); + data.parameters[Params::Kp] = static_cast(1.0); + data.parameters[Params::Ki] = static_cast(0.0); + data.parameters[Params::Qmax] = static_cast(1.0); + data.parameters[Params::Qmin] = static_cast(-1.0); + data.parameters[Params::fdbd1] = static_cast(-0.01); + data.parameters[Params::fdbd2] = static_cast(0.01); + data.parameters[Params::Ddn] = static_cast(0.0); + data.parameters[Params::Dup] = static_cast(0.0); + data.parameters[Params::femax] = static_cast(0.1); + data.parameters[Params::femin] = static_cast(-0.1); + data.parameters[Params::Kpg] = static_cast(1.0); + data.parameters[Params::Kig] = static_cast(0.0); + data.parameters[Params::Pmax] = static_cast(1.0); + data.parameters[Params::Pmin] = static_cast(0.0); + + return data; + } + }; + } // namespace Testing +} // namespace GridKit diff --git a/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp b/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp new file mode 100644 index 000000000..05843080e --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/runConverterRepcaTests.cpp @@ -0,0 +1,21 @@ +#include "ConverterRepcaTests.hpp" + +int main() +{ + GridKit::Testing::TestingResults result; + + GridKit::Testing::ConverterRepcaTests test; + + result += test.constructionAndValidation(); + result += test.repcaSignalsInitializationAndResidual(); + result += test.rejectsHalfConnectedBranchSignals(); + result += test.rejectsLineDropCompensationWithoutCurrentSignals(); + result += test.convertsSystemBaseBranchPowerToComponentBase(); + result += test.mvaZeroUsesSystemBase(); + result += test.rejectsNonSteadyConnectedReferences(); + result += test.initializationUsesBranchSignals(); + result += test.omittedFrequencyPortsDefaultToZero(); + result += test.jsonParseAndSystemAssembly(); + + return result.summary(); +}