v2.0.0
Loading...
Searching...
No Matches
fwd_eeg_sphere_model.cpp
Go to the documentation of this file.
1//=============================================================================================================
17
18//=============================================================================================================
19// INCLUDES
20//=============================================================================================================
21
23
26
27#include <qmath.h>
28
29#include <Eigen/Core>
30
31#include <algorithm>
32#include <vector>
33#include <Eigen/Dense>
34
35//=============================================================================================================
36// USED NAMESPACES
37//=============================================================================================================
38
39using namespace Eigen;
40using namespace UTILSLIB;
41using namespace FWDLIB;
42
43//=============================================================================================================
44// Local constants and helpers
45//=============================================================================================================
46
47namespace
48{
49constexpr int FAIL = -1;
50constexpr int OK = 0;
51constexpr int MAXTERMS = 1000;
52constexpr double EPS = 1e-10;
53constexpr double SIN_EPS = 1e-3;
54} // anonymous namespace
55
56//=============================================================================================================
57// DEFINE MEMBER METHODS
58//=============================================================================================================
59
61: nterms(0)
62, nfit(0)
63, scale_pos(0)
64{
65 r0.setZero();
66}
67
68//=============================================================================================================
69
71{
72 int k;
73
74 if (!p_FwdEegSphereModel.name.isEmpty())
75 this->name = p_FwdEegSphereModel.name;
76 if (p_FwdEegSphereModel.nlayer() > 0) {
77 for (k = 0; k < p_FwdEegSphereModel.nlayer(); k++)
78 this->layers.push_back(p_FwdEegSphereModel.layers[k]);
79 }
80 this->r0 = p_FwdEegSphereModel.r0;
81 if (p_FwdEegSphereModel.nterms > 0) {
82 this->fn = VectorXd(p_FwdEegSphereModel.nterms);
83 this->nterms = p_FwdEegSphereModel.nterms;
84 for (k = 0; k < p_FwdEegSphereModel.nterms; k++)
85 this->fn[k] = p_FwdEegSphereModel.fn[k];
86 }
87 if (p_FwdEegSphereModel.nfit > 0) {
88 this->mu = VectorXf(p_FwdEegSphereModel.nfit);
89 this->lambda = VectorXf(p_FwdEegSphereModel.nfit);
90 this->nfit = p_FwdEegSphereModel.nfit;
91 for (k = 0; k < p_FwdEegSphereModel.nfit; k++) {
92 this->mu[k] = p_FwdEegSphereModel.mu[k];
93 this->lambda[k] = p_FwdEegSphereModel.lambda[k];
94 }
95 }
96 this->scale_pos = p_FwdEegSphereModel.scale_pos;
97}
98
99//=============================================================================================================
100
102 int nlayer,
103 const VectorXf& rads,
104 const VectorXf& sigmas)
105/*
106 * Produce a new sphere model structure
107 */
108{
109 auto new_model = std::make_unique<FwdEegSphereModel>();
110
111 new_model->name = name;
112
113 for (int k = 0; k < nlayer; k++) {
114 FwdEegSphereLayer layer;
115 layer.rad = layer.rel_rad = rads[k];
116 layer.sigma = sigmas[k];
117 new_model->layers.push_back(layer);
118 }
119 /*
120 * Sort...
121 */
122 std::sort(new_model->layers.begin(), new_model->layers.end(), FwdEegSphereLayer::comp_layers);
123
124 /*
125 * Scale the radiuses
126 */
127 float R = new_model->layers[nlayer - 1].rad;
128 float rR = new_model->layers[nlayer - 1].rel_rad;
129 for (int k = 0; k < nlayer; k++) {
130 new_model->layers[k].rad = new_model->layers[k].rad / R;
131 new_model->layers[k].rel_rad = new_model->layers[k].rel_rad / rR;
132 }
133 return new_model;
134}
135
136//=============================================================================================================
137
141
142//=============================================================================================================
143
144FwdEegSphereModel::UPtr FwdEegSphereModel::setup_eeg_sphere_model(const QString& eeg_model_file, QString eeg_model_name, float eeg_sphere_rad)
145{
146 if (eeg_model_name.isEmpty())
147 eeg_model_name = QString("Default");
148
150 eeg_models->fwd_list_eeg_sphere_models();
151
152 FwdEegSphereModel::UPtr eeg_model(eeg_models->fwd_select_eeg_sphere_model(eeg_model_name));
153 if (!eeg_model) {
154 return nullptr;
155 }
156
157 if (!eeg_model->fwd_setup_eeg_sphere_model(eeg_sphere_rad, true, 3)) {
158 return nullptr;
159 }
160
161 qInfo("Using EEG sphere model \"%s\" with scalp radius %7.1f mm",
162 eeg_model->name.toUtf8().constData(), 1000 * eeg_sphere_rad);
163 return eeg_model;
164}
165
166//=============================================================================================================
167
169
170{
171 fitUser u = new fitUserRec();
172 u->y.resize(nterms - 1);
173 u->resi.resize(nterms - 1);
174 u->M = MatrixXd::Zero(nterms - 1, nfit - 1);
175 u->uu = MatrixXd::Zero(nfit - 1, nterms - 1);
176 u->vv = MatrixXd::Zero(nfit - 1, nfit - 1);
177 u->sing.resize(nfit);
178 u->fn.resize(nterms);
179 u->w.resize(nterms);
180 u->nfit = nfit;
181 u->nterms = nterms;
182 return u;
183}
184
185//=============================================================================================================
186// fwd_multi_spherepot.c
188{
189 MatrixXd M, Mn, help, Mm;
190 static MatrixXd mat1;
191 static MatrixXd mat2;
192 static MatrixXd mat3;
193 static VectorXd c1;
194 static VectorXd c2;
195 static VectorXd cr;
196 static VectorXd cr_mult;
197 double div, div_mult;
198 double n1;
199#ifdef TEST
200 double rel1, rel2;
201 double b, c;
202#endif
203 int k;
204
205 if (this->nlayer() == 0 || this->nlayer() == 1)
206 return 1.0;
207 /*
208 * Now follows the tricky case
209 */
210#ifdef TEST
211 if (this->nlayer() == 2) {
212 rel1 = layers[0].sigma / layers[1].sigma;
213 n1 = n + 1.0;
214 div_mult = 2.0 * n + 1;
215 b = pow(this->layers[0].rel_rad, div_mult);
216 return div_mult / ((n1 + n * rel1) + b * n1 * (rel1 - 1.0));
217 } else if (this->nlayer() == 3) {
218 rel1 = this->layers[0].sigma / this->layers[1].sigma;
219 rel2 = this->layers[1].sigma / this->layers[2].sigma;
220 n1 = n + 1.0;
221 div_mult = 2.0 * n + 1.0;
222 b = pow(this->layers[0].rel_rad, div_mult);
223 c = pow(this->layers[1].rel_rad, div_mult);
224 div_mult = div_mult * div_mult;
225 div = (b * n * n1 * (rel1 - 1.0) * (rel2 - 1.0) + c * (rel1 * n + n1) * (rel2 * n + n1)) / c +
226 n1 * (b * (rel1 - 1.0) * (rel2 * n1 + n) + c * (rel1 * n + n1) * (rel2 - 1.0));
227 return div_mult / div;
228 }
229#endif
230 if (n == 1) {
231 /*
232 * Initialize the arrays
233 */
234 c1.resize(this->nlayer() - 1);
235 c2.resize(this->nlayer() - 1);
236 cr.resize(this->nlayer() - 1);
237 cr_mult.resize(this->nlayer() - 1);
238 for (k = 0; k < this->nlayer() - 1; k++) {
239 c1[k] = this->layers[k].sigma / this->layers[k + 1].sigma;
240 c2[k] = c1[k] - 1.0;
241 cr_mult[k] = this->layers[k].rel_rad;
242 cr[k] = cr_mult[k];
243 cr_mult[k] = cr_mult[k] * cr_mult[k];
244 }
245 if (mat1.cols() == 0)
246 mat1 = MatrixXd(2, 2);
247 if (mat2.cols() == 0)
248 mat2 = MatrixXd(2, 2);
249 if (mat3.cols() == 0)
250 mat3 = MatrixXd(2, 2);
251 }
252 /*
253 * Increment the radius coefficients
254 */
255 for (k = 0; k < this->nlayer() - 1; k++)
256 cr[k] = cr[k] * cr_mult[k];
257 /*
258 * Multiply the matrices
259 */
260 M = mat1;
261 Mn = mat2;
262 Mm = mat3;
263 M(0, 0) = M(1, 1) = 1.0;
264 M(0, 1) = M(1, 0) = 0.0;
265 div = 1.0;
266 div_mult = 2.0 * n + 1.0;
267 n1 = n + 1.0;
268
269 for (k = this->nlayer() - 2; k >= 0; k--) {
270 Mm(0, 0) = (n + n1 * c1[k]);
271 Mm(0, 1) = n1 * c2[k] / cr[k];
272 Mm(1, 0) = n * c2[k] * cr[k];
273 Mm(1, 1) = n1 + n * c1[k];
274
275 Mn(0, 0) = Mm(0, 0) * M(0, 0) + Mm(0, 1) * M(1, 0);
276 Mn(0, 1) = Mm(0, 0) * M(0, 1) + Mm(0, 1) * M(1, 1);
277 Mn(1, 0) = Mm(1, 0) * M(0, 0) + Mm(1, 1) * M(1, 0);
278 Mn(1, 1) = Mm(1, 0) * M(0, 1) + Mm(1, 1) * M(1, 1);
279 help = M;
280 M = Mn;
281 Mn = help;
282 div = div * div_mult;
283 }
284 return n * div / (n * M(1, 1) + n1 * M(1, 0));
285}
286
287//=============================================================================================================
288// fwd_multi_spherepot.c
289void FwdEegSphereModel::next_legen(int n, double x, double& p0, double& p01, double& p1, double& p11)
290/*
291 * Compute the next Legendre polynomials of the
292 * first and second kind using the recursion formulas.
293 *
294 * The routine initializes automatically with the known values
295 * when n = 1
296 */
297{
298 double help0, help1;
299
300 if (n > 1) {
301 help0 = p0;
302 help1 = p1;
303 p0 = ((2 * n - 1) * x * help0 - (n - 1) * (p01)) / n;
304 p1 = ((2 * n - 1) * x * help1 - n * (p11)) / (n - 1);
305 p01 = help0;
306 p11 = help1;
307 } else if (n == 0) {
308 p0 = 1.0;
309 p1 = 0.0;
310 } else if (n == 1) {
311 p01 = 1.0;
312 p0 = x;
313 p11 = 0.0;
314 p1 = sqrt(1.0 - x * x);
315 }
316 return;
317}
318
319//=============================================================================================================
320
321void FwdEegSphereModel::calc_pot_components(double beta, double cgamma, double& Vrp, double& Vtp, const Eigen::VectorXd& fn, int nterms)
322{
323 double Vt = 0.0;
324 double Vr = 0.0;
325 double p0, p01, p1, p11;
326 double betan, multn;
327 int n;
328
329 betan = 1.0;
330 p0 = p01 = p1 = p11 = 0.0;
331 for (n = 1; n <= nterms; n++) {
332 if (betan < EPS) {
333 break;
334 }
335 next_legen(n, cgamma, p0, p01, p1, p11);
336 multn = betan * fn[n - 1]; /* The 2*n + 1 factor is included in fn */
337 Vr = Vr + multn * p0;
338 Vt = Vt + multn * p1 / n;
339 betan = beta * betan;
340 }
341 Vrp = Vr;
342 Vtp = Vt;
343 return;
344}
345
346//=============================================================================================================
347// fwd_multi_spherepot.c
348int FwdEegSphereModel::fwd_eeg_multi_spherepot(const Eigen::Vector3f& rd_in, const Eigen::Vector3f& Q_in, const Eigen::Matrix<float, Eigen::Dynamic, 3, Eigen::RowMajor>& el, int neeg, Eigen::VectorXf& Vval, void* client)
349/*
350 * Compute the electric potentials in a set of electrodes in spherically
351 * Symmetric head model.
352 *
353 * The code is based on the formulas presented in
354 *
355 * Z. Zhang, A fast method to compute surface potentials
356 * generated by dipoles within multilayer anisotropic spheres,
357 * Phys. Med. Biol., 40, 335 - 349, 1995.
358 *
359 * and
360 *
361 * J.C. Moscher, R.M. Leahy, and P.S. Lewis, Matrix Kernels for
362 * Modeling of EEG and MEG Data, Los Alamos Technical Report,
363 * LA-UR-96-1993, 1996.
364 *
365 * This version does not use the acceleration with help of equivalent sources
366 * in the homogeneous model
367 *
368 */
369{
370 auto* m = static_cast<FwdEegSphereModel*>(client);
371 Eigen::Vector3f rd = rd_in - m->r0;
372 Eigen::Vector3f Q = Q_in;
373 Eigen::Vector3f pos;
374 int k;
375 float pos2, rd_len, pos_len;
376 double beta, cos_gamma, Vr, Vt;
377 Eigen::Vector3f vec1, vec2;
378 float v1, v2;
379 float cos_beta, Qr, Qt, Q2, c;
380 float pi4_inv = static_cast<float>(0.25 / M_PI);
381 float sigmaM_inv;
382 /*
383 * Precompute the coefficients
384 */
385 if (m->fn.size() == 0 || m->nterms != MAXTERMS) {
386 m->fn.resize(MAXTERMS);
387 m->nterms = MAXTERMS;
388 for (k = 0; k < MAXTERMS; k++)
389 m->fn[k] = (2 * k + 3) * m->fwd_eeg_get_multi_sphere_model_coeff(k + 1);
390 }
391 /*
392 * Move to the sphere coordinates
393 */
394 rd_len = rd.norm();
395 Q2 = Q.dot(Q);
396 /*
397 * Ignore dipoles outside the innermost sphere
398 */
399 if (rd_len >= m->layers[0].rad) {
400 for (k = 0; k < neeg; k++)
401 Vval[k] = 0.0;
402 return OK;
403 }
404 /*
405 * Special case: rd and Q are parallel
406 */
407 c = rd.dot(Q) / (rd_len * sqrt(Q2));
408 if ((1.0 - c * c) < SIN_EPS) { /* Almost parallel:
409 * Q is purely radial */
410 Qr = sqrt(Q2);
411 Qt = 0.0;
412 cos_beta = 0.0;
413 v1 = 0.0;
414 vec1.setZero();
415 } else {
416 vec1 = rd.cross(Q);
417 v1 = vec1.norm();
418 cos_beta = 0.0;
419 Qr = Qt = 0.0;
420 }
421 for (k = 0; k < neeg; k++) {
422 pos = el.row(k).transpose() - m->r0;
423 /*
424 * Should the position be scaled or not?
425 */
426 if (m->scale_pos) {
427 pos_len = m->layers[m->nlayer() - 1].rad / pos.norm();
428#ifdef DEBUG
429 qInfo("%10.4f %10.4f %10.4f %10.4f", pos_len, 1000 * pos[0], 1000 * pos[1], 1000 * pos[2]);
430#endif
431 pos *= pos_len;
432 }
433 pos2 = pos.dot(pos);
434 pos_len = sqrt(pos2);
435 /*
436 * Calculate the two ingredients for the final result
437 */
438 cos_gamma = pos.dot(rd) / (rd_len * pos_len);
439 beta = rd_len / pos_len;
440 calc_pot_components(beta, cos_gamma, Vr, Vt, m->fn, m->nterms);
441 /*
442 * Then compute the combined result
443 */
444 if (v1 > 0.0) {
445 vec2 = rd.cross(pos);
446 v2 = vec2.norm();
447
448 if (v2 > 0.0)
449 cos_beta = vec1.dot(vec2) / (v1 * v2);
450 else
451 cos_beta = 0.0;
452
453 Qr = Q.dot(rd) / rd_len;
454 Qt = sqrt(Q2 - Qr * Qr);
455 }
456 Vval[k] = static_cast<double>(pi4_inv) * (static_cast<double>(Qr) * Vr + static_cast<double>(Qt) * cos_beta * Vt) / pos2;
457 }
458 /*
459 * Scale by the conductivity if we have the layers
460 * defined
461 */
462 if (m->nlayer() > 0) {
463 sigmaM_inv = 1.0 / m->layers[m->nlayer() - 1].sigma;
464 for (k = 0; k < neeg; k++)
465 Vval[k] = Vval[k] * sigmaM_inv;
466 }
467 return OK;
468}
469
470//=============================================================================================================
471// fwd_multi_spherepot.c
472int FwdEegSphereModel::fwd_eeg_multi_spherepot_coil1(const Eigen::Vector3f& rd, const Eigen::Vector3f& Q, FwdCoilSet& els, Eigen::Ref<Eigen::VectorXf> Vval, void* client) /* Client data will be the sphere model definition */
473/*
474 * Calculate the EEG in the sphere model using the fwdCoilSet structure
475 *
476 * This version does not use the acceleration with help of equivalent sources
477 * in the homogeneous model
478 *
479 */
480{
481 VectorXf vval_one;
482 float val;
483 int nvval = 0;
484 int k, c;
485 FwdCoil* el;
486
487 Vval.resize(els.ncoil());
488 for (k = 0; k < els.ncoil(); k++, el++) {
489 el = els.coils[k].get();
490 if (el->coil_class == FWD_COILC_EEG) {
491 if (el->np > nvval) {
492 vval_one.resize(el->np);
493 nvval = el->np;
494 }
495 if (fwd_eeg_multi_spherepot(rd, Q, el->rmag, el->np, vval_one, client) != OK) {
496 return FAIL;
497 }
498 for (c = 0, val = 0.0; c < el->np; c++)
499 val += el->w[c] * vval_one[c];
500 Vval[k] = val;
501 }
502 }
503 return OK;
504}
505
506//=============================================================================================================
507// fwd_multi_spherepot.c
508bool FwdEegSphereModel::fwd_eeg_spherepot_vec(const Eigen::Vector3f& rd_in, const Eigen::Matrix<float, Eigen::Dynamic, 3, Eigen::RowMajor>& el, int neeg, Eigen::MatrixXf& Vval_vec, void* client)
509{
510 auto* m = static_cast<FwdEegSphereModel*>(client);
511 float fact = 0.25f / static_cast<float>(M_PI);
512 Eigen::Vector3f a_vec;
513 float a, a2, a3;
514 float rrd, rd2, rd2_inv, r, r2, ra, rda;
515 float F;
516 float c1, c2, m1, m2;
517 int k, eq;
518 Eigen::Vector3f orig_rd = rd_in - m->r0;
519 Eigen::Vector3f rd;
520 Eigen::Vector3f pos;
521 float pos_len;
522 /*
523 * Initialize the arrays
524 */
525 for (k = 0; k < neeg; k++) {
526 Vval_vec(0, k) = 0.0;
527 Vval_vec(1, k) = 0.0;
528 Vval_vec(2, k) = 0.0;
529 }
530 /*
531 * Ignore dipoles outside the innermost sphere
532 */
533 if (orig_rd.norm() >= m->layers[0].rad)
534 return true;
535 /*
536 * Default to homogeneous model if no model was previously set
537 */
538#ifdef FOO
539 if (nequiv == 0) /* what to do */
540 eeg_set_homog_sphere_model();
541#endif
542 /*
543 * Make a weighted sum over the equivalence parameters
544 */
545 for (eq = 0; eq < m->nfit; eq++) {
546 /*
547 * Scale the dipole position
548 */
549 rd = m->mu[eq] * orig_rd;
550
551 rd2 = rd.dot(rd);
552 rd2_inv = 1.0 / rd2;
553
554 /*
555 * Go over all electrodes
556 */
557 for (k = 0; k < neeg; k++) {
558 pos = el.row(k).transpose() - m->r0;
559 /*
560 * Scale location onto the surface of the sphere
561 */
562 if (m->scale_pos) {
563 pos_len = m->layers[m->nlayer() - 1].rad / pos.norm();
564 pos *= pos_len;
565 }
566
567 /* Vector from dipole to the field point */
568
569 a_vec = pos - rd;
570
571 /* Compute the dot products needed */
572
573 a2 = a_vec.dot(a_vec);
574 a = sqrt(a2);
575 a3 = 2.0 / (a2 * a);
576 r2 = pos.dot(pos);
577 r = sqrt(r2);
578 rrd = pos.dot(rd);
579 ra = r2 - rrd;
580 rda = rrd - rd2;
581
582 /* The main ingredients */
583
584 F = a * (r * a + ra);
585 c1 = a3 * rda + 1.0 / a - 1.0 / r;
586 c2 = a3 + (a + r) / (r * F);
587
588 /* Mix them together and scale by lambda/(rd*rd) */
589
590 m1 = (c1 - c2 * rrd);
591 m2 = c2 * rd2;
592
593 Vval_vec(0, k) = Vval_vec(0, k) + m->lambda[eq] * rd2_inv * (m1 * rd[0] + m2 * pos[0]);
594 Vval_vec(1, k) = Vval_vec(1, k) + m->lambda[eq] * rd2_inv * (m1 * rd[1] + m2 * pos[1]);
595 Vval_vec(2, k) = Vval_vec(2, k) + m->lambda[eq] * rd2_inv * (m1 * rd[2] + m2 * pos[2]);
596 } /* All electrodes done */
597 } /* All equivalent dipoles done */
598 /*
599 * Finish by scaling by 1/(4*M_PI);
600 */
601 for (k = 0; k < neeg; k++) {
602 Vval_vec(0, k) = fact * Vval_vec(0, k);
603 Vval_vec(1, k) = fact * Vval_vec(1, k);
604 Vval_vec(2, k) = fact * Vval_vec(2, k);
605 }
606 return true;
607}
608
609//=============================================================================================================
610// fwd_multi_spherepot.c
611int FwdEegSphereModel::fwd_eeg_spherepot_coil_vec(const Eigen::Vector3f& rd, FwdCoilSet& els, Eigen::Ref<Eigen::MatrixXf> Vval_vec, void* client)
612{
613 Eigen::MatrixXf vval_one;
614 float val;
615 int nvval = 0;
616 int k, c, p;
617 FwdCoil* el;
618
619 for (k = 0; k < els.ncoil(); k++, el++) {
620 el = els.coils[k].get();
621 if (el->coil_class == FWD_COILC_EEG) {
622 if (el->np > nvval) {
623 vval_one.resize(3, el->np);
624 nvval = el->np;
625 }
626 if (!fwd_eeg_spherepot_vec(rd, el->rmag, el->np, vval_one, client)) {
627 return FAIL;
628 }
629 for (p = 0; p < 3; p++) {
630 for (c = 0, val = 0.0; c < el->np; c++)
631 val += el->w[c] * vval_one(p, c);
632 Vval_vec(p, k) = val;
633 }
634 }
635 }
636 return OK;
637}
638
639//=============================================================================================================
640
641int FwdEegSphereModel::fwd_eeg_spherepot_grad_coil(const Eigen::Vector3f& rd, const Eigen::Vector3f& Q, FwdCoilSet& coils, Eigen::Ref<Eigen::VectorXf> Vval, Eigen::Ref<Eigen::VectorXf> xgrad, Eigen::Ref<Eigen::VectorXf> ygrad, Eigen::Ref<Eigen::VectorXf> zgrad, void* client) /* Client data to be passed to some foward modelling routines */
642/*
643 * Quick and dirty solution: use differences
644 *
645 * This routine uses the acceleration with help of equivalent sources
646 * in the homogeneous sphere.
647 *
648 */
649{
650 Eigen::Vector3f my_rd;
651 float step = 0.0005f;
652 float step2 = 2 * step;
653 int p, q;
654
655 Eigen::Ref<Eigen::VectorXf>* grads[3] = {&xgrad, &ygrad, &zgrad};
656
657 for (p = 0; p < 3; p++) {
658 my_rd = rd;
659 my_rd[p] += step;
660 if (fwd_eeg_spherepot_coil(my_rd, Q, coils, *grads[p], client) == FAIL)
661 return FAIL;
662 my_rd = rd;
663 my_rd[p] -= step;
664 if (fwd_eeg_spherepot_coil(my_rd, Q, coils, Vval, client) == FAIL)
665 return FAIL;
666 for (q = 0; q < coils.ncoil(); q++)
667 (*grads[p])[q] = ((*grads[p])[q] - Vval[q]) / step2;
668 }
669 if (fwd_eeg_spherepot_coil(rd, Q, coils, Vval, client) == FAIL)
670 return FAIL;
671 return OK;
672}
673
674//=============================================================================================================
675// fwd_multi_spherepot.c
676int FwdEegSphereModel::fwd_eeg_spherepot(const Eigen::Vector3f& rd_in,
677 const Eigen::Vector3f& Q_in,
678 const Eigen::Matrix<float, Eigen::Dynamic, 3, Eigen::RowMajor>& el,
679 int neeg,
680 VectorXf& Vval,
681 void* client)
682/*
683 * This routine calculates the potentials for a specific dipole direction
684 *
685 * This routine uses the acceleration with help of equivalent sources
686 * in the homogeneous sphere.
687 */
688{
689 auto* m = static_cast<FwdEegSphereModel*>(client);
690 float fact = static_cast<float>(0.25 / M_PI);
691 Eigen::Vector3f a_vec;
692 float a, a2, a3;
693 float rrd, rd2, rd2_inv, r, r2, ra, rda;
694 float F;
695 float c1, c2, m1, m2, f1, f2;
696 int k, eq;
697 Eigen::Vector3f orig_rd = rd_in - m->r0;
698 Eigen::Vector3f rd;
699 Eigen::Vector3f Q = Q_in;
700 Eigen::Vector3f pos;
701 float pos_len;
702 /*
703 * Initialize the arrays
704 */
705 for (k = 0; k < neeg; k++)
706 Vval[k] = 0.0;
707 /*
708 * Ignore dipoles outside the innermost sphere
709 */
710 if (orig_rd.norm() >= m->layers[0].rad)
711 return true;
712 /*
713 * Default to homogeneous model if no model was previously set
714 */
715#ifdef FOO
716 if (nequiv == 0) /* what to do */
717 eeg_set_homog_sphere_model();
718#endif
719 /*
720 * Make a weighted sum over the equivalence parameters
721 */
722 for (eq = 0; eq < m->nfit; eq++) {
723 /*
724 * Scale the dipole position
725 */
726 rd = m->mu[eq] * orig_rd;
727
728 rd2 = rd.dot(rd);
729 rd2_inv = 1.0 / rd2;
730
731 f1 = rd.dot(Q);
732 /*
733 * Go over all electrodes
734 */
735 for (k = 0; k < neeg; k++) {
736 pos = el.row(k).transpose() - m->r0;
737 /*
738 * Scale location onto the surface of the sphere
739 */
740 if (m->scale_pos) {
741 pos_len = m->layers[m->nlayer() - 1].rad / pos.norm();
742 pos *= pos_len;
743 }
744
745 /* Vector from dipole to the field point */
746
747 a_vec = pos - rd;
748
749 /* Compute the dot products needed */
750
751 a2 = a_vec.dot(a_vec);
752 a = sqrt(a2);
753 a3 = 2.0 / (a2 * a);
754 r2 = pos.dot(pos);
755 r = sqrt(r2);
756 rrd = pos.dot(rd);
757 ra = r2 - rrd;
758 rda = rrd - rd2;
759
760 /* The main ingredients */
761
762 F = a * (r * a + ra);
763 c1 = a3 * rda + 1.0 / a - 1.0 / r;
764 c2 = a3 + (a + r) / (r * F);
765
766 /* Mix them together and scale by lambda/(rd*rd) */
767
768 m1 = (c1 - c2 * rrd);
769 m2 = c2 * rd2;
770
771 f2 = pos.dot(Q);
772 Vval[k] = Vval[k] + m->lambda[eq] * rd2_inv * (m1 * f1 + m2 * f2);
773 } /* All electrodes done */
774 } /* All equivalent dipoles done */
775 /*
776 * Finish by scaling by 1/(4*M_PI);
777 */
778 for (k = 0; k < neeg; k++)
779 Vval[k] = fact * Vval[k];
780 return OK;
781}
782
783//=============================================================================================================
784// fwd_multi_spherepot.c
785int FwdEegSphereModel::fwd_eeg_spherepot_coil(const Eigen::Vector3f& rd, const Eigen::Vector3f& Q, FwdCoilSet& els, Eigen::Ref<Eigen::VectorXf> Vval, void* client)
786{
787 VectorXf vval_one;
788 float val;
789 int nvval = 0;
790 int k, c;
791 FwdCoil* el;
792
793 for (k = 0; k < els.ncoil(); k++, el++) {
794 el = els.coils[k].get();
795 if (el->coil_class == FWD_COILC_EEG) {
796 if (el->np > nvval) {
797 vval_one.resize(el->np);
798 nvval = el->np;
799 }
800 if (fwd_eeg_spherepot(rd, Q, el->rmag, el->np, vval_one, client) != OK) {
801 return FAIL;
802 }
803 for (c = 0, val = 0.0; c < el->np; c++)
804 val += el->w[c] * vval_one[c];
805 Vval[k] = val;
806 }
807 }
808 return OK;
809}
810
811//=============================================================================================================
812// fwd_eeg_sphere_models.c
813bool FwdEegSphereModel::fwd_setup_eeg_sphere_model(float rad, bool fit_berg_scherg, int nFit)
814{
815 int nTerms = 200;
816 float rv;
817
818 /*
819 * Scale the relative radiuses
820 */
821 for (int k = 0; k < this->nlayer(); k++)
822 this->layers[k].rad = rad * this->layers[k].rel_rad;
823
824 if (fit_berg_scherg) {
825 if (this->fwd_eeg_fit_berg_scherg(nTerms, nFit, rv)) {
826 qInfo("Equiv. model fitting -> RV = %g %%", 100 * rv);
827 for (int k = 0; k < nFit; k++)
828 qInfo("mu%d = %g\tlambda%d = %g", k + 1, this->mu[k], k + 1, this->layers[this->nlayer() - 1].sigma * this->lambda[k]);
829 } else
830 return false;
831 }
832
833 qInfo("Defined EEG sphere model with rad = %7.2f mm", 1000.0 * rad);
834 return true;
835}
836
837static void compute_svd(Eigen::MatrixXd& mat,
838 Eigen::VectorXd& sing,
839 Eigen::MatrixXd& uu,
840 Eigen::MatrixXd* vv)
841/*
842 * Compute the SVD of mat.
843 * Results are stored in sing, uu, and optionally vv.
844 * mat is not modified.
845 */
846{
847 int udim = std::min(static_cast<int>(mat.rows()), static_cast<int>(mat.cols()));
848
849 Eigen::JacobiSVD<Eigen::MatrixXd> svd(mat, Eigen::ComputeFullU | Eigen::ComputeFullV);
850
851 sing = svd.singularValues();
852 uu = svd.matrixU().transpose().topRows(udim);
853
854 if (vv != nullptr)
855 *vv = svd.matrixV().transpose();
856}
857
858/*
859 * Include the simplex and SVD code here.
860 * It is not too much of a problem
861 */
862
863
864namespace FWDLIB
865{
866
871{
872 double lambda;
873 double mu;
874};
875
876} // namespace
877
878static void sort_parameters(VectorXd& mu, VectorXd& lambda, int nfit)
879{
880 std::vector<BergSchergPar> pars(nfit);
881
882 for (int k = 0; k < nfit; k++) {
883 pars[k].mu = mu[k];
884 pars[k].lambda = lambda[k];
885 }
886
887 std::sort(pars.begin(), pars.end(), [](const BergSchergPar& a, const BergSchergPar& b) {
888 return a.mu > b.mu;
889 });
890
891 for (int k = 0; k < nfit; k++) {
892 mu[k] = pars[k].mu;
893 lambda[k] = pars[k].lambda;
894 }
895}
896
897static bool report_fit([[maybe_unused]] int loop,
898 [[maybe_unused]] const VectorXd& fitpar,
899 [[maybe_unused]] double Smin,
900 double /*fval_hi*/,
901 double /*par_diff*/)
902{
903#ifdef LOG_FIT
904 for (int k = 0; k < fitpar.size(); k++)
905 qInfo("%g ", mu[k]);
906 qInfo("%g", Smin);
907#endif
908 return true;
909}
910
911static MatrixXd get_initial_simplex(const VectorXd& pars,
912 double simplex_size)
913
914{
915 int npar = pars.size();
916
917 MatrixXd simplex = MatrixXd::Zero(npar + 1, npar);
918
919 simplex.rowwise() += pars.transpose();
920
921 for (int k = 1; k < npar + 1; k++)
922 simplex(k, k - 1) += simplex_size;
923
924 return simplex;
925}
926
928{
929 double mu1n, k1;
930 int k, p;
931 /*
932 * y is the data to be fitted (nterms-1 x 1)
933 * M is the model matrix (nterms-1 x nfit-1)
934 */
935 for (k = 0; k < u->nterms - 1; k++) {
936 k1 = k + 1;
937 mu1n = pow(mu[0], k1);
938 u->y[k] = u->w[k] * (u->fn[k + 1] - mu1n * u->fn[0]);
939 for (p = 0; p < u->nfit - 1; p++)
940 u->M(k, p) = u->w[k] * (pow(mu[p + 1], k1) - mu1n);
941 }
942}
943
944// fwd_fit_berg_scherg.c
946 VectorXd& lambda,
947 fitUser u)
948/*
949 * Compute the best-fitting linear parameters
950 * Return the corresponding RV
951 */
952{
953 int k, p, q;
954 VectorXd vec(u->nfit - 1);
955 double sum;
956
958
959 compute_svd(u->M, u->sing, u->uu, &u->vv);
960 /*
961 * Compute the residuals
962 */
963 for (k = 0; k < u->nterms - 1; k++)
964 u->resi[k] = u->y[k];
965
966 for (p = 0; p < u->nfit - 1; p++) {
967 vec[p] = u->uu.row(p).head(u->nterms - 1).dot(u->y.head(u->nterms - 1));
968 for (k = 0; k < u->nterms - 1; k++)
969 u->resi[k] = u->resi[k] - u->uu(p, k) * vec[p];
970 vec[p] = vec[p] / u->sing[p];
971 }
972
973 for (p = 0; p < u->nfit - 1; p++) {
974 for (q = 0, sum = 0.0; q < u->nfit - 1; q++)
975 sum += u->vv(q, p) * vec[q];
976 lambda[p + 1] = sum;
977 }
978 for (p = 1, sum = 0.0; p < u->nfit; p++)
979 sum += lambda[p];
980 lambda[0] = u->fn[0] - sum;
981 return u->resi.head(u->nterms - 1).squaredNorm() / u->y.head(u->nterms - 1).squaredNorm();
982}
983
984// fwd_fit_berg_scherg.c
985double FwdEegSphereModel::one_step(const VectorXd& mu, const void* user_data)
986/*
987 * Evaluate the residual sum of squares fit for one set of
988 * mu values
989 */
990{
991 int k, p;
992 double dot;
993 fitUser u = (fitUser)user_data;
994
995 for (k = 0; k < u->nfit; k++) {
996 if (std::fabs(mu[k]) > 1.0)
997 return 1.0;
998 }
999 /*
1000 * Compose the data for the linear fitting
1001 */
1003 /*
1004 * Compute SVD
1005 */
1006 compute_svd(u->M, u->sing, u->uu, nullptr);
1007 /*
1008 * Compute the residuals
1009 */
1010 for (k = 0; k < u->nterms - 1; k++)
1011 u->resi[k] = u->y[k];
1012 for (p = 0; p < u->nfit - 1; p++) {
1013 dot = u->uu.row(p).head(u->nterms - 1).dot(u->y.head(u->nterms - 1));
1014 for (k = 0; k < u->nterms - 1; k++)
1015 u->resi[k] = u->resi[k] - u->uu(p, k) * dot;
1016 }
1017 /*
1018 * Return their sum of squares
1019 */
1020 return u->resi.head(u->nterms - 1).squaredNorm();
1021}
1022
1023// fwd_fit_berg_scherg.c
1024bool FwdEegSphereModel::fwd_eeg_fit_berg_scherg(int nTerms, /* Number of terms to use in the series expansion
1025 * when fitting the parameters */
1026 int nFit, /* Number of equivalent dipoles to fit */
1027 float& rv)
1028/*
1029 * This routine fits the Berg-Scherg equivalent spherical model
1030 * dipole parameters by minimizing the difference between the
1031 * actual and approximative series expansions
1032 */
1033{
1034 bool res = false;
1035 int k;
1036 double rd, R, f;
1037 double simplex_size = 0.01;
1038 MatrixXd simplex;
1039 VectorXd func_val;
1040 double ftol = 1e-9;
1041 VectorXd lambdaFit;
1042 VectorXd muFit;
1043 int neval;
1044 int max_eval = 1000;
1045 int report = 1;
1046 fitUser u = new_fit_user(nFit, nTerms);
1047
1048 if (nFit < 2) {
1049 qWarning("fwd_fit_berg_scherg does not work with less than two equivalent sources.");
1050 return false;
1051 }
1052
1053 /*
1054 * (1) Calculate the coefficients of the true expansion
1055 */
1056 for (k = 0; k < nTerms; k++)
1057 u->fn[k] = this->fwd_eeg_get_multi_sphere_model_coeff(k + 1);
1058
1059 /*
1060 * (2) Calculate the weighting
1061 */
1062 rd = R = this->layers[0].rad;
1063 for (k = 1; k < this->nlayer(); k++) {
1064 if (this->layers[k].rad > R)
1065 R = this->layers[k].rad;
1066 if (this->layers[k].rad < rd)
1067 rd = this->layers[k].rad;
1068 }
1069 f = rd / R;
1070
1071#ifdef ZHANG
1072 /*
1073 * This is the Zhang weighting
1074 */
1075 for (k = 1; k < nTerms; k++)
1076 u->w[k - 1] = pow(f, k);
1077#else
1078 /*
1079 * This is the correct weighting
1080 */
1081 for (k = 1; k < nTerms; k++)
1082 u->w[k - 1] = sqrt((2.0 * k + 1) * (3.0 * k + 1.0) / k) * pow(f, (k - 1.0));
1083#endif
1084
1085 /*
1086 * (3) Prepare for simplex minimization
1087 */
1088 func_val = VectorXd(nFit + 1);
1089 lambdaFit = VectorXd(nFit);
1090 muFit = VectorXd(nFit);
1091 /*
1092 * (4) Rather arbitrary initial guess
1093 */
1094 for (k = 0; k < nFit; k++) {
1095 muFit[k] = (k + 1) * 0.1 * f;
1096 }
1097
1098 simplex = get_initial_simplex(muFit, simplex_size);
1099 for (k = 0; k < nFit + 1; k++)
1100 func_val[k] = one_step(VectorXd(simplex.row(k).transpose()), u);
1101
1102 // Capture user data in type-safe lambdaFit — no void* needed
1103 auto cost = [u](const VectorXd& x) -> double {
1104 return one_step(x, u);
1105 };
1106
1107 /*
1108 * (5) Do the nonlinear minimization
1109 */
1111 func_val,
1112 ftol,
1113 0.0,
1114 cost,
1115 max_eval,
1116 neval,
1117 report,
1118 report_fit);
1119
1120 if (res) {
1121 for (k = 0; k < nFit; k++)
1122 muFit[k] = simplex(0, k);
1123
1124 /*
1125 * (6) Do the final step: calculation of the linear parameters
1126 */
1127 rv = compute_linear_parameters(muFit, lambdaFit, u);
1128
1129 sort_parameters(muFit, lambdaFit, nFit);
1130#ifdef LOG_FIT
1131 qInfo("RV = %g %%", 100 * rv);
1132#endif
1133 this->mu.resize(nFit);
1134 this->lambda.resize(nFit);
1135 this->nfit = nFit;
1136 for (k = 0; k < nFit; k++) {
1137 this->mu[k] = muFit[k];
1138 /*
1139 * This division takes into account the actual conductivities
1140 */
1141 this->lambda[k] = lambdaFit[k] / this->layers[this->nlayer() - 1].sigma;
1142#ifdef LOG_FIT
1143 qInfo("lambdaFit%d = %g\tmu%d = %g", k + 1, lambdaFit[k], k + 1, muFit[k]);
1144#endif
1145 }
1146 }
1147
1148 delete u;
1149 return res;
1150}
Eigen::Matrix3f R
Eigen::JacobiSVD< Eigen::Matrix3f > svd(S, Eigen::ComputeFullU|Eigen::ComputeFullV)
constexpr double EPS
#define M_PI
Multi-shell spherical head model with Berg-Scherg equivalent-source approximation for fast EEG forwar...
Named container of FwdEegSphereModel objects loaded from an mne_setup_eeg_sphere_model parameter file...
constexpr int FAIL
constexpr int OK
Header-only Nelder–Mead simplex minimiser with pluggable cost and report callables.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
Forward modelling — BEM solver, spherical models, sensor/coil definitions and the lead-field assembly...
Definition compute_fwd.h:85
constexpr int FWD_COILC_EEG
Definition fwd_coil.h:69
fitUserRec * fitUser
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 > 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
One concentric shell (outer radius rad, conductivity sigma and the derived ratios) of a multi-shell d...
static bool comp_layers(const FwdEegSphereLayer &v1, const FwdEegSphereLayer &v2)
Berg-Scherg parameter pair (magnitude and distance multiplier) for an equivalent dipole in the EEG sp...
Workspace for the linear least-squares fit of Berg-Scherg parameters in the EEG sphere model (SVD mat...
static double compute_linear_parameters(const Eigen::VectorXd &mu, Eigen::VectorXd &lambda, fitUser u)
static int fwd_eeg_multi_spherepot_coil1(const Eigen::Vector3f &rd, const Eigen::Vector3f &Q, FwdCoilSet &els, Eigen::Ref< Eigen::VectorXf > Vval, void *client)
static FwdEegSphereModel::UPtr setup_eeg_sphere_model(const QString &eeg_model_file, QString eeg_model_name, float eeg_sphere_rad)
bool fwd_setup_eeg_sphere_model(float rad, bool fit_berg_scherg, int nFit)
static void calc_pot_components(double beta, double cgamma, double &Vrp, double &Vtp, const Eigen::VectorXd &fn, int nterms)
static fitUser new_fit_user(int nfit, int nterms)
static int fwd_eeg_spherepot(const Eigen::Vector3f &rd, const Eigen::Vector3f &Q, const Eigen::Matrix< float, Eigen::Dynamic, 3, Eigen::RowMajor > &el, int neeg, Eigen::VectorXf &Vval, void *client)
static bool fwd_eeg_spherepot_vec(const Eigen::Vector3f &rd, const Eigen::Matrix< float, Eigen::Dynamic, 3, Eigen::RowMajor > &el, int neeg, Eigen::MatrixXf &Vval_vec, void *client)
static void compose_linear_fitting_data(const Eigen::VectorXd &mu, fitUser u)
static int fwd_eeg_spherepot_grad_coil(const Eigen::Vector3f &rd, const Eigen::Vector3f &Q, FwdCoilSet &coils, Eigen::Ref< Eigen::VectorXf > Vval, Eigen::Ref< Eigen::VectorXf > xgrad, Eigen::Ref< Eigen::VectorXf > ygrad, Eigen::Ref< Eigen::VectorXf > zgrad, void *client)
static int fwd_eeg_multi_spherepot(const Eigen::Vector3f &rd, const Eigen::Vector3f &Q, const Eigen::Matrix< float, Eigen::Dynamic, 3, Eigen::RowMajor > &el, int neeg, Eigen::VectorXf &Vval, void *client)
bool fwd_eeg_fit_berg_scherg(int nTerms, int nFit, float &rv)
static int fwd_eeg_spherepot_coil_vec(const Eigen::Vector3f &rd, FwdCoilSet &els, Eigen::Ref< Eigen::MatrixXf > Vval_vec, void *client)
static FwdEegSphereModel::UPtr fwd_create_eeg_sphere_model(const QString &name, int nlayer, const Eigen::VectorXf &rads, const Eigen::VectorXf &sigmas)
std::vector< FwdEegSphereLayer > layers
static void next_legen(int n, double x, double &p0, double &p01, double &p1, double &p11)
std::unique_ptr< FwdEegSphereModel > UPtr
static int fwd_eeg_spherepot_coil(const Eigen::Vector3f &rd, const Eigen::Vector3f &Q, FwdCoilSet &els, Eigen::Ref< Eigen::VectorXf > Vval, void *client)
double fwd_eeg_get_multi_sphere_model_coeff(int n)
static double one_step(const Eigen::VectorXd &mu, const void *user_data)
static FwdEegSphereModelSet * fwd_load_eeg_sphere_models(const QString &p_sFileName, FwdEegSphereModelSet *now)
std::unique_ptr< FwdEegSphereModelSet > UPtr
static bool simplex_minimize(Eigen::Matrix< T, Eigen::Dynamic, Eigen::Dynamic > &p, Eigen::Matrix< T, Eigen::Dynamic, 1 > &y, T ftol, T stol, CostFunc &&func, int max_eval, int &neval, int report, ReportFunc &&report_func)