527 lines
16 KiB
Python
527 lines
16 KiB
Python
"""Compute resolution matrix for linear estimators."""
|
|
|
|
# Authors: The MNE-Python contributors.
|
|
# License: BSD-3-Clause
|
|
# Copyright the MNE-Python contributors.
|
|
|
|
from copy import deepcopy
|
|
|
|
import numpy as np
|
|
|
|
from mne.minimum_norm.inverse import InverseOperator
|
|
|
|
from .._fiff.constants import FIFF
|
|
from .._fiff.pick import pick_channels_forward
|
|
from ..evoked import EvokedArray
|
|
from ..forward.forward import Forward, convert_forward_solution
|
|
from ..label import Label
|
|
from ..source_estimate import _get_src_type, _make_stc, _prepare_label_extraction
|
|
from ..source_space._source_space import SourceSpaces, _get_vertno
|
|
from ..utils import _validate_type, logger, verbose
|
|
from .inverse import apply_inverse
|
|
|
|
|
|
@verbose
|
|
def make_inverse_resolution_matrix(
|
|
forward, inverse_operator, method="dSPM", lambda2=1.0 / 9.0, verbose=None
|
|
):
|
|
"""Compute resolution matrix for linear inverse operator.
|
|
|
|
Parameters
|
|
----------
|
|
forward : instance of Forward
|
|
Forward Operator.
|
|
inverse_operator : instance of InverseOperator
|
|
Inverse operator.
|
|
method : 'MNE' | 'dSPM' | 'sLORETA'
|
|
Inverse method to use (MNE, dSPM, sLORETA).
|
|
lambda2 : float
|
|
The regularisation parameter.
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
resmat: array, shape (n_orient_inv * n_dipoles, n_orient_fwd * n_dipoles)
|
|
Resolution matrix (inverse operator times forward operator).
|
|
The result of applying the inverse operator to the forward operator.
|
|
If source orientations are not fixed, all source components will be
|
|
computed (i.e. for n_orient_inv > 1 or n_orient_fwd > 1).
|
|
The columns of the resolution matrix are the point-spread functions
|
|
(PSFs) and the rows are the cross-talk functions (CTFs).
|
|
"""
|
|
# make sure forward and inverse operator match
|
|
inv = inverse_operator
|
|
fwd = _convert_forward_match_inv(forward, inv)
|
|
|
|
# don't include bad channels
|
|
# only use good channels from inverse operator
|
|
bads_inv = inv["info"]["bads"]
|
|
# good channels
|
|
ch_names = [c for c in inv["info"]["ch_names"] if (c not in bads_inv)]
|
|
fwd = pick_channels_forward(fwd, ch_names, ordered=True)
|
|
|
|
# get leadfield matrix from forward solution
|
|
leadfield = fwd["sol"]["data"]
|
|
invmat = _get_matrix_from_inverse_operator(inv, fwd, method=method, lambda2=lambda2)
|
|
resmat = invmat.dot(leadfield)
|
|
logger.info(
|
|
f"Dimensions of resolution matrix: {resmat.shape[0]} by {resmat.shape[1]}."
|
|
)
|
|
return resmat
|
|
|
|
|
|
@verbose
|
|
def _get_psf_ctf(
|
|
resmat,
|
|
src,
|
|
idx,
|
|
*,
|
|
func,
|
|
mode,
|
|
n_comp,
|
|
norm,
|
|
return_pca_vars,
|
|
vector=False,
|
|
verbose=None,
|
|
):
|
|
"""Get point-spread (PSFs) or cross-talk (CTFs) functions."""
|
|
# check for consistencies in input parameters
|
|
_check_get_psf_ctf_params(mode, n_comp, return_pca_vars)
|
|
|
|
# backward compatibility
|
|
if norm is True:
|
|
norm = "max"
|
|
|
|
# get relevant vertices in source space
|
|
src_orig = src
|
|
_validate_type(src_orig, (InverseOperator, Forward, SourceSpaces), "src")
|
|
if not isinstance(src, SourceSpaces):
|
|
src = src["src"]
|
|
verts_all = _vertices_for_get_psf_ctf(idx, src)
|
|
vertno = _get_vertno(src)
|
|
n_verts = sum(len(v) for v in vertno)
|
|
src_type = _get_src_type(src, vertno)
|
|
subject = src._subject
|
|
if vector and src_type == "surface":
|
|
_validate_type(
|
|
src_orig,
|
|
(Forward, InverseOperator),
|
|
"src",
|
|
extra="when creating a vector surface source estimate",
|
|
)
|
|
nn = src_orig["source_nn"]
|
|
else:
|
|
nn = np.repeat(np.eye(3, 3)[np.newaxis], n_verts, 0)
|
|
|
|
n_r, n_c = resmat.shape
|
|
if ((n_verts != n_r) and (n_r / 3 != n_verts)) or (
|
|
(n_verts != n_c) and (n_c / 3 != n_verts)
|
|
):
|
|
msg = (
|
|
f"Number of vertices ({n_verts}) and corresponding dimension of"
|
|
f"resolution matrix ({n_r}, {n_c}) do not match"
|
|
)
|
|
raise ValueError(msg)
|
|
|
|
# the following will operate on columns of funcs
|
|
if func == "ctf":
|
|
resmat = resmat.T
|
|
n_r, n_c = n_c, n_r
|
|
|
|
# Functions and variances per label
|
|
stcs = []
|
|
pca_vars = []
|
|
|
|
# if 3 orientations per vertex, redefine indices to columns of resolution
|
|
# matrix
|
|
if n_verts != n_c:
|
|
# change indices to three indices per vertex
|
|
for [i, verts] in enumerate(verts_all):
|
|
verts_vec = np.empty(3 * len(verts), dtype=int)
|
|
for [j, v] in enumerate(verts):
|
|
verts_vec[3 * j : 3 * j + 3] = 3 * verts[j] + np.array([0, 1, 2])
|
|
verts_all[i] = verts_vec # use these as indices
|
|
|
|
for verts in verts_all:
|
|
# get relevant PSFs or CTFs for specified vertices
|
|
if isinstance(verts, int):
|
|
verts = [verts] # to keep array dimensions
|
|
funcs = resmat[:, verts]
|
|
|
|
# normalise PSFs/CTFs if requested
|
|
if norm is not None:
|
|
funcs = _normalise_psf_ctf(funcs, norm)
|
|
|
|
# summarise PSFs/CTFs across vertices if requested
|
|
pca_var = None # variances computed only if return_pca_vars=True
|
|
if mode is not None:
|
|
funcs, pca_var = _summarise_psf_ctf(
|
|
funcs, mode, n_comp, return_pca_vars, nn
|
|
)
|
|
|
|
if not vector: # if one value per vertex requested
|
|
if n_verts != n_r: # if 3 orientations per vertex, combine
|
|
funcs_int = np.empty([int(n_r / 3), funcs.shape[1]])
|
|
for i in np.arange(0, n_verts):
|
|
funcs_vert = funcs[3 * i : 3 * i + 3, :]
|
|
funcs_int[i, :] = np.sqrt((funcs_vert**2).sum(axis=0))
|
|
funcs = funcs_int
|
|
|
|
stc = _make_stc(
|
|
funcs,
|
|
vertno,
|
|
src_type,
|
|
tmin=0.0,
|
|
tstep=1.0,
|
|
subject=subject,
|
|
vector=vector,
|
|
source_nn=nn,
|
|
)
|
|
stcs.append(stc)
|
|
pca_vars.append(pca_var)
|
|
|
|
# if just one list or label specified, simplify output
|
|
if len(stcs) == 1:
|
|
stcs = stc
|
|
if len(pca_vars) == 1:
|
|
pca_vars = pca_var
|
|
if pca_var is not None:
|
|
return stcs, pca_vars
|
|
else:
|
|
return stcs
|
|
|
|
|
|
def _check_get_psf_ctf_params(mode, n_comp, return_pca_vars):
|
|
"""Check input parameters of _get_psf_ctf() for consistency."""
|
|
if mode in [None, "sum", "mean"] and n_comp > 1:
|
|
msg = f"n_comp must be 1 for mode={mode}."
|
|
raise ValueError(msg)
|
|
if mode != "pca" and return_pca_vars:
|
|
msg = "SVD variances can only be returned if mode=pca."
|
|
raise ValueError(msg)
|
|
|
|
|
|
def _vertices_for_get_psf_ctf(idx, src):
|
|
"""Get vertices in source space for PSFs/CTFs in _get_psf_ctf()."""
|
|
# idx must be list
|
|
# if label(s) specified get the indices, otherwise just carry on
|
|
if type(idx[0]) is Label:
|
|
# specify without source time courses, gets indices per label
|
|
verts_labs, _ = _prepare_label_extraction(
|
|
stc=None,
|
|
labels=idx,
|
|
src=src,
|
|
mode="mean",
|
|
allow_empty=False,
|
|
use_sparse=False,
|
|
)
|
|
# verts_labs can be list of lists
|
|
# concatenate indices per label across hemispheres
|
|
# one list item per label
|
|
verts = []
|
|
|
|
for v in verts_labs:
|
|
# if two hemispheres present
|
|
if isinstance(v, list):
|
|
# indices for both hemispheres in one list
|
|
this_verts = np.concatenate((v[0], v[1]))
|
|
else:
|
|
this_verts = np.array(v)
|
|
verts.append(this_verts)
|
|
# check if list of list or just list
|
|
else:
|
|
if isinstance(idx[0], list): # if list of list of integers
|
|
verts = idx
|
|
else: # if list of integers
|
|
verts = [idx]
|
|
|
|
return verts
|
|
|
|
|
|
def _normalise_psf_ctf(funcs, norm):
|
|
"""Normalise PSFs/CTFs in _get_psf_ctf()."""
|
|
# normalise PSFs/CTFs if specified
|
|
if norm == "max":
|
|
maxval = max(-funcs.min(), funcs.max())
|
|
funcs = funcs / maxval
|
|
elif norm == "norm": # normalise to maximum norm across columns
|
|
norms = np.linalg.norm(funcs, axis=0)
|
|
funcs = funcs / norms.max()
|
|
|
|
return funcs
|
|
|
|
|
|
def _summarise_psf_ctf(funcs, mode, n_comp, return_pca_vars, nn):
|
|
"""Summarise PSFs/CTFs across vertices."""
|
|
s_var = None # only computed for return_pca_vars=True
|
|
|
|
if mode == "maxval": # pick PSF/CTF with maximum absolute value
|
|
absvals = np.maximum(-np.min(funcs, axis=0), np.max(funcs, axis=0))
|
|
if n_comp > 1: # only keep requested number of sorted PSFs/CTFs
|
|
sortidx = np.argsort(absvals)
|
|
maxidx = sortidx[-n_comp:]
|
|
else: # faster if only one required
|
|
maxidx = [absvals.argmax()]
|
|
funcs = funcs[:, maxidx]
|
|
|
|
elif mode == "maxnorm": # pick PSF/CTF with maximum norm
|
|
norms = np.linalg.norm(funcs, axis=0)
|
|
if n_comp > 1: # only keep requested number of sorted PSFs/CTFs
|
|
sortidx = np.argsort(norms)
|
|
maxidx = sortidx[-n_comp:]
|
|
else: # faster if only one required
|
|
maxidx = [norms.argmax()]
|
|
funcs = funcs[:, maxidx]
|
|
|
|
elif mode == "sum": # sum across PSFs/CTFs
|
|
funcs = np.sum(funcs, axis=1, keepdims=True)
|
|
|
|
elif mode == "mean": # mean of PSFs/CTFs
|
|
funcs = np.mean(funcs, axis=1, keepdims=True)
|
|
|
|
elif mode == "pca": # SVD across PSFs/CTFs
|
|
# compute SVD of PSFs/CTFs across vertices
|
|
u, s, _ = np.linalg.svd(funcs, full_matrices=False, compute_uv=True)
|
|
if n_comp > 1:
|
|
funcs = u[:, :n_comp]
|
|
else:
|
|
funcs = u[:, 0, np.newaxis]
|
|
# if explained variances for SVD components requested
|
|
if return_pca_vars:
|
|
# explained variance of individual SVD components
|
|
s2 = s * s
|
|
s_var = 100 * s2[:n_comp] / s2.sum()
|
|
|
|
return funcs, s_var
|
|
|
|
|
|
@verbose
|
|
def get_point_spread(
|
|
resmat,
|
|
src,
|
|
idx,
|
|
mode=None,
|
|
*,
|
|
n_comp=1,
|
|
norm=False,
|
|
return_pca_vars=False,
|
|
vector=False,
|
|
verbose=None,
|
|
):
|
|
"""Get point-spread (PSFs) functions for vertices.
|
|
|
|
Parameters
|
|
----------
|
|
resmat : array, shape (n_dipoles, n_dipoles)
|
|
Forward Operator.
|
|
src : instance of SourceSpaces | instance of InverseOperator | instance of Forward
|
|
Source space used to compute resolution matrix.
|
|
Must be an InverseOperator if ``vector=True`` and a surface
|
|
source space is used.
|
|
%(idx_pctf)s
|
|
%(mode_pctf)s
|
|
%(n_comp_pctf_n)s
|
|
%(norm_pctf)s
|
|
%(return_pca_vars_pctf)s
|
|
%(vector_pctf)s
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
%(stcs_pctf)s
|
|
%(pca_vars_pctf)s
|
|
""" # noqa: E501
|
|
return _get_psf_ctf(
|
|
resmat,
|
|
src,
|
|
idx,
|
|
func="psf",
|
|
mode=mode,
|
|
n_comp=n_comp,
|
|
norm=norm,
|
|
return_pca_vars=return_pca_vars,
|
|
vector=vector,
|
|
)
|
|
|
|
|
|
@verbose
|
|
def get_cross_talk(
|
|
resmat,
|
|
src,
|
|
idx,
|
|
mode=None,
|
|
*,
|
|
n_comp=1,
|
|
norm=False,
|
|
return_pca_vars=False,
|
|
vector=False,
|
|
verbose=None,
|
|
):
|
|
"""Get cross-talk (CTFs) function for vertices.
|
|
|
|
Parameters
|
|
----------
|
|
resmat : array, shape (n_dipoles, n_dipoles)
|
|
Forward Operator.
|
|
src : instance of SourceSpaces | instance of InverseOperator | instance of Forward
|
|
Source space used to compute resolution matrix.
|
|
Must be an InverseOperator if ``vector=True`` and a surface
|
|
source space is used.
|
|
%(idx_pctf)s
|
|
%(mode_pctf)s
|
|
%(n_comp_pctf_n)s
|
|
%(norm_pctf)s
|
|
%(return_pca_vars_pctf)s
|
|
%(vector_pctf)s
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
%(stcs_pctf)s
|
|
%(pca_vars_pctf)s
|
|
""" # noqa: E501
|
|
return _get_psf_ctf(
|
|
resmat,
|
|
src,
|
|
idx,
|
|
func="ctf",
|
|
mode=mode,
|
|
n_comp=n_comp,
|
|
norm=norm,
|
|
return_pca_vars=return_pca_vars,
|
|
vector=vector,
|
|
)
|
|
|
|
|
|
def _convert_forward_match_inv(fwd, inv):
|
|
"""Ensure forward and inverse operators match.
|
|
|
|
Inverse operator and forward operator must have same surface orientations,
|
|
but can have different source orientation constraints.
|
|
"""
|
|
_validate_type(fwd, Forward, "fwd")
|
|
_validate_type(inv, InverseOperator, "inverse_operator")
|
|
# did inverse operator use fixed orientation?
|
|
is_fixed_inv = _check_fixed_ori(inv)
|
|
# did forward operator use fixed orientation?
|
|
is_fixed_fwd = _check_fixed_ori(fwd)
|
|
|
|
# if inv or fwd fixed: do nothing
|
|
# if inv loose: surf_ori must be True
|
|
# if inv free: surf_ori must be False
|
|
if not is_fixed_inv and not is_fixed_fwd:
|
|
inv_surf_ori = inv._is_surf_ori
|
|
if inv_surf_ori != fwd["surf_ori"]:
|
|
fwd = convert_forward_solution(
|
|
fwd, surf_ori=inv_surf_ori, force_fixed=False
|
|
)
|
|
|
|
return fwd
|
|
|
|
|
|
def _prepare_info(inverse_operator):
|
|
"""Get a usable dict."""
|
|
# in order to convert sub-leadfield matrix to evoked data type (pretending
|
|
# it's an epoch, see in loop below), uses 'info' from inverse solution
|
|
# because this has all the correct projector information
|
|
info = deepcopy(inverse_operator["info"])
|
|
with info._unlock():
|
|
info["sfreq"] = 1000.0 # necessary
|
|
info["projs"] = inverse_operator["projs"]
|
|
info["custom_ref_applied"] = False
|
|
return info
|
|
|
|
|
|
def _get_matrix_from_inverse_operator(
|
|
inverse_operator, forward, method="dSPM", lambda2=1.0 / 9.0
|
|
):
|
|
"""Get inverse matrix from an inverse operator.
|
|
|
|
Currently works only for fixed/loose orientation constraints
|
|
For loose orientation constraint, the CTFs are computed for the normal
|
|
component (pick_ori='normal').
|
|
|
|
Parameters
|
|
----------
|
|
inverse_operator : instance of InverseOperator
|
|
The inverse operator.
|
|
forward : instance of Forward
|
|
The forward operator.
|
|
method : 'MNE' | 'dSPM' | 'sLORETA'
|
|
Inverse methods (for apply_inverse).
|
|
lambda2 : float
|
|
The regularization parameter (for apply_inverse).
|
|
|
|
Returns
|
|
-------
|
|
invmat : array, shape (n_dipoles, n_channels)
|
|
Inverse matrix associated with inverse operator and specified
|
|
parameters.
|
|
"""
|
|
# make sure forward and inverse operators match with respect to
|
|
# surface orientation
|
|
_convert_forward_match_inv(forward, inverse_operator)
|
|
|
|
info_inv = _prepare_info(inverse_operator)
|
|
|
|
# only use channels that are good for inverse operator and forward sol
|
|
ch_names_inv = info_inv["ch_names"]
|
|
n_chs_inv = len(ch_names_inv)
|
|
bads_inv = inverse_operator["info"]["bads"]
|
|
|
|
# indices of bad channels
|
|
ch_idx_bads = [ch_names_inv.index(ch) for ch in bads_inv]
|
|
|
|
# create identity matrix as input for inverse operator
|
|
# set elements to zero for non-selected channels
|
|
id_mat = np.eye(n_chs_inv)
|
|
|
|
# convert identity matrix to evoked data type (pretending it's an epoch)
|
|
ev_id = EvokedArray(id_mat, info=info_inv, tmin=0.0)
|
|
|
|
# apply inverse operator to identity matrix in order to get inverse matrix
|
|
# free orientation constraint not possible because apply_inverse would
|
|
# combine components
|
|
|
|
# check if inverse operator uses fixed source orientations
|
|
is_fixed_inv = _check_fixed_ori(inverse_operator)
|
|
|
|
# choose pick_ori according to inverse operator
|
|
if is_fixed_inv:
|
|
pick_ori = None
|
|
else:
|
|
pick_ori = "vector"
|
|
|
|
# columns for bad channels will be zero
|
|
invmat_op = apply_inverse(
|
|
ev_id, inverse_operator, lambda2=lambda2, method=method, pick_ori=pick_ori
|
|
)
|
|
|
|
# turn source estimate into numpy array
|
|
invmat = invmat_op.data
|
|
|
|
# remove columns for bad channels
|
|
# take into account it may be 3D array
|
|
invmat = np.delete(invmat, ch_idx_bads, axis=invmat.ndim - 1)
|
|
|
|
# if 3D array, i.e. multiple values per location (fixed and loose),
|
|
# reshape into 2D array
|
|
if invmat.ndim == 3:
|
|
v0o1 = invmat[0, 1].copy()
|
|
v3o2 = invmat[3, 2].copy()
|
|
shape = invmat.shape
|
|
invmat = invmat.reshape(shape[0] * shape[1], shape[2])
|
|
# make sure that reshaping worked
|
|
assert np.array_equal(v0o1, invmat[1])
|
|
assert np.array_equal(v3o2, invmat[11])
|
|
|
|
logger.info(f"Dimension of Inverse Matrix: {invmat.shape}")
|
|
|
|
return invmat
|
|
|
|
|
|
def _check_fixed_ori(inst):
|
|
"""Check if inverse or forward was computed for fixed orientations."""
|
|
is_fixed = inst["source_ori"] != FIFF.FIFFV_MNE_FREE_ORI
|
|
return is_fixed
|