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