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