DPsim
Loading...
Searching...
No Matches
StateSpaceModalAnalysis.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 Institute for Automation of Complex Power Systems, EONERC, RWTH Aachen University
2// SPDX-License-Identifier: MPL-2.0
3
4#include <Eigen/Eigenvalues>
5#include <Eigen/LU>
6
9
10#include <cmath>
11#include <limits>
12#include <stdexcept>
13#include <string>
14
15namespace DPsim {
16
17namespace {
18
19Matrix parkTransformDQ0(Real theta) {
20 Matrix transform(3, 3);
21
22 const Real k = std::sqrt(2.0 / 3.0);
23 const Real k0 = 1.0 / std::sqrt(3.0);
24
25 transform.row(0) << k * std::cos(theta), k * std::cos(theta - 2.0 * PI / 3.0),
26 k * std::cos(theta + 2.0 * PI / 3.0);
27
28 transform.row(1) << -k * std::sin(theta),
29 -k * std::sin(theta - 2.0 * PI / 3.0),
30 -k * std::sin(theta + 2.0 * PI / 3.0);
31
32 transform.row(2) << k0, k0, k0;
33
34 return transform;
35}
36
37String fallbackStateName(UInt index) { return "x" + std::to_string(index); }
38
39} // namespace
40
42 const MNAStateSpaceExtractor &extractor)
43 : mExtractor(extractor) {}
44
46 if (!mExtractor.isInitialized())
47 throw std::logic_error("StateSpaceModalAnalysis requires an initialized "
48 "MNAStateSpaceExtractor.");
49
50 const Matrix Ad = buildDiscreteStateMatrixInAnalysisFrame();
51
52 if (Ad.rows() == 0) {
53 mDiscreteEigenvalues.resize(0);
54 mContinuousEigenvalues.resize(0);
55
56 mRightEigenvectors.resize(0, 0);
57 mLeftEigenvectors.resize(0, 0);
58 mParticipationFactors.resize(0, 0);
59
60 mStateNames.clear();
61
62 return;
63 }
64
65 if (Ad.rows() != Ad.cols())
66 throw std::logic_error(
67 "StateSpaceModalAnalysis requires a square state matrix.");
68
69 mStateNames = buildStateNamesInAnalysisFrame();
70
71 Eigen::EigenSolver<Matrix> eigenSolver(Ad, true);
72
73 if (eigenSolver.info() != Eigen::Success)
74 throw std::runtime_error(
75 "StateSpaceModalAnalysis: eigenvalue computation failed.");
76
77 mDiscreteEigenvalues = eigenSolver.eigenvalues();
78
79 mContinuousEigenvalues.resize(mDiscreteEigenvalues.rows());
80
81 for (Eigen::Index idx = 0; idx < mDiscreteEigenvalues.rows(); ++idx)
82 mContinuousEigenvalues(idx) =
83 mapDiscreteToContinuous(mDiscreteEigenvalues(idx));
84
85 mRightEigenvectors = eigenSolver.eigenvectors();
86
87 Eigen::FullPivLU<CPS::MatrixComp> eigenvectorLu(mRightEigenvectors);
88
89 if (!eigenvectorLu.isInvertible())
90 throw std::runtime_error(
91 "StateSpaceModalAnalysis: cannot compute participation factors because "
92 "the eigenvector matrix is singular.");
93
94 mLeftEigenvectors = eigenvectorLu.inverse();
95
96 mParticipationFactors = CPS::Math::elementwiseProduct(
97 mRightEigenvectors, mLeftEigenvectors.transpose());
98}
99
100Matrix
101StateSpaceModalAnalysis::buildDiscreteStateMatrixInAnalysisFrame() const {
102 const Matrix &nativeAd = mExtractor.getDiscreteStateMatrix();
103
104 if (mAnalysisFrame == StateSpaceAnalysisFrame::Native)
105 return nativeAd;
106
107 if (mAnalysisFrame == StateSpaceAnalysisFrame::GlobalDQ0) {
108 if (!mExtractor.hasExtractionTime()) {
109 throw std::logic_error(
110 "GlobalDQ0 modal analysis requires a valid extraction timestamp.");
111 }
112
113 if (mGlobalOmega <= 0.0) {
114 throw std::logic_error(
115 "GlobalDQ0 modal analysis requires a positive frame angular speed.");
116 }
117
118 const Real time = mExtractor.getLastExtractionTime();
119 const Real timeStep = mExtractor.getTimeStep();
120
121 const Real thetaNow = mGlobalTheta0 + mGlobalOmega * time;
122 const Real thetaNext = thetaNow + mGlobalOmega * timeStep;
123
124 const Matrix transformNow = buildGlobalDq0Transformation(thetaNow);
125 const Matrix transformNext = buildGlobalDq0Transformation(thetaNext);
126
127 // For a time-dependent discrete coordinate transformation
128 // xGlobalDq0[k] = T[k] xNative[k], the transformed transition matrix is
129 // AdGlobalDq0[k] = T[k+1] AdNative[k] T[k]^{-1}.
130 //
131 // The Park transform is power-invariant, so T^{-1} = T^T.
132 return transformNext * nativeAd * transformNow.transpose();
133 }
134
135 throw std::logic_error("Unsupported state-space analysis frame.");
136}
137
138Matrix StateSpaceModalAnalysis::buildGlobalDq0Transformation(Real theta) const {
139 const UInt stateCount = mExtractor.getStateCount();
140
141 Matrix transform = Matrix::Identity(stateCount, stateCount);
142
143 const Matrix park = parkTransformDQ0(theta);
144
145 for (const auto &abcBlock : mExtractor.getMetadata().abcStateBlocks) {
146 for (UInt row = 0; row < 3; ++row) {
147 for (UInt col = 0; col < 3; ++col) {
148 transform(abcBlock.indices[row], abcBlock.indices[col]) =
149 park(row, col);
150 }
151 }
152 }
153
154 return transform;
155}
156
157std::vector<String>
158StateSpaceModalAnalysis::buildStateNamesInAnalysisFrame() const {
159 const UInt stateCount = mExtractor.getStateCount();
160 const auto &metadata = mExtractor.getMetadata();
161
162 std::vector<String> stateNames(stateCount);
163
164 for (UInt idx = 0; idx < stateCount; ++idx) {
165 if (idx < metadata.stateNames.size() && !metadata.stateNames[idx].empty())
166 stateNames[idx] = metadata.stateNames[idx];
167 else
168 stateNames[idx] = fallbackStateName(idx);
169 }
170
171 if (mAnalysisFrame == StateSpaceAnalysisFrame::Native)
172 return stateNames;
173
174 if (mAnalysisFrame == StateSpaceAnalysisFrame::GlobalDQ0) {
175 for (const auto &abcBlock : metadata.abcStateBlocks) {
176 if (abcBlock.name.empty())
177 throw std::logic_error(
178 "GlobalDQ0 modal analysis requires named abc state blocks.");
179
180 stateNames[abcBlock.indices[0]] = abcBlock.name + "_d";
181 stateNames[abcBlock.indices[1]] = abcBlock.name + "_q";
182 stateNames[abcBlock.indices[2]] = abcBlock.name + "_0";
183 }
184
185 return stateNames;
186 }
187
188 throw std::logic_error("Unsupported state-space analysis frame.");
189}
190
192StateSpaceModalAnalysis::mapDiscreteToContinuous(const CPS::Complex &z) const {
193 const CPS::Complex one(1.0, 0.0);
194 const CPS::Complex denominator = z + one;
195
196 if (std::abs(denominator) <= DOUBLE_EPSILON)
197 return CPS::Complex(std::numeric_limits<Real>::infinity(), 0.0);
198
199 return (2.0 / mExtractor.getTimeStep()) * (z - one) / denominator;
200}
201
202} // namespace DPsim
static auto elementwiseProduct(const Eigen::MatrixBase< DerivedA > &a, const Eigen::MatrixBase< DerivedB > &b)
Elementwise product of two same-shaped Eigen expressions.
Definition MathUtils.h:41
const Matrix & getDiscreteStateMatrix() const
StateSpaceModalAnalysis(const MNAStateSpaceExtractor &extractor)
void update()
Update modal quantities from the current extracted state matrix.
#define PI
Definition Definitions.h:43
#define DOUBLE_EPSILON
Definition Definitions.h:14
std::complex< Real > Complex
Definition Definitions.h:63
CPS::Real Real
Definition Definitions.h:18
CPS::String String
Definition Definitions.h:20
CPS::Matrix Matrix
Definition Definitions.h:24
CPS::UInt UInt
Definition Definitions.h:23