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