v2.0.0
Loading...
Searching...
No Matches
fwd_field_map.cpp
Go to the documentation of this file.
1//=============================================================================================================
22
23//=============================================================================================================
24// INCLUDES
25//=============================================================================================================
26
27#include "fwd_field_map.h"
28
29#include <fiff/fiff_proj.h>
30#include <fiff/fiff_file.h>
31
32#include <Eigen/SVD>
33#include <QRegularExpression>
34#include <algorithm>
35#include <cmath>
36#include <vector>
37
38//=============================================================================================================
39// USED NAMESPACES
40//=============================================================================================================
41
42using namespace Eigen;
43using namespace FWDLIB;
44
45//=============================================================================================================
46// LOCAL CONSTANTS AND HELPERS
47//=============================================================================================================
48
49namespace
50{
51
52constexpr int kNCoeff = 100; // Legendre polynomial terms
53constexpr double kMegConst = 4e-14 * M_PI; // mu_0^2 / (4*pi)
54constexpr double kEegConst = 1.0 / (4.0 * M_PI); // 1 / (4*pi)
55constexpr double kEegIntradScale = 0.7; // EEG integration radius scale
56
57constexpr float kGradStd = 5e-13f; // gradiometer noise std (5 fT/cm)
58constexpr float kMagStd = 20e-15f; // magnetometer noise std (20 fT)
59constexpr float kEegStd = 1e-6f; // EEG noise std (1 µV)
60
61//=============================================================================================================
62
63// Legendre polynomial P_n(x) with first and second derivatives for n = 0..ncoeff-1.
64void computeLegendreDer(double x, int ncoeff,
65 double* p, double* pd, double* pdd)
66{
67 p[0] = 1.0;
68 pd[0] = 0.0;
69 pdd[0] = 0.0;
70 if (ncoeff < 2)
71 return;
72 p[1] = x;
73 pd[1] = 1.0;
74 pdd[1] = 0.0;
75 for (int n = 2; n < ncoeff; ++n) {
76 double old_p = p[n - 1];
77 double old_pd = pd[n - 1];
78 p[n] = ((2 * n - 1) * x * old_p - (n - 1) * p[n - 2]) / n;
79 pd[n] = n * old_p + x * old_pd;
80 pdd[n] = (n + 1) * old_pd + x * pdd[n - 1];
81 }
82}
83
84//=============================================================================================================
85
86// Legendre polynomial P_n(x) for n = 0..ncoeff-1 (three-term recurrence).
87void computeLegendreVal(double x, int ncoeff, double* p)
88{
89 p[0] = 1.0;
90 if (ncoeff < 2)
91 return;
92 p[1] = x;
93 for (int n = 2; n < ncoeff; ++n) {
94 p[n] = ((2 * n - 1) * x * p[n - 1] - (n - 1) * p[n - 2]) / n;
95 }
96}
97
98//=============================================================================================================
99
100// MEG Legendre series sums (four components, n = 1..kNCoeff-1).
101void compSumsMeg(double beta, double ctheta, double sums[4])
102{
103 double p[kNCoeff], pd[kNCoeff], pdd[kNCoeff];
104 computeLegendreDer(ctheta, kNCoeff, p, pd, pdd);
105
106 sums[0] = sums[1] = sums[2] = sums[3] = 0.0;
107 double betan = beta; // accumulates beta^(n+1)
108 for (int n = 1; n < kNCoeff; ++n) {
109 betan *= beta; // beta^(n+1)
110 double dn = static_cast<double>(n);
111 double multn = dn / (2.0 * dn + 1.0); // n / (2n+1)
112 double mult = multn / (dn + 1.0); // n / ((2n+1)(n+1))
113
114 sums[0] += (dn + 1.0) * multn * p[n] * betan;
115 sums[1] += multn * pd[n] * betan;
116 sums[2] += mult * pd[n] * betan;
117 sums[3] += mult * pdd[n] * betan;
118 }
119}
120
121//=============================================================================================================
122
123// EEG Legendre series sum (n = 1..kNCoeff-1).
124double compSumEeg(double beta, double ctheta)
125{
126 double p[kNCoeff];
127 computeLegendreVal(ctheta, kNCoeff, p);
128
129 double sum = 0.0;
130 double betan = 1.0;
131 for (int n = 1; n < kNCoeff; ++n) {
132 betan *= beta; // beta^n
133 double dn = static_cast<double>(n);
134 double factor = 2.0 * dn + 1.0;
135 sum += p[n] * betan * factor * factor / dn;
136 }
137 return sum;
138}
139
140//=============================================================================================================
141
142// MEG sphere dot product for two integration points.
143double sphereDotMeg(double intrad,
144 const Vector3d& rr1, double lr1, const Vector3d& cosmag1,
145 const Vector3d& rr2, double lr2, const Vector3d& cosmag2)
146{
147 if (lr1 == 0.0 || lr2 == 0.0)
148 return 0.0;
149
150 double beta = (intrad * intrad) / (lr1 * lr2);
151 double ct = std::clamp(rr1.dot(rr2), -1.0, 1.0);
152
153 double sums[4];
154 compSumsMeg(beta, ct, sums);
155
156 double n1c1 = cosmag1.dot(rr1);
157 double n1c2 = cosmag1.dot(rr2);
158 double n2c1 = cosmag2.dot(rr1);
159 double n2c2 = cosmag2.dot(rr2);
160 double n1n2 = cosmag1.dot(cosmag2);
161
162 double part1 = ct * n1c1 * n2c2;
163 double part2 = n1c1 * n2c1 + n1c2 * n2c2;
164
165 double result = n1c1 * n2c2 * sums[0] + (2.0 * part1 - part2) * sums[1] + (n1n2 + part1 - part2) * sums[2] + (n1c2 - ct * n1c1) * (n2c1 - ct * n2c2) * sums[3];
166
167 result *= kMegConst / (lr1 * lr2);
168 return result;
169}
170
171//=============================================================================================================
172
173// EEG sphere dot product for two integration points.
174double sphereDotEeg(double intrad,
175 const Vector3d& rr1, double lr1,
176 const Vector3d& rr2, double lr2)
177{
178 if (lr1 == 0.0 || lr2 == 0.0)
179 return 0.0;
180
181 double beta = (intrad * intrad) / (lr1 * lr2);
182 double ct = std::clamp(rr1.dot(rr2), -1.0, 1.0);
183
184 double sum = compSumEeg(beta, ct);
185 return kEegConst * sum / (lr1 * lr2);
186}
187
188//=============================================================================================================
189
190// Per-coil data: normalised positions relative to sphere origin.
191struct CoilData
192{
193 Eigen::MatrixX3d rmag; // normalised position vectors (np × 3)
194 Eigen::VectorXd rlen; // magnitudes (np)
195 Eigen::MatrixX3d cosmag; // direction vectors (np × 3)
196 Eigen::VectorXd w; // integration weights (np)
197
198 int np() const
199 {
200 return static_cast<int>(rlen.size());
201 }
202};
203
204//=============================================================================================================
205
206// Extract and normalise coil integration-point data relative to sphere origin.
207CoilData extractCoilData(const FwdCoil* coil, const Vector3d& r0)
208{
209 CoilData cd;
210 const int n = coil->np;
211 cd.rmag.resize(n, 3);
212 cd.rlen.resize(n);
213 cd.cosmag.resize(n, 3);
214 cd.w.resize(n);
215
216 for (int i = 0; i < n; ++i) {
217 Vector3d rel = coil->rmag.row(i).cast<double>().transpose() - r0;
218 double len = rel.norm();
219 if (len > 0.0)
220 cd.rmag.row(i) = (rel / len).transpose();
221 else
222 cd.rmag.row(i).setZero();
223 cd.rlen(i) = len;
224 cd.cosmag.row(i) = coil->cosmag.row(i).cast<double>();
225 cd.w(i) = static_cast<double>(coil->w[i]);
226 }
227 return cd;
228}
229
230//=============================================================================================================
231
232// Compute sensor self-dot-product matrix (nchan x nchan, symmetric).
233MatrixXd doSelfDots(double intrad, const FwdCoilSet& coils, const Vector3d& r0, bool isMeg)
234{
235 const int nc = coils.ncoil();
236 std::vector<CoilData> cdata(nc);
237 for (int i = 0; i < nc; ++i) {
238 cdata[i] = extractCoilData(coils.coils[i].get(), r0);
239 }
240
241 MatrixXd products = MatrixXd::Zero(nc, nc);
242 for (int ci1 = 0; ci1 < nc; ++ci1) {
243 for (int ci2 = 0; ci2 <= ci1; ++ci2) {
244 double dot = 0.0;
245 const CoilData& c1 = cdata[ci1];
246 const CoilData& c2 = cdata[ci2];
247 for (int i = 0; i < c1.np(); ++i) {
248 for (int j = 0; j < c2.np(); ++j) {
249 double ww = c1.w(i) * c2.w(j);
250 if (isMeg) {
251 dot += ww * sphereDotMeg(intrad, c1.rmag.row(i).transpose(), c1.rlen(i), c1.cosmag.row(i).transpose(), c2.rmag.row(j).transpose(), c2.rlen(j), c2.cosmag.row(j).transpose());
252 } else {
253 dot += ww * sphereDotEeg(intrad, c1.rmag.row(i).transpose(), c1.rlen(i), c2.rmag.row(j).transpose(), c2.rlen(j));
254 }
255 }
256 }
257 products(ci1, ci2) = dot;
258 products(ci2, ci1) = dot;
259 }
260 }
261 return products;
262}
263
264//=============================================================================================================
265
266// Compute surface-to-sensor dot-product matrix (nvert x nchan).
267MatrixXd doSurfaceDots(double intrad, const FwdCoilSet& coils,
268 const MatrixX3f& rr, const MatrixX3f& nn,
269 const Vector3d& r0, bool isMeg)
270{
271 const int nc = coils.ncoil();
272 const int nv = rr.rows();
273
274 std::vector<CoilData> cdata(nc);
275 for (int i = 0; i < nc; ++i) {
276 cdata[i] = extractCoilData(coils.coils[i].get(), r0);
277 }
278
279 MatrixXd products = MatrixXd::Zero(nv, nc);
280 for (int vi = 0; vi < nv; ++vi) {
281 // Vertex position relative to origin (normalised)
282 Vector3d rel = rr.row(vi).cast<double>() - r0.transpose();
283 double lsurf = rel.norm();
284 Vector3d rsurf = (lsurf > 0.0) ? Vector3d(rel / lsurf) : Vector3d::Zero();
285 Vector3d nsurf = nn.row(vi).cast<double>(); // surface normal (MEG cosmag)
286
287 for (int ci = 0; ci < nc; ++ci) {
288 const CoilData& c = cdata[ci];
289 double dot = 0.0;
290 for (int j = 0; j < c.np(); ++j) {
291 if (isMeg) {
292 dot += c.w(j) * sphereDotMeg(intrad, rsurf, lsurf, nsurf, c.rmag.row(j).transpose(), c.rlen(j), c.cosmag.row(j).transpose());
293 } else {
294 dot += c.w(j) * sphereDotEeg(intrad, rsurf, lsurf, c.rmag.row(j).transpose(), c.rlen(j));
295 }
296 }
297 products(vi, ci) = dot;
298 }
299 }
300 return products;
301}
302
303//=============================================================================================================
304
305// MEG ad-hoc noise standard deviations.
306VectorXd adHocMegStds(const FwdCoilSet& coils)
307{
308 VectorXd stds(coils.ncoil());
309 for (int k = 0; k < coils.ncoil(); ++k) {
310 stds(k) = coils.coils[k]->is_axial_coil() ? static_cast<double>(kMagStd)
311 : static_cast<double>(kGradStd);
312 }
313 return stds;
314}
315
316//=============================================================================================================
317
318// EEG ad-hoc noise standard deviation (uniform).
319VectorXd adHocEegStds(int ncoil)
320{
321 return VectorXd::Constant(ncoil, static_cast<double>(kEegStd));
322}
323
324//=============================================================================================================
325
326// Compute mapping matrix via whitened SVD pseudo-inverse.
327// Returns float for GPU-friendly rendering; all internal math is double.
328std::unique_ptr<MatrixXf> computeMappingMatrix(const MatrixXd& selfDots,
329 const MatrixXd& surfaceDots,
330 const VectorXd& noiseStds,
331 double miss,
332 const MatrixXd& projOp = MatrixXd(),
333 bool applyAvgRef = false)
334{
335 if (selfDots.rows() == 0 || surfaceDots.rows() == 0) {
336 return nullptr;
337 }
338
339 const int nchan = selfDots.rows();
340
341 // Apply SSP projector to self-dots
342 MatrixXd projDots;
343 bool hasProj = (projOp.rows() == nchan && projOp.cols() == nchan);
344 if (hasProj) {
345 projDots = projOp.transpose() * selfDots * projOp;
346 } else {
347 projDots = selfDots;
348 }
349
350 // Build whitener
351 VectorXd whitener(nchan);
352 for (int i = 0; i < nchan; ++i) {
353 whitener(i) = (noiseStds(i) > 0.0) ? (1.0 / noiseStds(i)) : 0.0;
354 }
355
356 // Whiten self-dots
357 MatrixXd whitenedDots = whitener.asDiagonal() * projDots * whitener.asDiagonal();
358
359 // SVD pseudo-inverse with eigenvalue truncation
360 JacobiSVD<MatrixXd> svd(whitenedDots, ComputeFullU | ComputeFullV);
361 VectorXd s = svd.singularValues();
362 if (s.size() == 0 || s(0) <= 0.0) {
363 return nullptr;
364 }
365
366 // Eigenvalue truncation: keep components explaining >= (1-miss) of total variance
367 VectorXd varexp(s.size());
368 varexp(0) = s(0);
369 for (int i = 1; i < s.size(); ++i) {
370 varexp(i) = varexp(i - 1) + s(i);
371 }
372 double totalVar = varexp(s.size() - 1);
373
374 int n = s.size(); // keep all by default
375 for (int i = 0; i < s.size(); ++i) {
376 if (varexp(i) / totalVar >= (1.0 - miss)) {
377 n = i + 1;
378 break;
379 }
380 }
381
382 // Truncated pseudo-inverse
383 VectorXd sinv = VectorXd::Zero(s.size());
384 for (int i = 0; i < n; ++i) {
385 sinv(i) = (s(i) > 0.0) ? (1.0 / s(i)) : 0.0;
386 }
387 MatrixXd inv = svd.matrixV() * sinv.asDiagonal() * svd.matrixU().transpose();
388
389 // Unwhiten inverse
390 MatrixXd invWhitened = whitener.asDiagonal() * inv * whitener.asDiagonal();
391
392 // Apply projector to inverse
393 MatrixXd invWhitenedProj;
394 if (hasProj) {
395 invWhitenedProj = projOp.transpose() * invWhitened;
396 } else {
397 invWhitenedProj = invWhitened;
398 }
399
400 // Compute final mapping
401 MatrixXd mapping = surfaceDots * invWhitenedProj;
402
403 // Apply average reference for EEG
404 if (applyAvgRef) {
405 VectorXd colMeans = mapping.colwise().mean();
406 mapping.rowwise() -= colMeans.transpose();
407 }
408
409 // Convert to float
410 MatrixXf mappingF = mapping.cast<float>();
411 return std::make_unique<MatrixXf>(std::move(mappingF));
412}
413
414} // anonymous namespace
415
416//=============================================================================================================
417// DEFINE MEMBER METHODS
418//=============================================================================================================
419
420std::unique_ptr<MatrixXf> FwdFieldMap::computeMegMapping(
421 const FwdCoilSet& coils,
422 const MatrixX3f& vertices,
423 const MatrixX3f& normals,
424 const Vector3f& origin,
425 float intrad,
426 float miss)
427{
428 if (coils.ncoil() <= 0 || vertices.rows() == 0 || normals.rows() != vertices.rows()) {
429 return nullptr;
430 }
431
432 const Vector3d r0 = origin.cast<double>();
433
434 MatrixXd selfDots = doSelfDots(intrad, coils, r0, /*isMeg=*/true);
435 MatrixXd surfaceDots = doSurfaceDots(intrad, coils, vertices, normals, r0, /*isMeg=*/true);
436 VectorXd stds = adHocMegStds(coils);
437
438 return computeMappingMatrix(selfDots, surfaceDots, stds, static_cast<double>(miss));
439}
440
441//=============================================================================================================
442
443std::unique_ptr<MatrixXf> FwdFieldMap::computeMegMapping(
444 const FwdCoilSet& coils,
445 const MatrixX3f& vertices,
446 const MatrixX3f& normals,
447 const Vector3f& origin,
448 const FIFFLIB::FiffInfo& info,
449 const QStringList& chNames,
450 float intrad,
451 float miss)
452{
453 if (coils.ncoil() <= 0 || vertices.rows() == 0 || normals.rows() != vertices.rows()) {
454 return nullptr;
455 }
456
457 const Vector3d r0 = origin.cast<double>();
458
459 MatrixXd selfDots = doSelfDots(intrad, coils, r0, /*isMeg=*/true);
460 MatrixXd surfaceDots = doSurfaceDots(intrad, coils, vertices, normals, r0, /*isMeg=*/true);
461 VectorXd stds = adHocMegStds(coils);
462
463 // Build SSP projector
464 MatrixXd projOp;
465 FIFFLIB::FiffProj::make_projector(info.projs, chNames, projOp, info.bads);
466
467 return computeMappingMatrix(selfDots, surfaceDots, stds,
468 static_cast<double>(miss), projOp, false);
469}
470
471//=============================================================================================================
472
473std::unique_ptr<MatrixXf> FwdFieldMap::computeEegMapping(
474 const FwdCoilSet& coils,
475 const MatrixX3f& vertices,
476 const Vector3f& origin,
477 float intrad,
478 float miss)
479{
480 if (coils.ncoil() <= 0 || vertices.rows() == 0) {
481 return nullptr;
482 }
483
484 const double eegIntrad = intrad * kEegIntradScale;
485 const Vector3d r0 = origin.cast<double>();
486
487 // EEG sphere dot does not use surface normals, so pass zeros
488 MatrixX3f dummyNormals = MatrixX3f::Zero(vertices.rows(), 3);
489
490 MatrixXd selfDots = doSelfDots(eegIntrad, coils, r0, /*isMeg=*/false);
491 MatrixXd surfaceDots = doSurfaceDots(eegIntrad, coils, vertices, dummyNormals, r0, /*isMeg=*/false);
492 VectorXd stds = adHocEegStds(coils.ncoil());
493
494 return computeMappingMatrix(selfDots, surfaceDots, stds, static_cast<double>(miss));
495}
496
497//=============================================================================================================
498
499std::unique_ptr<MatrixXf> FwdFieldMap::computeEegMapping(
500 const FwdCoilSet& coils,
501 const MatrixX3f& vertices,
502 const Vector3f& origin,
503 const FIFFLIB::FiffInfo& info,
504 const QStringList& chNames,
505 float intrad,
506 float miss)
507{
508 if (coils.ncoil() <= 0 || vertices.rows() == 0) {
509 return nullptr;
510 }
511
512 const double eegIntrad = intrad * kEegIntradScale;
513 const Vector3d r0 = origin.cast<double>();
514
515 MatrixX3f dummyNormals = MatrixX3f::Zero(vertices.rows(), 3);
516 MatrixXd selfDots = doSelfDots(eegIntrad, coils, r0, /*isMeg=*/false);
517 MatrixXd surfaceDots = doSurfaceDots(eegIntrad, coils, vertices, dummyNormals, r0, /*isMeg=*/false);
518 VectorXd stds = adHocEegStds(coils.ncoil());
519
520 // Build SSP projector
521 MatrixXd projOp;
522 FIFFLIB::FiffProj::make_projector(info.projs, chNames, projOp, info.bads);
523
524 // Check for average EEG reference projection
525 bool hasAvgRef = false;
526 for (const auto& proj : info.projs) {
527 if (proj.kind == FIFFV_PROJ_ITEM_EEG_AVREF ||
528 proj.desc.contains(QRegularExpression("^Average .* reference$",
529 QRegularExpression::CaseInsensitiveOption))) {
530 hasAvgRef = true;
531 break;
532 }
533 }
534
535 return computeMappingMatrix(selfDots, surfaceDots, stds,
536 static_cast<double>(miss), projOp, hasAvgRef);
537}
FIFF tag-kind, block-kind and type-code numerical definitions, authoritative for FIFFLIB.
#define FIFFV_PROJ_ITEM_EEG_AVREF
Definition fiff_file.h:821
Eigen::JacobiSVD< Eigen::Matrix3f > svd(S, Eigen::ComputeFullU|Eigen::ComputeFullV)
SSP projection item: a named projection vector set with active/desired flags, parsed from FIFFB_PROJ_...
#define M_PI
Sphere-model field interpolator that maps measured MEG/EEG values onto a dense scalp or cortical surf...
Forward modelling — BEM solver, spherical models, sensor/coil definitions and the lead-field assembly...
Definition compute_fwd.h:85
QList< FiffProj > projs
Definition fiff_info.h:292
static fiff_int_t make_projector(const QList< FiffProj > &projs, const QStringList &ch_names, Eigen::MatrixXd &proj, const QStringList &bads=defaultQStringList, Eigen::MatrixXd &U=defaultMatrixXd)
Single MEG sensor coil or EEG electrode — stores the coil-local frame and the (r_mag,...
Definition fwd_coil.h:93
Eigen::Matrix< float, Eigen::Dynamic, 3, Eigen::RowMajor > cosmag
Definition fwd_coil.h:178
Eigen::Matrix< float, Eigen::Dynamic, 3, Eigen::RowMajor > rmag
Definition fwd_coil.h:177
Eigen::VectorXf w
Definition fwd_coil.h:179
Container of FwdCoil instances acting both as the in-memory image of the coil_def....
std::vector< FwdCoil::UPtr > coils
static std::unique_ptr< Eigen::MatrixXf > computeEegMapping(const FwdCoilSet &coils, const Eigen::MatrixX3f &vertices, const Eigen::Vector3f &origin, float intrad=0.06f, float miss=1e-3f)
static std::unique_ptr< Eigen::MatrixXf > computeMegMapping(const FwdCoilSet &coils, const Eigen::MatrixX3f &vertices, const Eigen::MatrixX3f &normals, const Eigen::Vector3f &origin, float intrad=0.06f, float miss=1e-4f)