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