Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions GridKit/Model/PhasorDynamics/Converter/REGCA/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ $Q_0$ | [p.u.] | `q0` | Initial reactive power
$S^\mathrm{base}$ | [MVA] | `mva` | REGCA component power base | 100.0 |
$T_\mathrm{g}$ | [sec] | `Tg` | Converter current-control lag time constant | 0.02 | Block name: `Tg`
$T_M$ | [sec] | `TM` | Terminal voltage sensor time constant | 0.02 | Block name: `Tfltr`
$R_q^{\max}$ | [p.u./s] | `Rqmax` | Reactive-current recovery positive rate limit | 999.0 | Block name: `Iqrmax`
$R_q^{\min}$ | [p.u./s] | `Rqmin` | Reactive-current recovery negative rate limit | -999.0 | Block name: `Iqrmin`
$R_p^{\max}$ | [p.u./s] | `Rpmax` | Active-current magnitude recovery rate limit | 999.0 | Block name: `rrpwr`
$R_q^{\max}$ | [p.u./s] | `Rqmax` | Reactive-current recovery positive rate limit | 999.0 | Block name: `Iqrmax`; disabled when $R_q^{\max}\le 0$
$R_q^{\min}$ | [p.u./s] | `Rqmin` | Reactive-current recovery negative rate limit | -999.0 | Block name: `Iqrmin`; disabled when $R_q^{\min}\ge 0$
$R_p^{\max}$ | [p.u./s] | `Rpmax` | Active-current magnitude recovery rate limit | 999.0 | Block name: `rrpwr`; must be nonnegative
$s_L$ | [binary] | `sL` | LVPL switch | 1 | Block name: `LPVLSW`
$I_{L1}$ | [p.u.] | `IL1` | LVPL upper-current ceiling | 1.1 | Block name: `LVPL1`
$V_{L0}$ | [p.u.] | `VL0` | LVPL zero-crossing voltage | 0.4 | Block name: `zerox`
Expand All @@ -65,13 +65,11 @@ every other condition is a configuration error.
S^\mathrm{base}
&> 0 \\
R_p^{\max}
&> 0 \\
R_q^{\min}
&< 0 < R_q^{\max} \\
s_L
&\in \{0,1\} \\
&\ge 0 \\
I_{L1}
&\ge 0 \\
s_L
&\in \{0,1\} \\
0
&\le V_{L0} < V_{L1} \\
0
Expand Down Expand Up @@ -171,16 +169,19 @@ f_\mathrm{p}^{\lim}

### Differential Equations

The $I_q$ limiter branch is selected by the initial reactive power $Q_0$.
The $I_q$ limiter branch is selected by the initial reactive power $Q_0$ and
the sign that enables the corresponding limit.

```math
\begin{aligned}
0 &= -\dot V_M + \dfrac{1}{T_M} (V_T - V_M) \\
0 &= -\dot I_q +
\begin{cases}
\text{min}(f_\mathrm{q}, R_q^{\max}) & Q_0 > 0 \\
f_\mathrm{q} & Q_0 = 0 \\
\text{max}(f_\mathrm{q}, R_q^{\min}) & Q_0 < 0
\text{min}(f_\mathrm{q}, R_q^{\max})
& Q_0 > 0 \land R_q^{\max} > 0 \\
\text{max}(f_\mathrm{q}, R_q^{\min})
& Q_0 < 0 \land R_q^{\min} < 0 \\
f_\mathrm{q} & \text{otherwise}
\end{cases} \\
0 &= -\dot I_p +
\begin{cases}
Expand Down
7 changes: 3 additions & 4 deletions GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaImpl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@ namespace GridKit

use_rqmax_ = ZERO<RealT>;
use_rqmin_ = ZERO<RealT>;
if (q0_ > ZERO<RealT>)
if (q0_ > ZERO<RealT> && Rqmax_ > ZERO<RealT>)
{
use_rqmax_ = ONE<RealT>;
}
else if (q0_ < ZERO<RealT>)
else if (q0_ < ZERO<RealT> && Rqmin_ < ZERO<RealT>)
{
use_rqmin_ = ONE<RealT>;
}
Expand Down Expand Up @@ -436,8 +436,7 @@ namespace GridKit
}

check(mva_base_ > ZERO<RealT>, "mva must be positive");
check(Rpmax_ > ZERO<RealT>, "Rpmax must be positive");
check(Rqmin_ < ZERO<RealT> && ZERO<RealT> < Rqmax_, "Rqmin < 0 < Rqmax is required");
check(Rpmax_ >= ZERO<RealT>, "Rpmax must be non-negative");
check(IL1_ >= ZERO<RealT>, "IL1 must be non-negative");
check(KL_ > ZERO<RealT>, "LVPL release slope must be positive");
check(ZERO<RealT> <= VL0_ && VL0_ < VL1_, "VL0/VL1 must satisfy 0 <= VL0 < VL1");
Expand Down
3 changes: 2 additions & 1 deletion GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1a.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ namespace GridKit
EV, ///< \f$e_V\f$ Algebraic voltage-error summing output [p.u.]
VLL, ///< \f$V_{\mathrm{LL}}\f$ Algebraic input lead-lag output [p.u.]
VHV, ///< \f$V_{\mathrm{HV}}\f$ Algebraic high-value gate output [p.u.]
SE, ///< \f$S_E\f$ Algebraic exciter saturation coefficient [p.u.]
SE, ///< \f$s_e\f$ Scaled-quadratic saturation contribution [p.u.]
VFE, ///< \f$V_{\mathrm{FE}}\f$ Algebraic exciter feedback drive [p.u.]
EFD, ///< \f$E_{\mathrm{fd}}\f$ Algebraic field-voltage output [p.u.]
MAXIMUM, ///< Number of ESDC1A internal variables
Expand Down Expand Up @@ -166,6 +166,7 @@ namespace GridKit
RealT lim_on_{1};
RealT SA_{0};
RealT SB_{0};
RealT Ke_eff_{Ke_};

IdxT parameter_error_count_{0};

Expand Down
2 changes: 1 addition & 1 deletion GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aData.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ namespace GridKit
vc, ///< \f$V_C\f$ Filtered terminal-voltage magnitude [p.u.]
vr, ///< \f$V_R\f$ Voltage-regulator output [p.u.]
vf, ///< \f$V_F\f$ Stabilizing feedback state [p.u.]
se, ///< \f$S_E\f$ Exciter saturation coefficient [p.u.]
se, ///< \f$s_e\f$ Scaled-quadratic saturation contribution [p.u.]
vfe ///< \f$V_{\mathrm{FE}}\f$ Exciter feedback drive [p.u.]
};

Expand Down
93 changes: 55 additions & 38 deletions GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aImpl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ namespace GridKit
}

check(Ka_ > ZERO<RealT>, "Ka must be positive");
check(Tc_ >= ZERO<RealT>, "Tc must be non-negative");
check(Vrmin_ <= Vrmax_, "Vrmin must be less than or equal to Vrmax");
check(UEL_ >= static_cast<IdxT>(0) && UEL_ <= static_cast<IdxT>(3),
"UEL must be 0, 1, 2, or 3");
Expand All @@ -176,14 +175,11 @@ namespace GridKit
{
check(E1_ > ZERO<RealT>, "E1 must be positive when saturation is enabled");
check(E2_ > ZERO<RealT>, "E2 must be positive when saturation is enabled");
check(Se1_ > ZERO<RealT>, "Se1 must be positive when saturation is enabled");
check(Se2_ > ZERO<RealT>, "Se2 must be positive when saturation is enabled");

const bool saturation_points_are_ordered =
(E2_ > E1_ && Se2_ > Se1_)
|| (E2_ < E1_ && Se2_ < Se1_);
check(saturation_points_are_ordered,
"E1/E2 and Se1/Se2 must be ordered consistently");
check(Se1_ >= ZERO<RealT>, "Se1 must be non-negative when saturation is enabled");
check(Se2_ >= ZERO<RealT>, "Se2 must be non-negative when saturation is enabled");

const bool sat_ordered = (E2_ > E1_ && Se2_ > Se1_) || (E2_ < E1_ && Se2_ < Se1_);
check(sat_ordered, "E1/E2 and Se1/Se2 must be ordered consistently");
}

if (!signals_.template isAssigned<Esdc1aInternalVariables::EFD>())
Expand Down Expand Up @@ -226,6 +222,16 @@ namespace GridKit
* preserving the seeded `efd`, latches attached Known inputs, and
* publishes the reference to an attached `vref` signal.
*
* @warning IEEE Std 421.5-2016 states: “In some programs, if
* \f$K_{E}\f$ is entered as zero, \f$K_{E}\f$ is automatically
* calculated by the program to represent a self-excited shunt
* field and a trimmed rheostat as its initial condition.” GridKit
* preserves the configured \f$K_{E}\f$ and resolves
* \f$K_{E}^{\mathrm{eff}}\f$ using the PSS/E-compatible
* \f$V_R = V_R^{\max}/10 = 0.1 V_R^{\max}\f$ rule. The divisor
* 10 is unitless and represents 10% of the maximum regulator
* output.
*
* @return Zero on success; nonzero when the configuration or operating point is rejected.
*/
template <typename scalar_type, typename index_type>
Expand Down Expand Up @@ -314,8 +320,19 @@ namespace GridKit
return 1;
}

const ScalarT se0 = SB_ * Math::qramp(efdp0 - SA_);
const ScalarT vfe0 = (Ke_ + se0) * efdp0;
const ScalarT se0 = SB_ * Math::qramp(efdp0 - SA_);

if (Ke_ == ZERO<RealT>)
{
Ke_eff_ = (Vrmax_ / 10.0 - static_cast<RealT>(se0))
/ static_cast<RealT>(efdp0);
}
else
{
Ke_eff_ = Ke_;
}

const ScalarT vfe0 = Ke_eff_ * efdp0 + se0;
const ScalarT vr0 = vfe0;
const ScalarT vhv0 = vr0 / Ka_;

Expand Down Expand Up @@ -344,15 +361,14 @@ namespace GridKit
vll0 = vuel0 + inverseRamp(gate_margin0);
}

const ScalarT vf0 = ScalarT{ZERO<RealT>};
const ScalarT ev0 = vll0;
const ScalarT xll0 = ev0;
const ScalarT vref0 = ev0 + vc0 + vf0 - vs0 - uel_on_ * vuel0;
const ScalarT vref0 = ev0 + vc0 - vs0 - uel_on_ * vuel0;

y[EFDP] = efdp0;
y[VC] = vc0;
y[VR] = vr0;
y[VF] = vf0;
y[VF] = ZERO<RealT>;
y[XLL] = xll0;
y[EV] = ev0;
y[VLL] = vll0;
Expand Down Expand Up @@ -560,7 +576,7 @@ namespace GridKit

const ScalarT ec = std::sqrt(wb[0] * wb[0] + wb[1] * wb[1]);
const ScalarT ev_target = vref + vs + uel_on_ * vuel - vc - vf;
const ScalarT vfe_target = (Ke_ + se) * efdp;
const ScalarT vfe_target = Ke_eff_ * efdp + se;
const ScalarT efdp_rate = (vr - vfe) / Te_;
const ScalarT limited_efdp_rate = awmin(efdp, efdp_rate, ZERO<RealT>);

Expand Down Expand Up @@ -749,7 +765,7 @@ namespace GridKit
* @brief Resolve the parameter-derived constants and selector masks
*
* Raises the transducer, regulator, lead-lag, exciter, and feedback
* lags to the well-posedness floor, fits the quadratic saturation
* lags to the well-posedness floor, fits the scaled-quadratic saturation
* curve, and turns the three selectors into multiplicative masks. The
* masks let the residual select signal routing without
* parameter-dependent control flow, which keeps its structure fixed for
Expand Down Expand Up @@ -814,23 +830,37 @@ namespace GridKit
// The disabled test matches the verify() predicate exactly.
const bool saturation_disabled =
Se1_ == ZERO<RealT> && Se2_ == ZERO<RealT>;
const bool saturation_points_are_ordered =
(E2_ > E1_ && Se2_ > Se1_)
|| (E2_ < E1_ && Se2_ < Se1_);
const bool sat_ordered = (E2_ > E1_ && Se2_ > Se1_) || (E2_ < E1_ && Se2_ < Se1_);
const bool saturation_consistent =
E1_ > ZERO<RealT> && E2_ > ZERO<RealT>
&& Se1_ > ZERO<RealT> && Se2_ > ZERO<RealT>
&& saturation_points_are_ordered;
&& Se1_ >= ZERO<RealT> && Se2_ >= ZERO<RealT>
&& sat_ordered;
if (saturation_disabled || !saturation_consistent)
{
SA_ = ZERO<RealT>;
SB_ = ZERO<RealT>;
return;
}

const RealT C = std::sqrt(Se2_ / Se1_);
if (Se1_ == ZERO<RealT>)
{
const RealT dE = E2_ - E1_;
SA_ = E1_;
SB_ = Se2_ * E2_ / (dE * dE);
return;
}

if (Se2_ == ZERO<RealT>)
{
const RealT dE = E1_ - E2_;
SA_ = E2_;
SB_ = Se1_ * E1_ / (dE * dE);
return;
}

const RealT C = std::sqrt(Se2_ * E2_ / (Se1_ * E1_));
SA_ = (C * E1_ - E2_) / (C - ONE<RealT>);
SB_ = Se1_ / ((E1_ - SA_) * (E1_ - SA_));
SB_ = Se1_ * E1_ / ((E1_ - SA_) * (E1_ - SA_));
}

/**
Expand All @@ -840,30 +870,17 @@ namespace GridKit
* *input*, so the residual reproduces the requested output through the
* same smooth ramp it evaluates.
*
* For large positive values, the ramp is effectively equal to the input, so the
* inverse is effectively the output. In that regime this function returns `ramp_output` directly.
* This branching is numerically more robust.
*
* @param[in] ramp_output Strictly positive requested ramp output.
* @return The input the smooth ramp maps to the requested output.
*
* @pre @p ramp_output is finite and strictly positive.
* @warning This function contains conditional branching and may be used
* during initialization, but not during residual or Jacobian
* evaluation.
*/
template <typename scalar_type, typename index_type>
typename Esdc1a<scalar_type, index_type>::RealT
Esdc1a<scalar_type, index_type>::inverseRamp(RealT ramp_output) const
{
static constexpr RealT SOFTPLUS_WIDTH = static_cast<RealT>(50.0);

const RealT scaled_output = Math::MU<RealT> * ramp_output;
if (scaled_output > SOFTPLUS_WIDTH)
{
return ramp_output;
}
return std::log(std::expm1(scaled_output)) / Math::MU<RealT>;
const RealT mu = Math::MU<RealT>;
return ramp_output + std::log(-std::expm1(-mu * ramp_output)) / mu;
}

/**
Expand Down
Loading
Loading