v2.0.0
Loading...
Searching...
No Matches
inv_beamformer_compute.cpp
Go to the documentation of this file.
1//=============================================================================================================
21
22//=============================================================================================================
23// INCLUDES
24//=============================================================================================================
25
27
28#include <QDebug>
29
30//=============================================================================================================
31// EIGEN INCLUDES
32//=============================================================================================================
33
34#include <Eigen/Dense>
35#include <Eigen/Eigenvalues>
36#include <Eigen/SVD>
37
38#include <cmath>
39#include <algorithm>
40
41//=============================================================================================================
42// USED NAMESPACES
43//=============================================================================================================
44
45using namespace Eigen;
46using namespace INVLIB;
47
48//=============================================================================================================
49// STATIC HELPERS
50//=============================================================================================================
51
52namespace {
53
58MatrixXd invertSmallSym(const MatrixXd &X, bool reduceRank)
59{
60 const int n = static_cast<int>(X.rows());
61 if(n == 1) {
62 MatrixXd result(1, 1);
63 double val = X(0, 0);
64 result(0, 0) = (std::abs(val) > 1e-30) ? 1.0 / val : 1.0;
65 return result;
66 }
67 return InvBeamformerCompute::symMatPow(X, -1.0, reduceRank);
68}
69
70} // anonymous namespace
71
72//=============================================================================================================
73// DEFINE MEMBER METHODS
74//=============================================================================================================
75
76void InvBeamformerCompute::regPinv(const MatrixXd &C,
77 double reg,
78 MatrixXd &CInv,
79 double &loadingFactor,
80 int &rankOut)
81{
82 const int n = static_cast<int>(C.rows());
83
84 // Eigendecomposition of symmetric matrix
85 SelfAdjointEigenSolver<MatrixXd> eig(C);
86 VectorXd eigVals = eig.eigenvalues(); // ascending order
87 MatrixXd eigVecs = eig.eigenvectors();
88
89 // Determine rank: count eigenvalues above threshold
90 double maxEig = eigVals.maxCoeff();
91 double threshold = maxEig * 1e-10;
92 rankOut = 0;
93 for(int i = 0; i < n; ++i) {
94 if(eigVals(i) > threshold)
95 ++rankOut;
96 }
97
98 if(rankOut == 0) {
99 qWarning("InvBeamformerCompute::regPinv - Covariance matrix has zero rank!");
100 CInv = MatrixXd::Zero(n, n);
101 loadingFactor = 0.0;
102 return;
103 }
104
105 // Loading factor: reg * trace / rank
106 double trace = eigVals.sum();
107 loadingFactor = reg * trace / static_cast<double>(rankOut);
108
109 // Regularize: lambda_i += loading_factor (for significant eigenvalues)
110 // Then invert: 1 / (lambda_i + loading)
111 VectorXd eigValsInv(n);
112 for(int i = 0; i < n; ++i) {
113 if(eigVals(i) > threshold) {
114 eigValsInv(i) = 1.0 / (eigVals(i) + loadingFactor);
115 } else {
116 eigValsInv(i) = 0.0;
117 }
118 }
119
120 // Reconstruct inverse: V diag(1/lambda) V^T
121 CInv = eigVecs * eigValsInv.asDiagonal() * eigVecs.transpose();
122}
123
124//=============================================================================================================
125
126void InvBeamformerCompute::reduceLeadfieldRank(MatrixXd &Gk)
127{
128 // SVD of per-source leadfield: Gk (n_channels, n_orient)
129 JacobiSVD<MatrixXd> svd(Gk, ComputeThinU | ComputeThinV);
130 MatrixXd U = svd.matrixU();
131 VectorXd S = svd.singularValues();
132 MatrixXd V = svd.matrixV();
133
134 // Drop the smallest singular value
135 const int keep = static_cast<int>(S.size()) - 1;
136 if(keep <= 0) return;
137
138 // Reconstruct without smallest component
139 Gk = U.leftCols(keep) * S.head(keep).asDiagonal() * V.leftCols(keep).transpose();
140}
141
142//=============================================================================================================
143
144MatrixXd InvBeamformerCompute::symMatPow(const MatrixXd &X, double p, bool reduceRank)
145{
146 const int n = static_cast<int>(X.rows());
147 SelfAdjointEigenSolver<MatrixXd> eig(X);
148 VectorXd eigVals = eig.eigenvalues();
149 MatrixXd eigVecs = eig.eigenvectors();
150
151 // Find threshold
152 double maxEig = eigVals.cwiseAbs().maxCoeff();
153 double threshold = maxEig * 1e-10;
154
155 // Determine how many to keep
156 int startIdx = 0;
157 if(reduceRank) {
158 // Find first (smallest magnitude) eigenvalue above threshold, then skip it
159 for(int i = 0; i < n; ++i) {
160 if(std::abs(eigVals(i)) > threshold) {
161 startIdx = i + 1; // skip this one
162 break;
163 }
164 }
165 }
166
167 // Compute: V diag(lambda^p) V^T for significant eigenvalues
168 VectorXd eigPow = VectorXd::Zero(n);
169 for(int i = startIdx; i < n; ++i) {
170 if(std::abs(eigVals(i)) > threshold) {
171 eigPow(i) = std::pow(eigVals(i), p);
172 }
173 }
174
175 return eigVecs * eigPow.asDiagonal() * eigVecs.transpose();
176}
177
178//=============================================================================================================
179
181 const MatrixXd &Cm,
182 double reg,
183 int nOrient,
184 BeamformerWeightNorm weightNorm,
185 BeamformerPickOri pickOri,
186 bool reduceRank,
187 BeamformerInversion invMethod,
188 const MatrixX3d &nn,
189 MatrixXd &W,
190 MatrixX3d &maxPowerOri)
191{
192 const int nChannels = static_cast<int>(G.rows());
193 const int nDipoles = static_cast<int>(G.cols());
194 const int nSources = nDipoles / nOrient;
195
196 if(nSources * nOrient != nDipoles) {
197 qWarning("InvBeamformerCompute::computeBeamformer - G.cols() not divisible by nOrient!");
198 return false;
199 }
200 if(Cm.rows() != nChannels || Cm.cols() != nChannels) {
201 qWarning("InvBeamformerCompute::computeBeamformer - Cm dimension mismatch with leadfield!");
202 return false;
203 }
204
205 // -----------------------------------------------------------------------
206 // Step 1: Regularized pseudo-inverse of covariance
207 // -----------------------------------------------------------------------
208 MatrixXd CmInv;
209 double loadingFactor = 0.0;
210 int cmRank = 0;
211 regPinv(Cm, reg, CmInv, loadingFactor, cmRank);
212
213 // -----------------------------------------------------------------------
214 // Step 2--6: Per-source computation
215 // -----------------------------------------------------------------------
216 // Determine output orientation count
217 int nOrientOut = nOrient;
218 if(pickOri == BeamformerPickOri::Normal || pickOri == BeamformerPickOri::MaxPower) {
219 nOrientOut = 1;
220 }
221 // For Vector mode, keep all 3
222 if(pickOri == BeamformerPickOri::Vector) {
223 nOrientOut = nOrient;
224 }
225
226 W.resize(static_cast<Eigen::Index>(nSources) * nOrientOut, nChannels);
227 W.setZero();
228
229 if(pickOri == BeamformerPickOri::MaxPower) {
230 maxPowerOri.resize(nSources, 3);
231 maxPowerOri.setZero();
232 } else {
233 maxPowerOri.resize(0, 3);
234 }
235
236 for(int s = 0; s < nSources; ++s) {
237 // Extract per-source leadfield block: Gk (n_channels, n_orient)
238 MatrixXd Gk = G.middleCols(static_cast<Eigen::Index>(s) * nOrient, nOrient);
239
240 // Step 3: Optional rank reduction
241 if(reduceRank && nOrient > 1) {
242 reduceLeadfieldRank(Gk);
243 }
244
245 // ------------------------------------------------------------------
246 // Step 4: Orientation selection
247 // ------------------------------------------------------------------
248 int orientForFilter = nOrient;
249
250 if(pickOri == BeamformerPickOri::MaxPower) {
251 // Compute optimal orientation via max eigenvalue criterion
252 // bf_numer = Gk^T Cm^{-1} (n_orient, n_channels)
253 // bf_denom = Gk^T Cm^{-1} Gk (n_orient, n_orient)
254 MatrixXd bfNumer = Gk.transpose() * CmInv; // (n_orient, n_ch)
255 MatrixXd bfDenom = bfNumer * Gk; // (n_orient, n_orient)
256
257 MatrixXd oriNumer, oriDenom;
258 if(weightNorm == BeamformerWeightNorm::None) {
259 oriNumer = MatrixXd::Identity(nOrient, nOrient);
260 oriDenom = bfDenom;
261 } else {
262 // Sekihara & Nagarajan 2008, eq. 4.47
263 oriNumer = bfDenom;
264 oriDenom = Gk.transpose() * (CmInv * CmInv) * Gk; // (n_orient, n_orient)
265 }
266
267 // Compute oriDenom^{-1} @ oriNumer
268 MatrixXd oriDenomInv = invertSmallSym(oriDenom, reduceRank);
269 MatrixXd oriPick = oriDenomInv * oriNumer;
270
271 // Pick eigenvector with maximum absolute eigenvalue
272 // Note: oriPick is NOT necessarily symmetric -> use general eigensolver
273 EigenSolver<MatrixXd> eigSolve(oriPick);
274 VectorXcd eigVals = eigSolve.eigenvalues();
275 MatrixXcd eigVecs = eigSolve.eigenvectors();
276
277 int maxIdx = 0;
278 double maxVal = 0.0;
279 for(int i = 0; i < eigVals.size(); ++i) {
280 double absVal = std::abs(eigVals(i));
281 if(absVal > maxVal) {
282 maxVal = absVal;
283 maxIdx = i;
284 }
285 }
286
287 // Optimal orientation (real part)
288 Vector3d ori = eigVecs.col(maxIdx).real().head(3).normalized();
289
290 // Align sign with surface normal
291 if(nn.rows() > s) {
292 double dot = ori.dot(nn.row(s).transpose());
293 if(dot < 0.0) ori = -ori;
294 }
295
296 maxPowerOri.row(s) = ori.transpose();
297
298 // Project leadfield to optimal orientation
299 Gk = Gk * ori; // (n_channels, 1)
300 orientForFilter = 1;
301
302 } else if(pickOri == BeamformerPickOri::Normal && nOrient >= 3) {
303 // Extract Z-component (normal to surface in local source coords)
304 Gk = Gk.col(2); // (n_channels, 1)
305 orientForFilter = 1;
306 }
307
308 // ------------------------------------------------------------------
309 // Step 5: Compute unit-gain filter
310 // bf_numer = Gk^T Cm^{-1} (n_ori_filt, n_channels)
311 // bf_denom = Gk^T Cm^{-1} Gk (n_ori_filt, n_ori_filt)
312 // W_ug = bf_denom^{-1} bf_numer (n_ori_filt, n_channels)
313 // ------------------------------------------------------------------
314 MatrixXd bfNumer = Gk.transpose() * CmInv; // (orientForFilter, n_ch)
315 MatrixXd bfDenom = bfNumer * Gk; // (orientForFilter, orientForFilter)
316
317 MatrixXd bfDenomInv;
318 if(invMethod == BeamformerInversion::Single && orientForFilter > 1) {
319 // Scalar inversion of diagonal elements
320 bfDenomInv = MatrixXd::Zero(orientForFilter, orientForFilter);
321 for(int d = 0; d < orientForFilter; ++d) {
322 double val = bfDenom(d, d);
323 bfDenomInv(d, d) = (std::abs(val) > 1e-30) ? 1.0 / val : 0.0;
324 }
325 } else {
326 bfDenomInv = invertSmallSym(bfDenom, reduceRank);
327 }
328
329 MatrixXd Wug = bfDenomInv * bfNumer; // (orientForFilter, n_channels)
330
331 // ------------------------------------------------------------------
332 // Step 6: Weight normalization
333 // ------------------------------------------------------------------
334 if(weightNorm == BeamformerWeightNorm::UnitNoiseGain || weightNorm == BeamformerWeightNorm::NAI) {
335 // Sekihara 2008: normalize by sqrt(diag(W W^T))
336 MatrixXd noiseNorm = Wug * Wug.transpose(); // (orientForFilter, orientForFilter)
337
338 for(int d = 0; d < orientForFilter; ++d) {
339 double normVal = std::sqrt(std::abs(noiseNorm(d, d)));
340 if(normVal > 1e-30) {
341 Wug.row(d) /= normVal;
342 }
343 }
344
345 if(weightNorm == BeamformerWeightNorm::NAI) {
346 // Additional normalization by noise level
347 double noise = loadingFactor;
348 if(noise > 1e-30) {
349 Wug /= std::sqrt(noise);
350 }
351 }
352
353 } else if(weightNorm == BeamformerWeightNorm::UnitNoiseGainInv) {
354 // Rotation-invariant version: sqrtm(inner)^{-0.5} @ G^T Cm^{-1}
355 MatrixXd inner = bfNumer * bfNumer.transpose(); // (orientForFilter, orientForFilter)
356 MatrixXd innerPow = symMatPow(inner, -0.5, reduceRank);
357 Wug = innerPow * bfNumer;
358 }
359
360 // Store result
361 W.middleRows(static_cast<Eigen::Index>(s) * nOrientOut, nOrientOut) = Wug;
362 }
363
364 return true;
365}
366
367//=============================================================================================================
368
369VectorXd InvBeamformerCompute::computePower(const MatrixXd &Cm,
370 const MatrixXd &W,
371 int nOrient)
372{
373 const int nTotal = static_cast<int>(W.rows());
374 const int nSources = nTotal / nOrient;
375
376 VectorXd power(nSources);
377 for(int s = 0; s < nSources; ++s) {
378 // W_k: (n_orient, n_channels)
379 MatrixXd Wk = W.middleRows(static_cast<Eigen::Index>(s) * nOrient, nOrient);
380 // power = trace(W_k Cm W_k^T)
381 MatrixXd WCW = Wk * Cm * Wk.transpose();
382 power(s) = WCW.trace();
383 }
384
385 return power;
386}
constexpr int X
Eigen::Matrix3f S
Eigen::JacobiSVD< Eigen::Matrix3f > svd(S, Eigen::ComputeFullU|Eigen::ComputeFullV)
Shared math kernels (filter derivation, source power, regularised pseudo-inverse) used by both the LC...
Inverse source estimation (MNE, dSPM, sLORETA, dipole fitting).
static Eigen::MatrixXd symMatPow(const Eigen::MatrixXd &X, double p, bool reduceRank=false)
static Eigen::VectorXd computePower(const Eigen::MatrixXd &Cm, const Eigen::MatrixXd &W, int nOrient)
static bool computeBeamformer(const Eigen::MatrixXd &G, const Eigen::MatrixXd &Cm, double reg, int nOrient, BeamformerWeightNorm weightNorm, BeamformerPickOri pickOri, bool reduceRank, BeamformerInversion invMethod, const Eigen::MatrixX3d &nn, Eigen::MatrixXd &W, Eigen::MatrixX3d &maxPowerOri)