Skip to content

Commit 9665f47

Browse files
larsonerPragnyaKhandelwal
authored andcommitted
Fix BEM solving tolerance (#14071)
1 parent bca4c97 commit 9665f47

4 files changed

Lines changed: 40 additions & 16 deletions

File tree

mne/_fiff/tests/test_reference.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ def test_set_eeg_reference_rest():
413413
assert 0.006 < exp_var < 0.008
414414
evoked.set_eeg_reference("REST", forward=fwd)
415415
exp_var_old = 1 - np.linalg.norm(evoked.data[:, idx] - old) / norm
416-
assert 0.005 < exp_var_old <= 0.009
416+
assert 0.005 < exp_var_old <= 0.0095
417417
exp_var = 1 - np.linalg.norm(evoked.data[:, idx] - want) / norm
418418
assert 0.995 < exp_var <= 1
419419

mne/bem.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import shutil
1313
from collections import OrderedDict
1414
from copy import deepcopy
15-
from functools import partial
15+
from functools import cache, partial
1616
from pathlib import Path
1717

1818
import numpy as np
@@ -791,23 +791,44 @@ def _one_step(mu, u):
791791

792792
def _fwd_eeg_fit_berg_scherg(m, nterms, nfit):
793793
"""Fit the Berg-Scherg equivalent spherical model dipole parameters."""
794+
# The fit depends only on the relative radii and conductivities of the
795+
# layers, not the absolute head radius or origin, so cache on those
796+
# Using the exact relative-radius ratio also keeps scipy's COBYLA out of a
797+
# near-degenerate regime where it fails to converge
798+
rel_rads = tuple(float(layer["rel_rad"]) for layer in m["layers"])
799+
sigmas = tuple(float(layer["sigma"]) for layer in m["layers"])
800+
mu, lambda_, rv = _fit_berg_scherg_cached(rel_rads, sigmas, nterms, nfit)
801+
802+
m["mu"] = np.array(mu)
803+
# This division takes into account the actual conductivities
804+
m["lambda"] = np.array(lambda_) / m["layers"][-1]["sigma"]
805+
m["nfit"] = nfit
806+
return rv
807+
808+
809+
@cache
810+
def _fit_berg_scherg_cached(rel_rads, sigmas, nterms, nfit):
811+
"""Fit Berg-Scherg params (pure function of relative radii and sigmas)."""
794812
assert nfit >= 2
813+
# Only rel_rad and sigma are read to compute the coefficients and weighting.
814+
m = dict(layers=[dict(rel_rad=r, sigma=s) for r, s in zip(rel_rads, sigmas)])
795815
u = dict(nfit=nfit, nterms=nterms)
796816

797817
# (1) Calculate the coefficients of the true expansion
798818
u["fn"] = _fwd_eeg_get_multi_sphere_model_coeffs(m, nterms + 1)
799819

800-
# (2) Calculate the weighting
801-
f = min([layer["rad"] for layer in m["layers"]]) / max(
802-
[layer["rad"] for layer in m["layers"]]
803-
)
820+
# (2) Calculate the weighting from the relative-radius ratio
821+
f = min(rel_rads) / max(rel_rads)
804822

805823
# correct weighting
806824
k = np.arange(1, nterms + 1)
807825
u["w"] = np.sqrt((2.0 * k + 1) * (3.0 * k + 1.0) / k) * np.power(f, (k - 1.0))
808826
u["w"][-1] = 0
809827

810-
# Do the nonlinear minimization, constraining mu to the interval [-1, +1]
828+
# rhobeg (initial trust-region radius) is ~half the (-1, 1) variable range
829+
# rhoend (final radius) sets the resolution of mu. The dipoles sit at radii
830+
# proportional to mu (order 1) while the fit's residual var is ~1e-4, so resolving
831+
# below that gains nothing and can prevent convergence
811832
mu_0 = np.zeros(3)
812833
fun = partial(_one_step, u=u)
813834
catol = 1e-6
@@ -816,18 +837,13 @@ def _fwd_eeg_fit_berg_scherg(m, nterms, nfit):
816837
def cons(x):
817838
return max_ - np.abs(x)
818839

819-
mu = fmin_cobyla(fun, mu_0, [cons], rhobeg=0.5, rhoend=1e-5, catol=catol)
840+
mu = fmin_cobyla(fun, mu_0, [cons], rhobeg=0.5, rhoend=1e-4, catol=catol)
820841

821842
# (6) Do the final step: calculation of the linear parameters
822843
rv, lambda_ = _compute_linear_parameters(mu, u)
823844
order = np.argsort(mu)[::-1]
824845
mu, lambda_ = mu[order], lambda_[order] # sort: largest mu first
825-
826-
m["mu"] = mu
827-
# This division takes into account the actual conductivities
828-
m["lambda"] = lambda_ / m["layers"][-1]["sigma"]
829-
m["nfit"] = nfit
830-
return rv
846+
return tuple(mu), tuple(lambda_), rv
831847

832848

833849
@verbose

mne/chpi.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,10 @@ def _fit_magnetic_dipole(B_orig, x0, too_close, whitener, coils, guesses):
565565
idx = np.argmin(res)
566566
if res[idx] < res0:
567567
x0 = guesses["rr"][idx]
568+
# x0 is the previous time point's coil position (or a better guess-grid point), so a
569+
# rhobeg (initial trust-region radius) of 1 mm covers typical between-window motion.
570+
# rhoend (final radius) sets the position resolution; 10 um is finer than cHPI
571+
# accuracy but still converges in a few dozen evaluations
568572
x = fmin_cobyla(objective, x0, (), rhobeg=1e-3, rhoend=1e-5, disp=False)
569573
gof, moment = objective(x, return_moment=True)
570574
gof = 1.0 - gof / B2

mne/dipole.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,10 +1354,14 @@ def _fit_dipole(
13541354
)
13551355

13561356
# Tested minimizers:
1357-
# Simplex, BFGS, CG, COBYLA, L-BFGS-B, Powell, SLSQP, TNC
1357+
# Simplex, BFGS, CG, COBYLA, COBYQA, L-BFGS-B, Powell, SLSQP, TNC
13581358
# Several were similar, but COBYLA won for having a handy constraint
13591359
# function we can use to ensure we stay inside the inner skull /
1360-
# smallest sphere
1360+
# smallest sphere. COBYQA needs ~10x more constraint evaluations, and the
1361+
# inner-skull constraint is a surface search rather than a cheap formula
1362+
# rhobeg (initial trust-region radius) is a few times the 2 cm guess grid so the fit
1363+
# can leave a poorly chosen grid cell. rhoend (final radius, the ``tol`` argument)
1364+
# sets the position resolution; loosening it to 1e-4 already shifts fits by ~2 mm
13611365
rd_final = fmin_cobyla(
13621366
fun, x0, (constraint,), consargs=(), rhobeg=5e-2, rhoend=rhoend, disp=False
13631367
)

0 commit comments

Comments
 (0)