diff --git a/docs/hugo/content/en/docs/Models/power-electronics.md b/docs/hugo/content/en/docs/Models/power-electronics.md index 87367658ab..d54755442b 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,17 +432,11 @@ 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 , ``` -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 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..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,63 +3,84 @@ #pragma once +#include + #include 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). +/// @brief Averaged grid-following VSI (SSN), optional dual-sequence current control. class AvVoltSourceInverterStateSpace final : public MixedVTypeVariableSSNComp, public SharedFactory { private: + /// 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 }; - 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; + /// One complex quantity per phase. + using Complex3 = std::array; + + /// Enables the baseband negative-sequence current-control loop (+2 states). + const Bool mEnableNegSeqControl; + + 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 = 0.0; + /// Negative-sequence q-axis current reference; zero (default) = negative-sequence suppression. + Real mIRefNq = 0.0; const Attribute::Ptr mVcD; const Attribute::Ptr mVcQ; @@ -68,12 +89,52 @@ class AvVoltSourceInverterStateSpace final const Attribute::Ptr mPInst; const Attribute::Ptr mQInst; const Attribute::Ptr mOmegaPLL; + 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; @@ -81,16 +142,21 @@ 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, 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/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 00e416926c..382572e63f 100644 --- a/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp +++ b/dpsim-models/src/DP/DP_Ph3_AvVoltSourceInverterStateSpace.cpp @@ -15,18 +15,19 @@ const Real K32 = std::sqrt(1.5); } // namespace DP::Ph3::AvVoltSourceInverterStateSpace::AvVoltSourceInverterStateSpace( - String uid, String name, Logger::Level logLevel) - : MixedVTypeVariableSSNComp(uid, name, 8, 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), + String uid, String name, Logger::Level logLevel, Bool enableNegSeqControl) + : MixedVTypeVariableSSNComp(uid, name, enableNegSeqControl ? 10 : 8, 6, + logLevel), + mEnableNegSeqControl(enableNegSeqControl), 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 +35,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 +90,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); @@ -99,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 { @@ -114,27 +151,28 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( 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))}; - 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))}; + 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 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]; @@ -149,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; @@ -172,21 +212,33 @@ 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; - Complex vRef[3]; + + // 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. + Complex3 vRef; for (Int p = 0; p < 3; ++p) - vRef[p] = redistFactor[p] * vRefEnv0; + 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]; ifDot[p] = (vRef[p] - vc[p] - mRf * iF[p]) / mLf - j * mOmegaN * iF[p]; } - Matrix f = Matrix::Zero(20, 1); + Matrix f = Matrix::Zero(stateSize(), 1); f(Psi, 0) = mKpPLL * vcQ + mKiPLL * phiPLL; f(PhiPLL, 0) = vcQ; f(PFiltered, 0) = mOmegaCutoff * (pInst - pF); @@ -195,22 +247,20 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::buildStateSpaceModel( f(PhiQ, 0) = qF - mQRef; f(GammaD, 0) = iRefD - ircD; f(GammaQ, 0) = iRefQ - ircQ; - 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(20, 20); - B = Matrix::Zero(20, 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); @@ -276,6 +326,23 @@ 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. + if (mEnableNegSeqControl) { + A(GammaND, Psi) = ircNQ; + A(GammaNQ, Psi) = -ircND; + for (Int p = 0; p < 3; ++p) { + 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(); + } + } + // Filter capacitor rows (Vc_dot), fully decoupled per phase. for (Int p = 0; p < 3; ++p) { const Int reRow = mVcReCol[p]; @@ -291,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); @@ -303,21 +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. + if (mEnableNegSeqControl) { + 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(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]; @@ -332,44 +440,55 @@ 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; B(imRow, mUImCol[pSrc]) += dVRefUIm.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); - 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; + // 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 dNGammaND = projCoeff[pOut] * sens.negGammaND; + A(reRow, GammaND) += dNGammaND.real() / mLf; + A(imRow, GammaND) += dNGammaND.imag() / mLf; + 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] * 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; + } } - - F = Matrix::Zero(6, 1); } Bool DP::Ph3::AvVoltSourceInverterStateSpace::updateComponentParameters() { @@ -392,15 +511,21 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::updateLogAttributes( const Real psi = x(Psi, 0); const Complex rot = std::exp(Complex(0.0, -psi)); - const Complex projCoeff[3] = {Complex(1.0, 0.0), SHIFT_TO_PHASE_C, - SHIFT_TO_PHASE_B}; + const Complex expJPsi = std::conj(rot); + 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); + 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)); 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; @@ -411,6 +536,12 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::updateLogAttributes( **mVcQ = vcDQ.imag(); **mIrcD = ircDQ.real(); **mIrcQ = ircDQ.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; @@ -498,6 +629,7 @@ void DP::Ph3::AvVoltSourceInverterStateSpace::initializeFromNodesAndTerminals( x0(PhiQ, 0) = phiQ0; x0(GammaD, 0) = gammaD0; x0(GammaQ, 0) = gammaQ0; + // 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 bbbb7c1038..589d04ae4b 100644 --- a/dpsim/examples/cxx/Components/DP_Ph3_AvVoltSourceInverterStateSpace.cpp +++ b/dpsim/examples/cxx/Components/DP_Ph3_AvVoltSourceInverterStateSpace.cpp @@ -23,7 +23,9 @@ class Example_AvVoltSourceInverterStateSpace { const String simNameBase = "DP_Ph3_AvVoltSourceInverterStateSpace"; const auto systemPF = runPowerFlow(simNameBase + "_PF"); - runDPSimulation(simNameBase + "_DP", systemPF); + // Same SLG scenario, positive-sequence-only vs dual-sequence control. + runDPSimulation(simNameBase + "_DP_posSeq", systemPF, false); + runDPSimulation(simNameBase + "_DP_dualSeq", systemPF, true); } private: @@ -86,8 +88,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 +105,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); @@ -134,6 +136,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..f62350ccc1 100644 --- a/dpsim/src/pybind/DPComponents.cpp +++ b/dpsim/src/pybind/DPComponents.cpp @@ -668,21 +668,30 @@ 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, + // 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, "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", 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 cef7000bd1..d4a4a9bd3e 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,95 @@ "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 (\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", + "\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 +778,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." ] }, {