针对pulse-transit的工具
This commit is contained in:
8
dist/client/mne/forward/__init__.py
vendored
Normal file
8
dist/client/mne/forward/__init__.py
vendored
Normal 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
86
dist/client/mne/forward/__init__.pyi
vendored
Normal 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,
|
||||
)
|
||||
BIN
dist/client/mne/forward/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
dist/client/mne/forward/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/client/mne/forward/__pycache__/_compute_forward.cpython-310.pyc
vendored
Normal file
BIN
dist/client/mne/forward/__pycache__/_compute_forward.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/client/mne/forward/__pycache__/_field_interpolation.cpython-310.pyc
vendored
Normal file
BIN
dist/client/mne/forward/__pycache__/_field_interpolation.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/client/mne/forward/__pycache__/_lead_dots.cpython-310.pyc
vendored
Normal file
BIN
dist/client/mne/forward/__pycache__/_lead_dots.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/client/mne/forward/__pycache__/_make_forward.cpython-310.pyc
vendored
Normal file
BIN
dist/client/mne/forward/__pycache__/_make_forward.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/client/mne/forward/__pycache__/forward.cpython-310.pyc
vendored
Normal file
BIN
dist/client/mne/forward/__pycache__/forward.cpython-310.pyc
vendored
Normal file
Binary file not shown.
897
dist/client/mne/forward/_compute_forward.py
vendored
Normal file
897
dist/client/mne/forward/_compute_forward.py
vendored
Normal 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
|
||||
547
dist/client/mne/forward/_field_interpolation.py
vendored
Normal file
547
dist/client/mne/forward/_field_interpolation.py
vendored
Normal 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
610
dist/client/mne/forward/_lead_dots.py
vendored
Normal 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
938
dist/client/mne/forward/_make_forward.py
vendored
Normal 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
2197
dist/client/mne/forward/forward.py
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user