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