# Authors: The MNE-Python contributors. # License: BSD-3-Clause # Copyright the MNE-Python contributors. # The computations in this code were primarily derived from Matti Hämäläinen's # C code. # # Many of the idealized equations behind these calculations can be found in: # 1) Realistic conductivity geometry model of the human head for interpretation # of neuromagnetic data. Hämäläinen and Sarvas, 1989. Specific to MNE # 2) EEG and MEG: forward solutions for inverse methods. Mosher, Leahy, and # Lewis, 1999. Generalized discussion of forward solutions. from copy import deepcopy import numpy as np from .._fiff.constants import FIFF from ..bem import _import_openmeeg, _make_openmeeg_geometry from ..fixes import bincount, jit from ..parallel import parallel_func from ..surface import _jit_cross, _project_onto_surface from ..transforms import apply_trans, invert_transform from ..utils import _check_option, _pl, fill_doc, logger, verbose, warn # ############################################################################# # COIL SPECIFICATION AND FIELD COMPUTATION MATRIX def _dup_coil_set(coils, coord_frame, t): """Make a duplicate.""" if t is not None and coord_frame != t["from"]: raise RuntimeError("transformation frame does not match the coil set") coils = deepcopy(coils) if t is not None: coord_frame = t["to"] for coil in coils: for key in ("ex", "ey", "ez"): if key in coil: coil[key] = apply_trans(t["trans"], coil[key], False) coil["r0"] = apply_trans(t["trans"], coil["r0"]) coil["rmag"] = apply_trans(t["trans"], coil["rmag"]) coil["cosmag"] = apply_trans(t["trans"], coil["cosmag"], False) coil["coord_frame"] = t["to"] return coils, coord_frame def _check_coil_frame(coils, coord_frame, bem): """Check to make sure the coils are in the correct coordinate frame.""" if coord_frame != FIFF.FIFFV_COORD_MRI: if coord_frame == FIFF.FIFFV_COORD_HEAD: # Make a transformed duplicate coils, coord_frame = _dup_coil_set(coils, coord_frame, bem["head_mri_t"]) else: raise RuntimeError(f"Bad coil coordinate frame {coord_frame}") return coils, coord_frame @fill_doc def _lin_field_coeff(surf, mult, rmags, cosmags, ws, bins, n_jobs): """Parallel wrapper for _do_lin_field_coeff to compute linear coefficients. Parameters ---------- surf : dict Dict containing information for one surface of the BEM mult : float Multiplier for particular BEM surface (Iso Skull Approach discussed in Mosher et al., 1999 and Hämäläinen and Sarvas, 1989 Section III?) rmag : ndarray, shape (n_integration_pts, 3) 3D positions of MEG coil integration points (from coil['rmag']) cosmag : ndarray, shape (n_integration_pts, 3) Direction of the MEG coil integration points (from coil['cosmag']) ws : ndarray, shape (n_integration_pts,) Weights for MEG coil integration points bins : ndarray, shape (n_integration_points,) The sensor assignments for each rmag/cosmag/w. %(n_jobs)s Returns ------- coeff : list Linear coefficients with lead fields for each BEM vertex on each sensor (?) """ parallel, p_fun, n_jobs = parallel_func( _do_lin_field_coeff, n_jobs, max_jobs=len(surf["tris"]) ) nas = np.array_split coeffs = parallel( p_fun(surf["rr"], t, tn, ta, rmags, cosmags, ws, bins) for t, tn, ta in zip( nas(surf["tris"], n_jobs), nas(surf["tri_nn"], n_jobs), nas(surf["tri_area"], n_jobs), ) ) return mult * np.sum(coeffs, axis=0) @jit() def _do_lin_field_coeff(bem_rr, tris, tn, ta, rmags, cosmags, ws, bins): """Compute field coefficients (parallel-friendly). See section IV of Mosher et al., 1999 (specifically equation 35). Parameters ---------- bem_rr : ndarray, shape (n_BEM_vertices, 3) Positions on one BEM surface in 3-space. 2562 BEM vertices for BEM with 5120 triangles (ico-4) tris : ndarray, shape (n_BEM_vertices, 3) Vertex indices for each triangle (referring to bem_rr) tn : ndarray, shape (n_BEM_vertices, 3) Triangle unit normal vectors ta : ndarray, shape (n_BEM_vertices,) Triangle areas rmag : ndarray, shape (n_sensor_pts, 3) 3D positions of MEG coil integration points (from coil['rmag']) cosmag : ndarray, shape (n_sensor_pts, 3) Direction of the MEG coil integration points (from coil['cosmag']) ws : ndarray, shape (n_sensor_pts,) Weights for MEG coil integration points bins : ndarray, shape (n_sensor_pts,) The sensor assignments for each rmag/cosmag/w. Returns ------- coeff : ndarray, shape (n_MEG_sensors, n_BEM_vertices) Linear coefficients with effect of each BEM vertex on each sensor (?) """ coeff = np.zeros((bins[-1] + 1, len(bem_rr))) w_cosmags = ws.reshape(-1, 1) * cosmags diff = rmags.reshape(rmags.shape[0], 1, rmags.shape[1]) - bem_rr den = np.sum(diff * diff, axis=-1) den *= np.sqrt(den) den *= 3 for ti in range(len(tris)): tri, tri_nn, tri_area = tris[ti], tn[ti], ta[ti] # Accumulate the coefficients for each triangle node and add to the # corresponding coefficient matrix # Simple version (bem_lin_field_coeffs_simple) # The following is equivalent to: # tri_rr = bem_rr[tri] # for j, coil in enumerate(coils['coils']): # x = func(coil['rmag'], coil['cosmag'], # tri_rr, tri_nn, tri_area) # res = np.sum(coil['w'][np.newaxis, :] * x, axis=1) # coeff[j][tri + off] += mult * res c = np.empty((diff.shape[0], tri.shape[0], diff.shape[2])) _jit_cross(c, diff[:, tri], tri_nn) c *= w_cosmags.reshape(w_cosmags.shape[0], 1, w_cosmags.shape[1]) for ti in range(3): x = np.sum(c[:, ti], axis=-1) x /= den[:, tri[ti]] / tri_area coeff[:, tri[ti]] += bincount(bins, weights=x, minlength=bins[-1] + 1) return coeff def _concatenate_coils(coils): """Concatenate MEG coil parameters.""" rmags = np.concatenate([coil["rmag"] for coil in coils]) cosmags = np.concatenate([coil["cosmag"] for coil in coils]) ws = np.concatenate([coil["w"] for coil in coils]) n_int = np.array([len(coil["rmag"]) for coil in coils]) if n_int[-1] == 0: # We assume each sensor has at least one integration point, # which should be a safe assumption. But let's check it here, since # our code elsewhere relies on bins[-1] + 1 being the number of sensors raise RuntimeError("not supported") bins = np.repeat(np.arange(len(n_int)), n_int) return rmags, cosmags, ws, bins @fill_doc def _bem_specify_coils(bem, coils, coord_frame, mults, n_jobs): """Set up for computing the solution at a set of MEG coils. Parameters ---------- bem : instance of ConductorModel BEM information coils : list of dict, len(n_MEG_sensors) MEG sensor information dicts coord_frame : int Class constant identifying coordinate frame mults : ndarray, shape (1, n_BEM_vertices) Multiplier for every vertex in BEM %(n_jobs)s Returns ------- sol: ndarray, shape (n_MEG_sensors, n_BEM_vertices) MEG solution """ # Make sure MEG coils are in MRI coordinate frame to match BEM coords coils, coord_frame = _check_coil_frame(coils, coord_frame, bem) # leaving this in in case we want to easily add in the future # if method != 'simple': # in ['ferguson', 'urankar']: # raise NotImplementedError # Compute the weighting factors to obtain the magnetic field in the linear # potential approximation # Process each of the surfaces rmags, cosmags, ws, bins = _triage_coils(coils) del coils lens = np.cumsum(np.r_[0, [len(s["rr"]) for s in bem["surfs"]]]) sol = np.zeros((bins[-1] + 1, bem["solution"].shape[1])) lims = np.concatenate([np.arange(0, sol.shape[0], 100), [sol.shape[0]]]) # Put through the bem (in channel-based chunks to save memory) for start, stop in zip(lims[:-1], lims[1:]): mask = np.logical_and(bins >= start, bins < stop) r, c, w, b = rmags[mask], cosmags[mask], ws[mask], bins[mask] - start # Compute coeffs for each surface, one at a time for o1, o2, surf, mult in zip( lens[:-1], lens[1:], bem["surfs"], bem["field_mult"] ): coeff = _lin_field_coeff(surf, mult, r, c, w, b, n_jobs) sol[start:stop] += np.dot(coeff, bem["solution"][o1:o2]) sol *= mults return sol def _bem_specify_els(bem, els, mults): """Set up for computing the solution at a set of EEG electrodes. Parameters ---------- bem : instance of ConductorModel BEM information els : list of dict, len(n_EEG_sensors) List of EEG sensor information dicts mults: ndarray, shape (1, n_BEM_vertices) Multiplier for every vertex in BEM Returns ------- sol : ndarray, shape (n_EEG_sensors, n_BEM_vertices) EEG solution """ sol = np.zeros((len(els), bem["solution"].shape[1])) scalp = bem["surfs"][0] # Operate on all integration points for all electrodes (in MRI coords) rrs = np.concatenate( [apply_trans(bem["head_mri_t"]["trans"], el["rmag"]) for el in els], axis=0 ) ws = np.concatenate([el["w"] for el in els]) tri_weights, tri_idx = _project_onto_surface(rrs, scalp) tri_weights *= ws[:, np.newaxis] weights = np.matmul( tri_weights[:, np.newaxis], bem["solution"][scalp["tris"][tri_idx]] )[:, 0] # there are way more vertices than electrodes generally, so let's iterate # over the electrodes edges = np.concatenate([[0], np.cumsum([len(el["w"]) for el in els])]) for ii, (start, stop) in enumerate(zip(edges[:-1], edges[1:])): sol[ii] = weights[start:stop].sum(0) sol *= mults return sol # ############################################################################# # BEM COMPUTATION _MAG_FACTOR = 1e-7 # μ_0 / (4π) # def _bem_inf_pot(rd, Q, rp): # """The infinite medium potential in one direction. See Eq. (8) in # Mosher, 1999""" # NOTE: the (μ_0 / (4π) factor has been moved to _prep_field_communication # diff = rp - rd # (Observation point position) - (Source position) # diff2 = np.sum(diff * diff, axis=1) # Squared magnitude of diff # # (Dipole moment) dot (diff) / (magnitude ^ 3) # return np.sum(Q * diff, axis=1) / (diff2 * np.sqrt(diff2)) @jit() def _bem_inf_pots(mri_rr, bem_rr, mri_Q=None): """Compute the infinite medium potential in all 3 directions. Parameters ---------- mri_rr : ndarray, shape (n_dipole_vertices, 3) Chunk of 3D dipole positions in MRI coordinates bem_rr: ndarray, shape (n_BEM_vertices, 3) 3D vertex positions for one BEM surface mri_Q : ndarray, shape (3, 3) 3x3 head -> MRI transform. I.e., head_mri_t.dot(np.eye(3)) Returns ------- ndarray : shape(n_dipole_vertices, 3, n_BEM_vertices) """ # NOTE: the (μ_0 / (4π) factor has been moved to _prep_field_communication # Get position difference vector between BEM vertex and dipole diff = np.empty((len(mri_rr), 3, len(bem_rr))) for ri in range(mri_rr.shape[0]): rr = mri_rr[ri] this_diff = bem_rr - rr diff_norm = np.sum(this_diff * this_diff, axis=1) diff_norm *= np.sqrt(diff_norm) diff_norm[diff_norm == 0] = 1.0 if mri_Q is not None: this_diff = np.dot(this_diff, mri_Q.T) this_diff /= diff_norm.reshape(-1, 1) diff[ri] = this_diff.T return diff # This function has been refactored to process all points simultaneously # def _bem_inf_field(rd, Q, rp, d): # """Infinite-medium magnetic field. See (7) in Mosher, 1999""" # # Get vector from source to sensor integration point # diff = rp - rd # diff2 = np.sum(diff * diff, axis=1) # Get magnitude of diff # # # Compute cross product between diff and dipole to get magnetic field at # # integration point # x = fast_cross_3d(Q[np.newaxis, :], diff) # # # Take magnetic field dotted by integration point normal to get magnetic # # field threading the current loop. Divide by R^3 (equivalently, R^2 * R) # return np.sum(x * d, axis=1) / (diff2 * np.sqrt(diff2)) @jit() def _bem_inf_fields(rr, rmag, cosmag): """Compute infinite-medium magnetic field at one MEG sensor. This operates on all dipoles in all 3 basis directions. Parameters ---------- rr : ndarray, shape (n_source_points, 3) 3D dipole source positions rmag : ndarray, shape (n_sensor points, 3) 3D positions of 1 MEG coil's integration points (from coil['rmag']) cosmag : ndarray, shape (n_sensor_points, 3) Direction of 1 MEG coil's integration points (from coil['cosmag']) Returns ------- ndarray, shape (n_dipoles, 3, n_integration_pts) Magnetic field from all dipoles at each MEG sensor integration point """ # rr, rmag refactored according to Equation (19) in Mosher, 1999 # Knowing that we're doing all directions, refactor above function: # rr, 3, rmag diff = rmag.T.reshape(1, 3, rmag.shape[0]) - rr.reshape(rr.shape[0], 3, 1) diff_norm = np.sum(diff * diff, axis=1) # rr, rmag diff_norm *= np.sqrt(diff_norm) # Get magnitude of distance cubed diff_norm_ = diff_norm.reshape(-1) diff_norm_[diff_norm_ == 0] = 1 # avoid nans # This is the result of cross-prod calcs with basis vectors, # as if we had taken (Q=np.eye(3)), then multiplied by cosmags # factor, and then summed across directions x = np.empty((rr.shape[0], 3, rmag.shape[0])) x[:, 0] = diff[:, 1] * cosmag[:, 2] - diff[:, 2] * cosmag[:, 1] x[:, 1] = diff[:, 2] * cosmag[:, 0] - diff[:, 0] * cosmag[:, 2] x[:, 2] = diff[:, 0] * cosmag[:, 1] - diff[:, 1] * cosmag[:, 0] diff_norm = diff_norm_.reshape((rr.shape[0], 1, rmag.shape[0])) x /= diff_norm # x.shape == (rr.shape[0], 3, rmag.shape[0]) return x @fill_doc def _bem_pot_or_field(rr, mri_rr, mri_Q, coils, solution, bem_rr, n_jobs, coil_type): """Calculate the magnetic field or electric potential forward solution. The code is very similar between EEG and MEG potentials, so combine them. This does the work of "fwd_comp_field" (which wraps to "fwd_bem_field") and "fwd_bem_pot_els" in MNE-C. Parameters ---------- rr : ndarray, shape (n_dipoles, 3) 3D dipole source positions mri_rr : ndarray, shape (n_dipoles, 3) 3D source positions in MRI coordinates mri_Q : 3x3 head -> MRI transform. I.e., head_mri_t.dot(np.eye(3)) coils : list of dict, len(sensors) List of sensors where each element contains sensor specific information solution : ndarray, shape (n_sensors, n_BEM_rr) Comes from _bem_specify_coils bem_rr : ndarray, shape (n_BEM_vertices, 3) 3D vertex positions for all surfaces in the BEM %(n_jobs)s coil_type : str 'meg' or 'eeg' Returns ------- B : ndarray, shape (n_dipoles * 3, n_sensors) Forward solution for a set of sensors """ # Both MEG and EEG have the inifinite-medium potentials # This could be just vectorized, but eats too much memory, so instead we # reduce memory by chunking within _do_inf_pots and parallelize, too: parallel, p_fun, n_jobs = parallel_func(_do_inf_pots, n_jobs, max_jobs=len(rr)) nas = np.array_split B = np.sum( parallel( p_fun( mri_rr, sr.copy(), np.ascontiguousarray(mri_Q), np.array(sol) ) # copy and contig for sr, sol in zip(nas(bem_rr, n_jobs), nas(solution.T, n_jobs)) ), axis=0, ) # The copy()s above should make it so the whole objects don't need to be # pickled... # Only MEG coils are sensitive to the primary current distribution. if coil_type == "meg": # Primary current contribution (can be calc. in coil/dipole coords) parallel, p_fun, n_jobs = parallel_func(_do_prim_curr, n_jobs) pcc = np.concatenate(parallel(p_fun(r, coils) for r in nas(rr, n_jobs)), axis=0) B += pcc B *= _MAG_FACTOR return B def _do_prim_curr(rr, coils): """Calculate primary currents in a set of MEG coils. See Mosher et al., 1999 Section II for discussion of primary vs. volume currents. Parameters ---------- rr : ndarray, shape (n_dipoles, 3) 3D dipole source positions in head coordinates coils : list of dict List of MEG coils where each element contains coil specific information Returns ------- pc : ndarray, shape (n_sources, n_MEG_sensors) Primary current for set of MEG coils due to all sources """ rmags, cosmags, ws, bins = _triage_coils(coils) n_coils = bins[-1] + 1 del coils pc = np.empty((len(rr) * 3, n_coils)) for start, stop in _rr_bounds(rr, chunk=1): pp = _bem_inf_fields(rr[start:stop], rmags, cosmags) pp *= ws pp.shape = (3 * (stop - start), -1) pc[3 * start : 3 * stop] = [ bincount(bins, this_pp, bins[-1] + 1) for this_pp in pp ] return pc def _rr_bounds(rr, chunk=200): # chunk data nicely bounds = np.concatenate([np.arange(0, len(rr), chunk), [len(rr)]]) return zip(bounds[:-1], bounds[1:]) def _do_inf_pots(mri_rr, bem_rr, mri_Q, sol): """Calculate infinite potentials for MEG or EEG sensors using chunks. Parameters ---------- mri_rr : ndarray, shape (n_dipoles, 3) 3D dipole source positions in MRI coordinates bem_rr : ndarray, shape (n_BEM_vertices, 3) 3D vertex positions for all surfaces in the BEM mri_Q : 3x3 head -> MRI transform. I.e., head_mri_t.dot(np.eye(3)) sol : ndarray, shape (n_sensors_subset, n_BEM_vertices_subset) Comes from _bem_specify_coils Returns ------- B : ndarray, (n_dipoles * 3, n_sensors) Forward solution for sensors due to volume currents """ # Doing work of 'fwd_bem_pot_calc' in MNE-C # The following code is equivalent to this, but saves memory # v0s = _bem_inf_pots(rr, bem_rr, Q) # n_rr x 3 x n_bem_rr # v0s.shape = (len(rr) * 3, v0s.shape[2]) # B = np.dot(v0s, sol) # We chunk the source mri_rr's in order to save memory B = np.empty((len(mri_rr) * 3, sol.shape[1])) for start, stop in _rr_bounds(mri_rr): # v0 in Hämäläinen et al., 1989 == v_inf in Mosher, et al., 1999 v0s = _bem_inf_pots(mri_rr[start:stop], bem_rr, mri_Q) v0s = v0s.reshape(-1, v0s.shape[2]) B[3 * start : 3 * stop] = np.dot(v0s, sol) return B # ############################################################################# # SPHERE COMPUTATION def _sphere_pot_or_field(rr, mri_rr, mri_Q, coils, solution, bem_rr, n_jobs, coil_type): """Do potential or field for spherical model.""" fun = _eeg_spherepot_coil if coil_type == "eeg" else _sphere_field parallel, p_fun, n_jobs = parallel_func(fun, n_jobs, max_jobs=len(rr)) B = np.concatenate( parallel(p_fun(r, coils, sphere=solution) for r in np.array_split(rr, n_jobs)) ) return B def _sphere_field(rrs, coils, sphere): """Compute field for spherical model using Jukka Sarvas' field computation. Jukka Sarvas, "Basic mathematical and electromagnetic concepts of the biomagnetic inverse problem", Phys. Med. Biol. 1987, Vol. 32, 1, 11-22. The formulas have been manipulated for efficient computation by Matti Hämäläinen, February 1990 """ rmags, cosmags, ws, bins = _triage_coils(coils) return _do_sphere_field(rrs, rmags, cosmags, ws, bins, sphere["r0"]) @jit() def _do_sphere_field(rrs, rmags, cosmags, ws, bins, r0): n_coils = bins[-1] + 1 # Shift to the sphere model coordinates rrs = rrs - r0 B = np.zeros((3 * len(rrs), n_coils)) for ri in range(len(rrs)): rr = rrs[ri] # Check for a dipole at the origin if np.sqrt(np.dot(rr, rr)) <= 1e-10: continue this_poss = rmags - r0 # Vector from dipole to the field point a_vec = this_poss - rr a = np.sqrt(np.sum(a_vec * a_vec, axis=1)) r = np.sqrt(np.sum(this_poss * this_poss, axis=1)) rr0 = np.sum(this_poss * rr, axis=1) ar = (r * r) - rr0 ar0 = ar / a F = a * (r * a + ar) gr = (a * a) / r + ar0 + 2.0 * (a + r) g0 = a + 2 * r + ar0 # Compute the dot products needed re = np.sum(this_poss * cosmags, axis=1) r0e = np.sum(rr * cosmags, axis=1) g = (g0 * r0e - gr * re) / (F * F) good = (a > 0) | (r > 0) | ((a * r) + 1 > 1e-5) rr_ = rr.reshape(1, 3) v1 = np.empty((cosmags.shape[0], 3)) _jit_cross(v1, rr_, cosmags) v2 = np.empty((cosmags.shape[0], 3)) _jit_cross(v2, rr_, this_poss) xx = (good * ws).reshape(-1, 1) * ( v1 / F.reshape(-1, 1) + v2 * g.reshape(-1, 1) ) for jj in range(3): zz = bincount(bins, xx[:, jj], n_coils) B[3 * ri + jj, :] = zz B *= _MAG_FACTOR return B def _eeg_spherepot_coil(rrs, coils, sphere): """Calculate the EEG in the sphere model.""" rmags, cosmags, ws, bins = _triage_coils(coils) n_coils = bins[-1] + 1 del coils # Shift to the sphere model coordinates rrs = rrs - sphere["r0"] B = np.zeros((3 * len(rrs), n_coils)) for ri, rr in enumerate(rrs): # Only process dipoles inside the innermost sphere if np.sqrt(np.dot(rr, rr)) >= sphere["layers"][0]["rad"]: continue # fwd_eeg_spherepot_vec vval_one = np.zeros((len(rmags), 3)) # Make a weighted sum over the equivalence parameters for eq in range(sphere["nfit"]): # Scale the dipole position rd = sphere["mu"][eq] * rr rd2 = np.sum(rd * rd) rd2_inv = 1.0 / rd2 # Go over all electrodes this_pos = rmags - sphere["r0"] # Scale location onto the surface of the sphere (not used) # if sphere['scale_pos']: # pos_len = (sphere['layers'][-1]['rad'] / # np.sqrt(np.sum(this_pos * this_pos, axis=1))) # this_pos *= pos_len # Vector from dipole to the field point a_vec = this_pos - rd # Compute the dot products needed a = np.sqrt(np.sum(a_vec * a_vec, axis=1)) a3 = 2.0 / (a * a * a) r2 = np.sum(this_pos * this_pos, axis=1) r = np.sqrt(r2) rrd = np.sum(this_pos * rd, axis=1) ra = r2 - rrd rda = rrd - rd2 # The main ingredients F = a * (r * a + ra) c1 = a3 * rda + 1.0 / a - 1.0 / r c2 = a3 + (a + r) / (r * F) # Mix them together and scale by lambda/(rd*rd) m1 = c1 - c2 * rrd m2 = c2 * rd2 vval_one += ( sphere["lambda"][eq] * rd2_inv * (m1[:, np.newaxis] * rd + m2[:, np.newaxis] * this_pos) ) # compute total result xx = vval_one * ws[:, np.newaxis] zz = np.array([bincount(bins, x, bins[-1] + 1) for x in xx.T]) B[3 * ri : 3 * ri + 3, :] = zz # finishing by scaling by 1/(4*M_PI) B *= 0.25 / np.pi return B def _triage_coils(coils): return coils if isinstance(coils, tuple) else _concatenate_coils(coils) # ############################################################################# # MAGNETIC DIPOLE (e.g. CHPI) _MIN_DIST_LIMIT = 1e-5 def _magnetic_dipole_field_vec(rrs, coils, too_close="raise"): rmags, cosmags, ws, bins = _triage_coils(coils) fwd, min_dist = _compute_mdfv(rrs, rmags, cosmags, ws, bins, too_close) if min_dist < _MIN_DIST_LIMIT: msg = f"Coil too close (dist = {min_dist * 1000:g} mm)" if too_close == "raise": raise RuntimeError(msg) func = warn if too_close == "warning" else logger.info func(msg) return fwd @jit() def _compute_mdfv(rrs, rmags, cosmags, ws, bins, too_close): """Compute an MEG forward solution for a set of magnetic dipoles.""" # The code below is a more efficient version (~30x) of this: # for ri, rr in enumerate(rrs): # for k in range(len(coils)): # this_coil = coils[k] # # Go through all points # diff = this_coil['rmag'] - rr # dist2 = np.sum(diff * diff, axis=1)[:, np.newaxis] # dist = np.sqrt(dist2) # if (dist < 1e-5).any(): # raise RuntimeError('Coil too close') # dist5 = dist2 * dist2 * dist # sum_ = (3 * diff * np.sum(diff * this_coil['cosmag'], # axis=1)[:, np.newaxis] - # dist2 * this_coil['cosmag']) / dist5 # fwd[3*ri:3*ri+3, k] = 1e-7 * np.dot(this_coil['w'], sum_) fwd = np.zeros((3 * len(rrs), bins[-1] + 1)) min_dist = np.inf ws2 = ws.reshape(-1, 1) for ri in range(len(rrs)): rr = rrs[ri] diff = rmags - rr dist2_ = np.sum(diff * diff, axis=1) dist2 = dist2_.reshape(-1, 1) dist = np.sqrt(dist2) min_dist = min(dist.min(), min_dist) if min_dist < _MIN_DIST_LIMIT and too_close == "raise": break t_ = np.sum(diff * cosmags, axis=1) t = t_.reshape(-1, 1) sum_ = ws2 * (3 * diff * t - dist2 * cosmags) / (dist2 * dist2 * dist) for ii in range(3): fwd[3 * ri + ii] = bincount(bins, sum_[:, ii], bins[-1] + 1) fwd *= _MAG_FACTOR return fwd, min_dist # ############################################################################# # MAIN TRIAGING FUNCTION @verbose def _prep_field_computation(rr, *, sensors, bem, n_jobs, verbose=None): """Precompute and store some things that are used for both MEG and EEG. Calculation includes multiplication factors, coordinate transforms, compensations, and forward solutions. All are stored in modified fwd_data. Parameters ---------- rr : ndarray, shape (n_dipoles, 3) 3D dipole source positions in head coordinates bem : instance of ConductorModel Boundary Element Model information fwd_data : dict Dict containing sensor information in the head coordinate frame. Gets updated here with BEM and sensor information for later forward calculations. %(n_jobs)s %(verbose)s """ bem_rr = mults = mri_Q = head_mri_t = None if not bem["is_sphere"]: if bem["bem_method"] != FIFF.FIFFV_BEM_APPROX_LINEAR: raise RuntimeError("only linear collocation supported") # Store (and apply soon) μ_0/(4π) factor before source computations mults = np.repeat( bem["source_mult"] / (4.0 * np.pi), [len(s["rr"]) for s in bem["surfs"]] )[np.newaxis, :] # Get positions of BEM points for every surface bem_rr = np.concatenate([s["rr"] for s in bem["surfs"]]) # The dipole location and orientation must be transformed head_mri_t = bem["head_mri_t"] mri_Q = bem["head_mri_t"]["trans"][:3, :3].T solutions = dict() for coil_type in sensors: coils = sensors[coil_type]["defs"] if not bem["is_sphere"]: if coil_type == "meg": # MEG field computation matrices for BEM start = "Composing the field computation matrix" logger.info("\n" + start + "...") cf = FIFF.FIFFV_COORD_HEAD # multiply solution by "mults" here for simplicity solution = _bem_specify_coils(bem, coils, cf, mults, n_jobs) else: # Compute solution for EEG sensor logger.info("Setting up for EEG...") solution = _bem_specify_els(bem, coils, mults) else: solution = bem if coil_type == "eeg": logger.info( "Using the equivalent source approach in the " "homogeneous sphere for EEG" ) sensors[coil_type]["defs"] = _triage_coils(coils) solutions[coil_type] = solution # Get appropriate forward physics function depending on sphere or BEM model fun = _sphere_pot_or_field if bem["is_sphere"] else _bem_pot_or_field # Update fwd_data with # bem_rr (3D BEM vertex positions) # mri_Q (3x3 Head->MRI coord transformation applied to identity matrix) # head_mri_t (head->MRI coord transform dict) # fun (_bem_pot_or_field if not 'sphere'; otherwise _sph_pot_or_field) # solutions (len 2 list; [ndarray, shape (n_MEG_sens, n BEM vertices), # ndarray, shape (n_EEG_sens, n BEM vertices)] fwd_data = dict( bem_rr=bem_rr, mri_Q=mri_Q, head_mri_t=head_mri_t, fun=fun, solutions=solutions ) return fwd_data @fill_doc def _compute_forwards_meeg(rr, *, sensors, fwd_data, n_jobs, silent=False): """Compute MEG and EEG forward solutions for all sensor types.""" Bs = dict() # The dipole location and orientation must be transformed to mri coords mri_rr = None if fwd_data["head_mri_t"] is not None: mri_rr = np.ascontiguousarray(apply_trans(fwd_data["head_mri_t"]["trans"], rr)) mri_Q, bem_rr, fun = fwd_data["mri_Q"], fwd_data["bem_rr"], fwd_data["fun"] solutions = fwd_data["solutions"] del fwd_data for coil_type, sens in sensors.items(): coils = sens["defs"] compensator = sens.get("compensator", None) post_picks = sens.get("post_picks", None) solution = solutions.get(coil_type, None) # Do the actual forward calculation for a list MEG/EEG sensors if not silent: logger.info( f"Computing {coil_type.upper()} at {len(rr)} source location{_pl(rr)} " "(free orientations)..." ) # Calculate forward solution using spherical or BEM model B = fun( rr, mri_rr, mri_Q, coils=coils, solution=solution, bem_rr=bem_rr, n_jobs=n_jobs, coil_type=coil_type, ) # Compensate if needed (only done for MEG systems w/compensation) if compensator is not None: B = B @ compensator.T if post_picks is not None: B = B[:, post_picks] Bs[coil_type] = B return Bs @verbose def _compute_forwards(rr, *, bem, sensors, n_jobs, verbose=None): """Compute the MEG and EEG forward solutions.""" # Split calculation into two steps to save (potentially) a lot of time # when e.g. dipole fitting solver = bem.get("solver", "mne") _check_option("solver", solver, ("mne", "openmeeg")) if bem["is_sphere"] or solver == "mne": fwd_data = _prep_field_computation(rr, sensors=sensors, bem=bem, n_jobs=n_jobs) Bs = _compute_forwards_meeg( rr, sensors=sensors, fwd_data=fwd_data, n_jobs=n_jobs ) else: Bs = _compute_forwards_openmeeg(rr, bem=bem, sensors=sensors) n_sensors_want = sum(len(s["ch_names"]) for s in sensors.values()) n_sensors = sum(B.shape[1] for B in Bs.values()) n_sources = list(Bs.values())[0].shape[0] assert (n_sources, n_sensors) == (len(rr) * 3, n_sensors_want) return Bs def _compute_forwards_openmeeg(rr, *, bem, sensors): """Compute the MEG and EEG forward solutions for OpenMEEG.""" if len(bem["surfs"]) != 3: raise RuntimeError("Only 3-layer BEM is supported for OpenMEEG.") om = _import_openmeeg("compute a forward solution using OpenMEEG") hminv = om.SymMatrix(bem["solution"]) geom = _make_openmeeg_geometry(bem, invert_transform(bem["head_mri_t"])) # Make dipoles for all XYZ orientations dipoles = np.c_[ np.kron(rr.T, np.ones(3)[None, :]).T, np.kron(np.ones(len(rr))[:, None], np.eye(3)), ] dipoles = np.asfortranarray(dipoles) dipoles = om.Matrix(dipoles) dsm = om.DipSourceMat(geom, dipoles, "Brain") Bs = dict() if "eeg" in sensors: rmags, _, ws, bins = _concatenate_coils(sensors["eeg"]["defs"]) rmags = np.asfortranarray(rmags.astype(np.float64)) eeg_sensors = om.Sensors(om.Matrix(np.asfortranarray(rmags)), geom) h2em = om.Head2EEGMat(geom, eeg_sensors) eeg_fwd_full = om.GainEEG(hminv, dsm, h2em).array() Bs["eeg"] = np.array( [bincount(bins, ws * x, bins[-1] + 1) for x in eeg_fwd_full.T], float ) if "meg" in sensors: rmags, cosmags, ws, bins = _concatenate_coils(sensors["meg"]["defs"]) rmags = np.asfortranarray(rmags.astype(np.float64)) cosmags = np.asfortranarray(cosmags.astype(np.float64)) labels = [str(ii) for ii in range(len(rmags))] weights = radii = np.ones(len(labels)) meg_sensors = om.Sensors(labels, rmags, cosmags, weights, radii) h2mm = om.Head2MEGMat(geom, meg_sensors) ds2mm = om.DipSource2MEGMat(dipoles, meg_sensors) meg_fwd_full = om.GainMEG(hminv, dsm, h2mm, ds2mm).array() B = np.array( [bincount(bins, ws * x, bins[-1] + 1) for x in meg_fwd_full.T], float ) compensator = sensors["meg"].get("compensator", None) post_picks = sensors["meg"].get("post_picks", None) if compensator is not None: B = B @ compensator.T if post_picks is not None: B = B[:, post_picks] Bs["meg"] = B return Bs