Aluode/PerceptionLabPortable
0
1# Authors: The MNE-Python contributors.2# License: BSD-3-Clause3# Copyright the MNE-Python contributors.4 5# The computations in this code were primarily derived from Matti Hämäläinen's6# C code.7#8# Many of the idealized equations behind these calculations can be found in:9# 1) Realistic conductivity geometry model of the human head for interpretation10# of neuromagnetic data. Hämäläinen and Sarvas, 1989. Specific to MNE11# 2) EEG and MEG: forward solutions for inverse methods. Mosher, Leahy, and12# Lewis, 1999. Generalized discussion of forward solutions.13 14from copy import deepcopy15 16import numpy as np17 18from .._fiff.constants import FIFF19from ..bem import _import_openmeeg, _make_openmeeg_geometry20from ..fixes import bincount, jit21from ..parallel import parallel_func22from ..surface import _jit_cross, _project_onto_surface23from ..transforms import apply_trans, invert_transform24from ..utils import _check_option, _pl, fill_doc, logger, verbose, warn25 26# #############################################################################27# COIL SPECIFICATION AND FIELD COMPUTATION MATRIX28 29 30def _dup_coil_set(coils, coord_frame, t):31 """Make a duplicate."""32 if t is not None and coord_frame != t["from"]:33 raise RuntimeError("transformation frame does not match the coil set")34 coils = deepcopy(coils)35 if t is not None:36 coord_frame = t["to"]37 for coil in coils:38 assert isinstance(coil, dict), f"Coil must be a dict, got {type(coil)}"39 for key in ("ex", "ey", "ez"):40 if key in coil:41 coil[key] = apply_trans(t["trans"], coil[key], False)42 coil["r0"] = apply_trans(t["trans"], coil["r0"])43 coil["rmag"] = apply_trans(t["trans"], coil["rmag"])44 coil["cosmag"] = apply_trans(t["trans"], coil["cosmag"], False)45 coil["coord_frame"] = t["to"]46 return coils, coord_frame47 48 49def _check_coil_frame(coils, coord_frame, bem):50 """Check to make sure the coils are in the correct coordinate frame."""51 if coord_frame != FIFF.FIFFV_COORD_MRI:52 if coord_frame == FIFF.FIFFV_COORD_HEAD:53 # Make a transformed duplicate54 coils, coord_frame = _dup_coil_set(coils, coord_frame, bem["head_mri_t"])55 else:56 raise RuntimeError(f"Bad coil coordinate frame {coord_frame}")57 return coils, coord_frame58 59 60@fill_doc61def _lin_field_coeff(surf, mult, rmags, cosmags, ws, bins, n_jobs):62 """Parallel wrapper for _do_lin_field_coeff to compute linear coefficients.63 64 Parameters65 ----------66 surf : dict67 Dict containing information for one surface of the BEM68 mult : float69 Multiplier for particular BEM surface (Iso Skull Approach discussed in70 Mosher et al., 1999 and Hämäläinen and Sarvas, 1989 Section III?)71 rmag : ndarray, shape (n_integration_pts, 3)72 3D positions of MEG coil integration points (from coil['rmag'])73 cosmag : ndarray, shape (n_integration_pts, 3)74 Direction of the MEG coil integration points (from coil['cosmag'])75 ws : ndarray, shape (n_integration_pts,)76 Weights for MEG coil integration points77 bins : ndarray, shape (n_integration_points,)78 The sensor assignments for each rmag/cosmag/w.79 %(n_jobs)s80 81 Returns82 -------83 coeff : list84 Linear coefficients with lead fields for each BEM vertex on each sensor85 (?)86 """87 parallel, p_fun, n_jobs = parallel_func(88 _do_lin_field_coeff, n_jobs, max_jobs=len(surf["tris"])89 )90 nas = np.array_split91 coeffs = parallel(92 p_fun(surf["rr"], t, tn, ta, rmags, cosmags, ws, bins)93 for t, tn, ta in zip(94 nas(surf["tris"], n_jobs),95 nas(surf["tri_nn"], n_jobs),96 nas(surf["tri_area"], n_jobs),97 )98 )99 return mult * np.sum(coeffs, axis=0)100 101 102@jit()103def _do_lin_field_coeff(bem_rr, tris, tn, ta, rmags, cosmags, ws, bins):104 """Compute field coefficients (parallel-friendly).105 106 See section IV of Mosher et al., 1999 (specifically equation 35).107 108 Parameters109 ----------110 bem_rr : ndarray, shape (n_BEM_vertices, 3)111 Positions on one BEM surface in 3-space. 2562 BEM vertices for BEM with112 5120 triangles (ico-4)113 tris : ndarray, shape (n_BEM_vertices, 3)114 Vertex indices for each triangle (referring to bem_rr)115 tn : ndarray, shape (n_BEM_vertices, 3)116 Triangle unit normal vectors117 ta : ndarray, shape (n_BEM_vertices,)118 Triangle areas119 rmag : ndarray, shape (n_sensor_pts, 3)120 3D positions of MEG coil integration points (from coil['rmag'])121 cosmag : ndarray, shape (n_sensor_pts, 3)122 Direction of the MEG coil integration points (from coil['cosmag'])123 ws : ndarray, shape (n_sensor_pts,)124 Weights for MEG coil integration points125 bins : ndarray, shape (n_sensor_pts,)126 The sensor assignments for each rmag/cosmag/w.127 128 Returns129 -------130 coeff : ndarray, shape (n_MEG_sensors, n_BEM_vertices)131 Linear coefficients with effect of each BEM vertex on each sensor (?)132 """133 coeff = np.zeros((bins[-1] + 1, len(bem_rr)))134 w_cosmags = ws.reshape(-1, 1) * cosmags135 diff = rmags.reshape(rmags.shape[0], 1, rmags.shape[1]) - bem_rr136 den = np.sum(diff * diff, axis=-1)137 den *= np.sqrt(den)138 den *= 3139 for ti in range(len(tris)):140 tri, tri_nn, tri_area = tris[ti], tn[ti], ta[ti]141 # Accumulate the coefficients for each triangle node and add to the142 # corresponding coefficient matrix143 144 # Simple version (bem_lin_field_coeffs_simple)145 # The following is equivalent to:146 # tri_rr = bem_rr[tri]147 # for j, coil in enumerate(coils['coils']):148 # x = func(coil['rmag'], coil['cosmag'],149 # tri_rr, tri_nn, tri_area)150 # res = np.sum(coil['w'][np.newaxis, :] * x, axis=1)151 # coeff[j][tri + off] += mult * res152 153 c = np.empty((diff.shape[0], tri.shape[0], diff.shape[2]))154 _jit_cross(c, diff[:, tri], tri_nn)155 c *= w_cosmags.reshape(w_cosmags.shape[0], 1, w_cosmags.shape[1])156 for ti in range(3):157 x = np.sum(c[:, ti], axis=-1)158 x /= den[:, tri[ti]] / tri_area159 coeff[:, tri[ti]] += bincount(bins, weights=x, minlength=bins[-1] + 1)160 return coeff161 162 163def _concatenate_coils(coils):164 """Concatenate MEG coil parameters."""165 rmags = np.concatenate([coil["rmag"] for coil in coils])166 cosmags = np.concatenate([coil["cosmag"] for coil in coils])167 ws = np.concatenate([coil["w"] for coil in coils])168 n_int = np.array([len(coil["rmag"]) for coil in coils])169 if n_int[-1] == 0:170 # We assume each sensor has at least one integration point,171 # which should be a safe assumption. But let's check it here, since172 # our code elsewhere relies on bins[-1] + 1 being the number of sensors173 raise RuntimeError("not supported")174 bins = np.repeat(np.arange(len(n_int)), n_int)175 return rmags, cosmags, ws, bins176 177 178@fill_doc179def _bem_specify_coils(bem, coils, coord_frame, mults, n_jobs):180 """Set up for computing the solution at a set of MEG coils.181 182 Parameters183 ----------184 bem : instance of ConductorModel185 BEM information186 coils : list of dict, len(n_MEG_sensors)187 MEG sensor information dicts188 coord_frame : int189 Class constant identifying coordinate frame190 mults : ndarray, shape (1, n_BEM_vertices)191 Multiplier for every vertex in BEM192 %(n_jobs)s193 194 Returns195 -------196 sol: ndarray, shape (n_MEG_sensors, n_BEM_vertices)197 MEG solution198 """199 # Make sure MEG coils are in MRI coordinate frame to match BEM coords200 coils, coord_frame = _check_coil_frame(coils, coord_frame, bem)201 202 # leaving this in in case we want to easily add in the future203 # if method != 'simple': # in ['ferguson', 'urankar']:204 # raise NotImplementedError205 206 # Compute the weighting factors to obtain the magnetic field in the linear207 # potential approximation208 209 # Process each of the surfaces210 rmags, cosmags, ws, bins = _triage_coils(coils)211 del coils212 lens = np.cumsum(np.r_[0, [len(s["rr"]) for s in bem["surfs"]]])213 sol = np.zeros((bins[-1] + 1, bem["solution"].shape[1]))214 215 lims = np.concatenate([np.arange(0, sol.shape[0], 100), [sol.shape[0]]])216 # Put through the bem (in channel-based chunks to save memory)217 for start, stop in zip(lims[:-1], lims[1:]):218 mask = np.logical_and(bins >= start, bins < stop)219 r, c, w, b = rmags[mask], cosmags[mask], ws[mask], bins[mask] - start220 # Compute coeffs for each surface, one at a time221 for o1, o2, surf, mult in zip(222 lens[:-1], lens[1:], bem["surfs"], bem["field_mult"]223 ):224 coeff = _lin_field_coeff(surf, mult, r, c, w, b, n_jobs)225 sol[start:stop] += np.dot(coeff, bem["solution"][o1:o2])226 sol *= mults227 return sol228 229 230def _bem_specify_els(bem, els, mults):231 """Set up for computing the solution at a set of EEG electrodes.232 233 Parameters234 ----------235 bem : instance of ConductorModel236 BEM information237 els : list of dict, len(n_EEG_sensors)238 List of EEG sensor information dicts239 mults: ndarray, shape (1, n_BEM_vertices)240 Multiplier for every vertex in BEM241 242 Returns243 -------244 sol : ndarray, shape (n_EEG_sensors, n_BEM_vertices)245 EEG solution246 """247 sol = np.zeros((len(els), bem["solution"].shape[1]))248 scalp = bem["surfs"][0]249 250 # Operate on all integration points for all electrodes (in MRI coords)251 rrs = np.concatenate(252 [apply_trans(bem["head_mri_t"]["trans"], el["rmag"]) for el in els], axis=0253 )254 ws = np.concatenate([el["w"] for el in els])255 tri_weights, tri_idx = _project_onto_surface(rrs, scalp)256 tri_weights *= ws[:, np.newaxis]257 weights = np.matmul(258 tri_weights[:, np.newaxis], bem["solution"][scalp["tris"][tri_idx]]259 )[:, 0]260 # there are way more vertices than electrodes generally, so let's iterate261 # over the electrodes262 edges = np.concatenate([[0], np.cumsum([len(el["w"]) for el in els])])263 for ii, (start, stop) in enumerate(zip(edges[:-1], edges[1:])):264 sol[ii] = weights[start:stop].sum(0)265 sol *= mults266 return sol267 268 269# #############################################################################270# BEM COMPUTATION271 272_MAG_FACTOR = 1e-7 # μ_0 / (4π)273 274# def _bem_inf_pot(rd, Q, rp):275# """The infinite medium potential in one direction. See Eq. (8) in276# Mosher, 1999"""277# NOTE: the (μ_0 / (4π) factor has been moved to _prep_field_communication278# diff = rp - rd # (Observation point position) - (Source position)279# diff2 = np.sum(diff * diff, axis=1) # Squared magnitude of diff280# # (Dipole moment) dot (diff) / (magnitude ^ 3)281# return np.sum(Q * diff, axis=1) / (diff2 * np.sqrt(diff2))282 283 284@jit()285def _bem_inf_pots(mri_rr, bem_rr, mri_Q=None):286 """Compute the infinite medium potential in all 3 directions.287 288 Parameters289 ----------290 mri_rr : ndarray, shape (n_dipole_vertices, 3)291 Chunk of 3D dipole positions in MRI coordinates292 bem_rr: ndarray, shape (n_BEM_vertices, 3)293 3D vertex positions for one BEM surface294 mri_Q : ndarray, shape (3, 3)295 3x3 head -> MRI transform. I.e., head_mri_t.dot(np.eye(3))296 297 Returns298 -------299 ndarray : shape(n_dipole_vertices, 3, n_BEM_vertices)300 """301 # NOTE: the (μ_0 / (4π) factor has been moved to _prep_field_communication302 # Get position difference vector between BEM vertex and dipole303 diff = np.empty((len(mri_rr), 3, len(bem_rr)))304 for ri in range(mri_rr.shape[0]):305 rr = mri_rr[ri]306 this_diff = bem_rr - rr307 diff_norm = np.sum(this_diff * this_diff, axis=1)308 diff_norm *= np.sqrt(diff_norm)309 diff_norm[diff_norm == 0] = 1.0310 if mri_Q is not None:311 this_diff = np.dot(this_diff, mri_Q.T)312 this_diff /= diff_norm.reshape(-1, 1)313 diff[ri] = this_diff.T314 315 return diff316 317 318# This function has been refactored to process all points simultaneously319# def _bem_inf_field(rd, Q, rp, d):320# """Infinite-medium magnetic field. See (7) in Mosher, 1999"""321# # Get vector from source to sensor integration point322# diff = rp - rd323# diff2 = np.sum(diff * diff, axis=1) # Get magnitude of diff324#325# # Compute cross product between diff and dipole to get magnetic field at326# # integration point327# x = fast_cross_3d(Q[np.newaxis, :], diff)328#329# # Take magnetic field dotted by integration point normal to get magnetic330# # field threading the current loop. Divide by R^3 (equivalently, R^2 * R)331# return np.sum(x * d, axis=1) / (diff2 * np.sqrt(diff2))332 333 334@jit()335def _bem_inf_fields(rr, rmag, cosmag):336 """Compute infinite-medium magnetic field at one MEG sensor.337 338 This operates on all dipoles in all 3 basis directions.339 340 Parameters341 ----------342 rr : ndarray, shape (n_source_points, 3)343 3D dipole source positions344 rmag : ndarray, shape (n_sensor points, 3)345 3D positions of 1 MEG coil's integration points (from coil['rmag'])346 cosmag : ndarray, shape (n_sensor_points, 3)347 Direction of 1 MEG coil's integration points (from coil['cosmag'])348 349 Returns350 -------351 ndarray, shape (n_dipoles, 3, n_integration_pts)352 Magnetic field from all dipoles at each MEG sensor integration point353 """354 # rr, rmag refactored according to Equation (19) in Mosher, 1999355 # Knowing that we're doing all directions, refactor above function:356 357 # rr, 3, rmag358 diff = rmag.T.reshape(1, 3, rmag.shape[0]) - rr.reshape(rr.shape[0], 3, 1)359 diff_norm = np.sum(diff * diff, axis=1) # rr, rmag360 diff_norm *= np.sqrt(diff_norm) # Get magnitude of distance cubed361 diff_norm_ = diff_norm.reshape(-1)362 diff_norm_[diff_norm_ == 0] = 1 # avoid nans363 364 # This is the result of cross-prod calcs with basis vectors,365 # as if we had taken (Q=np.eye(3)), then multiplied by cosmags366 # factor, and then summed across directions367 x = np.empty((rr.shape[0], 3, rmag.shape[0]))368 x[:, 0] = diff[:, 1] * cosmag[:, 2] - diff[:, 2] * cosmag[:, 1]369 x[:, 1] = diff[:, 2] * cosmag[:, 0] - diff[:, 0] * cosmag[:, 2]370 x[:, 2] = diff[:, 0] * cosmag[:, 1] - diff[:, 1] * cosmag[:, 0]371 diff_norm = diff_norm_.reshape((rr.shape[0], 1, rmag.shape[0]))372 x /= diff_norm373 # x.shape == (rr.shape[0], 3, rmag.shape[0])374 return x375 376 377@fill_doc378def _bem_pot_or_field(rr, mri_rr, mri_Q, coils, solution, bem_rr, n_jobs, coil_type):379 """Calculate the magnetic field or electric potential forward solution.380 381 The code is very similar between EEG and MEG potentials, so combine them.382 This does the work of "fwd_comp_field" (which wraps to "fwd_bem_field")383 and "fwd_bem_pot_els" in MNE-C.384 385 Parameters386 ----------387 rr : ndarray, shape (n_dipoles, 3)388 3D dipole source positions389 mri_rr : ndarray, shape (n_dipoles, 3)390 3D source positions in MRI coordinates391 mri_Q :392 3x3 head -> MRI transform. I.e., head_mri_t.dot(np.eye(3))393 coils : list of dict, len(sensors)394 List of sensors where each element contains sensor specific information395 solution : ndarray, shape (n_sensors, n_BEM_rr)396 Comes from _bem_specify_coils397 bem_rr : ndarray, shape (n_BEM_vertices, 3)398 3D vertex positions for all surfaces in the BEM399 %(n_jobs)s400 coil_type : str401 'meg' or 'eeg'402 403 Returns404 -------405 B : ndarray, shape (n_dipoles * 3, n_sensors)406 Forward solution for a set of sensors407 """408 # Both MEG and EEG have the inifinite-medium potentials409 # This could be just vectorized, but eats too much memory, so instead we410 # reduce memory by chunking within _do_inf_pots and parallelize, too:411 parallel, p_fun, n_jobs = parallel_func(_do_inf_pots, n_jobs, max_jobs=len(rr))412 nas = np.array_split413 B = np.sum(414 parallel(415 p_fun(416 mri_rr, sr.copy(), np.ascontiguousarray(mri_Q), np.array(sol)417 ) # copy and contig418 for sr, sol in zip(nas(bem_rr, n_jobs), nas(solution.T, n_jobs))419 ),420 axis=0,421 )422 # The copy()s above should make it so the whole objects don't need to be423 # pickled...424 425 # Only MEG coils are sensitive to the primary current distribution.426 if coil_type == "meg":427 # Primary current contribution (can be calc. in coil/dipole coords)428 parallel, p_fun, n_jobs = parallel_func(_do_prim_curr, n_jobs)429 pcc = np.concatenate(parallel(p_fun(r, coils) for r in nas(rr, n_jobs)), axis=0)430 B += pcc431 B *= _MAG_FACTOR432 return B433 434 435def _do_prim_curr(rr, coils):436 """Calculate primary currents in a set of MEG coils.437 438 See Mosher et al., 1999 Section II for discussion of primary vs. volume439 currents.440 441 Parameters442 ----------443 rr : ndarray, shape (n_dipoles, 3)444 3D dipole source positions in head coordinates445 coils : list of dict446 List of MEG coils where each element contains coil specific information447 448 Returns449 -------450 pc : ndarray, shape (n_sources, n_MEG_sensors)451 Primary current for set of MEG coils due to all sources452 """453 rmags, cosmags, ws, bins = _triage_coils(coils)454 n_coils = bins[-1] + 1455 del coils456 pc = np.empty((len(rr) * 3, n_coils))457 for start, stop in _rr_bounds(rr, chunk=1):458 pp = _bem_inf_fields(rr[start:stop], rmags, cosmags)459 pp *= ws460 pp.shape = (3 * (stop - start), -1)461 pc[3 * start : 3 * stop] = [462 bincount(bins, this_pp, bins[-1] + 1) for this_pp in pp463 ]464 return pc465 466 467def _rr_bounds(rr, chunk=200):468 # chunk data nicely469 bounds = np.concatenate([np.arange(0, len(rr), chunk), [len(rr)]])470 return zip(bounds[:-1], bounds[1:])471 472 473def _do_inf_pots(mri_rr, bem_rr, mri_Q, sol):474 """Calculate infinite potentials for MEG or EEG sensors using chunks.475 476 Parameters477 ----------478 mri_rr : ndarray, shape (n_dipoles, 3)479 3D dipole source positions in MRI coordinates480 bem_rr : ndarray, shape (n_BEM_vertices, 3)481 3D vertex positions for all surfaces in the BEM482 mri_Q :483 3x3 head -> MRI transform. I.e., head_mri_t.dot(np.eye(3))484 sol : ndarray, shape (n_sensors_subset, n_BEM_vertices_subset)485 Comes from _bem_specify_coils486 487 Returns488 -------489 B : ndarray, (n_dipoles * 3, n_sensors)490 Forward solution for sensors due to volume currents491 """492 # Doing work of 'fwd_bem_pot_calc' in MNE-C493 # The following code is equivalent to this, but saves memory494 # v0s = _bem_inf_pots(rr, bem_rr, Q) # n_rr x 3 x n_bem_rr495 # v0s.shape = (len(rr) * 3, v0s.shape[2])496 # B = np.dot(v0s, sol)497 498 # We chunk the source mri_rr's in order to save memory499 B = np.empty((len(mri_rr) * 3, sol.shape[1]))500 for start, stop in _rr_bounds(mri_rr):501 # v0 in Hämäläinen et al., 1989 == v_inf in Mosher, et al., 1999502 v0s = _bem_inf_pots(mri_rr[start:stop], bem_rr, mri_Q)503 v0s = v0s.reshape(-1, v0s.shape[2])504 B[3 * start : 3 * stop] = np.dot(v0s, sol)505 return B506 507 508# #############################################################################509# SPHERE COMPUTATION510 511 512def _sphere_pot_or_field(rr, mri_rr, mri_Q, coils, solution, bem_rr, n_jobs, coil_type):513 """Do potential or field for spherical model."""514 fun = _eeg_spherepot_coil if coil_type == "eeg" else _sphere_field515 parallel, p_fun, n_jobs = parallel_func(fun, n_jobs, max_jobs=len(rr))516 B = np.concatenate(517 parallel(p_fun(r, coils, sphere=solution) for r in np.array_split(rr, n_jobs))518 )519 return B520 521 522def _sphere_field(rrs, coils, sphere):523 """Compute field for spherical model using Jukka Sarvas' field computation.524 525 Jukka Sarvas, "Basic mathematical and electromagnetic concepts of the526 biomagnetic inverse problem", Phys. Med. Biol. 1987, Vol. 32, 1, 11-22.527 528 The formulas have been manipulated for efficient computation529 by Matti Hämäläinen, February 1990530 """531 rmags, cosmags, ws, bins = _triage_coils(coils)532 return _do_sphere_field(rrs, rmags, cosmags, ws, bins, sphere["r0"])533 534 535@jit()536def _do_sphere_field(rrs, rmags, cosmags, ws, bins, r0):537 n_coils = bins[-1] + 1538 # Shift to the sphere model coordinates539 rrs = rrs - r0540 B = np.zeros((3 * len(rrs), n_coils))541 for ri in range(len(rrs)):542 rr = rrs[ri]543 # Check for a dipole at the origin544 if np.sqrt(np.dot(rr, rr)) <= 1e-10:545 continue546 this_poss = rmags - r0547 548 # Vector from dipole to the field point549 a_vec = this_poss - rr550 a = np.sqrt(np.sum(a_vec * a_vec, axis=1))551 r = np.sqrt(np.sum(this_poss * this_poss, axis=1))552 rr0 = np.sum(this_poss * rr, axis=1)553 ar = (r * r) - rr0554 ar0 = ar / a555 F = a * (r * a + ar)556 gr = (a * a) / r + ar0 + 2.0 * (a + r)557 g0 = a + 2 * r + ar0558 # Compute the dot products needed559 re = np.sum(this_poss * cosmags, axis=1)560 r0e = np.sum(rr * cosmags, axis=1)561 g = (g0 * r0e - gr * re) / (F * F)562 good = (a > 0) | (r > 0) | ((a * r) + 1 > 1e-5)563 rr_ = rr.reshape(1, 3)564 v1 = np.empty((cosmags.shape[0], 3))565 _jit_cross(v1, rr_, cosmags)566 v2 = np.empty((cosmags.shape[0], 3))567 _jit_cross(v2, rr_, this_poss)568 xx = (good * ws).reshape(-1, 1) * (569 v1 / F.reshape(-1, 1) + v2 * g.reshape(-1, 1)570 )571 for jj in range(3):572 zz = bincount(bins, xx[:, jj], n_coils)573 B[3 * ri + jj, :] = zz574 B *= _MAG_FACTOR575 return B576 577 578def _eeg_spherepot_coil(rrs, coils, sphere):579 """Calculate the EEG in the sphere model."""580 rmags, cosmags, ws, bins = _triage_coils(coils)581 n_coils = bins[-1] + 1582 del coils583 584 # Shift to the sphere model coordinates585 rrs = rrs - sphere["r0"]586 587 B = np.zeros((3 * len(rrs), n_coils))588 for ri, rr in enumerate(rrs):589 # Only process dipoles inside the innermost sphere590 if np.sqrt(np.dot(rr, rr)) >= sphere["layers"][0]["rad"]:591 continue592 # fwd_eeg_spherepot_vec593 vval_one = np.zeros((len(rmags), 3))594 595 # Make a weighted sum over the equivalence parameters596 for eq in range(sphere["nfit"]):597 # Scale the dipole position598 rd = sphere["mu"][eq] * rr599 rd2 = np.sum(rd * rd)600 rd2_inv = 1.0 / rd2601 # Go over all electrodes602 this_pos = rmags - sphere["r0"]603 604 # Scale location onto the surface of the sphere (not used)605 # if sphere['scale_pos']:606 # pos_len = (sphere['layers'][-1]['rad'] /607 # np.sqrt(np.sum(this_pos * this_pos, axis=1)))608 # this_pos *= pos_len609 610 # Vector from dipole to the field point611 a_vec = this_pos - rd612 613 # Compute the dot products needed614 a = np.sqrt(np.sum(a_vec * a_vec, axis=1))615 a3 = 2.0 / (a * a * a)616 r2 = np.sum(this_pos * this_pos, axis=1)617 r = np.sqrt(r2)618 rrd = np.sum(this_pos * rd, axis=1)619 ra = r2 - rrd620 rda = rrd - rd2621 622 # The main ingredients623 F = a * (r * a + ra)624 c1 = a3 * rda + 1.0 / a - 1.0 / r625 c2 = a3 + (a + r) / (r * F)626 627 # Mix them together and scale by lambda/(rd*rd)628 m1 = c1 - c2 * rrd629 m2 = c2 * rd2630 631 vval_one += (632 sphere["lambda"][eq]633 * rd2_inv634 * (m1[:, np.newaxis] * rd + m2[:, np.newaxis] * this_pos)635 )636 637 # compute total result638 xx = vval_one * ws[:, np.newaxis]639 zz = np.array([bincount(bins, x, bins[-1] + 1) for x in xx.T])640 B[3 * ri : 3 * ri + 3, :] = zz641 # finishing by scaling by 1/(4*M_PI)642 B *= 0.25 / np.pi643 return B644 645 646def _triage_coils(coils):647 return coils if isinstance(coils, tuple) else _concatenate_coils(coils)648 649 650# #############################################################################651# MAGNETIC DIPOLE (e.g. CHPI)652 653_MIN_DIST_LIMIT = 1e-5654 655 656def _magnetic_dipole_field_vec(rrs, coils, too_close="raise"):657 rmags, cosmags, ws, bins = _triage_coils(coils)658 fwd, min_dist = _compute_mdfv(rrs, rmags, cosmags, ws, bins, too_close)659 if min_dist < _MIN_DIST_LIMIT:660 msg = f"Coil too close (dist = {min_dist * 1000:g} mm)"661 if too_close == "raise":662 raise RuntimeError(msg)663 func = warn if too_close == "warning" else logger.info664 func(msg)665 return fwd666 667 668@jit()669def _compute_mdfv(rrs, rmags, cosmags, ws, bins, too_close):670 """Compute an MEG forward solution for a set of magnetic dipoles."""671 # The code below is a more efficient version (~30x) of this:672 # for ri, rr in enumerate(rrs):673 # for k in range(len(coils)):674 # this_coil = coils[k]675 # # Go through all points676 # diff = this_coil['rmag'] - rr677 # dist2 = np.sum(diff * diff, axis=1)[:, np.newaxis]678 # dist = np.sqrt(dist2)679 # if (dist < 1e-5).any():680 # raise RuntimeError('Coil too close')681 # dist5 = dist2 * dist2 * dist682 # sum_ = (3 * diff * np.sum(diff * this_coil['cosmag'],683 # axis=1)[:, np.newaxis] -684 # dist2 * this_coil['cosmag']) / dist5685 # fwd[3*ri:3*ri+3, k] = 1e-7 * np.dot(this_coil['w'], sum_)686 fwd = np.zeros((3 * len(rrs), bins[-1] + 1))687 min_dist = np.inf688 ws2 = ws.reshape(-1, 1)689 for ri in range(len(rrs)):690 rr = rrs[ri]691 diff = rmags - rr692 dist2_ = np.sum(diff * diff, axis=1)693 dist2 = dist2_.reshape(-1, 1)694 dist = np.sqrt(dist2)695 min_dist = min(dist.min(), min_dist)696 if min_dist < _MIN_DIST_LIMIT and too_close == "raise":697 break698 t_ = np.sum(diff * cosmags, axis=1)699 t = t_.reshape(-1, 1)700 sum_ = ws2 * (3 * diff * t - dist2 * cosmags) / (dist2 * dist2 * dist)701 for ii in range(3):702 fwd[3 * ri + ii] = bincount(bins, sum_[:, ii], bins[-1] + 1)703 fwd *= _MAG_FACTOR704 return fwd, min_dist705 706 707# #############################################################################708# MAIN TRIAGING FUNCTION709 710 711@verbose712def _prep_field_computation(*, sensors, bem, n_jobs, verbose=None):713 """Precompute and store some things that are used for both MEG and EEG.714 715 Calculation includes multiplication factors, coordinate transforms,716 compensations, and forward solutions. All are stored in modified fwd_data.717 718 Parameters719 ----------720 rr : ndarray, shape (n_dipoles, 3)721 3D dipole source positions in head coordinates722 bem : instance of ConductorModel723 Boundary Element Model information724 fwd_data : dict725 Dict containing sensor information in the head coordinate frame.726 Gets updated here with BEM and sensor information for later forward727 calculations.728 %(n_jobs)s729 %(verbose)s730 """731 bem_rr = mults = mri_Q = head_mri_t = None732 if not bem["is_sphere"]:733 if bem["bem_method"] != FIFF.FIFFV_BEM_APPROX_LINEAR:734 raise RuntimeError("only linear collocation supported")735 # Store (and apply soon) μ_0/(4π) factor before source computations736 mults = np.repeat(737 bem["source_mult"] / (4.0 * np.pi), [len(s["rr"]) for s in bem["surfs"]]738 )[np.newaxis, :]739 # Get positions of BEM points for every surface740 bem_rr = np.concatenate([s["rr"] for s in bem["surfs"]])741 742 # The dipole location and orientation must be transformed743 head_mri_t = bem["head_mri_t"]744 mri_Q = bem["head_mri_t"]["trans"][:3, :3].T745 746 solutions = dict()747 for coil_type in sensors:748 coils = sensors[coil_type]["defs"]749 if not bem["is_sphere"]:750 if coil_type == "meg":751 # MEG field computation matrices for BEM752 start = "Composing the field computation matrix"753 logger.info("\n" + start + "...")754 cf = FIFF.FIFFV_COORD_HEAD755 # multiply solution by "mults" here for simplicity756 solution = _bem_specify_coils(bem, coils, cf, mults, n_jobs)757 else:758 # Compute solution for EEG sensor759 logger.info("Setting up for EEG...")760 solution = _bem_specify_els(bem, coils, mults)761 else:762 solution = bem763 if coil_type == "eeg":764 logger.info(765 "Using the equivalent source approach in the "766 "homogeneous sphere for EEG"767 )768 sensors[coil_type]["defs"] = _triage_coils(coils)769 solutions[coil_type] = solution770 771 # Get appropriate forward physics function depending on sphere or BEM model772 fun = _sphere_pot_or_field if bem["is_sphere"] else _bem_pot_or_field773 774 # Update fwd_data with775 # bem_rr (3D BEM vertex positions)776 # mri_Q (3x3 Head->MRI coord transformation applied to identity matrix)777 # head_mri_t (head->MRI coord transform dict)778 # fun (_bem_pot_or_field if not 'sphere'; otherwise _sph_pot_or_field)779 # solutions (len 2 list; [ndarray, shape (n_MEG_sens, n BEM vertices),780 # ndarray, shape (n_EEG_sens, n BEM vertices)]781 fwd_data = dict(782 bem_rr=bem_rr, mri_Q=mri_Q, head_mri_t=head_mri_t, fun=fun, solutions=solutions783 )784 return fwd_data785 786 787@fill_doc788def _compute_forwards_meeg(rr, *, sensors, fwd_data, n_jobs, silent=False):789 """Compute MEG and EEG forward solutions for all sensor types."""790 Bs = dict()791 # The dipole location and orientation must be transformed to mri coords792 mri_rr = None793 if fwd_data["head_mri_t"] is not None:794 mri_rr = np.ascontiguousarray(apply_trans(fwd_data["head_mri_t"]["trans"], rr))795 mri_Q, bem_rr, fun = fwd_data["mri_Q"], fwd_data["bem_rr"], fwd_data["fun"]796 solutions = fwd_data["solutions"]797 del fwd_data798 rr = np.ascontiguousarray(rr) # usually true but not guaranteed, e.g. in dipole.py799 for coil_type, sens in sensors.items():800 coils = sens["defs"]801 compensator = sens.get("compensator", None)802 post_picks = sens.get("post_picks", None)803 solution = solutions.get(coil_type, None)804 805 # Do the actual forward calculation for a list MEG/EEG sensors806 if not silent:807 logger.info(808 f"Computing {coil_type.upper()} at {len(rr)} source location{_pl(rr)} "809 "(free orientations)..."810 )811 # Calculate forward solution using spherical or BEM model812 B = fun(813 rr,814 mri_rr,815 mri_Q,816 coils=coils,817 solution=solution,818 bem_rr=bem_rr,819 n_jobs=n_jobs,820 coil_type=coil_type,821 )822 823 # Compensate if needed (only done for MEG systems w/compensation)824 if compensator is not None:825 B = B @ compensator.T826 if post_picks is not None:827 B = B[:, post_picks]828 Bs[coil_type] = B829 return Bs830 831 832@verbose833def _compute_forwards(rr, *, bem, sensors, n_jobs, verbose=None):834 """Compute the MEG and EEG forward solutions."""835 # Split calculation into two steps to save (potentially) a lot of time836 # when e.g. dipole fitting837 solver = bem.get("solver", "mne")838 _check_option("solver", solver, ("mne", "openmeeg"))839 if bem["is_sphere"] or solver == "mne":840 # This modifies "sensors" in place, so let's copy it in case the calling841 # function needs to reuse it (e.g., in simulate_raw.py)842 sensors = deepcopy(sensors)843 fwd_data = _prep_field_computation(sensors=sensors, bem=bem, n_jobs=n_jobs)844 Bs = _compute_forwards_meeg(845 rr, sensors=sensors, fwd_data=fwd_data, n_jobs=n_jobs846 )847 else:848 Bs = _compute_forwards_openmeeg(rr, bem=bem, sensors=sensors)849 n_sensors_want = sum(len(s["ch_names"]) for s in sensors.values())850 n_sensors = sum(B.shape[1] for B in Bs.values())851 n_sources = list(Bs.values())[0].shape[0]852 assert (n_sources, n_sensors) == (len(rr) * 3, n_sensors_want)853 return Bs854 855 856def _compute_forwards_openmeeg(rr, *, bem, sensors):857 """Compute the MEG and EEG forward solutions for OpenMEEG."""858 if len(bem["surfs"]) != 3:859 raise RuntimeError("Only 3-layer BEM is supported for OpenMEEG.")860 om = _import_openmeeg("compute a forward solution using OpenMEEG")861 hminv = om.SymMatrix(bem["solution"])862 geom = _make_openmeeg_geometry(bem, invert_transform(bem["head_mri_t"]))863 864 # Make dipoles for all XYZ orientations865 dipoles = np.c_[866 np.kron(rr.T, np.ones(3)[None, :]).T,867 np.kron(np.ones(len(rr))[:, None], np.eye(3)),868 ]869 dipoles = np.asfortranarray(dipoles)870 dipoles = om.Matrix(dipoles)871 dsm = om.DipSourceMat(geom, dipoles, "Brain")872 Bs = dict()873 if "eeg" in sensors:874 rmags, _, ws, bins = _concatenate_coils(sensors["eeg"]["defs"])875 rmags = np.asfortranarray(rmags.astype(np.float64))876 eeg_sensors = om.Sensors(om.Matrix(np.asfortranarray(rmags)), geom)877 h2em = om.Head2EEGMat(geom, eeg_sensors)878 eeg_fwd_full = om.GainEEG(hminv, dsm, h2em).array()879 Bs["eeg"] = np.array(880 [bincount(bins, ws * x, bins[-1] + 1) for x in eeg_fwd_full.T], float881 )882 if "meg" in sensors:883 rmags, cosmags, ws, bins = _concatenate_coils(sensors["meg"]["defs"])884 rmags = np.asfortranarray(rmags.astype(np.float64))885 cosmags = np.asfortranarray(cosmags.astype(np.float64))886 labels = [str(ii) for ii in range(len(rmags))]887 weights = radii = np.ones(len(labels))888 meg_sensors = om.Sensors(labels, rmags, cosmags, weights, radii)889 h2mm = om.Head2MEGMat(geom, meg_sensors)890 ds2mm = om.DipSource2MEGMat(dipoles, meg_sensors)891 meg_fwd_full = om.GainMEG(hminv, dsm, h2mm, ds2mm).array()892 B = np.array(893 [bincount(bins, ws * x, bins[-1] + 1) for x in meg_fwd_full.T], float894 )895 compensator = sensors["meg"].get("compensator", None)896 post_picks = sensors["meg"].get("post_picks", None)897 if compensator is not None:898 B = B @ compensator.T899 if post_picks is not None:900 B = B[:, post_picks]901 Bs["meg"] = B902 return Bs903 