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