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