From a16355dde9c3734d2d3b0b130cb39fd522c0fa8e Mon Sep 17 00:00:00 2001 From: Leonardo Carreras Date: Wed, 22 Jul 2026 00:44:36 +0200 Subject: [PATCH 1/9] Add negative-sequence current control to DP::Ph3::AvVoltSourceInverterStateSpace Signed-off-by: Leonardo Carreras --- .../DP_Ph3_AvVoltSourceInverterStateSpace.h | 38 ++++-- .../DP_Ph3_AvVoltSourceInverterStateSpace.cpp | 129 ++++++++++++++++-- .../DP_Ph3_AvVoltSourceInverterStateSpace.cpp | 2 + dpsim/src/pybind/DPComponents.cpp | 7 +- 4 files changed, 149 insertions(+), 27 deletions(-) diff --git a/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h b/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h index 93178d9346..0ebc6a1341 100644 --- a/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h +++ b/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h @@ -9,7 +9,7 @@ namespace CPS { namespace DP { namespace Ph3 { -/// Averaged grid-following VSI SSN port of EMT::Ph3::AvVoltSourceInverterStateSpace: 8 real control states (single PLL, single positive-sequence dq frame) plus 6 complex per-phase envelope states (Vc_a/b/c, If_a/b/c). +/// Averaged grid-following VSI SSN port of EMT::Ph3::AvVoltSourceInverterStateSpace: 10 real control states (single PLL, positive-sequence dq frame plus a negative-sequence current-control loop, gamma_nd/gamma_nq) plus 6 complex per-phase envelope states (Vc_a/b/c, If_a/b/c). class AvVoltSourceInverterStateSpace final : public MixedVTypeVariableSSNComp, public SharedFactory { @@ -23,18 +23,20 @@ class AvVoltSourceInverterStateSpace final PhiQ = 5, GammaD = 6, GammaQ = 7, - VcARe = 8, - VcAIm = 9, - VcBRe = 10, - VcBIm = 11, - VcCRe = 12, - VcCIm = 13, - IfARe = 14, - IfAIm = 15, - IfBRe = 16, - IfBIm = 17, - IfCRe = 18, - IfCIm = 19 + GammaND = 8, + GammaNQ = 9, + VcARe = 10, + VcAIm = 11, + VcBRe = 12, + VcBIm = 13, + VcCRe = 14, + VcCIm = 15, + IfARe = 16, + IfAIm = 17, + IfBRe = 18, + IfBIm = 19, + IfCRe = 20, + IfCIm = 21 }; static constexpr Int mVcReCol[3] = {VcARe, VcBRe, VcCRe}; @@ -61,6 +63,11 @@ class AvVoltSourceInverterStateSpace final Real mKpCurrCtrl; Real mKiCurrCtrl; + /// Negative-sequence d-axis current reference; zero (default) = negative-sequence suppression. + Real mIRefNd; + /// Negative-sequence q-axis current reference; zero (default) = negative-sequence suppression. + Real mIRefNq; + const Attribute::Ptr mVcD; const Attribute::Ptr mVcQ; const Attribute::Ptr mIrcD; @@ -68,6 +75,8 @@ class AvVoltSourceInverterStateSpace final const Attribute::Ptr mPInst; const Attribute::Ptr mQInst; const Attribute::Ptr mOmegaPLL; + const Attribute::Ptr mIrcNd; + const Attribute::Ptr mIrcNq; /// Builds the affine real model (A,B,C,D,E,F) around (x,u): RHS + analytic Jacobian, E = f(x,u) - A*x - B*u. void buildStateSpaceModel(const Matrix &x, const Matrix &u, Matrix &A, @@ -90,7 +99,8 @@ class AvVoltSourceInverterStateSpace final void setParameters(Real lf, Real cf, Real rf, Real rc, Real omegaN, Real kpPLL, Real kiPLL, Real omegaCutoff, Real pRef, Real qRef, Real kpPowerCtrl, Real kiPowerCtrl, - Real kpCurrCtrl, Real kiCurrCtrl); + Real kpCurrCtrl, Real kiCurrCtrl, Real iRefNd = 0.0, + Real iRefNq = 0.0); void initializeFromNodesAndTerminals(Real frequency) override; }; diff --git a/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp b/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp index 00e416926c..56f0b01fb5 100644 --- a/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp +++ b/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp @@ -16,17 +16,19 @@ const Real K32 = std::sqrt(1.5); DP::Ph3::AvVoltSourceInverterStateSpace::AvVoltSourceInverterStateSpace( String uid, String name, Logger::Level logLevel) - : MixedVTypeVariableSSNComp(uid, name, 8, 6, logLevel), mLf(0.0), mCf(0.0), + : MixedVTypeVariableSSNComp(uid, name, 10, 6, logLevel), mLf(0.0), mCf(0.0), mRf(0.0), mRc(0.0), mOmegaN(0.0), mKpPLL(0.0), mKiPLL(0.0), mOmegaCutoff(0.0), mPRef(0.0), mQRef(0.0), mKpPowerCtrl(0.0), - mKiPowerCtrl(0.0), mKpCurrCtrl(0.0), mKiCurrCtrl(0.0), - mVcD(mAttributes->create("vc_d")), + mKiPowerCtrl(0.0), mKpCurrCtrl(0.0), mKiCurrCtrl(0.0), mIRefNd(0.0), + mIRefNq(0.0), mVcD(mAttributes->create("vc_d")), mVcQ(mAttributes->create("vc_q")), mIrcD(mAttributes->create("irc_d")), mIrcQ(mAttributes->create("irc_q")), mPInst(mAttributes->create("p_inst")), mQInst(mAttributes->create("q_inst")), - mOmegaPLL(mAttributes->create("omega_pll")) { + mOmegaPLL(mAttributes->create("omega_pll")), + mIrcNd(mAttributes->create("irc_n_d")), + mIrcNq(mAttributes->create("irc_n_q")) { **mVcD = 0.0; **mVcQ = 0.0; **mIrcD = 0.0; @@ -34,12 +36,14 @@ DP::Ph3::AvVoltSourceInverterStateSpace::AvVoltSourceInverterStateSpace( **mPInst = 0.0; **mQInst = 0.0; **mOmegaPLL = 0.0; + **mIrcNd = 0.0; + **mIrcNq = 0.0; } void DP::Ph3::AvVoltSourceInverterStateSpace::setParameters( Real lf, Real cf, Real rf, Real rc, Real omegaN, Real kpPLL, Real kiPLL, Real omegaCutoff, Real pRef, Real qRef, Real kpPowerCtrl, Real kiPowerCtrl, - Real kpCurrCtrl, Real kiCurrCtrl) { + Real kpCurrCtrl, Real kiCurrCtrl, Real iRefNd, Real iRefNq) { if (lf <= 0.0) throw std::invalid_argument("Filter inductance lf must be positive."); @@ -87,6 +91,8 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::setParameters( mKiPowerCtrl = kiPowerCtrl; mKpCurrCtrl = kpCurrCtrl; mKiCurrCtrl = kiCurrCtrl; + mIRefNd = iRefNd; + mIRefNq = iRefNq; const Matrix x0 = Matrix::Zero(stateSize(), 1); const Matrix u0 = Matrix::Zero(6, 1); @@ -111,6 +117,8 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( const Real phiQ = x(PhiQ, 0); const Real gammaD = x(GammaD, 0); const Real gammaQ = x(GammaQ, 0); + const Real gammaND = x(GammaND, 0); + const Real gammaNQ = x(GammaNQ, 0); const Complex j(0.0, 1.0); @@ -172,11 +180,37 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( const Complex vRefDQ = -mKpCurrCtrl * ircDQ + mKiCurrCtrl * gammaDQ + mKpCurrCtrl * iRefDQ; - // Bridge-voltage reference, distributed via the inverse Park. + // Positive-sequence bridge-voltage reference, distributed via the inverse Park. const Complex vRefEnv0 = K23 * vRefDQ * expJPsi; + + // Negative-sequence measurement: conjugate projection, rotate by e^{+j*psi}, conjugate -> baseband ircNDQ. + Complex nV(0.0, 0.0), nU(0.0, 0.0); + for (Int p = 0; p < 3; ++p) { + nV += redistFactor[p] * vc[p]; + nU += redistFactor[p] * uEnv[p]; + } + const Complex nI = (nV - nU) / mRc; + const Complex ircNDQ = 0.5 * K23 * expJPsi * std::conj(nI); + const Real ircND = ircNDQ.real(); + const Real ircNQ = ircNDQ.imag(); + + // Coefficient of conj(vc_p) in ircNDQ; the conjugate flips the imaginary-part derivative signs vs the positive loop. + Complex hIrcN[3]; + for (Int p = 0; p < 3; ++p) + hIrcN[p] = 0.5 * K23 * expJPsi * projCoeff[p] / mRc; + + // PI current control (positive-loop gains reused), then inverse negative Park. + const Real vRefNd = + -mKpCurrCtrl * ircND + mKiCurrCtrl * gammaND + mKpCurrCtrl * mIRefNd; + const Real vRefNq = + -mKpCurrCtrl * ircNQ + mKiCurrCtrl * gammaNQ + mKpCurrCtrl * mIRefNq; + const Complex vRefNDQ(vRefNd, vRefNq); + const Complex vRefNEnv0 = K23 * std::conj(vRefNDQ) * expJPsi; + + // Total reference: positive (redistFactor) + sequence-orthogonal negative (projCoeff) injection. Complex vRef[3]; for (Int p = 0; p < 3; ++p) - vRef[p] = redistFactor[p] * vRefEnv0; + vRef[p] = redistFactor[p] * vRefEnv0 + projCoeff[p] * vRefNEnv0; // RHS f(x,u) (x_dot = f(x,u)). Complex vcDot[3], ifDot[3]; @@ -186,7 +220,7 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( ifDot[p] = (vRef[p] - vc[p] - mRf * iF[p]) / mLf - j * mOmegaN * iF[p]; } - Matrix f = Matrix::Zero(20, 1); + Matrix f = Matrix::Zero(22, 1); f(Psi, 0) = mKpPLL * vcQ + mKiPLL * phiPLL; f(PhiPLL, 0) = vcQ; f(PFiltered, 0) = mOmegaCutoff * (pInst - pF); @@ -195,6 +229,8 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( f(PhiQ, 0) = qF - mQRef; f(GammaD, 0) = iRefD - ircD; f(GammaQ, 0) = iRefQ - ircQ; + f(GammaND, 0) = mIRefNd - ircND; + f(GammaNQ, 0) = mIRefNq - ircNQ; f(VcARe, 0) = vcDot[0].real(); f(VcAIm, 0) = vcDot[0].imag(); f(VcBRe, 0) = vcDot[1].real(); @@ -209,8 +245,8 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( f(IfCIm, 0) = ifDot[2].imag(); // Analytic Jacobian A = df/dx, B = df/du. - A = Matrix::Zero(20, 20); - B = Matrix::Zero(20, 6); + A = Matrix::Zero(22, 22); + B = Matrix::Zero(22, 6); // PLL rows: only vcQ feeds them; d(vcQ)/dpsi = -vcD (d(vcDQ)/dpsi = -j*vcDQ). A(Psi, Psi) = mKpPLL * (-vcD); @@ -276,6 +312,21 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( B(GammaQ, mUImCol[p]) = -dIrcQdUIm; } + // Negative-sequence current integrators: gammaND_dot = iRefNd - ircND, etc. + // psi enters via rotN = e^{+j*psi}: d(ircND)/dpsi = -ircNQ, d(ircNQ)/dpsi = ircND. + A(GammaND, Psi) = ircNQ; + A(GammaNQ, Psi) = -ircND; + for (Int p = 0; p < 3; ++p) { + A(GammaND, mVcReCol[p]) = -hIrcN[p].real(); + A(GammaND, mVcImCol[p]) = -hIrcN[p].imag(); + A(GammaNQ, mVcReCol[p]) = -hIrcN[p].imag(); + A(GammaNQ, mVcImCol[p]) = hIrcN[p].real(); + B(GammaND, mUReCol[p]) = hIrcN[p].real(); + B(GammaND, mUImCol[p]) = hIrcN[p].imag(); + B(GammaNQ, mUReCol[p]) = hIrcN[p].imag(); + B(GammaNQ, mUImCol[p]) = -hIrcN[p].real(); + } + // Filter capacitor rows (Vc_dot), fully decoupled per phase. for (Int p = 0; p < 3; ++p) { const Int reRow = mVcReCol[p]; @@ -318,6 +369,24 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( dVRefEnv0UIm[p] = K23 * expJPsi * (-mKpCurrCtrl * j * gIrcU[p]); } + // Negative injection vRefNEnv0 = K23*conj(vRefNDQ)*expJPsi: derivatives wrt psi and the two negative-loop states. + const Complex dVRefNEnv0DPsi = + j * K23 * expJPsi * + (mKpCurrCtrl * std::conj(ircNDQ) + std::conj(vRefNDQ)); + const Complex dVRefNEnv0DGammaND = K23 * expJPsi * mKiCurrCtrl; + const Complex dVRefNEnv0DGammaNQ = -j * K23 * expJPsi * mKiCurrCtrl; + + // d(vRefNEnv0)/d(vc_p), d(u_p) via conj(vRefNDQ)'s -kpCurrCtrl*ircND/Q term (conjugate-of-conjugate). + Complex dVRefNEnv0VcRe[3], dVRefNEnv0VcIm[3], dVRefNEnv0URe[3], + dVRefNEnv0UIm[3]; + for (Int p = 0; p < 3; ++p) { + const Complex base = mKpCurrCtrl * std::conj(hIrcN[p]) * K23 * expJPsi; + dVRefNEnv0VcRe[p] = -base; + dVRefNEnv0VcIm[p] = -j * base; + dVRefNEnv0URe[p] = base; + dVRefNEnv0UIm[p] = j * base; + } + for (Int pOut = 0; pOut < 3; ++pOut) { const Int reRow = mIfReCol[pOut]; const Int imRow = mIfImCol[pOut]; @@ -351,13 +420,39 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( B(reRow, mUImCol[pSrc]) += dVRefUIm.real() / mLf; B(imRow, mUImCol[pSrc]) += dVRefUIm.imag() / mLf; } + + // Negative-sequence injection coupling: vRef_p += projCoeff[pOut]*vRefNEnv0. + const Complex dNPsi = projCoeff[pOut] * dVRefNEnv0DPsi; + A(reRow, Psi) += dNPsi.real() / mLf; + A(imRow, Psi) += dNPsi.imag() / mLf; + const Complex dNGammaND = projCoeff[pOut] * dVRefNEnv0DGammaND; + A(reRow, GammaND) += dNGammaND.real() / mLf; + A(imRow, GammaND) += dNGammaND.imag() / mLf; + const Complex dNGammaNQ = projCoeff[pOut] * dVRefNEnv0DGammaNQ; + A(reRow, GammaNQ) += dNGammaNQ.real() / mLf; + A(imRow, GammaNQ) += dNGammaNQ.imag() / mLf; + for (Int pSrc = 0; pSrc < 3; ++pSrc) { + const Complex dNVcRe = projCoeff[pOut] * dVRefNEnv0VcRe[pSrc]; + const Complex dNVcIm = projCoeff[pOut] * dVRefNEnv0VcIm[pSrc]; + A(reRow, mVcReCol[pSrc]) += dNVcRe.real() / mLf; + A(imRow, mVcReCol[pSrc]) += dNVcRe.imag() / mLf; + A(reRow, mVcImCol[pSrc]) += dNVcIm.real() / mLf; + A(imRow, mVcImCol[pSrc]) += dNVcIm.imag() / mLf; + + const Complex dNURe = projCoeff[pOut] * dVRefNEnv0URe[pSrc]; + const Complex dNUIm = projCoeff[pOut] * dVRefNEnv0UIm[pSrc]; + B(reRow, mUReCol[pSrc]) += dNURe.real() / mLf; + B(imRow, mUReCol[pSrc]) += dNURe.imag() / mLf; + B(reRow, mUImCol[pSrc]) += dNUIm.real() / mLf; + B(imRow, mUImCol[pSrc]) += dNUIm.imag() / mLf; + } } // Offset E = f(x,u) - A*x - B*u. E = f - A * x - B * u; // SSN output: y_p = (u_p - vc_p)/Rc (exact, no relinearization needed). - C = Matrix::Zero(6, 20); + C = Matrix::Zero(6, 22); for (Int p = 0; p < 3; ++p) { C(2 * p, mVcReCol[p]) = -1.0 / mRc; C(2 * p + 1, mVcImCol[p]) = -1.0 / mRc; @@ -392,25 +487,32 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::updateLogAttributes( const Real psi = x(Psi, 0); const Complex rot = std::exp(Complex(0.0, -psi)); + const Complex expJPsi = std::conj(rot); const Complex projCoeff[3] = {Complex(1.0, 0.0), SHIFT_TO_PHASE_C, SHIFT_TO_PHASE_B}; - Complex pV(0.0, 0.0), pU(0.0, 0.0); + Complex pV(0.0, 0.0), pU(0.0, 0.0), nV(0.0, 0.0), nU(0.0, 0.0); for (Int p = 0; p < 3; ++p) { const Complex vc(x(mVcReCol[p], 0), x(mVcImCol[p], 0)); const Complex uEnv(u(mUReCol[p], 0), u(mUImCol[p], 0)); pV += projCoeff[p] * vc; pU += projCoeff[p] * uEnv; + nV += std::conj(projCoeff[p]) * vc; + nU += std::conj(projCoeff[p]) * uEnv; } const Complex pI = (pV - pU) / mRc; + const Complex nI = (nV - nU) / mRc; const Complex vcDQ = 0.5 * K23 * rot * pV; const Complex ircDQ = 0.5 * K23 * rot * pI; + const Complex ircNDQ = 0.5 * K23 * expJPsi * std::conj(nI); **mVcD = vcDQ.real(); **mVcQ = vcDQ.imag(); **mIrcD = ircDQ.real(); **mIrcQ = ircDQ.imag(); + **mIrcNd = ircNDQ.real(); + **mIrcNq = ircNDQ.imag(); **mPInst = **mVcD * **mIrcD + **mVcQ * **mIrcQ; **mQInst = -**mVcD * **mIrcQ + **mVcQ * **mIrcD; @@ -498,6 +600,9 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::initializeFromNodesAndTerminals( x0(PhiQ, 0) = phiQ0; x0(GammaD, 0) = gammaD0; x0(GammaQ, 0) = gammaQ0; + // Balanced start: no negative sequence, so the negative-loop integrators seed at zero. + x0(GammaND, 0) = 0.0; + x0(GammaNQ, 0) = 0.0; for (Int p = 0; p < 3; ++p) { x0(mVcReCol[p], 0) = vcAbc(p, 0).real(); diff --git a/dpsim/examples/cxx/Components/DP_Ph3_AvVoltSourceInverterStateSpace.cpp b/dpsim/examples/cxx/Components/DP_Ph3_AvVoltSourceInverterStateSpace.cpp index bbbb7c1038..b8b205d5a1 100644 --- a/dpsim/examples/cxx/Components/DP_Ph3_AvVoltSourceInverterStateSpace.cpp +++ b/dpsim/examples/cxx/Components/DP_Ph3_AvVoltSourceInverterStateSpace.cpp @@ -134,6 +134,8 @@ class Example_AvVoltSourceInverterStateSpace { logger->logAttribute("i_fault", fault->attribute("i_intf")); logger->logAttribute("vc_d", inverter->attribute("vc_d")); logger->logAttribute("vc_q", inverter->attribute("vc_q")); + logger->logAttribute("irc_n_d", inverter->attribute("irc_n_d")); + logger->logAttribute("irc_n_q", inverter->attribute("irc_n_q")); logger->logAttribute("p_inst", inverter->attribute("p_inst")); logger->logAttribute("q_inst", inverter->attribute("q_inst")); logger->logAttribute("omega_pll", inverter->attribute("omega_pll")); diff --git a/dpsim/src/pybind/DPComponents.cpp b/dpsim/src/pybind/DPComponents.cpp index ac9aa00272..33ab8baa3a 100644 --- a/dpsim/src/pybind/DPComponents.cpp +++ b/dpsim/src/pybind/DPComponents.cpp @@ -676,13 +676,18 @@ void addDPPh3Components(py::module_ mDPPh3) { &CPS::DP::Ph3::AvVoltSourceInverterStateSpace::setParameters, "Lf"_a, "Cf"_a, "Rf"_a, "Rc"_a, "omega_n"_a, "Kp_pll"_a, "Ki_pll"_a, "omega_cutoff"_a, "p_ref"_a, "q_ref"_a, "Kp_power_ctrl"_a, - "Ki_power_ctrl"_a, "Kp_curr_ctrl"_a, "Ki_curr_ctrl"_a) + "Ki_power_ctrl"_a, "Kp_curr_ctrl"_a, "Ki_curr_ctrl"_a, + "i_ref_n_d"_a = 0.0, "i_ref_n_q"_a = 0.0) .def("connect", &CPS::DP::Ph3::AvVoltSourceInverterStateSpace::connect) .def_property_readonly("x", createAttributeGetter("x")) .def_property_readonly("vc_d", createAttributeGetter("vc_d")) .def_property_readonly("vc_q", createAttributeGetter("vc_q")) .def_property_readonly("irc_d", createAttributeGetter("irc_d")) .def_property_readonly("irc_q", createAttributeGetter("irc_q")) + .def_property_readonly("irc_n_d", + createAttributeGetter("irc_n_d")) + .def_property_readonly("irc_n_q", + createAttributeGetter("irc_n_q")) .def_property_readonly("p_inst", createAttributeGetter("p_inst")) .def_property_readonly("q_inst", From fc34b08bae2bd1db6111e0691b3c954de64433e7 Mon Sep 17 00:00:00 2001 From: Leonardo Carreras Date: Thu, 23 Jul 2026 23:35:23 +0200 Subject: [PATCH 2/9] Make DP::Ph3 AvVSI negative-sequence control opt-in and demo both modes in the example Signed-off-by: Leonardo Carreras --- .../DP_Ph3_AvVoltSourceInverterStateSpace.h | 41 ++- .../DP_Ph3_AvVoltSourceInverterStateSpace.cpp | 240 ++++++++++-------- .../DP_Ph3_AvVoltSourceInverterStateSpace.cpp | 11 +- dpsim/src/pybind/DPComponents.cpp | 10 +- 4 files changed, 160 insertions(+), 142 deletions(-) diff --git a/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h b/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h index 0ebc6a1341..e180809e0e 100644 --- a/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h +++ b/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h @@ -9,12 +9,13 @@ namespace CPS { namespace DP { namespace Ph3 { -/// Averaged grid-following VSI SSN port of EMT::Ph3::AvVoltSourceInverterStateSpace: 10 real control states (single PLL, positive-sequence dq frame plus a negative-sequence current-control loop, gamma_nd/gamma_nq) plus 6 complex per-phase envelope states (Vc_a/b/c, If_a/b/c). +/// @brief Averaged grid-following VSI (SSN), optional dual-sequence current control. class AvVoltSourceInverterStateSpace final : public MixedVTypeVariableSSNComp, public SharedFactory { private: - enum StateIndex : Int { + /// Real control-state indices; GammaND/GammaNQ used only when the negative-sequence loop is enabled. + enum ControlStateIndex : Int { Psi = 0, PhiPLL = 1, PFiltered = 2, @@ -24,25 +25,17 @@ class AvVoltSourceInverterStateSpace final GammaD = 6, GammaQ = 7, GammaND = 8, - GammaNQ = 9, - VcARe = 10, - VcAIm = 11, - VcBRe = 12, - VcBIm = 13, - VcCRe = 14, - VcCIm = 15, - IfARe = 16, - IfAIm = 17, - IfBRe = 18, - IfBIm = 19, - IfCRe = 20, - IfCIm = 21 + GammaNQ = 9 }; - static constexpr Int mVcReCol[3] = {VcARe, VcBRe, VcCRe}; - static constexpr Int mVcImCol[3] = {VcAIm, VcBIm, VcCIm}; - static constexpr Int mIfReCol[3] = {IfARe, IfBRe, IfCRe}; - static constexpr Int mIfImCol[3] = {IfAIm, IfBIm, IfCIm}; + /// Enables the baseband negative-sequence current-control loop (+2 states). + const Bool mEnableNegSeqControl; + + /// Per-phase envelope column indices, set in the constructor. + Int mVcReCol[3]; + Int mVcImCol[3]; + Int mIfReCol[3]; + Int mIfImCol[3]; static constexpr Int mUReCol[3] = {0, 2, 4}; static constexpr Int mUImCol[3] = {1, 3, 5}; @@ -90,11 +83,15 @@ class AvVoltSourceInverterStateSpace final public: using SharedFactory::make; + /// @param enableNegSeqControl adds the baseband negative-sequence current-control loop (+2 states); positive-sequence-only when false. AvVoltSourceInverterStateSpace(String uid, String name, - Logger::Level logLevel = Logger::Level::off); + Logger::Level logLevel = Logger::Level::off, + Bool enableNegSeqControl = false); AvVoltSourceInverterStateSpace(String name, - Logger::Level logLevel = Logger::Level::off) - : AvVoltSourceInverterStateSpace(name, name, logLevel) {} + Logger::Level logLevel = Logger::Level::off, + Bool enableNegSeqControl = false) + : AvVoltSourceInverterStateSpace(name, name, logLevel, + enableNegSeqControl) {} void setParameters(Real lf, Real cf, Real rf, Real rc, Real omegaN, Real kpPLL, Real kiPLL, Real omegaCutoff, Real pRef, diff --git a/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp b/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp index 56f0b01fb5..ab133e594e 100644 --- a/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp +++ b/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp @@ -15,12 +15,14 @@ const Real K32 = std::sqrt(1.5); } // namespace DP::Ph3::AvVoltSourceInverterStateSpace::AvVoltSourceInverterStateSpace( - String uid, String name, Logger::Level logLevel) - : MixedVTypeVariableSSNComp(uid, name, 10, 6, logLevel), mLf(0.0), mCf(0.0), - mRf(0.0), mRc(0.0), mOmegaN(0.0), mKpPLL(0.0), mKiPLL(0.0), - mOmegaCutoff(0.0), mPRef(0.0), mQRef(0.0), mKpPowerCtrl(0.0), - mKiPowerCtrl(0.0), mKpCurrCtrl(0.0), mKiCurrCtrl(0.0), mIRefNd(0.0), - mIRefNq(0.0), mVcD(mAttributes->create("vc_d")), + String uid, String name, Logger::Level logLevel, Bool enableNegSeqControl) + : MixedVTypeVariableSSNComp(uid, name, enableNegSeqControl ? 10 : 8, 6, + logLevel), + mEnableNegSeqControl(enableNegSeqControl), mLf(0.0), mCf(0.0), mRf(0.0), + mRc(0.0), mOmegaN(0.0), mKpPLL(0.0), mKiPLL(0.0), mOmegaCutoff(0.0), + mPRef(0.0), mQRef(0.0), mKpPowerCtrl(0.0), mKiPowerCtrl(0.0), + mKpCurrCtrl(0.0), mKiCurrCtrl(0.0), mIRefNd(0.0), mIRefNq(0.0), + mVcD(mAttributes->create("vc_d")), mVcQ(mAttributes->create("vc_q")), mIrcD(mAttributes->create("irc_d")), mIrcQ(mAttributes->create("irc_q")), @@ -29,6 +31,15 @@ DP::Ph3::AvVoltSourceInverterStateSpace::AvVoltSourceInverterStateSpace( mOmegaPLL(mAttributes->create("omega_pll")), mIrcNd(mAttributes->create("irc_n_d")), mIrcNq(mAttributes->create("irc_n_q")) { + // Envelope states follow the control block, whose length (8 or 10) is set by + // the negative-sequence option, so their indices are computed here. + for (Int p = 0; p < 3; ++p) { + mVcReCol[p] = mRealStateCount + 2 * p; + mVcImCol[p] = mRealStateCount + 2 * p + 1; + mIfReCol[p] = mRealStateCount + 6 + 2 * p; + mIfImCol[p] = mRealStateCount + 6 + 2 * p + 1; + } + **mVcD = 0.0; **mVcQ = 0.0; **mIrcD = 0.0; @@ -117,17 +128,17 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( const Real phiQ = x(PhiQ, 0); const Real gammaD = x(GammaD, 0); const Real gammaQ = x(GammaQ, 0); - const Real gammaND = x(GammaND, 0); - const Real gammaNQ = x(GammaNQ, 0); + // GammaND/GammaNQ share indices 8/9 with envelope states when disabled, so read only when enabled. + const Real gammaND = mEnableNegSeqControl ? x(GammaND, 0) : 0.0; + const Real gammaNQ = mEnableNegSeqControl ? x(GammaNQ, 0) : 0.0; const Complex j(0.0, 1.0); - const Complex vc[3] = {Complex(x(VcARe, 0), x(VcAIm, 0)), - Complex(x(VcBRe, 0), x(VcBIm, 0)), - Complex(x(VcCRe, 0), x(VcCIm, 0))}; - const Complex iF[3] = {Complex(x(IfARe, 0), x(IfAIm, 0)), - Complex(x(IfBRe, 0), x(IfBIm, 0)), - Complex(x(IfCRe, 0), x(IfCIm, 0))}; + Complex vc[3], iF[3]; + for (Int p = 0; p < 3; ++p) { + vc[p] = Complex(x(mVcReCol[p], 0), x(mVcImCol[p], 0)); + iF[p] = Complex(x(mIfReCol[p], 0), x(mIfImCol[p], 0)); + } const Complex uEnv[3] = {Complex(u(0, 0), u(1, 0)), Complex(u(2, 0), u(3, 0)), Complex(u(4, 0), u(5, 0))}; @@ -183,31 +194,33 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( // Positive-sequence bridge-voltage reference, distributed via the inverse Park. const Complex vRefEnv0 = K23 * vRefDQ * expJPsi; - // Negative-sequence measurement: conjugate projection, rotate by e^{+j*psi}, conjugate -> baseband ircNDQ. - Complex nV(0.0, 0.0), nU(0.0, 0.0); - for (Int p = 0; p < 3; ++p) { - nV += redistFactor[p] * vc[p]; - nU += redistFactor[p] * uEnv[p]; + // Negative-sequence measurement + PI control (baseband +j*psi loop); zeros stay + // inert when disabled so the positive-only path keeps 20 states. hIrcN is the + // conj(vc_p) coefficient of ircNDQ (the conj flips imaginary-part signs vs the positive loop). + Complex ircNDQ(0.0, 0.0), vRefNDQ(0.0, 0.0), vRefNEnv0(0.0, 0.0); + Real ircND = 0.0, ircNQ = 0.0; + Complex hIrcN[3] = {Complex(0.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0)}; + if (mEnableNegSeqControl) { + Complex nV(0.0, 0.0), nU(0.0, 0.0); + for (Int p = 0; p < 3; ++p) { + nV += redistFactor[p] * vc[p]; + nU += redistFactor[p] * uEnv[p]; + } + const Complex nI = (nV - nU) / mRc; + ircNDQ = 0.5 * K23 * expJPsi * std::conj(nI); + ircND = ircNDQ.real(); + ircNQ = ircNDQ.imag(); + for (Int p = 0; p < 3; ++p) + hIrcN[p] = 0.5 * K23 * expJPsi * projCoeff[p] / mRc; + const Real vRefNd = + -mKpCurrCtrl * ircND + mKiCurrCtrl * gammaND + mKpCurrCtrl * mIRefNd; + const Real vRefNq = + -mKpCurrCtrl * ircNQ + mKiCurrCtrl * gammaNQ + mKpCurrCtrl * mIRefNq; + vRefNDQ = Complex(vRefNd, vRefNq); + vRefNEnv0 = K23 * std::conj(vRefNDQ) * expJPsi; } - const Complex nI = (nV - nU) / mRc; - const Complex ircNDQ = 0.5 * K23 * expJPsi * std::conj(nI); - const Real ircND = ircNDQ.real(); - const Real ircNQ = ircNDQ.imag(); - - // Coefficient of conj(vc_p) in ircNDQ; the conjugate flips the imaginary-part derivative signs vs the positive loop. - Complex hIrcN[3]; - for (Int p = 0; p < 3; ++p) - hIrcN[p] = 0.5 * K23 * expJPsi * projCoeff[p] / mRc; - - // PI current control (positive-loop gains reused), then inverse negative Park. - const Real vRefNd = - -mKpCurrCtrl * ircND + mKiCurrCtrl * gammaND + mKpCurrCtrl * mIRefNd; - const Real vRefNq = - -mKpCurrCtrl * ircNQ + mKiCurrCtrl * gammaNQ + mKpCurrCtrl * mIRefNq; - const Complex vRefNDQ(vRefNd, vRefNq); - const Complex vRefNEnv0 = K23 * std::conj(vRefNDQ) * expJPsi; - // Total reference: positive (redistFactor) + sequence-orthogonal negative (projCoeff) injection. + // Total reference: positive (redistFactor) + sequence-orthogonal negative (projCoeff) injection (zero if disabled). Complex vRef[3]; for (Int p = 0; p < 3; ++p) vRef[p] = redistFactor[p] * vRefEnv0 + projCoeff[p] * vRefNEnv0; @@ -220,7 +233,7 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( ifDot[p] = (vRef[p] - vc[p] - mRf * iF[p]) / mLf - j * mOmegaN * iF[p]; } - Matrix f = Matrix::Zero(22, 1); + Matrix f = Matrix::Zero(stateSize(), 1); f(Psi, 0) = mKpPLL * vcQ + mKiPLL * phiPLL; f(PhiPLL, 0) = vcQ; f(PFiltered, 0) = mOmegaCutoff * (pInst - pF); @@ -229,24 +242,20 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( f(PhiQ, 0) = qF - mQRef; f(GammaD, 0) = iRefD - ircD; f(GammaQ, 0) = iRefQ - ircQ; - f(GammaND, 0) = mIRefNd - ircND; - f(GammaNQ, 0) = mIRefNq - ircNQ; - f(VcARe, 0) = vcDot[0].real(); - f(VcAIm, 0) = vcDot[0].imag(); - f(VcBRe, 0) = vcDot[1].real(); - f(VcBIm, 0) = vcDot[1].imag(); - f(VcCRe, 0) = vcDot[2].real(); - f(VcCIm, 0) = vcDot[2].imag(); - f(IfARe, 0) = ifDot[0].real(); - f(IfAIm, 0) = ifDot[0].imag(); - f(IfBRe, 0) = ifDot[1].real(); - f(IfBIm, 0) = ifDot[1].imag(); - f(IfCRe, 0) = ifDot[2].real(); - f(IfCIm, 0) = ifDot[2].imag(); + if (mEnableNegSeqControl) { + f(GammaND, 0) = mIRefNd - ircND; + f(GammaNQ, 0) = mIRefNq - ircNQ; + } + for (Int p = 0; p < 3; ++p) { + f(mVcReCol[p], 0) = vcDot[p].real(); + f(mVcImCol[p], 0) = vcDot[p].imag(); + f(mIfReCol[p], 0) = ifDot[p].real(); + f(mIfImCol[p], 0) = ifDot[p].imag(); + } // Analytic Jacobian A = df/dx, B = df/du. - A = Matrix::Zero(22, 22); - B = Matrix::Zero(22, 6); + A = Matrix::Zero(stateSize(), stateSize()); + B = Matrix::Zero(stateSize(), 6); // PLL rows: only vcQ feeds them; d(vcQ)/dpsi = -vcD (d(vcDQ)/dpsi = -j*vcDQ). A(Psi, Psi) = mKpPLL * (-vcD); @@ -314,17 +323,19 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( // Negative-sequence current integrators: gammaND_dot = iRefNd - ircND, etc. // psi enters via rotN = e^{+j*psi}: d(ircND)/dpsi = -ircNQ, d(ircNQ)/dpsi = ircND. - A(GammaND, Psi) = ircNQ; - A(GammaNQ, Psi) = -ircND; - for (Int p = 0; p < 3; ++p) { - A(GammaND, mVcReCol[p]) = -hIrcN[p].real(); - A(GammaND, mVcImCol[p]) = -hIrcN[p].imag(); - A(GammaNQ, mVcReCol[p]) = -hIrcN[p].imag(); - A(GammaNQ, mVcImCol[p]) = hIrcN[p].real(); - B(GammaND, mUReCol[p]) = hIrcN[p].real(); - B(GammaND, mUImCol[p]) = hIrcN[p].imag(); - B(GammaNQ, mUReCol[p]) = hIrcN[p].imag(); - B(GammaNQ, mUImCol[p]) = -hIrcN[p].real(); + if (mEnableNegSeqControl) { + A(GammaND, Psi) = ircNQ; + A(GammaNQ, Psi) = -ircND; + for (Int p = 0; p < 3; ++p) { + A(GammaND, mVcReCol[p]) = -hIrcN[p].real(); + A(GammaND, mVcImCol[p]) = -hIrcN[p].imag(); + A(GammaNQ, mVcReCol[p]) = -hIrcN[p].imag(); + A(GammaNQ, mVcImCol[p]) = hIrcN[p].real(); + B(GammaND, mUReCol[p]) = hIrcN[p].real(); + B(GammaND, mUImCol[p]) = hIrcN[p].imag(); + B(GammaNQ, mUReCol[p]) = hIrcN[p].imag(); + B(GammaNQ, mUImCol[p]) = -hIrcN[p].real(); + } } // Filter capacitor rows (Vc_dot), fully decoupled per phase. @@ -369,22 +380,25 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( dVRefEnv0UIm[p] = K23 * expJPsi * (-mKpCurrCtrl * j * gIrcU[p]); } - // Negative injection vRefNEnv0 = K23*conj(vRefNDQ)*expJPsi: derivatives wrt psi and the two negative-loop states. - const Complex dVRefNEnv0DPsi = - j * K23 * expJPsi * - (mKpCurrCtrl * std::conj(ircNDQ) + std::conj(vRefNDQ)); - const Complex dVRefNEnv0DGammaND = K23 * expJPsi * mKiCurrCtrl; - const Complex dVRefNEnv0DGammaNQ = -j * K23 * expJPsi * mKiCurrCtrl; - - // d(vRefNEnv0)/d(vc_p), d(u_p) via conj(vRefNDQ)'s -kpCurrCtrl*ircND/Q term (conjugate-of-conjugate). - Complex dVRefNEnv0VcRe[3], dVRefNEnv0VcIm[3], dVRefNEnv0URe[3], - dVRefNEnv0UIm[3]; - for (Int p = 0; p < 3; ++p) { - const Complex base = mKpCurrCtrl * std::conj(hIrcN[p]) * K23 * expJPsi; - dVRefNEnv0VcRe[p] = -base; - dVRefNEnv0VcIm[p] = -j * base; - dVRefNEnv0URe[p] = base; - dVRefNEnv0UIm[p] = j * base; + // Negative injection vRefNEnv0 = K23*conj(vRefNDQ)*expJPsi: derivatives wrt psi, + // the two negative-loop states, and vc_p/u_p (via conj(vRefNDQ)'s -kpCurrCtrl*ircN + // term). All zero and unused when the negative loop is disabled. + Complex dVRefNEnv0DPsi(0.0, 0.0), dVRefNEnv0DGammaND(0.0, 0.0), + dVRefNEnv0DGammaNQ(0.0, 0.0); + Complex dVRefNEnv0VcRe[3] = {}, dVRefNEnv0VcIm[3] = {}, dVRefNEnv0URe[3] = {}, + dVRefNEnv0UIm[3] = {}; + if (mEnableNegSeqControl) { + dVRefNEnv0DPsi = j * K23 * expJPsi * + (mKpCurrCtrl * std::conj(ircNDQ) + std::conj(vRefNDQ)); + dVRefNEnv0DGammaND = K23 * expJPsi * mKiCurrCtrl; + dVRefNEnv0DGammaNQ = -j * K23 * expJPsi * mKiCurrCtrl; + for (Int p = 0; p < 3; ++p) { + const Complex base = mKpCurrCtrl * std::conj(hIrcN[p]) * K23 * expJPsi; + dVRefNEnv0VcRe[p] = -base; + dVRefNEnv0VcIm[p] = -j * base; + dVRefNEnv0URe[p] = base; + dVRefNEnv0UIm[p] = j * base; + } } for (Int pOut = 0; pOut < 3; ++pOut) { @@ -422,29 +436,31 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( } // Negative-sequence injection coupling: vRef_p += projCoeff[pOut]*vRefNEnv0. - const Complex dNPsi = projCoeff[pOut] * dVRefNEnv0DPsi; - A(reRow, Psi) += dNPsi.real() / mLf; - A(imRow, Psi) += dNPsi.imag() / mLf; - const Complex dNGammaND = projCoeff[pOut] * dVRefNEnv0DGammaND; - A(reRow, GammaND) += dNGammaND.real() / mLf; - A(imRow, GammaND) += dNGammaND.imag() / mLf; - const Complex dNGammaNQ = projCoeff[pOut] * dVRefNEnv0DGammaNQ; - A(reRow, GammaNQ) += dNGammaNQ.real() / mLf; - A(imRow, GammaNQ) += dNGammaNQ.imag() / mLf; - for (Int pSrc = 0; pSrc < 3; ++pSrc) { - const Complex dNVcRe = projCoeff[pOut] * dVRefNEnv0VcRe[pSrc]; - const Complex dNVcIm = projCoeff[pOut] * dVRefNEnv0VcIm[pSrc]; - A(reRow, mVcReCol[pSrc]) += dNVcRe.real() / mLf; - A(imRow, mVcReCol[pSrc]) += dNVcRe.imag() / mLf; - A(reRow, mVcImCol[pSrc]) += dNVcIm.real() / mLf; - A(imRow, mVcImCol[pSrc]) += dNVcIm.imag() / mLf; - - const Complex dNURe = projCoeff[pOut] * dVRefNEnv0URe[pSrc]; - const Complex dNUIm = projCoeff[pOut] * dVRefNEnv0UIm[pSrc]; - B(reRow, mUReCol[pSrc]) += dNURe.real() / mLf; - B(imRow, mUReCol[pSrc]) += dNURe.imag() / mLf; - B(reRow, mUImCol[pSrc]) += dNUIm.real() / mLf; - B(imRow, mUImCol[pSrc]) += dNUIm.imag() / mLf; + if (mEnableNegSeqControl) { + const Complex dNPsi = projCoeff[pOut] * dVRefNEnv0DPsi; + A(reRow, Psi) += dNPsi.real() / mLf; + A(imRow, Psi) += dNPsi.imag() / mLf; + const Complex dNGammaND = projCoeff[pOut] * dVRefNEnv0DGammaND; + A(reRow, GammaND) += dNGammaND.real() / mLf; + A(imRow, GammaND) += dNGammaND.imag() / mLf; + const Complex dNGammaNQ = projCoeff[pOut] * dVRefNEnv0DGammaNQ; + A(reRow, GammaNQ) += dNGammaNQ.real() / mLf; + A(imRow, GammaNQ) += dNGammaNQ.imag() / mLf; + for (Int pSrc = 0; pSrc < 3; ++pSrc) { + const Complex dNVcRe = projCoeff[pOut] * dVRefNEnv0VcRe[pSrc]; + const Complex dNVcIm = projCoeff[pOut] * dVRefNEnv0VcIm[pSrc]; + A(reRow, mVcReCol[pSrc]) += dNVcRe.real() / mLf; + A(imRow, mVcReCol[pSrc]) += dNVcRe.imag() / mLf; + A(reRow, mVcImCol[pSrc]) += dNVcIm.real() / mLf; + A(imRow, mVcImCol[pSrc]) += dNVcIm.imag() / mLf; + + const Complex dNURe = projCoeff[pOut] * dVRefNEnv0URe[pSrc]; + const Complex dNUIm = projCoeff[pOut] * dVRefNEnv0UIm[pSrc]; + B(reRow, mUReCol[pSrc]) += dNURe.real() / mLf; + B(imRow, mUReCol[pSrc]) += dNURe.imag() / mLf; + B(reRow, mUImCol[pSrc]) += dNUIm.real() / mLf; + B(imRow, mUImCol[pSrc]) += dNUIm.imag() / mLf; + } } } @@ -452,7 +468,7 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( E = f - A * x - B * u; // SSN output: y_p = (u_p - vc_p)/Rc (exact, no relinearization needed). - C = Matrix::Zero(6, 22); + C = Matrix::Zero(6, stateSize()); for (Int p = 0; p < 3; ++p) { C(2 * p, mVcReCol[p]) = -1.0 / mRc; C(2 * p + 1, mVcImCol[p]) = -1.0 / mRc; @@ -501,18 +517,20 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::updateLogAttributes( nU += std::conj(projCoeff[p]) * uEnv; } const Complex pI = (pV - pU) / mRc; - const Complex nI = (nV - nU) / mRc; const Complex vcDQ = 0.5 * K23 * rot * pV; const Complex ircDQ = 0.5 * K23 * rot * pI; - const Complex ircNDQ = 0.5 * K23 * expJPsi * std::conj(nI); **mVcD = vcDQ.real(); **mVcQ = vcDQ.imag(); **mIrcD = ircDQ.real(); **mIrcQ = ircDQ.imag(); - **mIrcNd = ircNDQ.real(); - **mIrcNq = ircNDQ.imag(); + if (mEnableNegSeqControl) { + const Complex nI = (nV - nU) / mRc; + const Complex ircNDQ = 0.5 * K23 * expJPsi * std::conj(nI); + **mIrcNd = ircNDQ.real(); + **mIrcNq = ircNDQ.imag(); + } **mPInst = **mVcD * **mIrcD + **mVcQ * **mIrcQ; **mQInst = -**mVcD * **mIrcQ + **mVcQ * **mIrcD; @@ -600,9 +618,7 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::initializeFromNodesAndTerminals( x0(PhiQ, 0) = phiQ0; x0(GammaD, 0) = gammaD0; x0(GammaQ, 0) = gammaQ0; - // Balanced start: no negative sequence, so the negative-loop integrators seed at zero. - x0(GammaND, 0) = 0.0; - x0(GammaNQ, 0) = 0.0; + // Balanced start: negative-loop integrators seed at zero (Zero() already, indices exist only when enabled). for (Int p = 0; p < 3; ++p) { x0(mVcReCol[p], 0) = vcAbc(p, 0).real(); diff --git a/dpsim/examples/cxx/Components/DP_Ph3_AvVoltSourceInverterStateSpace.cpp b/dpsim/examples/cxx/Components/DP_Ph3_AvVoltSourceInverterStateSpace.cpp index b8b205d5a1..e0d525c057 100644 --- a/dpsim/examples/cxx/Components/DP_Ph3_AvVoltSourceInverterStateSpace.cpp +++ b/dpsim/examples/cxx/Components/DP_Ph3_AvVoltSourceInverterStateSpace.cpp @@ -23,7 +23,10 @@ class Example_AvVoltSourceInverterStateSpace { const String simNameBase = "DP_Ph3_AvVoltSourceInverterStateSpace"; const auto systemPF = runPowerFlow(simNameBase + "_PF"); - runDPSimulation(simNameBase + "_DP", systemPF); + // Same SLG scenario run positive-sequence-only (20 states) and dual-sequence + // (22 states), so the two responses can be compared directly. + runDPSimulation(simNameBase + "_DP_posSeq", systemPF, false); + runDPSimulation(simNameBase + "_DP_dualSeq", systemPF, true); } private: @@ -86,8 +89,8 @@ class Example_AvVoltSourceInverterStateSpace { return system; } - void runDPSimulation(const String &simName, - const SystemTopology &systemPF) const { + void runDPSimulation(const String &simName, const SystemTopology &systemPF, + Bool enableNegSeqControl) const { Logger::setLogDir("logs/" + simName); auto nGrid = SimNode::make("nGrid", PhaseType::ABC); @@ -103,7 +106,7 @@ class Example_AvVoltSourceInverterStateSpace { Math::singlePhaseParameterToThreePhase(mLineConductance)); auto inverter = DP::Ph3::AvVoltSourceInverterStateSpace::make( - "INV_SSN_PLL", Logger::Level::info); + "INV_SSN_PLL", Logger::Level::info, enableNegSeqControl); inverter->setParameters(mLf, mCf, mRf, mRc, mSystemOmega, mKpPLL, mKiPLL, mOmegaCutoff, mPref, mQref, mKpPowerCtrl, mKiPowerCtrl, mKpCurrCtrl, mKiCurrCtrl); diff --git a/dpsim/src/pybind/DPComponents.cpp b/dpsim/src/pybind/DPComponents.cpp index 33ab8baa3a..aa6c1e393d 100644 --- a/dpsim/src/pybind/DPComponents.cpp +++ b/dpsim/src/pybind/DPComponents.cpp @@ -668,10 +668,12 @@ void addDPPh3Components(py::module_ mDPPh3) { std::shared_ptr, CPS::SimPowerComp>( mDPPh3, "AvVoltSourceInverterStateSpace", py::multiple_inheritance()) - .def(py::init(), "name"_a, - "loglevel"_a = CPS::Logger::Level::off) - .def(py::init(), "uid"_a, - "name"_a, "loglevel"_a = CPS::Logger::Level::off) + .def(py::init(), "name"_a, + "loglevel"_a = CPS::Logger::Level::off, + "enable_neg_seq_control"_a = false) + .def(py::init(), + "uid"_a, "name"_a, "loglevel"_a = CPS::Logger::Level::off, + "enable_neg_seq_control"_a = false) .def("set_parameters", &CPS::DP::Ph3::AvVoltSourceInverterStateSpace::setParameters, "Lf"_a, "Cf"_a, "Rf"_a, "Rc"_a, "omega_n"_a, "Kp_pll"_a, "Ki_pll"_a, From 53f684c3628a8fcd8eca6ab3a664cfe6d0efa1ca Mon Sep 17 00:00:00 2001 From: Leonardo Carreras Date: Fri, 24 Jul 2026 00:03:27 +0200 Subject: [PATCH 3/9] Append negative-sequence states at the tail so envelope indices stay fixed Signed-off-by: Leonardo Carreras --- .../DP_Ph3_AvVoltSourceInverterStateSpace.h | 30 +++++++++++++------ .../DP_Ph3_AvVoltSourceInverterStateSpace.cpp | 9 ------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h b/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h index e180809e0e..1403a21b1d 100644 --- a/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h +++ b/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h @@ -14,8 +14,9 @@ class AvVoltSourceInverterStateSpace final : public MixedVTypeVariableSSNComp, public SharedFactory { private: - /// Real control-state indices; GammaND/GammaNQ used only when the negative-sequence loop is enabled. - enum ControlStateIndex : Int { + /// Packed state indices. GammaND/GammaNQ are appended last so the envelope + /// block stays fixed and only exist when the negative-sequence loop is enabled. + enum StateIndex : Int { Psi = 0, PhiPLL = 1, PFiltered = 2, @@ -24,18 +25,29 @@ class AvVoltSourceInverterStateSpace final PhiQ = 5, GammaD = 6, GammaQ = 7, - GammaND = 8, - GammaNQ = 9 + VcARe = 8, + VcAIm = 9, + VcBRe = 10, + VcBIm = 11, + VcCRe = 12, + VcCIm = 13, + IfARe = 14, + IfAIm = 15, + IfBRe = 16, + IfBIm = 17, + IfCRe = 18, + IfCIm = 19, + GammaND = 20, + GammaNQ = 21 }; /// Enables the baseband negative-sequence current-control loop (+2 states). const Bool mEnableNegSeqControl; - /// Per-phase envelope column indices, set in the constructor. - Int mVcReCol[3]; - Int mVcImCol[3]; - Int mIfReCol[3]; - Int mIfImCol[3]; + static constexpr Int mVcReCol[3] = {VcARe, VcBRe, VcCRe}; + static constexpr Int mVcImCol[3] = {VcAIm, VcBIm, VcCIm}; + static constexpr Int mIfReCol[3] = {IfARe, IfBRe, IfCRe}; + static constexpr Int mIfImCol[3] = {IfAIm, IfBIm, IfCIm}; static constexpr Int mUReCol[3] = {0, 2, 4}; static constexpr Int mUImCol[3] = {1, 3, 5}; diff --git a/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp b/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp index ab133e594e..c3d8a49566 100644 --- a/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp +++ b/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp @@ -31,15 +31,6 @@ DP::Ph3::AvVoltSourceInverterStateSpace::AvVoltSourceInverterStateSpace( mOmegaPLL(mAttributes->create("omega_pll")), mIrcNd(mAttributes->create("irc_n_d")), mIrcNq(mAttributes->create("irc_n_q")) { - // Envelope states follow the control block, whose length (8 or 10) is set by - // the negative-sequence option, so their indices are computed here. - for (Int p = 0; p < 3; ++p) { - mVcReCol[p] = mRealStateCount + 2 * p; - mVcImCol[p] = mRealStateCount + 2 * p + 1; - mIfReCol[p] = mRealStateCount + 6 + 2 * p; - mIfImCol[p] = mRealStateCount + 6 + 2 * p + 1; - } - **mVcD = 0.0; **mVcQ = 0.0; **mIrcD = 0.0; From 99be18ae0e7e4b8be2835abe4426725cd5701556 Mon Sep 17 00:00:00 2001 From: Leonardo Carreras Date: Tue, 28 Jul 2026 14:11:01 +0200 Subject: [PATCH 4/9] Group DP Ph3 AvVSI control states contiguously after the envelope block Signed-off-by: Leonardo Carreras --- .../en/docs/Models/power-electronics.md | 16 +++---- .../DP_Ph3_AvVoltSourceInverterStateSpace.h | 44 +++++++++---------- .../DP/DP_Ph3_MixedVTypeVariableSSNComp.h | 3 +- .../DP_Ph3_AvVoltSourceInverterStateSpace.cpp | 16 +++---- .../DP_Ph3_AvVoltSourceInverterStateSpace.cpp | 3 +- 5 files changed, 40 insertions(+), 42 deletions(-) diff --git a/docs/hugo/content/en/docs/Models/power-electronics.md b/docs/hugo/content/en/docs/Models/power-electronics.md index 87367658ab..0e04ce8e42 100644 --- a/docs/hugo/content/en/docs/Models/power-electronics.md +++ b/docs/hugo/content/en/docs/Models/power-electronics.md @@ -414,11 +414,17 @@ The terminal input is the PCC voltage envelope of the three phases, u = \begin{bmatrix} U_a & U_b & U_c \end{bmatrix}^\top , ``` -and the state vector concatenates the 8 real control states ahead of the 6 complex per-phase envelopes, +and the state vector concatenates the 6 complex per-phase envelopes ahead of the 8 real control states, keeping the carrier-band and baseband blocks separate, ```math \mathbf{x} = \big[\, +V_{c,a} \;\; +V_{c,b} \;\; +V_{c,c} \;\; +I_{f,a} \;\; +I_{f,b} \;\; +I_{f,c} \;\; \psi \;\; \phi_{\mathrm{PLL}} \;\; P \;\; @@ -426,13 +432,7 @@ Q \;\; \phi_d \;\; \phi_q \;\; \gamma_d \;\; -\gamma_q \;\; -V_{c,a} \;\; -V_{c,b} \;\; -V_{c,c} \;\; -I_{f,a} \;\; -I_{f,b} \;\; -I_{f,c} +\gamma_q \,\big]^\top , ``` diff --git a/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h b/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h index 1403a21b1d..74e995831a 100644 --- a/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h +++ b/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h @@ -14,29 +14,29 @@ class AvVoltSourceInverterStateSpace final : public MixedVTypeVariableSSNComp, public SharedFactory { private: - /// Packed state indices. GammaND/GammaNQ are appended last so the envelope - /// block stays fixed and only exist when the negative-sequence loop is enabled. + /// Packed state indices: envelope block, then the contiguous control block. + /// GammaND/GammaNQ exist only when the negative-sequence loop is enabled. enum StateIndex : Int { - Psi = 0, - PhiPLL = 1, - PFiltered = 2, - QFiltered = 3, - PhiD = 4, - PhiQ = 5, - GammaD = 6, - GammaQ = 7, - VcARe = 8, - VcAIm = 9, - VcBRe = 10, - VcBIm = 11, - VcCRe = 12, - VcCIm = 13, - IfARe = 14, - IfAIm = 15, - IfBRe = 16, - IfBIm = 17, - IfCRe = 18, - IfCIm = 19, + VcARe = 0, + VcAIm = 1, + VcBRe = 2, + VcBIm = 3, + VcCRe = 4, + VcCIm = 5, + IfARe = 6, + IfAIm = 7, + IfBRe = 8, + IfBIm = 9, + IfCRe = 10, + IfCIm = 11, + Psi = 12, + PhiPLL = 13, + PFiltered = 14, + QFiltered = 15, + PhiD = 16, + PhiQ = 17, + GammaD = 18, + GammaQ = 19, GammaND = 20, GammaNQ = 21 }; diff --git a/dpsim-models/include/dpsim-models/DP/DP_Ph3_MixedVTypeVariableSSNComp.h b/dpsim-models/include/dpsim-models/DP/DP_Ph3_MixedVTypeVariableSSNComp.h index 12190590f1..9784147761 100644 --- a/dpsim-models/include/dpsim-models/DP/DP_Ph3_MixedVTypeVariableSSNComp.h +++ b/dpsim-models/include/dpsim-models/DP/DP_Ph3_MixedVTypeVariableSSNComp.h @@ -48,7 +48,8 @@ class MixedVTypeVariableSSNComp : public MNASimPowerComp, MatrixComp mW; MatrixComp mYHist; - /// Packed real state: [realStates..., Re(cplxState0), Im(cplxState0), ...]. + /// Packed real state of size stateSize(), each envelope taking a [Re,Im] + /// pair. Never sliced here: the derived component chooses the ordering. const Attribute::Ptr mX; MixedVTypeVariableSSNComp(String uid, String name, Int realStateCount, diff --git a/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp b/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp index c3d8a49566..dd28e4d116 100644 --- a/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp +++ b/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp @@ -119,7 +119,7 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( const Real phiQ = x(PhiQ, 0); const Real gammaD = x(GammaD, 0); const Real gammaQ = x(GammaQ, 0); - // GammaND/GammaNQ share indices 8/9 with envelope states when disabled, so read only when enabled. + // GammaND/GammaNQ are past the end of the positive-only state vector. const Real gammaND = mEnableNegSeqControl ? x(GammaND, 0) : 0.0; const Real gammaNQ = mEnableNegSeqControl ? x(GammaNQ, 0) : 0.0; @@ -185,9 +185,8 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( // Positive-sequence bridge-voltage reference, distributed via the inverse Park. const Complex vRefEnv0 = K23 * vRefDQ * expJPsi; - // Negative-sequence measurement + PI control (baseband +j*psi loop); zeros stay - // inert when disabled so the positive-only path keeps 20 states. hIrcN is the - // conj(vc_p) coefficient of ircNDQ (the conj flips imaginary-part signs vs the positive loop). + // Negative-sequence measurement + PI control (baseband +j*psi loop). + // hIrcN[p] is the conj(vc_p) coefficient of ircNDQ. Complex ircNDQ(0.0, 0.0), vRefNDQ(0.0, 0.0), vRefNEnv0(0.0, 0.0); Real ircND = 0.0, ircNQ = 0.0; Complex hIrcN[3] = {Complex(0.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0)}; @@ -211,7 +210,7 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( vRefNEnv0 = K23 * std::conj(vRefNDQ) * expJPsi; } - // Total reference: positive (redistFactor) + sequence-orthogonal negative (projCoeff) injection (zero if disabled). + // Total reference: positive (redistFactor) + negative (projCoeff) injection. Complex vRef[3]; for (Int p = 0; p < 3; ++p) vRef[p] = redistFactor[p] * vRefEnv0 + projCoeff[p] * vRefNEnv0; @@ -371,9 +370,8 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( dVRefEnv0UIm[p] = K23 * expJPsi * (-mKpCurrCtrl * j * gIrcU[p]); } - // Negative injection vRefNEnv0 = K23*conj(vRefNDQ)*expJPsi: derivatives wrt psi, - // the two negative-loop states, and vc_p/u_p (via conj(vRefNDQ)'s -kpCurrCtrl*ircN - // term). All zero and unused when the negative loop is disabled. + // Derivatives of vRefNEnv0 = K23*conj(vRefNDQ)*expJPsi wrt psi, the negative-loop + // states and vc_p/u_p. Complex dVRefNEnv0DPsi(0.0, 0.0), dVRefNEnv0DGammaND(0.0, 0.0), dVRefNEnv0DGammaNQ(0.0, 0.0); Complex dVRefNEnv0VcRe[3] = {}, dVRefNEnv0VcIm[3] = {}, dVRefNEnv0URe[3] = {}, @@ -609,7 +607,7 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::initializeFromNodesAndTerminals( x0(PhiQ, 0) = phiQ0; x0(GammaD, 0) = gammaD0; x0(GammaQ, 0) = gammaQ0; - // Balanced start: negative-loop integrators seed at zero (Zero() already, indices exist only when enabled). + // Balanced start: the negative-loop integrators stay at their Zero() seed. for (Int p = 0; p < 3; ++p) { x0(mVcReCol[p], 0) = vcAbc(p, 0).real(); diff --git a/dpsim/examples/cxx/Components/DP_Ph3_AvVoltSourceInverterStateSpace.cpp b/dpsim/examples/cxx/Components/DP_Ph3_AvVoltSourceInverterStateSpace.cpp index e0d525c057..589d04ae4b 100644 --- a/dpsim/examples/cxx/Components/DP_Ph3_AvVoltSourceInverterStateSpace.cpp +++ b/dpsim/examples/cxx/Components/DP_Ph3_AvVoltSourceInverterStateSpace.cpp @@ -23,8 +23,7 @@ class Example_AvVoltSourceInverterStateSpace { const String simNameBase = "DP_Ph3_AvVoltSourceInverterStateSpace"; const auto systemPF = runPowerFlow(simNameBase + "_PF"); - // Same SLG scenario run positive-sequence-only (20 states) and dual-sequence - // (22 states), so the two responses can be compared directly. + // Same SLG scenario, positive-sequence-only vs dual-sequence control. runDPSimulation(simNameBase + "_DP_posSeq", systemPF, false); runDPSimulation(simNameBase + "_DP_dualSeq", systemPF, true); } From 4160b62301dc04d1ecc2a7d1bf04f81dad0d328e Mon Sep 17 00:00:00 2001 From: Leonardo Carreras Date: Tue, 28 Jul 2026 14:53:15 +0200 Subject: [PATCH 5/9] Suppress the cppcheck bool-to-pointer false positive on the new pybind flag Signed-off-by: Leonardo Carreras --- dpsim/src/pybind/DPComponents.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dpsim/src/pybind/DPComponents.cpp b/dpsim/src/pybind/DPComponents.cpp index aa6c1e393d..f62350ccc1 100644 --- a/dpsim/src/pybind/DPComponents.cpp +++ b/dpsim/src/pybind/DPComponents.cpp @@ -670,9 +670,11 @@ void addDPPh3Components(py::module_ mDPPh3) { mDPPh3, "AvVoltSourceInverterStateSpace", py::multiple_inheritance()) .def(py::init(), "name"_a, "loglevel"_a = CPS::Logger::Level::off, + // cppcheck-suppress assignBoolToPointer "enable_neg_seq_control"_a = false) .def(py::init(), "uid"_a, "name"_a, "loglevel"_a = CPS::Logger::Level::off, + // cppcheck-suppress assignBoolToPointer "enable_neg_seq_control"_a = false) .def("set_parameters", &CPS::DP::Ph3::AvVoltSourceInverterStateSpace::setParameters, "Lf"_a, From 7f30584219810891fd3b4af069d39ac01e1ec6ba Mon Sep 17 00:00:00 2001 From: Leonardo Carreras Date: Tue, 28 Jul 2026 14:54:11 +0200 Subject: [PATCH 6/9] Cover the dual-sequence path in the DP Ph3 AvVSI notebook Signed-off-by: Leonardo Carreras --- ...P_Ph3_AvVoltSourceInverterStateSpace.ipynb | 109 +++++++++++++++++- 1 file changed, 104 insertions(+), 5 deletions(-) diff --git a/examples/Notebooks/Components/DP_Ph3_AvVoltSourceInverterStateSpace.ipynb b/examples/Notebooks/Components/DP_Ph3_AvVoltSourceInverterStateSpace.ipynb index cef7000bd1..ac58db1795 100644 --- a/examples/Notebooks/Components/DP_Ph3_AvVoltSourceInverterStateSpace.ipynb +++ b/examples/Notebooks/Components/DP_Ph3_AvVoltSourceInverterStateSpace.ipynb @@ -427,7 +427,7 @@ " sim.run()\n", "\n", "\n", - "def run_dp_simulation(system_pf, closed_matrix, sim_name):\n", + "def run_dp_simulation(system_pf, closed_matrix, sim_name, enable_neg_seq_control=False):\n", " Logger.set_log_dir(\"logs/\" + sim_name)\n", "\n", " n_grid = dp.SimNode(\"nGrid\", PhaseType.ABC)\n", @@ -443,7 +443,9 @@ " three_phase_param(line_conductance),\n", " )\n", "\n", - " inverter = dp_ph3.AvVoltSourceInverterStateSpace(\"INV_SSN_PLL\")\n", + " inverter = dp_ph3.AvVoltSourceInverterStateSpace(\n", + " \"INV_SSN_PLL\", enable_neg_seq_control=enable_neg_seq_control\n", + " )\n", " set_inverter_parameters(inverter)\n", "\n", " fault = dp_ph3.Switch(\"Fault\")\n", @@ -467,6 +469,9 @@ "\n", " logger = Logger(sim_name)\n", " add_inverter_logs(logger, n_pcc, inverter, fault)\n", + " # DP only: the negative-sequence dq measurement, zero unless the loop is on.\n", + " logger.log_attribute(\"irc_n_d\", inverter.attr(\"irc_n_d\"))\n", + " logger.log_attribute(\"irc_n_q\", inverter.attr(\"irc_n_q\"))\n", "\n", " sim = Simulation(sim_name)\n", " sim.set_system(system)\n", @@ -501,11 +506,15 @@ "sim_name_dp_balanced = \"DP_Ph3_AvVoltSourceInverterStateSpace_DP_balanced\"\n", "sim_name_emt_slg = \"DP_Ph3_AvVoltSourceInverterStateSpace_EMT_slg\"\n", "sim_name_dp_slg = \"DP_Ph3_AvVoltSourceInverterStateSpace_DP_slg\"\n", + "sim_name_dp_slg_dual = \"DP_Ph3_AvVoltSourceInverterStateSpace_DP_slg_dual\"\n", "\n", "run_emt_simulation(system_pf, balanced_closed_matrix(), sim_name_emt_balanced)\n", "run_dp_simulation(system_pf, balanced_closed_matrix(), sim_name_dp_balanced)\n", "run_emt_simulation(system_pf, slg_closed_matrix(), sim_name_emt_slg)\n", - "run_dp_simulation(system_pf, slg_closed_matrix(), sim_name_dp_slg)" + "run_dp_simulation(system_pf, slg_closed_matrix(), sim_name_dp_slg)\n", + "run_dp_simulation(\n", + " system_pf, slg_closed_matrix(), sim_name_dp_slg_dual, enable_neg_seq_control=True\n", + ")" ] }, { @@ -535,6 +544,7 @@ "dp_balanced = read_result(sim_name_dp_balanced)\n", "emt_slg = read_result(sim_name_emt_slg)\n", "dp_slg = read_result(sim_name_dp_slg)\n", + "dp_slg_dual = read_result(sim_name_dp_slg_dual)\n", "\n", "time = time_vector(dp_slg)\n", "\n", @@ -552,6 +562,7 @@ "for name, df in [\n", " (sim_name_emt_slg, emt_slg),\n", " (sim_name_dp_slg, dp_slg),\n", + " (sim_name_dp_slg_dual, dp_slg_dual),\n", " (sim_name_emt_balanced, emt_balanced),\n", " (sim_name_dp_balanced, dp_balanced),\n", "]:\n", @@ -658,6 +669,93 @@ "print(\"Carry-over sanity checks passed.\")" ] }, + { + "cell_type": "markdown", + "id": "70ae246d", + "source": [ + "## Negative-sequence current control\n", + "\n", + "The optional dual-sequence loop, enabled per instance, adds a baseband\n", + "negative-sequence measurement and PI regulator on top of the positive-sequence\n", + "controller. With both negative-sequence references left at zero it acts as a\n", + "suppressor, so under the phase-A fault the negative-sequence component of the\n", + "injected current has to drop while the positive-sequence component is left\n", + "alone. Both runs share the same fault and power flow and differ only in the\n", + "flag, so pre-fault they must agree." + ], + "metadata": {} + }, + { + "cell_type": "code", + "id": "97c94b9c", + "source": [ + "alpha = np.exp(2j * np.pi / 3)\n", + "\n", + "\n", + "def sequence_components(df, name):\n", + " # Same convention as the component: positive projects with (1, a, a^2),\n", + " # negative with the conjugate set.\n", + " phase_a = complex_phase(df, name, 0)\n", + " phase_b = complex_phase(df, name, 1)\n", + " phase_c = complex_phase(df, name, 2)\n", + " positive = (phase_a + alpha * phase_b + alpha**2 * phase_c) / 3.0\n", + " negative = (phase_a + alpha**2 * phase_b + alpha * phase_c) / 3.0\n", + " return np.abs(positive), np.abs(negative)\n", + "\n", + "\n", + "pos_single, neg_single = sequence_components(dp_slg, \"i_inv\")\n", + "pos_dual, neg_dual = sequence_components(dp_slg_dual, \"i_inv\")\n", + "\n", + "irc_n_single = np.abs(\n", + " scalar_signal(dp_slg, \"irc_n_d\") + 1j * scalar_signal(dp_slg, \"irc_n_q\")\n", + ")\n", + "irc_n_dual = np.abs(\n", + " scalar_signal(dp_slg_dual, \"irc_n_d\") + 1j * scalar_signal(dp_slg_dual, \"irc_n_q\")\n", + ")\n", + "\n", + "assert not dp_slg_dual.isna().any().any(), \"Dual-sequence run contains NaN or Inf.\"\n", + "\n", + "# The measurement is only computed when the loop is enabled.\n", + "assert rms(irc_n_single) == 0.0, \"Positive-sequence-only run reports a measurement.\"\n", + "assert (\n", + " irc_n_dual[post_fault_window].mean() > 1.0\n", + "), \"Dual-sequence run measures no negative-sequence current under the fault.\"\n", + "\n", + "# Balanced pre-fault, the flag must make no difference.\n", + "pre_fault_difference = rms((neg_dual - neg_single)[pre_fault_window])\n", + "assert (\n", + " pre_fault_difference < 1e-3\n", + "), f\"Pre-fault negative sequence differs by {pre_fault_difference:.2e} A.\"\n", + "\n", + "# Under the unbalanced fault, the loop suppresses negative sequence and leaves\n", + "# positive sequence alone.\n", + "neg_single_post = neg_single[post_fault_window].mean()\n", + "neg_dual_post = neg_dual[post_fault_window].mean()\n", + "pos_single_post = pos_single[post_fault_window].mean()\n", + "pos_dual_post = pos_dual[post_fault_window].mean()\n", + "\n", + "suppression = neg_dual_post / neg_single_post\n", + "positive_drift = abs(pos_dual_post - pos_single_post) / pos_single_post\n", + "\n", + "assert suppression < 0.8, f\"Negative sequence only fell to {suppression * 100:.0f}%.\"\n", + "assert (\n", + " positive_drift < 0.05\n", + "), f\"Positive sequence moved by {positive_drift * 100:.1f}% when enabling the loop.\"\n", + "\n", + "print(\n", + " f\"Negative sequence |I|: {neg_single_post:.1f} A -> {neg_dual_post:.1f} A \"\n", + " f\"({(1.0 - suppression) * 100:.0f}% suppressed).\"\n", + ")\n", + "print(\n", + " f\"Positive sequence |I|: {pos_single_post:.1f} A -> {pos_dual_post:.1f} A \"\n", + " f\"({positive_drift * 100:.2f}% drift).\"\n", + ")\n", + "print(\"Negative-sequence current control checks passed.\")" + ], + "metadata": {}, + "execution_count": null, + "outputs": [] + }, { "cell_type": "markdown", "id": "2d969aea", @@ -678,8 +776,9 @@ "stay mirror-symmetric; EMT's dq controller re-emits the double-frequency ripple\n", "as a negative-sequence response that separates them. Hence the looser\n", "healthy-phase tolerance, and the DP phase-B curve sitting on top of DP phase-C\n", - "in the plot below. Resolving the split needs a dual-sequence formulation, out\n", - "of scope here." + "in the plot below. Resolving the split needs the dual-sequence formulation\n", + "checked above, which stays off for this comparison so both domains run the same\n", + "positive-sequence-only control." ] }, { From 4bba682fcfda9a3334a045d7f18daae95c2b354b Mon Sep 17 00:00:00 2001 From: Leonardo Carreras Date: Tue, 28 Jul 2026 14:56:02 +0200 Subject: [PATCH 7/9] Document the optional negative-sequence current control Signed-off-by: Leonardo Carreras --- .../en/docs/Models/power-electronics.md | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/docs/hugo/content/en/docs/Models/power-electronics.md b/docs/hugo/content/en/docs/Models/power-electronics.md index 0e04ce8e42..d54755442b 100644 --- a/docs/hugo/content/en/docs/Models/power-electronics.md +++ b/docs/hugo/content/en/docs/Models/power-electronics.md @@ -436,7 +436,7 @@ Q \;\; \,\big]^\top , ``` -where $\psi := \theta_{\mathrm{PLL}} - \omega_n t$ again denotes the deviation of the PLL angle from the nominal carrier phase, retained as a state to preserve relinearization accuracy. Each per-phase envelope contributes its real and imaginary parts to the packed real vector, yielding 20 real states in total. +where $\psi := \theta_{\mathrm{PLL}} - \omega_n t$ again denotes the deviation of the PLL angle from the nominal carrier phase, retained as a state to preserve relinearization accuracy. Each per-phase envelope contributes its real and imaginary parts to the packed real vector, yielding 20 real states in total, or 22 with the optional negative-sequence loop described below. The model output is the per-phase interface current injected into the MNA system, @@ -568,7 +568,7 @@ R_f I_{f,p} the phases being coupled only through the shared control chain, that is, through $V_{\mathrm{ref},p}$. -At each simulation step the nonlinear model is linearized about the current operating point into the affine state-space form, with the 8 real control states and the real and imaginary parts of the 6 complex per-phase envelopes packed into a single real 20-vector, +At each simulation step the nonlinear model is linearized about the current operating point into the affine state-space form, with the real and imaginary parts of the 6 complex per-phase envelopes and the 8 real control states packed into a single real 20-vector, ```math \dot{\mathbf{x}} @@ -590,7 +590,61 @@ At each simulation step the nonlinear model is linearized about the current oper which is subsequently discretized and stamped into the DP MNA system. -Because the controller operates in a single positive-sequence $dq$ frame, only the positive-sequence component of an unbalanced terminal is regulated. The negative-sequence response is present in the per-phase filter envelopes but is not itself a control state, and the $2\omega_n$ ripple it would otherwise induce in the $dq$ frame is therefore not represented. A dual-sequence controller with a dedicated negative-sequence frame remains the subject of future work. +In this default configuration the controller operates in a single positive-sequence $dq$ frame, so only the positive-sequence component of an unbalanced terminal is regulated. The negative-sequence response is present in the per-phase filter envelopes but is not itself a control state, and the $2\omega_n$ ripple it would otherwise induce in the $dq$ frame is therefore not represented. + +## Optional negative-sequence current control + +The component accepts an `enableNegSeqControl` constructor flag, `false` by default. Setting it adds a second, negative-sequence current-control loop alongside the positive-sequence one, giving the dual-sequence structure of Yazdani and Iravani, chapter 8. The flag exists because the two configurations answer different questions: with the loop off the model has the same 20 states and the same eigenvalue count as its `EMT::Ph3` counterpart and is the right object for a cross-domain comparison, while with it on the model gains 2 states and can regulate an unbalanced terminal. + +The negative-sequence quantities are obtained by projecting the same three envelopes onto the conjugate sequence set, + +```math +\underline{V}_c^- = V_{c,a} + a^2 V_{c,b} + a\, V_{c,c}, +\qquad +\underline{I}_{rc}^- = \frac{\underline{V}_c^- - \underline{U}^-}{R_c}. +``` + +A negative-sequence component rotates backwards relative to the PLL frame, so in envelope terms its $dq$ image follows from conjugating the projected phasor and rotating by $+\psi$ rather than $-\psi$, + +```math +I_{rc,dq}^- = \tfrac{1}{2}\sqrt{\tfrac{2}{3}}\, e^{\,j\psi}\, \overline{\underline{I}_{rc}^-} . +``` + +Both sequence images are therefore baseband quantities, and the negative-sequence loop costs only the two real integrator states $\gamma_{nd}$, $\gamma_{nq}$, with no second carrier and no $2\omega_n$ term anywhere in the model. The loop itself is the same PI structure as the positive-sequence inner loop, + +```math +\dot{\gamma}_{nd} = i_{nd,\mathrm{ref}} - i_{rc,nd}, +\qquad +\dot{\gamma}_{nq} = i_{nq,\mathrm{ref}} - i_{rc,nq}, +``` + +```math +v_{nd,\mathrm{ref}} += +K_{p,I}(i_{nd,\mathrm{ref}} - i_{rc,nd}) + +K_{i,I}\gamma_{nd}, +\qquad +v_{nq,\mathrm{ref}} += +K_{p,I}(i_{nq,\mathrm{ref}} - i_{rc,nq}) + +K_{i,I}\gamma_{nq}, +``` + +reusing the inner-loop gains $K_{p,I}$ and $K_{i,I}$. Its output is redistributed to the per-phase bridge voltages through the sequence-orthogonal set, and adds to the positive-sequence command of the previous section, + +```math +V_{\mathrm{ref},p} += +\bar{a}_p \sqrt{\tfrac{2}{3}}\, V_{\mathrm{ref},dq}\, e^{j\psi} ++ +a_p \sqrt{\tfrac{2}{3}}\, \overline{V_{\mathrm{ref},dq}^-}\, e^{j\psi}, +\qquad +a_{a/b/c} = \{1,\; a,\; a^2\}. +``` + +The two references $i_{nd,\mathrm{ref}}$ and $i_{nq,\mathrm{ref}}$ are the last two arguments of `setParameters` and default to zero, which makes the loop a negative-sequence suppressor; a non-zero pair commands a deliberate negative-sequence injection instead, as required by some unbalanced fault ride-through grid codes. The measured $i_{rc,nd}$ and $i_{rc,nq}$ are exposed as the `irc_n_d` and `irc_n_q` attributes and stay at zero when the loop is disabled. + +The state vector grows to 22 by appending the two integrators after the control block, so that the envelope and positive-sequence control indices are unaffected by the flag. Under the single-line-to-ground fault of the accompanying notebook, enabling the loop suppresses the negative-sequence component of the injected current by about 40 percent while moving the positive-sequence component by less than 0.1 percent. ## References From 2e1b1d5fb3cefaf7974453fd2d3788aef29afd2f Mon Sep 17 00:00:00 2001 From: Leonardo Carreras Date: Tue, 28 Jul 2026 15:59:47 +0200 Subject: [PATCH 8/9] Compare the negative-sequence measurement against a tolerance Signed-off-by: Leonardo Carreras --- .../Components/DP_Ph3_AvVoltSourceInverterStateSpace.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Notebooks/Components/DP_Ph3_AvVoltSourceInverterStateSpace.ipynb b/examples/Notebooks/Components/DP_Ph3_AvVoltSourceInverterStateSpace.ipynb index ac58db1795..272942e7bd 100644 --- a/examples/Notebooks/Components/DP_Ph3_AvVoltSourceInverterStateSpace.ipynb +++ b/examples/Notebooks/Components/DP_Ph3_AvVoltSourceInverterStateSpace.ipynb @@ -716,7 +716,7 @@ "assert not dp_slg_dual.isna().any().any(), \"Dual-sequence run contains NaN or Inf.\"\n", "\n", "# The measurement is only computed when the loop is enabled.\n", - "assert rms(irc_n_single) == 0.0, \"Positive-sequence-only run reports a measurement.\"\n", + "assert rms(irc_n_single) < 1e-12, \"Positive-sequence-only run reports a measurement.\"\n", "assert (\n", " irc_n_dual[post_fault_window].mean() > 1.0\n", "), \"Dual-sequence run measures no negative-sequence current under the fault.\"\n", From dda24c3de62f42688b4b3347758fdb1233dcdf5a Mon Sep 17 00:00:00 2001 From: Leonardo Carreras Date: Tue, 28 Jul 2026 17:43:24 +0200 Subject: [PATCH 9/9] Expose DOUBLE_EPSILON and split the DP Ph3 AvVSI state-space build Signed-off-by: Leonardo Carreras --- .../DP_Ph3_AvVoltSourceInverterStateSpace.h | 97 +++++-- .../DP_Ph3_AvVoltSourceInverterStateSpace.cpp | 268 ++++++++++-------- dpsim/src/pybind/main.cpp | 1 + ...P_Ph3_AvVoltSourceInverterStateSpace.ipynb | 4 +- 4 files changed, 221 insertions(+), 149 deletions(-) diff --git a/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h b/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h index 74e995831a..5be405ce13 100644 --- a/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h +++ b/dpsim-models/include/dpsim-models/DP/DP_Ph3_AvVoltSourceInverterStateSpace.h @@ -3,6 +3,8 @@ #pragma once +#include + #include namespace CPS { @@ -41,37 +43,44 @@ class AvVoltSourceInverterStateSpace final GammaNQ = 21 }; + /// One complex quantity per phase. + using Complex3 = std::array; + /// Enables the baseband negative-sequence current-control loop (+2 states). const Bool mEnableNegSeqControl; - static constexpr Int mVcReCol[3] = {VcARe, VcBRe, VcCRe}; - static constexpr Int mVcImCol[3] = {VcAIm, VcBIm, VcCIm}; - static constexpr Int mIfReCol[3] = {IfARe, IfBRe, IfCRe}; - static constexpr Int mIfImCol[3] = {IfAIm, IfBIm, IfCIm}; - static constexpr Int mUReCol[3] = {0, 2, 4}; - static constexpr Int mUImCol[3] = {1, 3, 5}; - - Real mLf; - Real mCf; - Real mRf; - Real mRc; - - Real mOmegaN; - Real mKpPLL; - Real mKiPLL; - - Real mOmegaCutoff; - Real mPRef; - Real mQRef; - Real mKpPowerCtrl; - Real mKiPowerCtrl; - Real mKpCurrCtrl; - Real mKiCurrCtrl; + static constexpr std::array mVcReCol = {VcARe, VcBRe, VcCRe}; + static constexpr std::array mVcImCol = {VcAIm, VcBIm, VcCIm}; + static constexpr std::array mIfReCol = {IfARe, IfBRe, IfCRe}; + static constexpr std::array mIfImCol = {IfAIm, IfBIm, IfCIm}; + static constexpr std::array mUReCol = {0, 2, 4}; + static constexpr std::array mUImCol = {1, 3, 5}; + + /// Positive-sequence control states the bridge-voltage reference depends on. + static constexpr std::array mOwnCol = { + Psi, PFiltered, QFiltered, PhiD, PhiQ, GammaD, GammaQ}; + + Real mLf = 0.0; + Real mCf = 0.0; + Real mRf = 0.0; + Real mRc = 0.0; + + Real mOmegaN = 0.0; + Real mKpPLL = 0.0; + Real mKiPLL = 0.0; + + Real mOmegaCutoff = 0.0; + Real mPRef = 0.0; + Real mQRef = 0.0; + Real mKpPowerCtrl = 0.0; + Real mKiPowerCtrl = 0.0; + Real mKpCurrCtrl = 0.0; + Real mKiCurrCtrl = 0.0; /// Negative-sequence d-axis current reference; zero (default) = negative-sequence suppression. - Real mIRefNd; + Real mIRefNd = 0.0; /// Negative-sequence q-axis current reference; zero (default) = negative-sequence suppression. - Real mIRefNq; + Real mIRefNq = 0.0; const Attribute::Ptr mVcD; const Attribute::Ptr mVcQ; @@ -83,11 +92,49 @@ class AvVoltSourceInverterStateSpace final const Attribute::Ptr mIrcNd; const Attribute::Ptr mIrcNq; + /// Negative-sequence measurement, bridge-voltage reference and the conj(vc_p) coefficients of the measurement. + struct NegSeqTerms { + Complex ircNDQ{0.0, 0.0}; + Complex vRefNDQ{0.0, 0.0}; + Complex vRefNEnv0{0.0, 0.0}; + Complex3 hIrcN{}; + }; + + /// Sensitivities of the phase-a bridge-voltage reference envelopes to states and inputs. + struct RefSensitivities { + /// d(vRefEnv0)/d(state) for the mOwnCol states, in that order. + std::array posOwn{}; + Complex3 posVcRe{}; + Complex3 posVcIm{}; + Complex3 posURe{}; + Complex3 posUIm{}; + Complex negPsi{0.0, 0.0}; + Complex negGammaND{0.0, 0.0}; + Complex negGammaNQ{0.0, 0.0}; + Complex3 negVcRe{}; + Complex3 negVcIm{}; + Complex3 negURe{}; + Complex3 negUIm{}; + }; + /// Builds the affine real model (A,B,C,D,E,F) around (x,u): RHS + analytic Jacobian, E = f(x,u) - A*x - B*u. void buildStateSpaceModel(const Matrix &x, const Matrix &u, Matrix &A, Matrix &B, Matrix &C, Matrix &D, Matrix &E, Matrix &F) const; + /// Baseband negative-sequence measurement and PI control around the operating point. + NegSeqTerms computeNegSeqTerms(const Matrix &x, const Complex3 &vc, + const Complex3 &uEnv, + const Complex3 &redistFactor, + const Complex3 &projCoeff, + const Complex &expJPsi) const; + + /// Filter-inductor Jacobian rows: own-phase terms plus the coupling through both control chains. + void buildInductorRows(const Complex3 &redistFactor, + const Complex3 &projCoeff, + const RefSensitivities &sens, Matrix &A, + Matrix &B) const; + protected: Bool updateComponentParameters() override final; void updateLogAttributes(const Matrix &u) const override final; diff --git a/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp b/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp index dd28e4d116..382572e63f 100644 --- a/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp +++ b/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp @@ -18,10 +18,7 @@ DP::Ph3::AvVoltSourceInverterStateSpace::AvVoltSourceInverterStateSpace( String uid, String name, Logger::Level logLevel, Bool enableNegSeqControl) : MixedVTypeVariableSSNComp(uid, name, enableNegSeqControl ? 10 : 8, 6, logLevel), - mEnableNegSeqControl(enableNegSeqControl), mLf(0.0), mCf(0.0), mRf(0.0), - mRc(0.0), mOmegaN(0.0), mKpPLL(0.0), mKiPLL(0.0), mOmegaCutoff(0.0), - mPRef(0.0), mQRef(0.0), mKpPowerCtrl(0.0), mKiPowerCtrl(0.0), - mKpCurrCtrl(0.0), mKiCurrCtrl(0.0), mIRefNd(0.0), mIRefNq(0.0), + mEnableNegSeqControl(enableNegSeqControl), mVcD(mAttributes->create("vc_d")), mVcQ(mAttributes->create("vc_q")), mIrcD(mAttributes->create("irc_d")), @@ -107,6 +104,38 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::setParameters( eVector, fVector); } +DP::Ph3::AvVoltSourceInverterStateSpace::NegSeqTerms +DP::Ph3::AvVoltSourceInverterStateSpace::computeNegSeqTerms( + const Matrix &x, const Complex3 &vc, const Complex3 &uEnv, + const Complex3 &redistFactor, const Complex3 &projCoeff, + const Complex &expJPsi) const { + NegSeqTerms terms; + + // Negative-sequence measurement from nV = vc_a + a^2*vc_b + a*vc_c. + Complex nV(0.0, 0.0); + Complex nU(0.0, 0.0); + for (Int p = 0; p < 3; ++p) { + nV += redistFactor[p] * vc[p]; + nU += redistFactor[p] * uEnv[p]; + } + + const Complex nI = (nV - nU) / mRc; + terms.ircNDQ = 0.5 * K23 * expJPsi * std::conj(nI); + + // hIrcN[p] is the conj(vc_p) coefficient of ircNDQ. + for (Int p = 0; p < 3; ++p) + terms.hIrcN[p] = 0.5 * K23 * expJPsi * projCoeff[p] / mRc; + + const Real vRefNd = -mKpCurrCtrl * terms.ircNDQ.real() + + mKiCurrCtrl * x(GammaND, 0) + mKpCurrCtrl * mIRefNd; + const Real vRefNq = -mKpCurrCtrl * terms.ircNDQ.imag() + + mKiCurrCtrl * x(GammaNQ, 0) + mKpCurrCtrl * mIRefNq; + terms.vRefNDQ = Complex(vRefNd, vRefNq); + terms.vRefNEnv0 = K23 * std::conj(terms.vRefNDQ) * expJPsi; + + return terms; +} + void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( const Matrix &x, const Matrix &u, Matrix &A, Matrix &B, Matrix &C, Matrix &D, Matrix &E, Matrix &F) const { @@ -119,32 +148,31 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( const Real phiQ = x(PhiQ, 0); const Real gammaD = x(GammaD, 0); const Real gammaQ = x(GammaQ, 0); - // GammaND/GammaNQ are past the end of the positive-only state vector. - const Real gammaND = mEnableNegSeqControl ? x(GammaND, 0) : 0.0; - const Real gammaNQ = mEnableNegSeqControl ? x(GammaNQ, 0) : 0.0; const Complex j(0.0, 1.0); - Complex vc[3], iF[3]; + Complex3 vc; + Complex3 iF; for (Int p = 0; p < 3; ++p) { vc[p] = Complex(x(mVcReCol[p], 0), x(mVcImCol[p], 0)); iF[p] = Complex(x(mIfReCol[p], 0), x(mIfImCol[p], 0)); } - const Complex uEnv[3] = {Complex(u(0, 0), u(1, 0)), Complex(u(2, 0), u(3, 0)), - Complex(u(4, 0), u(5, 0))}; + const Complex3 uEnv = {Complex(u(0, 0), u(1, 0)), Complex(u(2, 0), u(3, 0)), + Complex(u(4, 0), u(5, 0))}; // Positive-sequence projection coefficients and inverse-Park redistribution factors. - const Complex projCoeff[3] = {Complex(1.0, 0.0), SHIFT_TO_PHASE_C, - SHIFT_TO_PHASE_B}; - const Complex redistFactor[3] = {std::conj(projCoeff[0]), - std::conj(projCoeff[1]), - std::conj(projCoeff[2])}; + const Complex3 projCoeff = {Complex(1.0, 0.0), SHIFT_TO_PHASE_C, + SHIFT_TO_PHASE_B}; + const Complex3 redistFactor = {std::conj(projCoeff[0]), + std::conj(projCoeff[1]), + std::conj(projCoeff[2])}; const Complex rot = std::exp(-j * psi); const Complex expJPsi = std::conj(rot); // Positive-sequence dq measurements from pV = vc_a + a*vc_b + a^2*vc_c. - Complex pV(0.0, 0.0), pU(0.0, 0.0); + Complex pV(0.0, 0.0); + Complex pU(0.0, 0.0); for (Int p = 0; p < 3; ++p) { pV += projCoeff[p] * vc[p]; pU += projCoeff[p] * uEnv[p]; @@ -159,7 +187,9 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( const Real ircQ = ircDQ.imag(); // gVc[p] = d(vcDQ)/d(vc_p); gIrcVc/gIrcU = d(ircDQ)/d(vc_p), d(ircDQ)/d(u_p). - Complex gVc[3], gIrcVc[3], gIrcU[3]; + Complex3 gVc; + Complex3 gIrcVc; + Complex3 gIrcU; for (Int p = 0; p < 3; ++p) { gVc[p] = 0.5 * K23 * rot * projCoeff[p]; gIrcVc[p] = gVc[p] / mRc; @@ -185,38 +215,23 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( // Positive-sequence bridge-voltage reference, distributed via the inverse Park. const Complex vRefEnv0 = K23 * vRefDQ * expJPsi; - // Negative-sequence measurement + PI control (baseband +j*psi loop). - // hIrcN[p] is the conj(vc_p) coefficient of ircNDQ. - Complex ircNDQ(0.0, 0.0), vRefNDQ(0.0, 0.0), vRefNEnv0(0.0, 0.0); - Real ircND = 0.0, ircNQ = 0.0; - Complex hIrcN[3] = {Complex(0.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0)}; - if (mEnableNegSeqControl) { - Complex nV(0.0, 0.0), nU(0.0, 0.0); - for (Int p = 0; p < 3; ++p) { - nV += redistFactor[p] * vc[p]; - nU += redistFactor[p] * uEnv[p]; - } - const Complex nI = (nV - nU) / mRc; - ircNDQ = 0.5 * K23 * expJPsi * std::conj(nI); - ircND = ircNDQ.real(); - ircNQ = ircNDQ.imag(); - for (Int p = 0; p < 3; ++p) - hIrcN[p] = 0.5 * K23 * expJPsi * projCoeff[p] / mRc; - const Real vRefNd = - -mKpCurrCtrl * ircND + mKiCurrCtrl * gammaND + mKpCurrCtrl * mIRefNd; - const Real vRefNq = - -mKpCurrCtrl * ircNQ + mKiCurrCtrl * gammaNQ + mKpCurrCtrl * mIRefNq; - vRefNDQ = Complex(vRefNd, vRefNq); - vRefNEnv0 = K23 * std::conj(vRefNDQ) * expJPsi; - } + // Negative-sequence measurement + PI control (baseband +j*psi loop). Left at + // its zero default when the loop is off, which drops it out of everything below. + NegSeqTerms neg; + if (mEnableNegSeqControl) + neg = computeNegSeqTerms(x, vc, uEnv, redistFactor, projCoeff, expJPsi); + + const Real ircND = neg.ircNDQ.real(); + const Real ircNQ = neg.ircNDQ.imag(); // Total reference: positive (redistFactor) + negative (projCoeff) injection. - Complex vRef[3]; + Complex3 vRef; for (Int p = 0; p < 3; ++p) - vRef[p] = redistFactor[p] * vRefEnv0 + projCoeff[p] * vRefNEnv0; + vRef[p] = redistFactor[p] * vRefEnv0 + projCoeff[p] * neg.vRefNEnv0; // RHS f(x,u) (x_dot = f(x,u)). - Complex vcDot[3], ifDot[3]; + Complex3 vcDot; + Complex3 ifDot; for (Int p = 0; p < 3; ++p) { vcDot[p] = iF[p] / mCf + (uEnv[p] - vc[p]) / (mCf * mRc) - j * mOmegaN * vc[p]; @@ -317,14 +332,14 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( A(GammaND, Psi) = ircNQ; A(GammaNQ, Psi) = -ircND; for (Int p = 0; p < 3; ++p) { - A(GammaND, mVcReCol[p]) = -hIrcN[p].real(); - A(GammaND, mVcImCol[p]) = -hIrcN[p].imag(); - A(GammaNQ, mVcReCol[p]) = -hIrcN[p].imag(); - A(GammaNQ, mVcImCol[p]) = hIrcN[p].real(); - B(GammaND, mUReCol[p]) = hIrcN[p].real(); - B(GammaND, mUImCol[p]) = hIrcN[p].imag(); - B(GammaNQ, mUReCol[p]) = hIrcN[p].imag(); - B(GammaNQ, mUImCol[p]) = -hIrcN[p].real(); + A(GammaND, mVcReCol[p]) = -neg.hIrcN[p].real(); + A(GammaND, mVcImCol[p]) = -neg.hIrcN[p].imag(); + A(GammaNQ, mVcReCol[p]) = -neg.hIrcN[p].imag(); + A(GammaNQ, mVcImCol[p]) = neg.hIrcN[p].real(); + B(GammaND, mUReCol[p]) = neg.hIrcN[p].real(); + B(GammaND, mUImCol[p]) = neg.hIrcN[p].imag(); + B(GammaNQ, mUReCol[p]) = neg.hIrcN[p].imag(); + B(GammaNQ, mUImCol[p]) = -neg.hIrcN[p].real(); } } @@ -343,6 +358,8 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( } // Filter inductor rows (If_dot): the per-phase coupling via vRef_p = redistFactor[p]*vRefEnv0. + RefSensitivities sens; + const Complex dVRefEnv0DPsi = j * K23 * expJPsi * (mKpCurrCtrl * ircDQ + vRefDQ); @@ -355,41 +372,60 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( const Complex dVRefEnv0DGammaD = K23 * expJPsi * mKiCurrCtrl; const Complex dVRefEnv0DGammaQ = K23 * expJPsi * j * mKiCurrCtrl; - const Int ownCols[7] = {Psi, PFiltered, QFiltered, PhiD, - PhiQ, GammaD, GammaQ}; - const Complex dVRefEnv0Own[7] = { - dVRefEnv0DPsi, dVRefEnv0DpF, dVRefEnv0DqF, dVRefEnv0DPhiD, - dVRefEnv0DPhiQ, dVRefEnv0DGammaD, dVRefEnv0DGammaQ}; + sens.posOwn = {dVRefEnv0DPsi, dVRefEnv0DpF, dVRefEnv0DqF, + dVRefEnv0DPhiD, dVRefEnv0DPhiQ, dVRefEnv0DGammaD, + dVRefEnv0DGammaQ}; // d(vRefDQ)/d(vc_p), d(vRefDQ)/d(u_p) via vRefDQ's -kpCurrCtrl*ircDQ term. - Complex dVRefEnv0VcRe[3], dVRefEnv0VcIm[3], dVRefEnv0URe[3], dVRefEnv0UIm[3]; for (Int p = 0; p < 3; ++p) { - dVRefEnv0VcRe[p] = K23 * expJPsi * (-mKpCurrCtrl * gIrcVc[p]); - dVRefEnv0VcIm[p] = K23 * expJPsi * (-mKpCurrCtrl * j * gIrcVc[p]); - dVRefEnv0URe[p] = K23 * expJPsi * (-mKpCurrCtrl * gIrcU[p]); - dVRefEnv0UIm[p] = K23 * expJPsi * (-mKpCurrCtrl * j * gIrcU[p]); + sens.posVcRe[p] = K23 * expJPsi * (-mKpCurrCtrl * gIrcVc[p]); + sens.posVcIm[p] = K23 * expJPsi * (-mKpCurrCtrl * j * gIrcVc[p]); + sens.posURe[p] = K23 * expJPsi * (-mKpCurrCtrl * gIrcU[p]); + sens.posUIm[p] = K23 * expJPsi * (-mKpCurrCtrl * j * gIrcU[p]); } // Derivatives of vRefNEnv0 = K23*conj(vRefNDQ)*expJPsi wrt psi, the negative-loop // states and vc_p/u_p. - Complex dVRefNEnv0DPsi(0.0, 0.0), dVRefNEnv0DGammaND(0.0, 0.0), - dVRefNEnv0DGammaNQ(0.0, 0.0); - Complex dVRefNEnv0VcRe[3] = {}, dVRefNEnv0VcIm[3] = {}, dVRefNEnv0URe[3] = {}, - dVRefNEnv0UIm[3] = {}; if (mEnableNegSeqControl) { - dVRefNEnv0DPsi = j * K23 * expJPsi * - (mKpCurrCtrl * std::conj(ircNDQ) + std::conj(vRefNDQ)); - dVRefNEnv0DGammaND = K23 * expJPsi * mKiCurrCtrl; - dVRefNEnv0DGammaNQ = -j * K23 * expJPsi * mKiCurrCtrl; + sens.negPsi = + j * K23 * expJPsi * + (mKpCurrCtrl * std::conj(neg.ircNDQ) + std::conj(neg.vRefNDQ)); + sens.negGammaND = K23 * expJPsi * mKiCurrCtrl; + sens.negGammaNQ = -j * K23 * expJPsi * mKiCurrCtrl; for (Int p = 0; p < 3; ++p) { - const Complex base = mKpCurrCtrl * std::conj(hIrcN[p]) * K23 * expJPsi; - dVRefNEnv0VcRe[p] = -base; - dVRefNEnv0VcIm[p] = -j * base; - dVRefNEnv0URe[p] = base; - dVRefNEnv0UIm[p] = j * base; + const Complex base = + mKpCurrCtrl * std::conj(neg.hIrcN[p]) * K23 * expJPsi; + sens.negVcRe[p] = -base; + sens.negVcIm[p] = -j * base; + sens.negURe[p] = base; + sens.negUIm[p] = j * base; } } + buildInductorRows(redistFactor, projCoeff, sens, A, B); + + // Offset E = f(x,u) - A*x - B*u. + E = f - A * x - B * u; + + // SSN output: y_p = (u_p - vc_p)/Rc (exact, no relinearization needed). + C = Matrix::Zero(6, stateSize()); + for (Int p = 0; p < 3; ++p) { + C(2 * p, mVcReCol[p]) = -1.0 / mRc; + C(2 * p + 1, mVcImCol[p]) = -1.0 / mRc; + } + + D = Matrix::Zero(6, 6); + for (Int p = 0; p < 3; ++p) { + D(2 * p, mUReCol[p]) = 1.0 / mRc; + D(2 * p + 1, mUImCol[p]) = 1.0 / mRc; + } + + F = Matrix::Zero(6, 1); +} + +void DP::Ph3::AvVoltSourceInverterStateSpace::buildInductorRows( + const Complex3 &redistFactor, const Complex3 &projCoeff, + const RefSensitivities &sens, Matrix &A, Matrix &B) const { for (Int pOut = 0; pOut < 3; ++pOut) { const Int reRow = mIfReCol[pOut]; const Int imRow = mIfImCol[pOut]; @@ -404,20 +440,20 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( // vRef_p coupling through the shared single-dq-frame control chain. for (Int k = 0; k < 7; ++k) { - const Complex dVRef = redistFactor[pOut] * dVRefEnv0Own[k]; - A(reRow, ownCols[k]) += dVRef.real() / mLf; - A(imRow, ownCols[k]) += dVRef.imag() / mLf; + const Complex dVRef = redistFactor[pOut] * sens.posOwn[k]; + A(reRow, mOwnCol[k]) += dVRef.real() / mLf; + A(imRow, mOwnCol[k]) += dVRef.imag() / mLf; } for (Int pSrc = 0; pSrc < 3; ++pSrc) { - const Complex dVRefVcRe = redistFactor[pOut] * dVRefEnv0VcRe[pSrc]; - const Complex dVRefVcIm = redistFactor[pOut] * dVRefEnv0VcIm[pSrc]; + const Complex dVRefVcRe = redistFactor[pOut] * sens.posVcRe[pSrc]; + const Complex dVRefVcIm = redistFactor[pOut] * sens.posVcIm[pSrc]; A(reRow, mVcReCol[pSrc]) += dVRefVcRe.real() / mLf; A(imRow, mVcReCol[pSrc]) += dVRefVcRe.imag() / mLf; A(reRow, mVcImCol[pSrc]) += dVRefVcIm.real() / mLf; A(imRow, mVcImCol[pSrc]) += dVRefVcIm.imag() / mLf; - const Complex dVRefURe = redistFactor[pOut] * dVRefEnv0URe[pSrc]; - const Complex dVRefUIm = redistFactor[pOut] * dVRefEnv0UIm[pSrc]; + const Complex dVRefURe = redistFactor[pOut] * sens.posURe[pSrc]; + const Complex dVRefUIm = redistFactor[pOut] * sens.posUIm[pSrc]; B(reRow, mUReCol[pSrc]) += dVRefURe.real() / mLf; B(imRow, mUReCol[pSrc]) += dVRefURe.imag() / mLf; B(reRow, mUImCol[pSrc]) += dVRefUIm.real() / mLf; @@ -425,51 +461,34 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( } // Negative-sequence injection coupling: vRef_p += projCoeff[pOut]*vRefNEnv0. + // The sensitivities stay zero when the loop is off, so this is a no-op then. + const Complex dNPsi = projCoeff[pOut] * sens.negPsi; + A(reRow, Psi) += dNPsi.real() / mLf; + A(imRow, Psi) += dNPsi.imag() / mLf; if (mEnableNegSeqControl) { - const Complex dNPsi = projCoeff[pOut] * dVRefNEnv0DPsi; - A(reRow, Psi) += dNPsi.real() / mLf; - A(imRow, Psi) += dNPsi.imag() / mLf; - const Complex dNGammaND = projCoeff[pOut] * dVRefNEnv0DGammaND; + const Complex dNGammaND = projCoeff[pOut] * sens.negGammaND; A(reRow, GammaND) += dNGammaND.real() / mLf; A(imRow, GammaND) += dNGammaND.imag() / mLf; - const Complex dNGammaNQ = projCoeff[pOut] * dVRefNEnv0DGammaNQ; + const Complex dNGammaNQ = projCoeff[pOut] * sens.negGammaNQ; A(reRow, GammaNQ) += dNGammaNQ.real() / mLf; A(imRow, GammaNQ) += dNGammaNQ.imag() / mLf; - for (Int pSrc = 0; pSrc < 3; ++pSrc) { - const Complex dNVcRe = projCoeff[pOut] * dVRefNEnv0VcRe[pSrc]; - const Complex dNVcIm = projCoeff[pOut] * dVRefNEnv0VcIm[pSrc]; - A(reRow, mVcReCol[pSrc]) += dNVcRe.real() / mLf; - A(imRow, mVcReCol[pSrc]) += dNVcRe.imag() / mLf; - A(reRow, mVcImCol[pSrc]) += dNVcIm.real() / mLf; - A(imRow, mVcImCol[pSrc]) += dNVcIm.imag() / mLf; - - const Complex dNURe = projCoeff[pOut] * dVRefNEnv0URe[pSrc]; - const Complex dNUIm = projCoeff[pOut] * dVRefNEnv0UIm[pSrc]; - B(reRow, mUReCol[pSrc]) += dNURe.real() / mLf; - B(imRow, mUReCol[pSrc]) += dNURe.imag() / mLf; - B(reRow, mUImCol[pSrc]) += dNUIm.real() / mLf; - B(imRow, mUImCol[pSrc]) += dNUIm.imag() / mLf; - } + } + for (Int pSrc = 0; pSrc < 3; ++pSrc) { + const Complex dNVcRe = projCoeff[pOut] * sens.negVcRe[pSrc]; + const Complex dNVcIm = projCoeff[pOut] * sens.negVcIm[pSrc]; + A(reRow, mVcReCol[pSrc]) += dNVcRe.real() / mLf; + A(imRow, mVcReCol[pSrc]) += dNVcRe.imag() / mLf; + A(reRow, mVcImCol[pSrc]) += dNVcIm.real() / mLf; + A(imRow, mVcImCol[pSrc]) += dNVcIm.imag() / mLf; + + const Complex dNURe = projCoeff[pOut] * sens.negURe[pSrc]; + const Complex dNUIm = projCoeff[pOut] * sens.negUIm[pSrc]; + B(reRow, mUReCol[pSrc]) += dNURe.real() / mLf; + B(imRow, mUReCol[pSrc]) += dNURe.imag() / mLf; + B(reRow, mUImCol[pSrc]) += dNUIm.real() / mLf; + B(imRow, mUImCol[pSrc]) += dNUIm.imag() / mLf; } } - - // Offset E = f(x,u) - A*x - B*u. - E = f - A * x - B * u; - - // SSN output: y_p = (u_p - vc_p)/Rc (exact, no relinearization needed). - C = Matrix::Zero(6, stateSize()); - for (Int p = 0; p < 3; ++p) { - C(2 * p, mVcReCol[p]) = -1.0 / mRc; - C(2 * p + 1, mVcImCol[p]) = -1.0 / mRc; - } - - D = Matrix::Zero(6, 6); - for (Int p = 0; p < 3; ++p) { - D(2 * p, mUReCol[p]) = 1.0 / mRc; - D(2 * p + 1, mUImCol[p]) = 1.0 / mRc; - } - - F = Matrix::Zero(6, 1); } Bool DP::Ph3::AvVoltSourceInverterStateSpace::updateComponentParameters() { @@ -493,10 +512,13 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::updateLogAttributes( const Real psi = x(Psi, 0); const Complex rot = std::exp(Complex(0.0, -psi)); const Complex expJPsi = std::conj(rot); - const Complex projCoeff[3] = {Complex(1.0, 0.0), SHIFT_TO_PHASE_C, - SHIFT_TO_PHASE_B}; + const Complex3 projCoeff = {Complex(1.0, 0.0), SHIFT_TO_PHASE_C, + SHIFT_TO_PHASE_B}; - Complex pV(0.0, 0.0), pU(0.0, 0.0), nV(0.0, 0.0), nU(0.0, 0.0); + Complex pV(0.0, 0.0); + Complex pU(0.0, 0.0); + Complex nV(0.0, 0.0); + Complex nU(0.0, 0.0); for (Int p = 0; p < 3; ++p) { const Complex vc(x(mVcReCol[p], 0), x(mVcImCol[p], 0)); const Complex uEnv(u(mUReCol[p], 0), u(mUImCol[p], 0)); diff --git a/dpsim/src/pybind/main.cpp b/dpsim/src/pybind/main.cpp index 18e57c5e70..5527940a31 100644 --- a/dpsim/src/pybind/main.cpp +++ b/dpsim/src/pybind/main.cpp @@ -154,6 +154,7 @@ PYBIND11_MODULE(dpsimpy, m) { m.attr("PEAK1PH_TO_RMS3PH") = PEAK1PH_TO_RMS3PH; m.attr("P_SNUB_TRANSFORMER") = P_SNUB_TRANSFORMER; m.attr("Q_SNUB_TRANSFORMER") = Q_SNUB_TRANSFORMER; + m.attr("DOUBLE_EPSILON") = DOUBLE_EPSILON; addAttributes(m); diff --git a/examples/Notebooks/Components/DP_Ph3_AvVoltSourceInverterStateSpace.ipynb b/examples/Notebooks/Components/DP_Ph3_AvVoltSourceInverterStateSpace.ipynb index 272942e7bd..d4a4a9bd3e 100644 --- a/examples/Notebooks/Components/DP_Ph3_AvVoltSourceInverterStateSpace.ipynb +++ b/examples/Notebooks/Components/DP_Ph3_AvVoltSourceInverterStateSpace.ipynb @@ -716,7 +716,9 @@ "assert not dp_slg_dual.isna().any().any(), \"Dual-sequence run contains NaN or Inf.\"\n", "\n", "# The measurement is only computed when the loop is enabled.\n", - "assert rms(irc_n_single) < 1e-12, \"Positive-sequence-only run reports a measurement.\"\n", + "assert (\n", + " rms(irc_n_single) < dpsimpy.DOUBLE_EPSILON\n", + "), \"Positive-sequence-only run reports a measurement.\"\n", "assert (\n", " irc_n_dual[post_fault_window].mean() > 1.0\n", "), \"Dual-sequence run measures no negative-sequence current under the fault.\"\n",