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