针对pulse-transit的工具

This commit is contained in:
2025-02-22 16:12:02 +08:00
commit 6bc25b4e3a
7719 changed files with 1530886 additions and 0 deletions

8
dist/client/mne/forward/__init__.py vendored Normal file
View File

@@ -0,0 +1,8 @@
# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.
"""Forward modeling code."""
import lazy_loader as lazy # for testing purposes
(__getattr__, __dir__, __all__) = lazy.attach_stub(__name__, __file__)

86
dist/client/mne/forward/__init__.pyi vendored Normal file
View File

@@ -0,0 +1,86 @@
__all__ = [
"Forward",
"_apply_forward",
"_as_meg_type_inst",
"_compute_forwards",
"_concatenate_coils",
"_create_meg_coils",
"_do_forward_solution",
"_fill_measurement_info",
"_lead_dots",
"_magnetic_dipole_field_vec",
"_make_surface_mapping",
"_map_meg_or_eeg_channels",
"_merge_fwds",
"_prep_eeg_channels",
"_prep_meg_channels",
"_prepare_for_forward",
"_read_coil_defs",
"_read_forward_meas_info",
"_select_orient_forward",
"_stc_src_sel",
"_subject_from_forward",
"_to_forward_dict",
"_transform_orig_meg_coils",
"apply_forward",
"apply_forward_raw",
"average_forward_solutions",
"compute_depth_prior",
"compute_orient_prior",
"convert_forward_solution",
"is_fixed_orient",
"make_field_map",
"make_forward_dipole",
"make_forward_solution",
"read_forward_solution",
"restrict_forward_to_label",
"restrict_forward_to_stc",
"use_coil_def",
"write_forward_solution",
]
from . import _lead_dots
from ._compute_forward import (
_compute_forwards,
_concatenate_coils,
_magnetic_dipole_field_vec,
)
from ._field_interpolation import (
_as_meg_type_inst,
_make_surface_mapping,
_map_meg_or_eeg_channels,
make_field_map,
)
from ._make_forward import (
_create_meg_coils,
_prep_eeg_channels,
_prep_meg_channels,
_prepare_for_forward,
_read_coil_defs,
_to_forward_dict,
_transform_orig_meg_coils,
make_forward_dipole,
make_forward_solution,
use_coil_def,
)
from .forward import (
Forward,
_apply_forward,
_do_forward_solution,
_fill_measurement_info,
_merge_fwds,
_read_forward_meas_info,
_select_orient_forward,
_stc_src_sel,
_subject_from_forward,
apply_forward,
apply_forward_raw,
average_forward_solutions,
compute_depth_prior,
compute_orient_prior,
convert_forward_solution,
is_fixed_orient,
read_forward_solution,
restrict_forward_to_label,
restrict_forward_to_stc,
write_forward_solution,
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,897 @@
# 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

View File

@@ -0,0 +1,547 @@
# 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.
import inspect
from copy import deepcopy
import numpy as np
from scipy.interpolate import interp1d
from .._fiff.constants import FIFF
from .._fiff.meas_info import _simplify_info
from .._fiff.pick import pick_info, pick_types
from .._fiff.proj import _has_eeg_average_ref_proj, make_projector
from ..bem import _check_origin
from ..cov import make_ad_hoc_cov
from ..epochs import BaseEpochs, EpochsArray
from ..evoked import Evoked, EvokedArray
from ..fixes import _safe_svd
from ..surface import get_head_surf, get_meg_helmet_surf
from ..transforms import _find_trans, _get_trans, transform_surface_to
from ..utils import _check_fname, _check_option, _pl, _reg_pinv, logger, verbose
from ._lead_dots import (
_do_cross_dots,
_do_self_dots,
_do_surface_dots,
_get_legen_table,
)
from ._make_forward import _create_eeg_els, _create_meg_coils, _read_coil_defs
def _setup_dots(mode, info, coils, ch_type):
"""Set up dot products."""
int_rad = 0.06
noise = make_ad_hoc_cov(info, dict(mag=20e-15, grad=5e-13, eeg=1e-6))
n_coeff, interp = (50, "nearest") if mode == "fast" else (100, "linear")
lut, n_fact = _get_legen_table(ch_type, False, n_coeff, verbose=False)
lut_fun = interp1d(np.linspace(-1, 1, lut.shape[0]), lut, interp, axis=0)
return int_rad, noise, lut_fun, n_fact
def _compute_mapping_matrix(fmd, info):
"""Do the hairy computations."""
logger.info(" Preparing the mapping matrix...")
# assemble a projector and apply it to the data
ch_names = fmd["ch_names"]
projs = info.get("projs", list())
proj_op = make_projector(projs, ch_names)[0]
proj_dots = np.dot(proj_op.T, np.dot(fmd["self_dots"], proj_op))
noise_cov = fmd["noise"]
# Whiten
if not noise_cov["diag"]:
raise NotImplementedError # this shouldn't happen
whitener = np.diag(1.0 / np.sqrt(noise_cov["data"].ravel()))
whitened_dots = np.dot(whitener.T, np.dot(proj_dots, whitener))
# SVD is numerically better than the eigenvalue composition even if
# mat is supposed to be symmetric and positive definite
if fmd.get("pinv_method", "tsvd") == "tsvd":
inv, fmd["nest"] = _pinv_trunc(whitened_dots, fmd["miss"])
else:
assert fmd["pinv_method"] == "tikhonov", fmd["pinv_method"]
inv, fmd["nest"] = _pinv_tikhonov(whitened_dots, fmd["miss"])
# Sandwich with the whitener
inv_whitened = np.dot(whitener.T, np.dot(inv, whitener))
# Take into account that the lead fields used to compute
# d->surface_dots were unprojected
inv_whitened_proj = proj_op.T @ inv_whitened
# Finally sandwich in the selection matrix
# This one picks up the correct lead field projection
mapping_mat = np.dot(fmd["surface_dots"], inv_whitened_proj)
# Optionally apply the average electrode reference to the final field map
if fmd["kind"] == "eeg" and _has_eeg_average_ref_proj(info):
logger.info(
" The map has an average electrode reference "
f"({mapping_mat.shape[0]} channels)"
)
mapping_mat -= np.mean(mapping_mat, axis=0)
return mapping_mat
def _pinv_trunc(x, miss):
"""Compute pseudoinverse, truncating at most "miss" fraction of varexp."""
u, s, v = _safe_svd(x, full_matrices=False)
# Eigenvalue truncation
varexp = np.cumsum(s)
varexp /= varexp[-1]
n = np.where(varexp >= (1.0 - miss))[0][0] + 1
logger.info(
" Truncating at %d/%d components to omit less than %g "
"(%0.2g)" % (n, len(s), miss, 1.0 - varexp[n - 1])
)
s = 1.0 / s[:n]
inv = ((u[:, :n] * s) @ v[:n]).T
return inv, n
def _pinv_tikhonov(x, reg):
# _reg_pinv requires square Hermitian, which we have here
inv, _, n = _reg_pinv(x, reg=reg, rank=None)
logger.info(
f" Truncating at {n}/{len(x)} components and regularizing "
f"with α={reg:0.1e}"
)
return inv, n
def _map_meg_or_eeg_channels(info_from, info_to, mode, origin, miss=None):
"""Find mapping from one set of channels to another.
Parameters
----------
info_from : instance of Info
The measurement data to interpolate from.
info_to : instance of Info
The measurement info to interpolate to.
mode : str
Either `'accurate'` or `'fast'`, determines the quality of the
Legendre polynomial expansion used. `'fast'` should be sufficient
for most applications.
origin : array-like, shape (3,) | str
Origin of the sphere in the head coordinate frame and in meters.
Can be ``'auto'``, which means a head-digitization-based origin
fit. Default is ``(0., 0., 0.04)``.
Returns
-------
mapping : array, shape (n_to, n_from)
A mapping matrix.
"""
# no need to apply trans because both from and to coils are in device
# coordinates
info_kinds = set(ch["kind"] for ch in info_to["chs"])
info_kinds |= set(ch["kind"] for ch in info_from["chs"])
if FIFF.FIFFV_REF_MEG_CH in info_kinds: # refs same as MEG
info_kinds |= set([FIFF.FIFFV_MEG_CH])
info_kinds -= set([FIFF.FIFFV_REF_MEG_CH])
info_kinds = sorted(info_kinds)
# This should be guaranteed by the callers
assert len(info_kinds) == 1 and info_kinds[0] in (
FIFF.FIFFV_MEG_CH,
FIFF.FIFFV_EEG_CH,
)
kind = "eeg" if info_kinds[0] == FIFF.FIFFV_EEG_CH else "meg"
#
# Step 1. Prepare the coil definitions
#
if kind == "meg":
templates = _read_coil_defs(verbose=False)
coils_from = _create_meg_coils(
info_from["chs"], "normal", info_from["dev_head_t"], templates
)
coils_to = _create_meg_coils(
info_to["chs"], "normal", info_to["dev_head_t"], templates
)
pinv_method = "tsvd"
miss = 1e-4
else:
coils_from = _create_eeg_els(info_from["chs"])
coils_to = _create_eeg_els(info_to["chs"])
pinv_method = "tikhonov"
miss = 1e-1
if _has_eeg_average_ref_proj(info_from) and not _has_eeg_average_ref_proj(
info_to
):
raise RuntimeError(
"info_to must have an average EEG reference projector if "
"info_from has one"
)
origin = _check_origin(origin, info_from)
#
# Step 2. Calculate the dot products
#
int_rad, noise, lut_fun, n_fact = _setup_dots(mode, info_from, coils_from, kind)
logger.info(
f" Computing dot products for {len(coils_from)} "
f"{kind.upper()} channel{_pl(coils_from)}..."
)
self_dots = _do_self_dots(
int_rad, False, coils_from, origin, kind, lut_fun, n_fact, n_jobs=None
)
logger.info(
f" Computing cross products for {len(coils_from)}"
f"{len(coils_to)} {kind.upper()} channel{_pl(coils_to)}..."
)
cross_dots = _do_cross_dots(
int_rad, False, coils_from, coils_to, origin, kind, lut_fun, n_fact
).T
ch_names = [c["ch_name"] for c in info_from["chs"]]
fmd = dict(
kind=kind,
ch_names=ch_names,
origin=origin,
noise=noise,
self_dots=self_dots,
surface_dots=cross_dots,
int_rad=int_rad,
miss=miss,
pinv_method=pinv_method,
)
#
# Step 3. Compute the mapping matrix
#
mapping = _compute_mapping_matrix(fmd, info_from)
return mapping
def _as_meg_type_inst(inst, ch_type="grad", mode="fast"):
"""Compute virtual evoked using interpolated fields in mag/grad channels.
Parameters
----------
inst : instance of mne.Evoked or mne.Epochs
The evoked or epochs object.
ch_type : str
The destination channel type. It can be 'mag' or 'grad'.
mode : str
Either `'accurate'` or `'fast'`, determines the quality of the
Legendre polynomial expansion used. `'fast'` should be sufficient
for most applications.
Returns
-------
inst : instance of mne.EvokedArray or mne.EpochsArray
The transformed evoked object containing only virtual channels.
"""
_check_option("ch_type", ch_type, ["mag", "grad"])
# pick the original and destination channels
pick_from = pick_types(inst.info, meg=True, eeg=False, ref_meg=False)
pick_to = pick_types(inst.info, meg=ch_type, eeg=False, ref_meg=False)
if len(pick_to) == 0:
raise ValueError(
"No channels matching the destination channel type"
" found in info. Please pass an evoked containing"
"both the original and destination channels. Only the"
" locations of the destination channels will be used"
" for interpolation."
)
info_from = pick_info(inst.info, pick_from)
info_to = pick_info(inst.info, pick_to)
# XXX someday we should probably expose the origin
mapping = _map_meg_or_eeg_channels(
info_from, info_to, origin=(0.0, 0.0, 0.04), mode=mode
)
# compute data by multiplying by the 'gain matrix' from
# original sensors to virtual sensors
if hasattr(inst, "get_data"):
kwargs = dict()
if "copy" in inspect.getfullargspec(inst.get_data).kwonlyargs:
kwargs["copy"] = False
data = inst.get_data(**kwargs)
else:
data = inst.data
ndim = data.ndim
if ndim == 2:
data = data[np.newaxis, :, :]
data_ = np.empty((data.shape[0], len(mapping), data.shape[2]), dtype=data.dtype)
for d, d_ in zip(data, data_):
d_[:] = np.dot(mapping, d[pick_from])
# keep only the destination channel types
info = pick_info(inst.info, sel=pick_to, copy=True)
# change channel names to emphasize they contain interpolated data
for ch in info["chs"]:
ch["ch_name"] += "_v"
info._update_redundant()
info._check_consistency()
if isinstance(inst, Evoked):
assert ndim == 2
data_ = data_[0] # undo new axis
inst_ = EvokedArray(
data_, info, tmin=inst.times[0], comment=inst.comment, nave=inst.nave
)
else:
assert isinstance(inst, BaseEpochs)
inst_ = EpochsArray(
data_,
info,
tmin=inst.tmin,
events=inst.events,
event_id=inst.event_id,
metadata=inst.metadata,
)
return inst_
@verbose
def _make_surface_mapping(
info,
surf,
ch_type="meg",
trans=None,
mode="fast",
n_jobs=None,
origin=(0.0, 0.0, 0.04),
verbose=None,
):
"""Re-map M/EEG data to a surface.
Parameters
----------
%(info_not_none)s
surf : dict
The surface to map the data to. The required fields are `'rr'`,
`'nn'`, and `'coord_frame'`. Must be in head coordinates.
ch_type : str
Must be either `'meg'` or `'eeg'`, determines the type of field.
trans : None | dict
If None, no transformation applied. Should be a Head<->MRI
transformation.
mode : str
Either `'accurate'` or `'fast'`, determines the quality of the
Legendre polynomial expansion used. `'fast'` should be sufficient
for most applications.
%(n_jobs)s
origin : array-like, shape (3,) | str
Origin of the sphere in the head coordinate frame and in meters.
The default is ``'auto'``, which means a head-digitization-based
origin fit.
%(verbose)s
Returns
-------
mapping : array
A n_vertices x n_sensors array that remaps the MEG or EEG data,
as `new_data = np.dot(mapping, data)`.
"""
if not all(key in surf for key in ["rr", "nn"]):
raise KeyError('surf must have both "rr" and "nn"')
if "coord_frame" not in surf:
raise KeyError(
'The surface coordinate frame must be specified in surf["coord_frame"]'
)
_check_option("mode", mode, ["accurate", "fast"])
# deal with coordinate frames here -- always go to "head" (easiest)
orig_surf = surf
surf = transform_surface_to(deepcopy(surf), "head", trans)
origin = _check_origin(origin, info)
#
# Step 1. Prepare the coil definitions
# Do the dot products, assume surf in head coords
#
_check_option("ch_type", ch_type, ["meg", "eeg"])
if ch_type == "meg":
picks = pick_types(info, meg=True, eeg=False, ref_meg=False)
logger.info("Prepare MEG mapping...")
else:
picks = pick_types(info, meg=False, eeg=True, ref_meg=False)
logger.info("Prepare EEG mapping...")
if len(picks) == 0:
raise RuntimeError("cannot map, no channels found")
# XXX this code does not do any checking for compensation channels,
# but it seems like this must be intentional from the ref_meg=False
# (presumably from the C code)
dev_head_t = info["dev_head_t"]
info = pick_info(_simplify_info(info), picks)
info["dev_head_t"] = dev_head_t
# create coil defs in head coordinates
if ch_type == "meg":
# Put them in head coordinates
coils = _create_meg_coils(info["chs"], "normal", info["dev_head_t"])
type_str = "coils"
miss = 1e-4 # Smoothing criterion for MEG
else: # EEG
coils = _create_eeg_els(info["chs"])
type_str = "electrodes"
miss = 1e-3 # Smoothing criterion for EEG
#
# Step 2. Calculate the dot products
#
int_rad, noise, lut_fun, n_fact = _setup_dots(mode, info, coils, ch_type)
logger.info("Computing dot products for %i %s..." % (len(coils), type_str))
self_dots = _do_self_dots(
int_rad, False, coils, origin, ch_type, lut_fun, n_fact, n_jobs
)
sel = np.arange(len(surf["rr"])) # eventually we should do sub-selection
logger.info("Computing dot products for %i surface locations..." % len(sel))
surface_dots = _do_surface_dots(
int_rad, False, coils, surf, sel, origin, ch_type, lut_fun, n_fact, n_jobs
)
#
# Step 4. Return the result
#
fmd = dict(
kind=ch_type,
surf=surf,
ch_names=info["ch_names"],
coils=coils,
origin=origin,
noise=noise,
self_dots=self_dots,
surface_dots=surface_dots,
int_rad=int_rad,
miss=miss,
)
logger.info("Field mapping data ready")
fmd["data"] = _compute_mapping_matrix(fmd, info)
# bring the original back, whatever coord frame it was in
fmd["surf"] = orig_surf
# Remove some unnecessary fields
del fmd["self_dots"]
del fmd["surface_dots"]
del fmd["int_rad"]
del fmd["miss"]
return fmd
@verbose
def make_field_map(
evoked,
trans="auto",
subject=None,
subjects_dir=None,
ch_type=None,
mode="fast",
meg_surf="helmet",
origin=(0.0, 0.0, 0.04),
n_jobs=None,
*,
head_source=("bem", "head"),
verbose=None,
):
"""Compute surface maps used for field display in 3D.
Parameters
----------
evoked : Evoked | Epochs | Raw
The measurement file. Need to have info attribute.
%(trans)s ``"auto"`` (default) will load trans from the FreeSurfer
directory specified by ``subject`` and ``subjects_dir`` parameters.
.. versionchanged:: 0.19
Support for ``'fsaverage'`` argument.
subject : str | None
The subject name corresponding to FreeSurfer environment
variable SUBJECT. If None, map for EEG data will not be available.
subjects_dir : path-like
The path to the freesurfer subjects reconstructions.
It corresponds to Freesurfer environment variable SUBJECTS_DIR.
ch_type : None | ``'eeg'`` | ``'meg'``
If None, a map for each available channel type will be returned.
Else only the specified type will be used.
mode : ``'accurate'`` | ``'fast'``
Either ``'accurate'`` or ``'fast'``, determines the quality of the
Legendre polynomial expansion used. ``'fast'`` should be sufficient
for most applications.
meg_surf : 'helmet' | 'head'
Should be ``'helmet'`` or ``'head'`` to specify in which surface
to compute the MEG field map. The default value is ``'helmet'``.
origin : array-like, shape (3,) | 'auto'
Origin of the sphere in the head coordinate frame and in meters.
Can be ``'auto'``, which means a head-digitization-based origin
fit. Default is ``(0., 0., 0.04)``.
.. versionadded:: 0.11
%(n_jobs)s
%(head_source)s
.. versionadded:: 1.1
%(verbose)s
Returns
-------
surf_maps : list
The surface maps to be used for field plots. The list contains
separate ones for MEG and EEG (if both MEG and EEG are present).
"""
info = evoked.info
if ch_type is None:
types = [t for t in ["eeg", "meg"] if t in evoked]
else:
_check_option("ch_type", ch_type, ["eeg", "meg"])
types = [ch_type]
if subjects_dir is not None:
subjects_dir = _check_fname(
subjects_dir,
overwrite="read",
must_exist=True,
name="subjects_dir",
need_dir=True,
)
if isinstance(trans, str) and trans == "auto":
# let's try to do this in MRI coordinates so they're easy to plot
trans = _find_trans(subject, subjects_dir)
trans, trans_type = _get_trans(trans, fro="head", to="mri")
if "eeg" in types and trans_type == "identity":
logger.info("No trans file available. EEG data ignored.")
types.remove("eeg")
if len(types) == 0:
raise RuntimeError("No data available for mapping.")
_check_option("meg_surf", meg_surf, ["helmet", "head"])
surfs = []
for this_type in types:
if this_type == "meg" and meg_surf == "helmet":
surf = get_meg_helmet_surf(info, trans)
else:
surf = get_head_surf(subject, source=head_source, subjects_dir=subjects_dir)
surfs.append(surf)
surf_maps = list()
for this_type, this_surf in zip(types, surfs):
this_map = _make_surface_mapping(
evoked.info,
this_surf,
this_type,
trans,
n_jobs=n_jobs,
origin=origin,
mode=mode,
)
surf_maps.append(this_map)
return surf_maps

610
dist/client/mne/forward/_lead_dots.py vendored Normal file
View File

@@ -0,0 +1,610 @@
# 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.
import os
import os.path as op
import numpy as np
from numpy.polynomial import legendre
from ..parallel import parallel_func
from ..utils import _get_extra_data_path, fill_doc, logger, verbose
##############################################################################
# FAST LEGENDRE (DERIVATIVE) POLYNOMIALS USING LOOKUP TABLE
def _next_legen_der(n, x, p0, p01, p0d, p0dd):
"""Compute the next Legendre polynomial and its derivatives."""
# only good for n > 1 !
old_p0 = p0
old_p0d = p0d
p0 = ((2 * n - 1) * x * old_p0 - (n - 1) * p01) / n
p0d = n * old_p0 + x * old_p0d
p0dd = (n + 1) * old_p0d + x * p0dd
return p0, p0d, p0dd
def _get_legen(x, n_coeff=100):
"""Get Legendre polynomials expanded about x."""
return legendre.legvander(x, n_coeff - 1)
def _get_legen_der(xx, n_coeff=100):
"""Get Legendre polynomial derivatives expanded about x."""
coeffs = np.empty((len(xx), n_coeff, 3))
for c, x in zip(coeffs, xx):
p0s, p0ds, p0dds = c[:, 0], c[:, 1], c[:, 2]
p0s[:2] = [1.0, x]
p0ds[:2] = [0.0, 1.0]
p0dds[:2] = [0.0, 0.0]
for n in range(2, n_coeff):
p0s[n], p0ds[n], p0dds[n] = _next_legen_der(
n, x, p0s[n - 1], p0s[n - 2], p0ds[n - 1], p0dds[n - 1]
)
return coeffs
@verbose
def _get_legen_table(
ch_type,
volume_integral=False,
n_coeff=100,
n_interp=20000,
force_calc=False,
verbose=None,
):
"""Return a (generated) LUT of Legendre (derivative) polynomial coeffs."""
if n_interp % 2 != 0:
raise RuntimeError("n_interp must be even")
fname = op.join(_get_extra_data_path(), "tables")
if not op.isdir(fname):
# Updated due to API change (GH 1167)
os.makedirs(fname)
if ch_type == "meg":
fname = op.join(fname, f"legder_{n_coeff}_{n_interp}.bin")
leg_fun = _get_legen_der
extra_str = " derivative"
lut_shape = (n_interp + 1, n_coeff, 3)
else: # 'eeg'
fname = op.join(fname, f"legval_{n_coeff}_{n_interp}.bin")
leg_fun = _get_legen
extra_str = ""
lut_shape = (n_interp + 1, n_coeff)
if not op.isfile(fname) or force_calc:
logger.info(f"Generating Legendre{extra_str} table...")
x_interp = np.linspace(-1, 1, n_interp + 1)
lut = leg_fun(x_interp, n_coeff).astype(np.float32)
if not force_calc:
with open(fname, "wb") as fid:
fid.write(lut.tobytes())
else:
logger.info(f"Reading Legendre{extra_str} table...")
with open(fname, "rb", buffering=0) as fid:
lut = np.fromfile(fid, np.float32)
lut.shape = lut_shape
# we need this for the integration step
n_fact = np.arange(1, n_coeff, dtype=float)
if ch_type == "meg":
n_facts = list() # multn, then mult, then multn * (n + 1)
if volume_integral:
n_facts.append(n_fact / ((2.0 * n_fact + 1.0) * (2.0 * n_fact + 3.0)))
else:
n_facts.append(n_fact / (2.0 * n_fact + 1.0))
n_facts.append(n_facts[0] / (n_fact + 1.0))
n_facts.append(n_facts[0] * (n_fact + 1.0))
# skip the first set of coefficients because they are not used
lut = lut[:, 1:, [0, 1, 1, 2]] # for multiplicative convenience later
# reshape this for convenience, too
n_facts = np.array(n_facts)[[2, 0, 1, 1], :].T
n_facts = np.ascontiguousarray(n_facts)
n_fact = n_facts
else: # 'eeg'
n_fact = (2.0 * n_fact + 1.0) * (2.0 * n_fact + 1.0) / n_fact
# skip the first set of coefficients because they are not used
lut = lut[:, 1:].copy()
return lut, n_fact
def _comp_sum_eeg(beta, ctheta, lut_fun, n_fact):
"""Lead field dot products using Legendre polynomial (P_n) series."""
# Compute the sum occurring in the evaluation.
# The result is
# sums[:] (2n+1)^2/n beta^n P_n
n_chunk = 50000000 // (8 * max(n_fact.shape) * 2)
lims = np.concatenate([np.arange(0, beta.size, n_chunk), [beta.size]])
s0 = np.empty(beta.shape)
for start, stop in zip(lims[:-1], lims[1:]):
coeffs = lut_fun(ctheta[start:stop])
betans = np.tile(beta[start:stop][:, np.newaxis], (1, n_fact.shape[0]))
np.cumprod(betans, axis=1, out=betans) # run inplace
coeffs *= betans
s0[start:stop] = np.dot(coeffs, n_fact) # == weighted sum across cols
return s0
def _comp_sums_meg(beta, ctheta, lut_fun, n_fact, volume_integral):
"""Lead field dot products using Legendre polynomial (P_n) series.
Parameters
----------
beta : array, shape (n_points * n_points, 1)
Coefficients of the integration.
ctheta : array, shape (n_points * n_points, 1)
Cosine of the angle between the sensor integration points.
lut_fun : callable
Look-up table for evaluating Legendre polynomials.
n_fact : array
Coefficients in the integration sum.
volume_integral : bool
If True, compute volume integral.
Returns
-------
sums : array, shape (4, n_points * n_points)
The results.
"""
# Compute the sums occurring in the evaluation.
# Two point magnetometers on the xz plane are assumed.
# The four sums are:
# * sums[:, 0] n(n+1)/(2n+1) beta^(n+1) P_n
# * sums[:, 1] n/(2n+1) beta^(n+1) P_n'
# * sums[:, 2] n/((2n+1)(n+1)) beta^(n+1) P_n'
# * sums[:, 3] n/((2n+1)(n+1)) beta^(n+1) P_n''
# This is equivalent, but slower:
# sums = np.sum(bbeta[:, :, np.newaxis].T * n_fact * coeffs, axis=1)
# sums = np.rollaxis(sums, 2)
# or
# sums = np.einsum('ji,jk,ijk->ki', bbeta, n_fact, lut_fun(ctheta)))
sums = np.empty((n_fact.shape[1], len(beta)))
# beta can be e.g. 3 million elements, which ends up using lots of memory
# so we split up the computations into ~50 MB blocks
n_chunk = 50000000 // (8 * max(n_fact.shape) * 2)
lims = np.concatenate([np.arange(0, beta.size, n_chunk), [beta.size]])
for start, stop in zip(lims[:-1], lims[1:]):
bbeta = np.tile(beta[start:stop][np.newaxis], (n_fact.shape[0], 1))
bbeta[0] *= beta[start:stop]
np.cumprod(bbeta, axis=0, out=bbeta) # run inplace
np.einsum(
"ji,jk,ijk->ki",
bbeta,
n_fact,
lut_fun(ctheta[start:stop]),
out=sums[:, start:stop],
)
return sums
###############################################################################
# SPHERE DOTS
_meg_const = 4e-14 * np.pi # This is \mu_0^2/4\pi
_eeg_const = 1.0 / (4.0 * np.pi)
def _fast_sphere_dot_r0(
r,
rr1_orig,
rr2s,
lr1,
lr2s,
cosmags1,
cosmags2s,
w1,
w2s,
volume_integral,
lut,
n_fact,
ch_type,
):
"""Lead field dot product computation for M/EEG in the sphere model.
Parameters
----------
r : float
The integration radius. It is used to calculate beta as:
beta = (r * r) / (lr1 * lr2).
rr1 : array, shape (n_points x 3)
Normalized position vectors of integrations points in first sensor.
rr2s : list
Normalized position vector of integration points in second sensor.
lr1 : array, shape (n_points x 1)
Magnitude of position vector of integration points in first sensor.
lr2s : list
Magnitude of position vector of integration points in second sensor.
cosmags1 : array, shape (n_points x 1)
Direction of integration points in first sensor.
cosmags2s : list
Direction of integration points in second sensor.
w1 : array, shape (n_points x 1) | None
Weights of integration points in the first sensor.
w2s : list
Weights of integration points in the second sensor.
volume_integral : bool
If True, compute volume integral.
lut : callable
Look-up table for evaluating Legendre polynomials.
n_fact : array
Coefficients in the integration sum.
ch_type : str
The channel type. It can be 'meg' or 'eeg'.
Returns
-------
result : float
The integration sum.
"""
if w1 is None: # operating on surface, treat independently
out_shape = (len(rr2s), len(rr1_orig))
sum_axis = 1 # operate along second axis only at the end
else:
out_shape = (len(rr2s),)
sum_axis = None # operate on flattened array at the end
out = np.empty(out_shape)
rr2 = np.concatenate(rr2s)
lr2 = np.concatenate(lr2s)
cosmags2 = np.concatenate(cosmags2s)
# outer product, sum over coords
ct = np.einsum("ik,jk->ij", rr1_orig, rr2)
np.clip(ct, -1, 1, ct)
# expand axes
rr1 = rr1_orig[:, np.newaxis, :] # (n_rr1, n_rr2, n_coord) e.g. 4x4x3
rr2 = rr2[np.newaxis, :, :]
lr1lr2 = lr1[:, np.newaxis] * lr2[np.newaxis, :]
beta = (r * r) / lr1lr2
if ch_type == "meg":
sums = _comp_sums_meg(
beta.flatten(), ct.flatten(), lut, n_fact, volume_integral
)
sums.shape = (4,) + beta.shape
# Accumulate the result, a little bit streamlined version
# cosmags1 = cosmags1[:, np.newaxis, :]
# cosmags2 = cosmags2[np.newaxis, :, :]
# n1c1 = np.sum(cosmags1 * rr1, axis=2)
# n1c2 = np.sum(cosmags1 * rr2, axis=2)
# n2c1 = np.sum(cosmags2 * rr1, axis=2)
# n2c2 = np.sum(cosmags2 * rr2, axis=2)
# n1n2 = np.sum(cosmags1 * cosmags2, axis=2)
n1c1 = np.einsum("ik,ijk->ij", cosmags1, rr1)
n1c2 = np.einsum("ik,ijk->ij", cosmags1, rr2)
n2c1 = np.einsum("jk,ijk->ij", cosmags2, rr1)
n2c2 = np.einsum("jk,ijk->ij", cosmags2, rr2)
n1n2 = np.einsum("ik,jk->ij", cosmags1, cosmags2)
part1 = ct * n1c1 * n2c2
part2 = n1c1 * n2c1 + n1c2 * n2c2
result = (
n1c1 * n2c2 * sums[0]
+ (2.0 * part1 - part2) * sums[1]
+ (n1n2 + part1 - part2) * sums[2]
+ (n1c2 - ct * n1c1) * (n2c1 - ct * n2c2) * sums[3]
)
# Give it a finishing touch!
result *= _meg_const / lr1lr2
if volume_integral:
result *= r
else: # 'eeg'
result = _comp_sum_eeg(beta.flatten(), ct.flatten(), lut, n_fact)
result.shape = beta.shape
# Give it a finishing touch!
result *= _eeg_const
result /= lr1lr2
# now we add them all up with weights
offset = 0
result *= np.concatenate(w2s)
if w1 is not None:
result *= w1[:, np.newaxis]
for ii, w2 in enumerate(w2s):
out[ii] = np.sum(result[:, offset : offset + len(w2)], axis=sum_axis)
offset += len(w2)
return out
@fill_doc
def _do_self_dots(intrad, volume, coils, r0, ch_type, lut, n_fact, n_jobs):
"""Perform the lead field dot product integrations.
Parameters
----------
intrad : float
The integration radius. It is used to calculate beta as:
beta = (intrad * intrad) / (r1 * r2).
volume : bool
If True, perform volume integral.
coils : list of dict
The coils.
r0 : array, shape (3 x 1)
The origin of the sphere.
ch_type : str
The channel type. It can be 'meg' or 'eeg'.
lut : callable
Look-up table for evaluating Legendre polynomials.
n_fact : array
Coefficients in the integration sum.
%(n_jobs)s
Returns
-------
products : array, shape (n_coils, n_coils)
The integration products.
"""
if ch_type == "eeg":
intrad = intrad * 0.7
# convert to normalized distances from expansion center
rmags = [coil["rmag"] - r0[np.newaxis, :] for coil in coils]
rlens = [np.sqrt(np.sum(r * r, axis=1)) for r in rmags]
rmags = [r / rl[:, np.newaxis] for r, rl in zip(rmags, rlens)]
cosmags = [coil["cosmag"] for coil in coils]
ws = [coil["w"] for coil in coils]
parallel, p_fun, n_jobs = parallel_func(_do_self_dots_subset, n_jobs)
prods = parallel(
p_fun(intrad, rmags, rlens, cosmags, ws, volume, lut, n_fact, ch_type, idx)
for idx in np.array_split(np.arange(len(rmags)), n_jobs)
)
products = np.sum(prods, axis=0)
return products
def _do_self_dots_subset(
intrad, rmags, rlens, cosmags, ws, volume, lut, n_fact, ch_type, idx
):
"""Parallelize."""
# all possible combinations of two magnetometers
products = np.zeros((len(rmags), len(rmags)))
for ci1 in idx:
ci2 = ci1 + 1
res = _fast_sphere_dot_r0(
intrad,
rmags[ci1],
rmags[:ci2],
rlens[ci1],
rlens[:ci2],
cosmags[ci1],
cosmags[:ci2],
ws[ci1],
ws[:ci2],
volume,
lut,
n_fact,
ch_type,
)
products[ci1, :ci2] = res
products[:ci2, ci1] = res
return products
def _do_cross_dots(intrad, volume, coils1, coils2, r0, ch_type, lut, n_fact):
"""Compute lead field dot product integrations between two coil sets.
The code is a direct translation of MNE-C code found in
`mne_map_data/lead_dots.c`.
Parameters
----------
intrad : float
The integration radius. It is used to calculate beta as:
beta = (intrad * intrad) / (r1 * r2).
volume : bool
If True, compute volume integral.
coils1 : list of dict
The original coils.
coils2 : list of dict
The coils to which data is being mapped.
r0 : array, shape (3 x 1).
The origin of the sphere.
ch_type : str
The channel type. It can be 'meg' or 'eeg'
lut : callable
Look-up table for evaluating Legendre polynomials.
n_fact : array
Coefficients in the integration sum.
Returns
-------
products : array, shape (n_coils, n_coils)
The integration products.
"""
if ch_type == "eeg":
intrad = intrad * 0.7
rmags1 = [coil["rmag"] - r0[np.newaxis, :] for coil in coils1]
rmags2 = [coil["rmag"] - r0[np.newaxis, :] for coil in coils2]
rlens1 = [np.sqrt(np.sum(r * r, axis=1)) for r in rmags1]
rlens2 = [np.sqrt(np.sum(r * r, axis=1)) for r in rmags2]
rmags1 = [r / rl[:, np.newaxis] for r, rl in zip(rmags1, rlens1)]
rmags2 = [r / rl[:, np.newaxis] for r, rl in zip(rmags2, rlens2)]
ws1 = [coil["w"] for coil in coils1]
ws2 = [coil["w"] for coil in coils2]
cosmags1 = [coil["cosmag"] for coil in coils1]
cosmags2 = [coil["cosmag"] for coil in coils2]
products = np.zeros((len(rmags1), len(rmags2)))
for ci1 in range(len(coils1)):
res = _fast_sphere_dot_r0(
intrad,
rmags1[ci1],
rmags2,
rlens1[ci1],
rlens2,
cosmags1[ci1],
cosmags2,
ws1[ci1],
ws2,
volume,
lut,
n_fact,
ch_type,
)
products[ci1, :] = res
return products
@fill_doc
def _do_surface_dots(
intrad, volume, coils, surf, sel, r0, ch_type, lut, n_fact, n_jobs
):
"""Compute the map construction products.
Parameters
----------
intrad : float
The integration radius. It is used to calculate beta as:
beta = (intrad * intrad) / (r1 * r2)
volume : bool
If True, compute a volume integral.
coils : list of dict
The coils.
surf : dict
The surface on which the field is interpolated.
sel : array
Indices of the surface vertices to select.
r0 : array, shape (3 x 1)
The origin of the sphere.
ch_type : str
The channel type. It can be 'meg' or 'eeg'.
lut : callable
Look-up table for Legendre polynomials.
n_fact : array
Coefficients in the integration sum.
%(n_jobs)s
Returns
-------
products : array, shape (n_coils, n_coils)
The integration products.
"""
# convert to normalized distances from expansion center
rmags = [coil["rmag"] - r0[np.newaxis, :] for coil in coils]
rlens = [np.sqrt(np.sum(r * r, axis=1)) for r in rmags]
rmags = [r / rl[:, np.newaxis] for r, rl in zip(rmags, rlens)]
cosmags = [coil["cosmag"] for coil in coils]
ws = [coil["w"] for coil in coils]
rref = None
refl = None
# virt_ref = False
if ch_type == "eeg":
intrad = intrad * 0.7
# The virtual ref code is untested and unused, so it is
# commented out for now
# if virt_ref:
# rref = virt_ref[np.newaxis, :] - r0[np.newaxis, :]
# refl = np.sqrt(np.sum(rref * rref, axis=1))
# rref /= refl[:, np.newaxis]
rsurf = surf["rr"][sel] - r0[np.newaxis, :]
lsurf = np.sqrt(np.sum(rsurf * rsurf, axis=1))
rsurf /= lsurf[:, np.newaxis]
this_nn = surf["nn"][sel]
# loop over the coils
parallel, p_fun, n_jobs = parallel_func(_do_surface_dots_subset, n_jobs)
prods = parallel(
p_fun(
intrad,
rsurf,
rmags,
rref,
refl,
lsurf,
rlens,
this_nn,
cosmags,
ws,
volume,
lut,
n_fact,
ch_type,
idx,
)
for idx in np.array_split(np.arange(len(rmags)), n_jobs)
)
products = np.sum(prods, axis=0)
return products
def _do_surface_dots_subset(
intrad,
rsurf,
rmags,
rref,
refl,
lsurf,
rlens,
this_nn,
cosmags,
ws,
volume,
lut,
n_fact,
ch_type,
idx,
):
"""Parallelize.
Parameters
----------
refl : array | None
If ch_type is 'eeg', the magnitude of position vector of the
virtual reference (never used).
lsurf : array
Magnitude of position vector of the surface points.
rlens : list of arrays of length n_coils
Magnitude of position vector.
this_nn : array, shape (n_vertices, 3)
Surface normals.
cosmags : list of array.
Direction of the integration points in the coils.
ws : list of array
Integration weights of the coils.
volume : bool
If True, compute volume integral.
lut : callable
Look-up table for evaluating Legendre polynomials.
n_fact : array
Coefficients in the integration sum.
ch_type : str
'meg' or 'eeg'
idx : array, shape (n_coils x 1)
Index of coil.
Returns
-------
products : array, shape (n_coils, n_coils)
The integration products.
"""
products = _fast_sphere_dot_r0(
intrad,
rsurf,
rmags,
lsurf,
rlens,
this_nn,
cosmags,
None,
ws,
volume,
lut,
n_fact,
ch_type,
).T
if rref is not None:
raise NotImplementedError # we don't ever use this, isn't tested
# vres = _fast_sphere_dot_r0(
# intrad, rref, rmags, refl, rlens, this_nn, cosmags, None, ws,
# volume, lut, n_fact, ch_type)
# products -= vres
return products

938
dist/client/mne/forward/_make_forward.py vendored Normal file
View File

@@ -0,0 +1,938 @@
# 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.
import os
import os.path as op
from contextlib import contextmanager
from copy import deepcopy
from pathlib import Path
import numpy as np
from .._fiff.compensator import get_current_comp, make_compensator
from .._fiff.constants import FIFF, FWD
from .._fiff.meas_info import Info, read_info
from .._fiff.pick import _has_kit_refs, pick_info, pick_types
from .._fiff.tag import _loc_to_coil_trans, _loc_to_eeg_loc
from ..bem import ConductorModel, _bem_find_surface, read_bem_solution
from ..source_estimate import VolSourceEstimate
from ..source_space._source_space import (
_complete_vol_src,
_ensure_src,
_filter_source_spaces,
_make_discrete_source_space,
)
from ..surface import _CheckInside, _normalize_vectors
from ..transforms import (
Transform,
_coord_frame_name,
_ensure_trans,
_get_trans,
_print_coord_trans,
apply_trans,
invert_transform,
transform_surface_to,
)
from ..utils import _check_fname, _pl, _validate_type, logger, verbose, warn
from ._compute_forward import _compute_forwards
from .forward import _FWD_ORDER, Forward, _merge_fwds, convert_forward_solution
_accuracy_dict = dict(
point=FWD.COIL_ACCURACY_POINT,
normal=FWD.COIL_ACCURACY_NORMAL,
accurate=FWD.COIL_ACCURACY_ACCURATE,
)
_extra_coil_def_fname = None
@verbose
def _read_coil_defs(verbose=None):
"""Read a coil definition file.
Parameters
----------
%(verbose)s
Returns
-------
res : list of dict
The coils. It is a dictionary with valid keys:
'cosmag' | 'coil_class' | 'coord_frame' | 'rmag' | 'type' |
'chname' | 'accuracy'.
cosmag contains the direction of the coils and rmag contains the
position vector.
Notes
-----
The global variable "_extra_coil_def_fname" can be used to prepend
additional definitions. These are never added to the registry.
"""
coil_dir = op.join(op.split(__file__)[0], "..", "data")
coils = list()
if _extra_coil_def_fname is not None:
coils += _read_coil_def_file(_extra_coil_def_fname, use_registry=False)
coils += _read_coil_def_file(op.join(coil_dir, "coil_def.dat"))
return coils
# Typically we only have 1 or 2 coil def files, but they can end up being
# read a lot. Let's keep a list of them and just reuse them:
_coil_registry = {}
def _read_coil_def_file(fname, use_registry=True):
"""Read a coil def file."""
if not use_registry or fname not in _coil_registry:
big_val = 0.5
coils = list()
with open(fname) as fid:
lines = fid.readlines()
lines = lines[::-1]
while len(lines) > 0:
line = lines.pop().strip()
if line[0] == "#" and len(line) > 0:
continue
desc_start = line.find('"')
desc_end = len(line) - 1
assert line.strip()[desc_end] == '"'
desc = line[desc_start:desc_end]
vals = np.fromstring(line[:desc_start].strip(), dtype=float, sep=" ")
assert len(vals) == 6
npts = int(vals[3])
coil = dict(
coil_type=vals[1],
coil_class=vals[0],
desc=desc,
accuracy=vals[2],
size=vals[4],
base=vals[5],
)
# get parameters of each component
rmag = list()
cosmag = list()
w = list()
for p in range(npts):
# get next non-comment line
line = lines.pop()
while line[0] == "#":
line = lines.pop()
vals = np.fromstring(line, sep=" ")
if len(vals) != 7:
raise RuntimeError(
f"Could not interpret line {p + 1} as 7 points:\n{line}"
)
# Read and verify data for each integration point
w.append(vals[0])
rmag.append(vals[[1, 2, 3]])
cosmag.append(vals[[4, 5, 6]])
w = np.array(w)
rmag = np.array(rmag)
cosmag = np.array(cosmag)
size = np.sqrt(np.sum(cosmag**2, axis=1))
if np.any(np.sqrt(np.sum(rmag**2, axis=1)) > big_val):
raise RuntimeError("Unreasonable integration point")
if np.any(size <= 0):
raise RuntimeError("Unreasonable normal")
cosmag /= size[:, np.newaxis]
coil.update(dict(w=w, cosmag=cosmag, rmag=rmag))
coils.append(coil)
if use_registry:
_coil_registry[fname] = coils
if use_registry:
coils = deepcopy(_coil_registry[fname])
logger.info("%d coil definition%s read", len(coils), _pl(coils))
return coils
def _create_meg_coil(coilset, ch, acc, do_es):
"""Create a coil definition using templates, transform if necessary."""
# Also change the coordinate frame if so desired
if ch["kind"] not in [FIFF.FIFFV_MEG_CH, FIFF.FIFFV_REF_MEG_CH]:
raise RuntimeError(f"{ch['ch_name']} is not a MEG channel")
# Simple linear search from the coil definitions
for coil in coilset:
if coil["coil_type"] == (ch["coil_type"] & 0xFFFF) and coil["accuracy"] == acc:
break
else:
raise RuntimeError(
"Desired coil definition not found "
f"(type = {ch['coil_type']} acc = {acc})"
)
# Apply a coordinate transformation if so desired
coil_trans = _loc_to_coil_trans(ch["loc"])
# Create the result
res = dict(
chname=ch["ch_name"],
coil_class=coil["coil_class"],
accuracy=coil["accuracy"],
base=coil["base"],
size=coil["size"],
type=ch["coil_type"],
w=coil["w"],
desc=coil["desc"],
coord_frame=FIFF.FIFFV_COORD_DEVICE,
rmag_orig=coil["rmag"],
cosmag_orig=coil["cosmag"],
coil_trans_orig=coil_trans,
r0=coil_trans[:3, 3],
rmag=apply_trans(coil_trans, coil["rmag"]),
cosmag=apply_trans(coil_trans, coil["cosmag"], False),
)
if do_es:
r0_exey = np.dot(coil["rmag"][:, :2], coil_trans[:3, :2].T) + coil_trans[:3, 3]
res.update(
ex=coil_trans[:3, 0],
ey=coil_trans[:3, 1],
ez=coil_trans[:3, 2],
r0_exey=r0_exey,
)
return res
def _create_eeg_el(ch, t=None):
"""Create an electrode definition, transform coords if necessary."""
if ch["kind"] != FIFF.FIFFV_EEG_CH:
raise RuntimeError(
f"{ch['ch_name']} is not an EEG channel. Cannot create an electrode "
"definition."
)
if t is None:
t = Transform("head", "head") # identity, no change
if t.from_str != "head":
raise RuntimeError("Inappropriate coordinate transformation")
r0ex = _loc_to_eeg_loc(ch["loc"])
if r0ex.shape[1] == 1: # no reference
w = np.array([1.0])
else: # has reference
w = np.array([1.0, -1.0])
# Optional coordinate transformation
r0ex = apply_trans(t["trans"], r0ex.T)
# The electrode location
cosmag = r0ex.copy()
_normalize_vectors(cosmag)
res = dict(
chname=ch["ch_name"],
coil_class=FWD.COILC_EEG,
w=w,
accuracy=_accuracy_dict["normal"],
type=ch["coil_type"],
coord_frame=t["to"],
rmag=r0ex,
cosmag=cosmag,
)
return res
def _create_meg_coils(chs, acc, t=None, coilset=None, do_es=False):
"""Create a set of MEG coils in the head coordinate frame."""
acc = _accuracy_dict[acc] if isinstance(acc, str) else acc
coilset = _read_coil_defs(verbose=False) if coilset is None else coilset
coils = [_create_meg_coil(coilset, ch, acc, do_es) for ch in chs]
_transform_orig_meg_coils(coils, t, do_es=do_es)
return coils
def _transform_orig_meg_coils(coils, t, do_es=True):
"""Transform original (device) MEG coil positions."""
if t is None:
return
for coil in coils:
coil_trans = np.dot(t["trans"], coil["coil_trans_orig"])
coil.update(
coord_frame=t["to"],
r0=coil_trans[:3, 3],
rmag=apply_trans(coil_trans, coil["rmag_orig"]),
cosmag=apply_trans(coil_trans, coil["cosmag_orig"], False),
)
if do_es:
r0_exey = (
np.dot(coil["rmag_orig"][:, :2], coil_trans[:3, :2].T)
+ coil_trans[:3, 3]
)
coil.update(
ex=coil_trans[:3, 0],
ey=coil_trans[:3, 1],
ez=coil_trans[:3, 2],
r0_exey=r0_exey,
)
def _create_eeg_els(chs):
"""Create a set of EEG electrodes in the head coordinate frame."""
return [_create_eeg_el(ch) for ch in chs]
@verbose
def _setup_bem(bem, bem_extra, neeg, mri_head_t, allow_none=False, verbose=None):
"""Set up a BEM for forward computation, making a copy and modifying."""
if allow_none and bem is None:
return None
logger.info("")
_validate_type(bem, ("path-like", ConductorModel), bem)
if not isinstance(bem, ConductorModel):
logger.info(f"Setting up the BEM model using {bem_extra}...\n")
bem = read_bem_solution(bem)
else:
bem = bem.copy()
if bem["is_sphere"]:
logger.info("Using the sphere model.\n")
if len(bem["layers"]) == 0 and neeg > 0:
raise RuntimeError(
"Spherical model has zero shells, cannot use with EEG data"
)
if bem["coord_frame"] != FIFF.FIFFV_COORD_HEAD:
raise RuntimeError("Spherical model is not in head coordinates")
else:
if bem["surfs"][0]["coord_frame"] != FIFF.FIFFV_COORD_MRI:
raise RuntimeError(
f'BEM is in {_coord_frame_name(bem["surfs"][0]["coord_frame"])} '
'coordinates, should be in MRI'
)
if neeg > 0 and len(bem["surfs"]) == 1:
raise RuntimeError(
"Cannot use a homogeneous (1-layer BEM) model "
"for EEG forward calculations, consider "
"using a 3-layer BEM instead"
)
logger.info("Employing the head->MRI coordinate transform with the BEM model.")
# fwd_bem_set_head_mri_t: Set the coordinate transformation
bem["head_mri_t"] = _ensure_trans(mri_head_t, "head", "mri")
logger.info(f"BEM model {op.split(bem_extra)[1]} is now set up")
logger.info("")
return bem
@verbose
def _prep_meg_channels(
info,
accuracy="accurate",
exclude=(),
*,
ignore_ref=False,
head_frame=True,
do_es=False,
verbose=None,
):
"""Prepare MEG coil definitions for forward calculation."""
# Find MEG channels
ref_meg = True if not ignore_ref else False
picks = pick_types(info, meg=True, ref_meg=ref_meg, exclude=exclude)
# Make sure MEG coils exist
if len(picks) <= 0:
raise RuntimeError("Could not find any MEG channels")
info_meg = pick_info(info, picks)
del picks
# Get channel info and names for MEG channels
logger.info(f'Read {len(info_meg["chs"])} MEG channels from info')
# Get MEG compensation channels
compensator = post_picks = None
ch_names = info_meg["ch_names"]
if not ignore_ref:
ref_picks = pick_types(info, meg=False, ref_meg=True, exclude=exclude)
ncomp = len(ref_picks)
if ncomp > 0:
logger.info(f"Read {ncomp} MEG compensation channels from info")
# We need to check to make sure these are NOT KIT refs
if _has_kit_refs(info, ref_picks):
raise NotImplementedError(
"Cannot create forward solution with KIT reference "
'channels. Consider using "ignore_ref=True" in '
"calculation"
)
logger.info(f'{len(info["comps"])} compensation data sets in info')
# Compose a compensation data set if necessary
# adapted from mne_make_ctf_comp() from mne_ctf_comp.c
logger.info("Setting up compensation data...")
comp_num = get_current_comp(info)
if comp_num is None or comp_num == 0:
logger.info(" No compensation set. Nothing more to do.")
else:
compensator = make_compensator(
info_meg, 0, comp_num, exclude_comp_chs=False
)
logger.info(f" Desired compensation data ({comp_num}) found.")
logger.info(" All compensation channels found.")
logger.info(" Preselector created.")
logger.info(" Compensation data matrix created.")
logger.info(" Postselector created.")
post_picks = pick_types(info_meg, meg=True, ref_meg=False, exclude=exclude)
ch_names = [ch_names[pick] for pick in post_picks]
# Create coil descriptions with transformation to head or device frame
templates = _read_coil_defs()
if head_frame:
_print_coord_trans(info["dev_head_t"])
transform = info["dev_head_t"]
else:
transform = None
megcoils = _create_meg_coils(
info_meg["chs"], accuracy, transform, templates, do_es=do_es
)
# Check that coordinate frame is correct and log it
if head_frame:
assert megcoils[0]["coord_frame"] == FIFF.FIFFV_COORD_HEAD
logger.info("MEG coil definitions created in head coordinates.")
else:
assert megcoils[0]["coord_frame"] == FIFF.FIFFV_COORD_DEVICE
logger.info("MEG coil definitions created in device coordinate.")
return dict(
defs=megcoils,
ch_names=ch_names,
compensator=compensator,
info=info_meg,
post_picks=post_picks,
)
@verbose
def _prep_eeg_channels(info, exclude=(), verbose=None):
"""Prepare EEG electrode definitions for forward calculation."""
info_extra = "info"
# Find EEG electrodes
picks = pick_types(info, meg=False, eeg=True, ref_meg=False, exclude=exclude)
# Make sure EEG electrodes exist
neeg = len(picks)
if neeg <= 0:
raise RuntimeError("Could not find any EEG channels")
# Get channel info and names for EEG channels
eegchs = pick_info(info, picks)["chs"]
eegnames = [info["ch_names"][p] for p in picks]
logger.info(f"Read {len(picks):3} EEG channels from {info_extra}")
# Create EEG electrode descriptions
eegels = _create_eeg_els(eegchs)
logger.info("Head coordinate coil definitions created.")
return dict(defs=eegels, ch_names=eegnames)
@verbose
def _prepare_for_forward(
src,
mri_head_t,
info,
bem,
mindist,
n_jobs,
bem_extra="",
trans="",
info_extra="",
meg=True,
eeg=True,
ignore_ref=False,
allow_bem_none=False,
verbose=None,
):
"""Prepare for forward computation.
The sensors dict contains keys for each sensor type, e.g. 'meg', 'eeg'.
The vale for each of these is a dict that comes from _prep_meg_channels or
_prep_eeg_channels. Each dict contains:
- defs : a list of dicts (one per channel) with 'rmag', 'cosmag', etc.
- ch_names: a list of str channel names corresponding to the defs
- compensator (optional): the ndarray compensation matrix to apply
- post_picks (optional): the ndarray of indices to pick after applying the
compensator
"""
# Read the source locations
logger.info("")
# let's make a copy in case we modify something
src = _ensure_src(src).copy()
nsource = sum(s["nuse"] for s in src)
if nsource == 0:
raise RuntimeError(
"No sources are active in these source spaces. "
'"do_all" option should be used.'
)
logger.info(
"Read %d source spaces a total of %d active source locations"
% (len(src), nsource)
)
# Delete some keys to clean up the source space:
for key in ["working_dir", "command_line"]:
if key in src.info:
del src.info[key]
# Read the MRI -> head coordinate transformation
logger.info("")
_print_coord_trans(mri_head_t)
# make a new dict with the relevant information
arg_list = [info_extra, trans, src, bem_extra, meg, eeg, mindist, n_jobs, verbose]
cmd = f"make_forward_solution({', '.join(str(a) for a in arg_list)})"
mri_id = dict(machid=np.zeros(2, np.int32), version=0, secs=0, usecs=0)
info_trans = str(trans) if isinstance(trans, Path) else trans
info = Info(
chs=info["chs"],
comps=info["comps"],
dev_head_t=info["dev_head_t"],
mri_file=info_trans,
mri_id=mri_id,
meas_file=info_extra,
meas_id=None,
working_dir=os.getcwd(),
command_line=cmd,
bads=info["bads"],
mri_head_t=mri_head_t,
)
info._update_redundant()
info._check_consistency()
logger.info("")
sensors = dict()
if meg and len(pick_types(info, meg=True, ref_meg=False, exclude=[])) > 0:
sensors["meg"] = _prep_meg_channels(info, ignore_ref=ignore_ref)
if eeg and len(pick_types(info, eeg=True, exclude=[])) > 0:
sensors["eeg"] = _prep_eeg_channels(info)
# Check that some channels were found
if len(sensors) == 0:
raise RuntimeError("No MEG or EEG channels found.")
# pick out final info
info = pick_info(
info, pick_types(info, meg=meg, eeg=eeg, ref_meg=False, exclude=[])
)
# Transform the source spaces into the appropriate coordinates
# (will either be HEAD or MRI)
for s in src:
transform_surface_to(s, "head", mri_head_t)
logger.info(
f"Source spaces are now in {_coord_frame_name(s['coord_frame'])} coordinates."
)
# Prepare the BEM model
eegnames = sensors.get("eeg", dict()).get("ch_names", [])
bem = _setup_bem(
bem, bem_extra, len(eegnames), mri_head_t, allow_none=allow_bem_none
)
del eegnames
# Circumvent numerical problems by excluding points too close to the skull,
# and check that sensors are not inside any BEM surface
if bem is not None:
if not bem["is_sphere"]:
check_surface = "inner skull surface"
inner_skull = _bem_find_surface(bem, "inner_skull")
check_inside = _filter_source_spaces(
inner_skull, mindist, mri_head_t, src, n_jobs
)
logger.info("")
if len(bem["surfs"]) == 3:
check_surface = "scalp surface"
check_inside = _CheckInside(_bem_find_surface(bem, "head"))
else:
check_surface = "outermost sphere shell"
if len(bem["layers"]) == 0:
def check_inside(x):
return np.zeros(len(x), bool)
else:
def check_inside(x):
return (
np.linalg.norm(x - bem["r0"], axis=1) < bem["layers"][-1]["rad"]
)
if "meg" in sensors:
meg_loc = apply_trans(
invert_transform(mri_head_t),
np.array([coil["r0"] for coil in sensors["meg"]["defs"]]),
)
n_inside = check_inside(meg_loc).sum()
if n_inside:
raise RuntimeError(
f"Found {n_inside} MEG sensor{_pl(n_inside)} inside the "
f"{check_surface}, perhaps coordinate frames and/or "
"coregistration must be incorrect"
)
rr = np.concatenate([s["rr"][s["vertno"]] for s in src])
if len(rr) < 1:
raise RuntimeError(
"No points left in source space after excluding "
"points close to inner skull."
)
# deal with free orientations:
source_nn = np.tile(np.eye(3), (len(rr), 1))
update_kwargs = dict(
nchan=len(info["ch_names"]),
nsource=len(rr),
info=info,
src=src,
source_nn=source_nn,
source_rr=rr,
surf_ori=False,
mri_head_t=mri_head_t,
)
return sensors, rr, info, update_kwargs, bem
@verbose
def make_forward_solution(
info,
trans,
src,
bem,
meg=True,
eeg=True,
*,
mindist=0.0,
ignore_ref=False,
n_jobs=None,
verbose=None,
):
"""Calculate a forward solution for a subject.
Parameters
----------
%(info_str)s
%(trans)s
.. versionchanged:: 0.19
Support for ``'fsaverage'`` argument.
src : path-like | instance of SourceSpaces
Either a path to a source space file or a loaded or generated
:class:`~mne.SourceSpaces`.
bem : path-like | ConductorModel
Filename of the BEM (e.g., ``"sample-5120-5120-5120-bem-sol.fif"``) to
use, or a loaded :class:`~mne.bem.ConductorModel`. See
:func:`~mne.make_bem_model` and :func:`~mne.make_bem_solution` to create a
:class:`mne.bem.ConductorModel`.
meg : bool
If True (default), include MEG computations.
eeg : bool
If True (default), include EEG computations.
mindist : float
Minimum distance of sources from inner skull surface (in mm).
ignore_ref : bool
If True, do not include reference channels in compensation. This
option should be True for KIT files, since forward computation
with reference channels is not currently supported.
%(n_jobs)s
%(verbose)s
Returns
-------
fwd : instance of Forward
The forward solution.
See Also
--------
convert_forward_solution
Notes
-----
The ``--grad`` option from MNE-C (to compute gradients) is not implemented
here.
To create a fixed-orientation forward solution, use this function
followed by :func:`mne.convert_forward_solution`.
.. note::
If the BEM solution was computed with `OpenMEEG <https://openmeeg.github.io>`__
in :func:`mne.make_bem_solution`, then OpenMEEG will automatically
be used to compute the forward solution.
.. versionchanged:: 1.2
Added support for OpenMEEG-based forward solution calculations.
"""
# Currently not (sup)ported:
# 1. --grad option (gradients of the field, not used much)
# 2. --fixed option (can be computed post-hoc)
# 3. --mricoord option (probably not necessary)
# read the transformation from MRI to HEAD coordinates
# (could also be HEAD to MRI)
mri_head_t, trans = _get_trans(trans)
if isinstance(bem, ConductorModel):
bem_extra = "instance of ConductorModel"
else:
bem_extra = bem
_validate_type(info, ("path-like", Info), "info")
if not isinstance(info, Info):
info_extra = op.split(info)[1]
info = _check_fname(info, must_exist=True, overwrite="read", name="info")
info = read_info(info, verbose=False)
else:
info_extra = "instance of Info"
# Report the setup
logger.info(f"Source space : {src}")
logger.info(f"MRI -> head transform : {trans}")
logger.info(f"Measurement data : {info_extra}")
if isinstance(bem, ConductorModel) and bem["is_sphere"]:
logger.info(f"Sphere model : origin at {bem['r0']} mm")
logger.info("Standard field computations")
else:
logger.info(f"Conductor model : {bem_extra}")
logger.info("Accurate field computations")
logger.info(
"Do computations in %s coordinates", _coord_frame_name(FIFF.FIFFV_COORD_HEAD)
)
logger.info("Free source orientations")
# Create MEG coils and EEG electrodes in the head coordinate frame
sensors, rr, info, update_kwargs, bem = _prepare_for_forward(
src,
mri_head_t,
info,
bem,
mindist,
n_jobs,
bem_extra,
trans,
info_extra,
meg,
eeg,
ignore_ref,
)
del (src, mri_head_t, trans, info_extra, bem_extra, mindist, meg, eeg, ignore_ref)
# Time to do the heavy lifting: MEG first, then EEG
fwds = _compute_forwards(rr, bem=bem, sensors=sensors, n_jobs=n_jobs)
# merge forwards
fwds = {
key: _to_forward_dict(fwds[key], sensors[key]["ch_names"])
for key in _FWD_ORDER
if key in fwds
}
fwd = _merge_fwds(fwds, verbose=False)
del fwds
logger.info("")
# Don't transform the source spaces back into MRI coordinates (which is
# done in the C code) because mne-python assumes forward solution source
# spaces are in head coords.
fwd.update(**update_kwargs)
logger.info("Finished.")
return fwd
@verbose
def make_forward_dipole(dipole, bem, info, trans=None, n_jobs=None, *, verbose=None):
"""Convert dipole object to source estimate and calculate forward operator.
The instance of Dipole is converted to a discrete source space,
which is then combined with a BEM or a sphere model and
the sensor information in info to form a forward operator.
The source estimate object (with the forward operator) can be projected to
sensor-space using :func:`mne.simulation.simulate_evoked`.
.. note:: If the (unique) time points of the dipole object are unevenly
spaced, the first output will be a list of single-timepoint
source estimates.
Parameters
----------
%(dipole)s
bem : str | dict
The BEM filename (str) or a loaded sphere model (dict).
info : instance of Info
The measurement information dictionary. It is sensor-information etc.,
e.g., from a real data file.
trans : str | None
The head<->MRI transform filename. Must be provided unless BEM
is a sphere model.
%(n_jobs)s
%(verbose)s
Returns
-------
fwd : instance of Forward
The forward solution corresponding to the source estimate(s).
stc : instance of VolSourceEstimate | list of VolSourceEstimate
The dipoles converted to a discrete set of points and associated
time courses. If the time points of the dipole are unevenly spaced,
a list of single-timepoint source estimates are returned.
See Also
--------
mne.simulation.simulate_evoked
Notes
-----
.. versionadded:: 0.12.0
"""
if isinstance(dipole, list):
from ..dipole import _concatenate_dipoles # To avoid circular import
dipole = _concatenate_dipoles(dipole)
# Make copies to avoid mangling original dipole
times = dipole.times.copy()
pos = dipole.pos.copy()
amplitude = dipole.amplitude.copy()
ori = dipole.ori.copy()
# Convert positions to discrete source space (allows duplicate rr & nn)
# NB information about dipole orientation enters here, then no more
sources = dict(rr=pos, nn=ori)
# Dipole objects must be in the head frame
src = _complete_vol_src([_make_discrete_source_space(sources, coord_frame="head")])
# Forward operator created for channels in info (use pick_info to restrict)
# Use defaults for most params, including min_dist
fwd = make_forward_solution(info, trans, src, bem, n_jobs=n_jobs, verbose=verbose)
# Convert from free orientations to fixed (in-place)
convert_forward_solution(
fwd, surf_ori=False, force_fixed=True, copy=False, use_cps=False, verbose=None
)
# Check for omissions due to proximity to inner skull in
# make_forward_solution, which will result in an exception
if fwd["src"][0]["nuse"] != len(pos):
inuse = fwd["src"][0]["inuse"].astype(bool)
head = "The following dipoles are outside the inner skull boundary"
msg = len(head) * "#" + "\n" + head + "\n"
for t, pos in zip(times[np.logical_not(inuse)], pos[np.logical_not(inuse)]):
msg += (
f" t={t * 1000.0:.0f} ms, pos=({pos[0] * 1000.0:.0f}, "
f"{pos[1] * 1000.0:.0f}, {pos[2] * 1000.0:.0f}) mm\n"
)
msg += len(head) * "#"
logger.error(msg)
raise ValueError("One or more dipoles outside the inner skull.")
# multiple dipoles (rr and nn) per time instant allowed
# uneven sampling in time returns list
timepoints = np.unique(times)
if len(timepoints) > 1:
tdiff = np.diff(timepoints)
if not np.allclose(tdiff, tdiff[0]):
warn(
"Unique time points of dipoles unevenly spaced: returned "
"stc will be a list, one for each time point."
)
tstep = -1.0
else:
tstep = tdiff[0]
elif len(timepoints) == 1:
tstep = 0.001
# Build the data matrix, essentially a block-diagonal with
# n_rows: number of dipoles in total (dipole.amplitudes)
# n_cols: number of unique time points in dipole.times
# amplitude with identical value of times go together in one col (others=0)
data = np.zeros((len(amplitude), len(timepoints))) # (n_d, n_t)
row = 0
for tpind, tp in enumerate(timepoints):
amp = amplitude[np.isin(times, tp)]
data[row : row + len(amp), tpind] = amp
row += len(amp)
if tstep > 0:
stc = VolSourceEstimate(
data,
vertices=[fwd["src"][0]["vertno"]],
tmin=timepoints[0],
tstep=tstep,
subject=None,
)
else: # Must return a list of stc, one for each time point
stc = []
for col, tp in enumerate(timepoints):
stc += [
VolSourceEstimate(
data[:, col][:, np.newaxis],
vertices=[fwd["src"][0]["vertno"]],
tmin=tp,
tstep=0.001,
subject=None,
)
]
return fwd, stc
def _to_forward_dict(
fwd,
names,
fwd_grad=None,
coord_frame=FIFF.FIFFV_COORD_HEAD,
source_ori=FIFF.FIFFV_MNE_FREE_ORI,
):
"""Convert forward solution matrices to dicts."""
assert names is not None
sol = dict(
data=fwd.T, nrow=fwd.shape[1], ncol=fwd.shape[0], row_names=names, col_names=[]
)
fwd = Forward(
sol=sol,
source_ori=source_ori,
nsource=sol["ncol"],
coord_frame=coord_frame,
sol_grad=None,
nchan=sol["nrow"],
_orig_source_ori=source_ori,
_orig_sol=sol["data"].copy(),
_orig_sol_grad=None,
)
if fwd_grad is not None:
sol_grad = dict(
data=fwd_grad.T,
nrow=fwd_grad.shape[1],
ncol=fwd_grad.shape[0],
row_names=names,
col_names=[],
)
fwd.update(dict(sol_grad=sol_grad), _orig_sol_grad=sol_grad["data"].copy())
return fwd
@contextmanager
def use_coil_def(fname):
"""Use a custom coil definition file.
Parameters
----------
fname : path-like
The filename of the coil definition file.
Returns
-------
context : contextmanager
The context for using the coil definition.
Notes
-----
This is meant to be used a context manager such as:
>>> with use_coil_def(my_fname): # doctest:+SKIP
... make_forward_solution(...)
This allows using custom coil definitions with functions that require
forward modeling.
"""
global _extra_coil_def_fname
_extra_coil_def_fname = fname
try:
yield
finally:
_extra_coil_def_fname = None

2197
dist/client/mne/forward/forward.py vendored Normal file

File diff suppressed because it is too large Load Diff