v2.0.0
Loading...
Searching...
No Matches
sss.cpp
Go to the documentation of this file.
1//=============================================================================================================
33
34//=============================================================================================================
35// INCLUDES
36//=============================================================================================================
37
38#include "sss.h"
39
40//=============================================================================================================
41// FIFF INCLUDES
42//=============================================================================================================
43
44#include <fiff/fiff_constants.h>
45#include <fiff/fiff_ch_info.h>
46
47//=============================================================================================================
48// EIGEN INCLUDES
49//=============================================================================================================
50
51#include <Eigen/Dense>
52#include <Eigen/SVD>
53
54//=============================================================================================================
55// QT INCLUDES
56//=============================================================================================================
57
58#include <QDebug>
59
60//=============================================================================================================
61// C++ INCLUDES
62//=============================================================================================================
63
64#include <cmath>
65
66//=============================================================================================================
67// USED NAMESPACES
68//=============================================================================================================
69
70using namespace UTILSLIB;
71using namespace FIFFLIB;
72using namespace Eigen;
73
74//=============================================================================================================
75// CONSTANTS
76//=============================================================================================================
77
78static constexpr double SSS_PI = M_PI;
79
80//=============================================================================================================
81// PRIVATE HELPERS
82//=============================================================================================================
83
84namespace {
85
86//=============================================================================================================
91MatrixXd regPinv(const MatrixXd& A, double reg = 1e-5)
92{
93 JacobiSVD<MatrixXd> svd(A, ComputeThinU | ComputeThinV);
94 const VectorXd& sv = svd.singularValues();
95 double threshold = reg * sv(0);
96 VectorXd invSv = sv;
97 for (int i = 0; i < invSv.size(); ++i) {
98 invSv(i) = (sv(i) > threshold) ? 1.0 / sv(i) : 0.0;
99 }
100 return svd.matrixV() * invSv.asDiagonal() * svd.matrixU().transpose();
101}
102
103//=============================================================================================================
107MatrixXd orthonormalCols(const MatrixXd& A)
108{
109 if (A.size() == 0) {
110 return MatrixXd(A.rows(), 0);
111 }
112
113 ColPivHouseholderQR<MatrixXd> qr(A);
114 qr.setThreshold(1e-12);
115 const int rank = qr.rank();
116 if (rank <= 0) {
117 return MatrixXd(A.rows(), 0);
118 }
119
120 return qr.householderQ() * MatrixXd::Identity(A.rows(), rank);
121}
122
123} // anonymous namespace
124
125//=============================================================================================================
126// SSS MEMBER DEFINITIONS
127//=============================================================================================================
128
129void SSS::computeNormALP(int lmax, double cosTheta, double sinTheta,
130 MatrixXd& P, MatrixXd& dP)
131{
132 // Guard against pole singularity
133 if (sinTheta < 1e-12) {
134 sinTheta = 1e-12;
135 }
136
137 const int sz = lmax + 2;
138 P.resize(sz, sz);
139 dP.resize(sz, sz);
140 P.setZero();
141 dP.setZero();
142
143 // ---- Build un-normalised ALPs using standard recurrence ----
144 // P_0^0 = 1
145 // P_1^0 = cos θ
146 // P_1^1 = -sin θ
147 // P_l^l = -(2l-1) * sin θ * P_{l-1}^{l-1}
148 // P_l^{l-1} = (2l-1) * cos θ * P_{l-1}^{l-1}
149 // P_l^m = [(2l-1)*cos θ * P_{l-1}^m - (l-1+m)*P_{l-2}^m] / (l-m)
150
151 // Temporary table (un-normalised)
152 MatrixXd Praw(sz, sz);
153 Praw.setZero();
154 Praw(0, 0) = 1.0;
155 if (lmax >= 1) {
156 Praw(1, 0) = cosTheta;
157 Praw(1, 1) = -sinTheta;
158 }
159 for (int l = 2; l <= lmax + 1; ++l) {
160 Praw(l, l) = -(2 * l - 1) * sinTheta * Praw(l - 1, l - 1);
161 Praw(l, l - 1) = (2 * l - 1) * cosTheta * Praw(l - 1, l - 1);
162 for (int m = 0; m <= l - 2; ++m) {
163 Praw(l, m) = ((2 * l - 1) * cosTheta * Praw(l - 1, m)
164 - (l - 1 + m) * Praw(l - 2, m))
165 / static_cast<double>(l - m);
166 }
167 }
168
169 // ---- Apply 4π-normalisation and compute dP/dθ ----
170 for (int l = 1; l <= lmax; ++l) {
171 for (int m = 0; m <= l; ++m) {
172 // Normalisation factor N_l^m
173 // (l-m)! / (l+m)!
174 double fac = 1.0;
175 for (int k = l - m + 1; k <= l + m; ++k) {
176 fac /= static_cast<double>(k);
177 }
178 double norm = (m == 0)
179 ? std::sqrt((2.0 * l + 1.0) / (4.0 * SSS_PI) * fac)
180 : std::sqrt(2.0 * (2.0 * l + 1.0) / (4.0 * SSS_PI) * fac);
181
182 P(l, m) = norm * Praw(l, m);
183
184 // dP_l^m/dθ using:
185 // sin θ * dP_l^m/dθ = l * cos θ * P_l^m - (l+m) * P_{l-1}^m
186 // (applied to un-normalised P, then multiplied by norm / sinθ)
187 double sinThetaDeriv = static_cast<double>(l) * cosTheta * Praw(l, m);
188 if (l - 1 >= m) {
189 sinThetaDeriv -= static_cast<double>(l + m) * Praw(l - 1, m);
190 }
191 dP(l, m) = norm * sinThetaDeriv / sinTheta;
192 }
193 }
194}
195
196//=============================================================================================================
197
198Vector3d SSS::basisGradCart(int l, int m, bool bInternal,
199 const Vector3d& rPos,
200 const MatrixXd& P, const MatrixXd& dP,
201 double cosTheta, double sinTheta,
202 double cosPhi, double sinPhi)
203{
204 // Precompute |r| and its powers
205 double r = rPos.norm();
206 if (r < 1e-12) {
207 return Vector3d::Zero();
208 }
209
210 const int absM = std::abs(m);
211
212 // ---- Y_l^m and its derivatives ----
213 //
214 // Y_l^m (real):
215 // m = 0 : N_l^0 * P_l^0(cos θ)
216 // m > 0 : N_l^m * P_l^m(cos θ) * cos(m φ)
217 // m < 0 : N_l^|m| * P_l^|m|(cos θ) * sin(|m| φ)
218 //
219 // Derivatives:
220 // dY/dθ: N * dP/dθ * {1, cos(mφ), sin(|m|φ)}
221 // dY/dφ: N * P * {0, -m sin(mφ), |m| cos(|m|φ)}
222
223 double Plm = P(l, absM); // normalised, at (θ,φ)
224 double dPlm = dP(l, absM); // normalised dP/dθ
225
226 double angFactor, dAngFactor_theta, dAngFactor_phi;
227 if (m == 0) {
228 angFactor = 1.0;
229 dAngFactor_theta = 0.0; // handled through dPlm
230 dAngFactor_phi = 0.0;
231 } else if (m > 0) {
232 double cosmPhi = std::cos(static_cast<double>(m) * std::atan2(sinPhi, cosPhi));
233 double sinmPhi = std::sin(static_cast<double>(m) * std::atan2(sinPhi, cosPhi));
234 angFactor = cosmPhi;
235 dAngFactor_theta = 0.0; // cos(mφ) doesn't depend on θ
236 dAngFactor_phi = -static_cast<double>(m) * sinmPhi;
237 } else {
238 // m < 0
239 double cosmPhi = std::cos(static_cast<double>(absM) * std::atan2(sinPhi, cosPhi));
240 double sinmPhi = std::sin(static_cast<double>(absM) * std::atan2(sinPhi, cosPhi));
241 angFactor = sinmPhi;
242 dAngFactor_theta = 0.0;
243 dAngFactor_phi = static_cast<double>(absM) * cosmPhi;
244 }
245
246 // Y_l^m at this point
247 double Ylm = Plm * angFactor;
248
249 // dY_l^m/dθ = dP/dθ * angFactor (for m=0 angFactor=1, for m≠0 it's cos/sin but θ-independent)
250 double dYdTheta = dPlm * angFactor;
251
252 // dY_l^m/dφ = P_l^|m| * dAngFactor/dφ
253 double dYdPhi = Plm * dAngFactor_phi;
254
255 // ---- Gradient in spherical coordinates ----
256 // For internal: grad(r^l * Y_l^m)
257 // G_r = l * r^{l-1} * Y_l^m
258 // G_θ = r^{l-1} * dY/dθ
259 // G_φ = r^{l-1} / sinθ * dY/dφ (handle sinθ below)
260 //
261 // For external: grad(r^{-(l+1)} * Y_l^m)
262 // G_r = -(l+1) * r^{-(l+2)} * Y_l^m
263 // G_θ = r^{-(l+2)} * dY/dθ
264 // G_φ = r^{-(l+2)} / sinθ * dY/dφ
265
266 double radPow, Gr_coeff, Gtu_coeff;
267 if (bInternal) {
268 radPow = std::pow(r, l - 1);
269 Gr_coeff = static_cast<double>(l) * radPow;
270 Gtu_coeff = radPow; // G_θ and sinθ*G_φ share this
271 } else {
272 radPow = std::pow(r, -(l + 2));
273 Gr_coeff = -static_cast<double>(l + 1) * radPow;
274 Gtu_coeff = radPow;
275 }
276
277 double Gr = Gr_coeff * Ylm;
278 double Gtheta = Gtu_coeff * dYdTheta;
279 double GphiTimesSin = Gtu_coeff * dYdPhi; // = G_φ * sinθ (avoids 1/sinθ singularity)
280
281 // ---- Convert (Gr, Gθ, sinθ * Gφ) to Cartesian ----
282 // r̂ = (sinθ cosφ, sinθ sinφ, cosθ)
283 // θ̂ = (cosθ cosφ, cosθ sinφ, -sinθ)
284 // φ̂ = (-sinφ, cosφ, 0 )
285 //
286 // grad_cart = Gr * r̂ + Gθ * θ̂ + Gφ * φ̂
287 // = Gr * r̂ + Gθ * θ̂ + (sinθ * Gφ) / sinθ * φ̂
288 // Numerically we use GphiTimesSin / sinθ for the φ̂ component.
289
290 double Gphi = (sinTheta > 1e-12) ? GphiTimesSin / sinTheta : 0.0;
291
292 double gx = Gr * sinTheta * cosPhi + Gtheta * cosTheta * cosPhi - Gphi * sinPhi;
293 double gy = Gr * sinTheta * sinPhi + Gtheta * cosTheta * sinPhi + Gphi * cosPhi;
294 double gz = Gr * cosTheta - Gtheta * sinTheta;
295
296 return Vector3d(gx, gy, gz);
297}
298
299//=============================================================================================================
300
301SSS::Basis SSS::computeBasis(const FiffInfo& fiffInfo, const Params& params)
302{
303 Basis basis;
304 basis.iOrderIn = params.iOrderIn;
305 basis.iOrderOut = params.iOrderOut;
306 basis.iNin = params.iOrderIn * (params.iOrderIn + 2);
307 basis.iNout = params.iOrderOut * (params.iOrderOut + 2);
308
309 // ---- Collect MEG channel indices and geometry ----
310 for (int i = 0; i < fiffInfo.nchan; ++i) {
311 int kind = fiffInfo.chs[i].kind;
312 if (kind == FIFFV_MEG_CH || kind == FIFFV_REF_MEG_CH) {
313 basis.megChannelIdx.append(i);
314 }
315 }
316
317 const int nMeg = basis.megChannelIdx.size();
318 if (nMeg == 0) {
319 qWarning() << "SSS::computeBasis: no MEG channels found in FiffInfo.";
320 return basis;
321 }
322
323 basis.matSin.resize(nMeg, basis.iNin);
324 basis.matSout.resize(nMeg, basis.iNout);
325 basis.matSin.setZero();
326 basis.matSout.setZero();
327
328 const int lmax = std::max(params.iOrderIn, params.iOrderOut);
329
330 // ---- Fill basis matrices row-by-row (one sensor per row) ----
331 for (int si = 0; si < nMeg; ++si) {
332 const FiffChInfo& ch = fiffInfo.chs[basis.megChannelIdx[si]];
333
334 // Sensor position relative to SSS origin (metres)
335 Vector3d rPos(static_cast<double>(ch.chpos.r0(0)) - params.origin(0),
336 static_cast<double>(ch.chpos.r0(1)) - params.origin(1),
337 static_cast<double>(ch.chpos.r0(2)) - params.origin(2));
338
339 // Sensor normal (ez of coil coordinate system)
340 Vector3d normal(static_cast<double>(ch.chpos.ez(0)),
341 static_cast<double>(ch.chpos.ez(1)),
342 static_cast<double>(ch.chpos.ez(2)));
343
344 // Normalise the normal vector
345 double nNorm = normal.norm();
346 if (nNorm < 1e-12) {
347 continue;
348 }
349 normal /= nNorm;
350
351 // Convert to spherical coordinates
352 double r = rPos.norm();
353 if (r < 1e-12) {
354 continue;
355 }
356 double cosTheta = rPos(2) / r;
357 double sinTheta = std::sqrt(rPos(0) * rPos(0) + rPos(1) * rPos(1)) / r;
358 if (sinTheta < 1e-12) sinTheta = 1e-12;
359 double phi = std::atan2(rPos(1), rPos(0));
360 double cosPhi = std::cos(phi);
361 double sinPhi = std::sin(phi);
362
363 // Normalised ALP tables for this sensor
364 MatrixXd P, dP;
365 computeNormALP(lmax, cosTheta, sinTheta, P, dP);
366
367 // Internal basis columns: iterate (l=1..L_in, m=-l..l)
368 int colIn = 0;
369 for (int l = 1; l <= params.iOrderIn; ++l) {
370 for (int m = -l; m <= l; ++m) {
371 Vector3d grad = basisGradCart(l, m, /*internal=*/true,
372 rPos, P, dP,
373 cosTheta, sinTheta, cosPhi, sinPhi);
374 basis.matSin(si, colIn) = normal.dot(grad);
375 ++colIn;
376 }
377 }
378
379 // External basis columns: iterate (l=1..L_out, m=-l..l)
380 int colOut = 0;
381 for (int l = 1; l <= params.iOrderOut; ++l) {
382 for (int m = -l; m <= l; ++m) {
383 Vector3d grad = basisGradCart(l, m, /*internal=*/false,
384 rPos, P, dP,
385 cosTheta, sinTheta, cosPhi, sinPhi);
386 basis.matSout(si, colOut) = normal.dot(grad);
387 ++colOut;
388 }
389 }
390 }
391
392 // ---- Compute combined pseudoinverse and internal projector ----
393 //
394 // S = [S_in | S_out] (n_meg × (N_in + N_out))
395 // pinv(S) = V * diag(1/σ_i) * U^T with Tikhonov regularisation
396 // P_in = S_in * pinv(S)[:N_in, :]
397 MatrixXd S(nMeg, basis.iNin + basis.iNout);
398 S.leftCols(basis.iNin) = basis.matSin;
399 S.rightCols(basis.iNout) = basis.matSout;
400
401 basis.matPinvAll = regPinv(S, params.dRegIn); // (N_in+N_out) × n_meg
402
403 // Build an oblique projector onto the internal subspace along the external
404 // subspace. This preserves internal components better than the direct
405 // joint-fit projector while still annihilating pure external components.
406 const MatrixXd Qin = orthonormalCols(basis.matSin);
407 const MatrixXd Qout = orthonormalCols(basis.matSout);
408
409 if (Qin.cols() == 0) {
410 basis.matProjIn = MatrixXd::Zero(nMeg, nMeg);
411 return basis;
412 }
413
414 const MatrixXd I = MatrixXd::Identity(nMeg, nMeg);
415 const MatrixXd QoutPerp = Qout.cols() > 0 ? (I - Qout * Qout.transpose()) : I;
416 const MatrixXd gram = Qin.transpose() * QoutPerp * Qin;
417 basis.matProjIn = Qin * regPinv(gram, params.dRegIn) * Qin.transpose() * QoutPerp;
418
419 return basis;
420}
421
422//=============================================================================================================
423
424MatrixXd SSS::apply(const MatrixXd& matData, const Basis& basis)
425{
426 if (basis.megChannelIdx.isEmpty()) {
427 return matData;
428 }
429
430 const int nMeg = basis.megChannelIdx.size();
431 MatrixXd matOut = matData;
432
433 // Extract MEG rows
434 MatrixXd megData(nMeg, matData.cols());
435 for (int i = 0; i < nMeg; ++i) {
436 megData.row(i) = matData.row(basis.megChannelIdx[i]);
437 }
438
439 // Apply internal projector: data_sss = P_in * data_meg
440 MatrixXd megSss = basis.matProjIn * megData;
441
442 // Write back
443 for (int i = 0; i < nMeg; ++i) {
444 matOut.row(basis.megChannelIdx[i]) = megSss.row(i);
445 }
446
447 return matOut;
448}
449
450//=============================================================================================================
451
452MatrixXd SSS::applyTemporal(const MatrixXd& matData,
453 const Basis& basis,
454 int iBufferLength,
455 double dCorrLimit)
456{
457 if (basis.megChannelIdx.isEmpty()) {
458 return matData;
459 }
460
461 const int nMeg = basis.megChannelIdx.size();
462 const int nSamp = static_cast<int>(matData.cols());
463 const int bufLen = std::min(iBufferLength, nSamp);
464
465 MatrixXd matOut = matData;
466
467 // Extract MEG rows
468 MatrixXd megData(nMeg, nSamp);
469 for (int i = 0; i < nMeg; ++i) {
470 megData.row(i) = matData.row(basis.megChannelIdx[i]);
471 }
472
473 // Decompose time series into expansion coefficients
474 // c_in (N_in × nSamp) = pinv_all[:N_in , :] * megData
475 // c_out (N_out × nSamp) = pinv_all[N_in: , :] * megData
476 MatrixXd cIn = basis.matPinvAll.topRows(basis.iNin) * megData; // N_in × nSamp
477 MatrixXd cOut = basis.matPinvAll.bottomRows(basis.iNout) * megData; // N_out × nSamp
478
479 // Process in sliding windows
480 int offset = 0;
481 while (offset < nSamp) {
482 int winLen = std::min(bufLen, nSamp - offset);
483
484 // Window slices
485 MatrixXd cInWin = cIn.middleCols(offset, winLen); // N_in × winLen
486 MatrixXd cOutWin = cOut.middleCols(offset, winLen); // N_out × winLen
487
488 // ---- Temporal tSSS projection ----
489 // SVD of external coefficient matrix (column = time point):
490 // cOutWin = U * S * V^T (N_out × winLen)
491 // Right singular vectors V (winLen × min) form the temporal subspace of external signals.
492 JacobiSVD<MatrixXd> svd(cOutWin, ComputeThinU | ComputeThinV);
493 const VectorXd& sv = svd.singularValues();
494 const MatrixXd& V = svd.matrixV(); // winLen × rank_out
495
496 // Determine how many external temporal components to suppress
497 double svMax = (sv.size() > 0) ? sv(0) : 0.0;
498 if (svMax < 1e-30) {
499 offset += winLen;
500 continue;
501 }
502
503 // Build temporal projector to remove the correlated subspace
504 // P_remove: (winLen × winLen) = V_r * V_r^T
505 // Applied: cInWin_clean = cInWin * (I - P_remove)
506 MatrixXd Vr; // winLen × n_remove
507 int nRemove = 0;
508 for (int k = 0; k < sv.size(); ++k) {
509 if (sv(k) / svMax > dCorrLimit) {
510 ++nRemove;
511 } else {
512 break;
513 }
514 }
515
516 if (nRemove > 0) {
517 Vr = V.leftCols(nRemove); // winLen × nRemove
518 // cInWin_clean = cInWin * (I - Vr * Vr^T)
519 MatrixXd cInWinClean = cInWin - cInWin * (Vr * Vr.transpose());
520 cIn.middleCols(offset, winLen) = cInWinClean;
521 }
522
523 offset += winLen;
524 }
525
526 // Reconstruct cleaned MEG data from internal expansion
527 MatrixXd megTsss = basis.matSin * cIn; // n_meg × nSamp
528
529 // Write back
530 for (int i = 0; i < nMeg; ++i) {
531 matOut.row(basis.megChannelIdx[i]) = megTsss.row(i);
532 }
533
534 return matOut;
535}
#define M_PI
Fiff constants.
#define FIFFV_REF_MEG_CH
#define FIFFV_MEG_CH
FiffChInfo class declaration.
Declaration of the SSS class implementing Signal Space Separation (SSS) and temporal Signal Space Sep...
FIFF file I/O and data structures (raw, epochs, evoked, covariance, forward).
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
double dRegIn
Definition sss.h:107
Eigen::Vector3d origin
Definition sss.h:106
SSSParams Params
Definition sss.h:113
static Basis computeBasis(const FIFFLIB::FiffInfo &fiffInfo, const Params &params=Params())
Definition sss.cpp:301
static Eigen::MatrixXd apply(const Eigen::MatrixXd &matData, const Basis &basis)
Definition sss.cpp:424
static Eigen::MatrixXd applyTemporal(const Eigen::MatrixXd &matData, const Basis &basis, int iBufferLength=10000, double dCorrLimit=0.98)
Definition sss.cpp:452
Precomputed SSS basis and projectors for a given sensor array.
Definition sss.h:122
Eigen::MatrixXd matSout
Definition sss.h:124
Eigen::MatrixXd matSin
Definition sss.h:123
Eigen::MatrixXd matPinvAll
Definition sss.h:126
QVector< int > megChannelIdx
Definition sss.h:127
Eigen::MatrixXd matProjIn
Definition sss.h:125
Channel info descriptor.
Eigen::Vector3f r0
Eigen::Vector3f ez
FIFF measurement file information.
Definition fiff_info.h:86
QList< FiffChInfo > chs