898 lines
33 KiB
Python
898 lines
33 KiB
Python
# 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
|