initial commit
This commit is contained in:
8
mne/beamformer/__init__.py
Normal file
8
mne/beamformer/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# Authors: The MNE-Python contributors.
|
||||
# License: BSD-3-Clause
|
||||
# Copyright the MNE-Python contributors.
|
||||
|
||||
"""Beamformers for source localization."""
|
||||
import lazy_loader as lazy
|
||||
|
||||
(__getattr__, __dir__, __all__) = lazy.attach_stub(__name__, __file__)
|
||||
34
mne/beamformer/__init__.pyi
Normal file
34
mne/beamformer/__init__.pyi
Normal file
@@ -0,0 +1,34 @@
|
||||
__all__ = [
|
||||
"Beamformer",
|
||||
"apply_dics",
|
||||
"apply_dics_csd",
|
||||
"apply_dics_epochs",
|
||||
"apply_dics_tfr_epochs",
|
||||
"apply_lcmv",
|
||||
"apply_lcmv_cov",
|
||||
"apply_lcmv_epochs",
|
||||
"apply_lcmv_raw",
|
||||
"make_dics",
|
||||
"make_lcmv",
|
||||
"make_lcmv_resolution_matrix",
|
||||
"rap_music",
|
||||
"read_beamformer",
|
||||
"trap_music",
|
||||
]
|
||||
from ._compute_beamformer import Beamformer, read_beamformer
|
||||
from ._dics import (
|
||||
apply_dics,
|
||||
apply_dics_csd,
|
||||
apply_dics_epochs,
|
||||
apply_dics_tfr_epochs,
|
||||
make_dics,
|
||||
)
|
||||
from ._lcmv import (
|
||||
apply_lcmv,
|
||||
apply_lcmv_cov,
|
||||
apply_lcmv_epochs,
|
||||
apply_lcmv_raw,
|
||||
make_lcmv,
|
||||
)
|
||||
from ._rap_music import rap_music, trap_music
|
||||
from .resolution_matrix import make_lcmv_resolution_matrix
|
||||
603
mne/beamformer/_compute_beamformer.py
Normal file
603
mne/beamformer/_compute_beamformer.py
Normal file
@@ -0,0 +1,603 @@
|
||||
"""Functions shared between different beamformer types."""
|
||||
|
||||
# Authors: The MNE-Python contributors.
|
||||
# License: BSD-3-Clause
|
||||
# Copyright the MNE-Python contributors.
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .._fiff.proj import Projection, make_projector
|
||||
from ..cov import Covariance, make_ad_hoc_cov
|
||||
from ..forward.forward import _restrict_forward_to_src_sel, is_fixed_orient
|
||||
from ..minimum_norm.inverse import _get_vertno, _prepare_forward
|
||||
from ..source_space._source_space import label_src_vertno_sel
|
||||
from ..time_frequency.csd import CrossSpectralDensity
|
||||
from ..utils import (
|
||||
_check_option,
|
||||
_check_src_normal,
|
||||
_import_h5io_funcs,
|
||||
_pl,
|
||||
_reg_pinv,
|
||||
_sym_mat_pow,
|
||||
check_fname,
|
||||
logger,
|
||||
verbose,
|
||||
warn,
|
||||
)
|
||||
|
||||
|
||||
def _check_proj_match(proj, filters):
|
||||
"""Check whether SSP projections in data and spatial filter match."""
|
||||
proj_data, _, _ = make_projector(proj, filters["ch_names"])
|
||||
if not np.allclose(
|
||||
proj_data, filters["proj"], atol=np.finfo(float).eps, rtol=1e-13
|
||||
):
|
||||
raise ValueError(
|
||||
"The SSP projections present in the data "
|
||||
"do not match the projections used when "
|
||||
"calculating the spatial filter."
|
||||
)
|
||||
|
||||
|
||||
def _check_src_type(filters):
|
||||
"""Check whether src_type is in filters and set custom warning."""
|
||||
if "src_type" not in filters:
|
||||
filters["src_type"] = None
|
||||
warn_text = (
|
||||
"The spatial filter does not contain src_type and a robust "
|
||||
"guess of src_type is not possible without src. Consider "
|
||||
"recomputing the filter."
|
||||
)
|
||||
return filters, warn_text
|
||||
|
||||
|
||||
def _prepare_beamformer_input(
|
||||
info,
|
||||
forward,
|
||||
label=None,
|
||||
pick_ori=None,
|
||||
noise_cov=None,
|
||||
rank=None,
|
||||
pca=False,
|
||||
loose=None,
|
||||
combine_xyz="fro",
|
||||
exp=None,
|
||||
limit=None,
|
||||
allow_fixed_depth=True,
|
||||
limit_depth_chs=False,
|
||||
):
|
||||
"""Input preparation common for LCMV, DICS, and RAP-MUSIC."""
|
||||
_check_option("pick_ori", pick_ori, ("normal", "max-power", "vector", None))
|
||||
|
||||
# Restrict forward solution to selected vertices
|
||||
if label is not None:
|
||||
_, src_sel = label_src_vertno_sel(label, forward["src"])
|
||||
forward = _restrict_forward_to_src_sel(forward, src_sel)
|
||||
|
||||
if loose is None:
|
||||
loose = 0.0 if is_fixed_orient(forward) else 1.0
|
||||
# TODO: Deduplicate with _check_one_ch_type, should not be necessary
|
||||
# (DICS hits this code path, LCMV does not)
|
||||
if noise_cov is None:
|
||||
noise_cov = make_ad_hoc_cov(info, std=1.0)
|
||||
(
|
||||
forward,
|
||||
info_picked,
|
||||
gain,
|
||||
_,
|
||||
orient_prior,
|
||||
_,
|
||||
trace_GRGT,
|
||||
noise_cov,
|
||||
whitener,
|
||||
) = _prepare_forward(
|
||||
forward,
|
||||
info,
|
||||
noise_cov,
|
||||
"auto",
|
||||
loose,
|
||||
rank=rank,
|
||||
pca=pca,
|
||||
use_cps=True,
|
||||
exp=exp,
|
||||
limit_depth_chs=limit_depth_chs,
|
||||
combine_xyz=combine_xyz,
|
||||
limit=limit,
|
||||
allow_fixed_depth=allow_fixed_depth,
|
||||
)
|
||||
is_free_ori = not is_fixed_orient(forward) # could have been changed
|
||||
nn = forward["source_nn"]
|
||||
if is_free_ori: # take Z coordinate
|
||||
nn = nn[2::3]
|
||||
nn = nn.copy()
|
||||
vertno = _get_vertno(forward["src"])
|
||||
if forward["surf_ori"]:
|
||||
nn[...] = [0, 0, 1] # align to local +Z coordinate
|
||||
if pick_ori is not None and not is_free_ori:
|
||||
raise ValueError(
|
||||
f"Normal or max-power orientation (got {pick_ori!r}) can only be picked "
|
||||
"when a forward operator with free orientation is used."
|
||||
)
|
||||
if pick_ori == "normal" and not forward["surf_ori"]:
|
||||
raise ValueError(
|
||||
"Normal orientation can only be picked when a forward operator oriented in "
|
||||
"surface coordinates is used."
|
||||
)
|
||||
_check_src_normal(pick_ori, forward["src"])
|
||||
del forward, info
|
||||
|
||||
# Undo the scaling that MNE prefers
|
||||
scale = np.sqrt((noise_cov["eig"] > 0).sum() / trace_GRGT)
|
||||
gain /= scale
|
||||
if orient_prior is not None:
|
||||
orient_std = np.sqrt(orient_prior)
|
||||
else:
|
||||
orient_std = np.ones(gain.shape[1])
|
||||
|
||||
# Get the projector
|
||||
proj, _, _ = make_projector(info_picked["projs"], info_picked["ch_names"])
|
||||
return (is_free_ori, info_picked, proj, vertno, gain, whitener, nn, orient_std)
|
||||
|
||||
|
||||
def _reduce_leadfield_rank(G):
|
||||
"""Reduce the rank of the leadfield."""
|
||||
# decompose lead field
|
||||
u, s, v = np.linalg.svd(G, full_matrices=False)
|
||||
|
||||
# backproject, omitting one direction (equivalent to setting the smallest
|
||||
# singular value to zero)
|
||||
G = np.matmul(u[:, :, :-1], s[:, :-1, np.newaxis] * v[:, :-1, :])
|
||||
|
||||
return G
|
||||
|
||||
|
||||
def _sym_inv_sm(x, reduce_rank, inversion, sk):
|
||||
"""Symmetric inversion with single- or matrix-style inversion."""
|
||||
if x.shape[1:] == (1, 1):
|
||||
with np.errstate(divide="ignore", invalid="ignore"):
|
||||
x_inv = 1.0 / x
|
||||
x_inv[~np.isfinite(x_inv)] = 1.0
|
||||
else:
|
||||
assert x.shape[1:] == (3, 3)
|
||||
if inversion == "matrix":
|
||||
x_inv = _sym_mat_pow(x, -1, reduce_rank=reduce_rank)
|
||||
# Reapply source covariance after inversion
|
||||
x_inv *= sk[:, :, np.newaxis]
|
||||
x_inv *= sk[:, np.newaxis, :]
|
||||
else:
|
||||
# Invert for each dipole separately using plain division
|
||||
diags = np.diagonal(x, axis1=1, axis2=2)
|
||||
assert not reduce_rank # guaranteed earlier
|
||||
with np.errstate(divide="ignore"):
|
||||
diags = 1.0 / diags
|
||||
# set the diagonal of each 3x3
|
||||
x_inv = np.zeros_like(x)
|
||||
for k in range(x.shape[0]):
|
||||
this = diags[k]
|
||||
# Reapply source covariance after inversion
|
||||
this *= sk[k] * sk[k]
|
||||
x_inv[k].flat[::4] = this
|
||||
return x_inv
|
||||
|
||||
|
||||
def _compute_beamformer(
|
||||
G,
|
||||
Cm,
|
||||
reg,
|
||||
n_orient,
|
||||
weight_norm,
|
||||
pick_ori,
|
||||
reduce_rank,
|
||||
rank,
|
||||
inversion,
|
||||
nn,
|
||||
orient_std,
|
||||
whitener,
|
||||
):
|
||||
"""Compute a spatial beamformer filter (LCMV or DICS).
|
||||
|
||||
For more detailed information on the parameters, see the docstrings of
|
||||
`make_lcmv` and `make_dics`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : ndarray, shape (n_dipoles, n_channels)
|
||||
The leadfield.
|
||||
Cm : ndarray, shape (n_channels, n_channels)
|
||||
The data covariance matrix.
|
||||
reg : float
|
||||
Regularization parameter.
|
||||
n_orient : int
|
||||
Number of dipole orientations defined at each source point
|
||||
weight_norm : None | 'unit-noise-gain' | 'nai'
|
||||
The weight normalization scheme to use.
|
||||
pick_ori : None | 'normal' | 'max-power'
|
||||
The source orientation to compute the beamformer in.
|
||||
reduce_rank : bool
|
||||
Whether to reduce the rank by one during computation of the filter.
|
||||
rank : dict | None | 'full' | 'info'
|
||||
See compute_rank.
|
||||
inversion : 'matrix' | 'single'
|
||||
The inversion scheme to compute the weights.
|
||||
nn : ndarray, shape (n_dipoles, 3)
|
||||
The source normals.
|
||||
orient_std : ndarray, shape (n_dipoles,)
|
||||
The std of the orientation prior used in weighting the lead fields.
|
||||
whitener : ndarray, shape (n_channels, n_channels)
|
||||
The whitener.
|
||||
|
||||
Returns
|
||||
-------
|
||||
W : ndarray, shape (n_dipoles, n_channels)
|
||||
The beamformer filter weights.
|
||||
"""
|
||||
_check_option(
|
||||
"weight_norm",
|
||||
weight_norm,
|
||||
["unit-noise-gain-invariant", "unit-noise-gain", "nai", None],
|
||||
)
|
||||
|
||||
# Whiten the data covariance
|
||||
Cm = whitener @ Cm @ whitener.T.conj()
|
||||
# Restore to properly Hermitian as large whitening coefs can have bad
|
||||
# rounding error
|
||||
Cm[:] = (Cm + Cm.T.conj()) / 2.0
|
||||
|
||||
assert Cm.shape == (G.shape[0],) * 2
|
||||
s, _ = np.linalg.eigh(Cm)
|
||||
if not (s >= -s.max() * 1e-7).all():
|
||||
# This shouldn't ever happen, but just in case
|
||||
warn(
|
||||
"data covariance does not appear to be positive semidefinite, "
|
||||
"results will likely be incorrect"
|
||||
)
|
||||
# Tikhonov regularization using reg parameter to control for
|
||||
# trade-off between spatial resolution and noise sensitivity
|
||||
# eq. 25 in Gross and Ioannides, 1999 Phys. Med. Biol. 44 2081
|
||||
Cm_inv, loading_factor, rank = _reg_pinv(Cm, reg, rank)
|
||||
|
||||
assert orient_std.shape == (G.shape[1],)
|
||||
n_sources = G.shape[1] // n_orient
|
||||
assert nn.shape == (n_sources, 3)
|
||||
|
||||
logger.info(f"Computing beamformer filters for {n_sources} source{_pl(n_sources)}")
|
||||
n_channels = G.shape[0]
|
||||
assert n_orient in (3, 1)
|
||||
Gk = np.reshape(G.T, (n_sources, n_orient, n_channels)).transpose(0, 2, 1)
|
||||
assert Gk.shape == (n_sources, n_channels, n_orient)
|
||||
sk = np.reshape(orient_std, (n_sources, n_orient))
|
||||
del G, orient_std
|
||||
|
||||
_check_option("reduce_rank", reduce_rank, (True, False))
|
||||
|
||||
# inversion of the denominator
|
||||
_check_option("inversion", inversion, ("matrix", "single"))
|
||||
if (
|
||||
inversion == "single"
|
||||
and n_orient > 1
|
||||
and pick_ori == "vector"
|
||||
and weight_norm == "unit-noise-gain-invariant"
|
||||
):
|
||||
raise ValueError(
|
||||
'Cannot use pick_ori="vector" with inversion="single" and '
|
||||
'weight_norm="unit-noise-gain-invariant"'
|
||||
)
|
||||
if reduce_rank and inversion == "single":
|
||||
raise ValueError(
|
||||
'reduce_rank cannot be used with inversion="single"; '
|
||||
'consider using inversion="matrix" if you have a '
|
||||
"rank-deficient forward model (i.e., from a sphere "
|
||||
"model with MEG channels), otherwise consider using "
|
||||
"reduce_rank=False"
|
||||
)
|
||||
if n_orient > 1:
|
||||
_, Gk_s, _ = np.linalg.svd(Gk, full_matrices=False)
|
||||
assert Gk_s.shape == (n_sources, n_orient)
|
||||
if not reduce_rank and (Gk_s[:, 0] > 1e6 * Gk_s[:, 2]).any():
|
||||
raise ValueError(
|
||||
"Singular matrix detected when estimating spatial filters. "
|
||||
"Consider reducing the rank of the forward operator by using "
|
||||
"reduce_rank=True."
|
||||
)
|
||||
del Gk_s
|
||||
|
||||
#
|
||||
# 1. Reduce rank of the lead field
|
||||
#
|
||||
if reduce_rank:
|
||||
Gk = _reduce_leadfield_rank(Gk)
|
||||
|
||||
def _compute_bf_terms(Gk, Cm_inv):
|
||||
bf_numer = np.matmul(Gk.swapaxes(-2, -1).conj(), Cm_inv)
|
||||
bf_denom = np.matmul(bf_numer, Gk)
|
||||
return bf_numer, bf_denom
|
||||
|
||||
#
|
||||
# 2. Reorient lead field in direction of max power or normal
|
||||
#
|
||||
if pick_ori == "max-power":
|
||||
assert n_orient == 3
|
||||
_, bf_denom = _compute_bf_terms(Gk, Cm_inv)
|
||||
if weight_norm is None:
|
||||
ori_numer = np.eye(n_orient)[np.newaxis]
|
||||
ori_denom = bf_denom
|
||||
else:
|
||||
# compute power, cf Sekihara & Nagarajan 2008, eq. 4.47
|
||||
ori_numer = bf_denom
|
||||
# Cm_inv should be Hermitian so no need for .T.conj()
|
||||
ori_denom = np.matmul(
|
||||
np.matmul(Gk.swapaxes(-2, -1).conj(), Cm_inv @ Cm_inv), Gk
|
||||
)
|
||||
ori_denom_inv = _sym_inv_sm(ori_denom, reduce_rank, inversion, sk)
|
||||
ori_pick = np.matmul(ori_denom_inv, ori_numer)
|
||||
assert ori_pick.shape == (n_sources, n_orient, n_orient)
|
||||
|
||||
# pick eigenvector that corresponds to maximum eigenvalue:
|
||||
eig_vals, eig_vecs = np.linalg.eig(ori_pick.real) # not Hermitian!
|
||||
# sort eigenvectors by eigenvalues for picking:
|
||||
order = np.argsort(np.abs(eig_vals), axis=-1)
|
||||
# eig_vals = np.take_along_axis(eig_vals, order, axis=-1)
|
||||
max_power_ori = eig_vecs[np.arange(len(eig_vecs)), :, order[:, -1]]
|
||||
assert max_power_ori.shape == (n_sources, n_orient)
|
||||
|
||||
# set the (otherwise arbitrary) sign to match the normal
|
||||
signs = np.sign(np.sum(max_power_ori * nn, axis=1, keepdims=True))
|
||||
signs[signs == 0] = 1.0
|
||||
max_power_ori *= signs
|
||||
|
||||
# Compute the lead field for the optimal orientation,
|
||||
# and adjust numer/denom
|
||||
Gk = np.matmul(Gk, max_power_ori[..., np.newaxis])
|
||||
n_orient = 1
|
||||
else:
|
||||
max_power_ori = None
|
||||
if pick_ori == "normal":
|
||||
Gk = Gk[..., 2:3]
|
||||
n_orient = 1
|
||||
|
||||
#
|
||||
# 3. Compute numerator and denominator of beamformer formula (unit-gain)
|
||||
#
|
||||
|
||||
bf_numer, bf_denom = _compute_bf_terms(Gk, Cm_inv)
|
||||
assert bf_denom.shape == (n_sources,) + (n_orient,) * 2
|
||||
assert bf_numer.shape == (n_sources, n_orient, n_channels)
|
||||
del Gk # lead field has been adjusted and should not be used anymore
|
||||
|
||||
#
|
||||
# 4. Invert the denominator
|
||||
#
|
||||
|
||||
# Here W is W_ug, i.e.:
|
||||
# G.T @ Cm_inv / (G.T @ Cm_inv @ G)
|
||||
bf_denom_inv = _sym_inv_sm(bf_denom, reduce_rank, inversion, sk)
|
||||
assert bf_denom_inv.shape == (n_sources, n_orient, n_orient)
|
||||
W = np.matmul(bf_denom_inv, bf_numer)
|
||||
assert W.shape == (n_sources, n_orient, n_channels)
|
||||
del bf_denom_inv, sk
|
||||
|
||||
#
|
||||
# 5. Re-scale filter weights according to the selected weight_norm
|
||||
#
|
||||
|
||||
# Weight normalization is done by computing, for each source::
|
||||
#
|
||||
# W_ung = W_ug / sqrt(W_ug @ W_ug.T)
|
||||
#
|
||||
# with W_ung referring to the unit-noise-gain (weight normalized) filter
|
||||
# and W_ug referring to the above-calculated unit-gain filter stored in W.
|
||||
|
||||
if weight_norm is not None:
|
||||
# Three different ways to calculate the normalization factors here.
|
||||
# Only matters when in vector mode, as otherwise n_orient == 1 and
|
||||
# they are all equivalent.
|
||||
#
|
||||
# In MNE < 0.21, we just used the Frobenius matrix norm:
|
||||
#
|
||||
# noise_norm = np.linalg.norm(W, axis=(1, 2), keepdims=True)
|
||||
# assert noise_norm.shape == (n_sources, 1, 1)
|
||||
# W /= noise_norm
|
||||
#
|
||||
# Sekihara 2008 says to use sqrt(diag(W_ug @ W_ug.T)), which is not
|
||||
# rotation invariant:
|
||||
if weight_norm in ("unit-noise-gain", "nai"):
|
||||
noise_norm = np.matmul(W, W.swapaxes(-2, -1).conj()).real
|
||||
noise_norm = np.reshape( # np.diag operation over last two axes
|
||||
noise_norm, (n_sources, -1, 1)
|
||||
)[:, :: n_orient + 1]
|
||||
np.sqrt(noise_norm, out=noise_norm)
|
||||
noise_norm[noise_norm == 0] = np.inf
|
||||
assert noise_norm.shape == (n_sources, n_orient, 1)
|
||||
W /= noise_norm
|
||||
else:
|
||||
assert weight_norm == "unit-noise-gain-invariant"
|
||||
# Here we use sqrtm. The shortcut:
|
||||
#
|
||||
# use = W
|
||||
#
|
||||
# ... does not match the direct route (it is rotated!), so we'll
|
||||
# use the direct one to match FieldTrip:
|
||||
use = bf_numer
|
||||
inner = np.matmul(use, use.swapaxes(-2, -1).conj())
|
||||
W = np.matmul(_sym_mat_pow(inner, -0.5), use)
|
||||
noise_norm = 1.0
|
||||
|
||||
if weight_norm == "nai":
|
||||
# Estimate noise level based on covariance matrix, taking the
|
||||
# first eigenvalue that falls outside the signal subspace or the
|
||||
# loading factor used during regularization, whichever is largest.
|
||||
if rank > len(Cm):
|
||||
# Covariance matrix is full rank, no noise subspace!
|
||||
# Use the loading factor as noise ceiling.
|
||||
if loading_factor == 0:
|
||||
raise RuntimeError(
|
||||
"Cannot compute noise subspace with a full-rank "
|
||||
"covariance matrix and no regularization. Try "
|
||||
"manually specifying the rank of the covariance "
|
||||
"matrix or using regularization."
|
||||
)
|
||||
noise = loading_factor
|
||||
else:
|
||||
noise, _ = np.linalg.eigh(Cm)
|
||||
noise = noise[-rank]
|
||||
noise = max(noise, loading_factor)
|
||||
W /= np.sqrt(noise)
|
||||
|
||||
W = W.reshape(n_sources * n_orient, n_channels)
|
||||
logger.info("Filter computation complete")
|
||||
return W, max_power_ori
|
||||
|
||||
|
||||
def _compute_power(Cm, W, n_orient):
|
||||
"""Use beamformer filters to compute source power.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
Cm : ndarray, shape (n_channels, n_channels)
|
||||
Data covariance matrix or CSD matrix.
|
||||
W : ndarray, shape (nvertices*norient, nchannels)
|
||||
Beamformer weights.
|
||||
|
||||
Returns
|
||||
-------
|
||||
power : ndarray, shape (nvertices,)
|
||||
Source power.
|
||||
"""
|
||||
n_sources = W.shape[0] // n_orient
|
||||
|
||||
Wk = W.reshape(n_sources, n_orient, W.shape[1])
|
||||
source_power = np.trace(
|
||||
(Wk @ Cm @ Wk.conj().transpose(0, 2, 1)).real, axis1=1, axis2=2
|
||||
)
|
||||
|
||||
return source_power
|
||||
|
||||
|
||||
class Beamformer(dict):
|
||||
"""A computed beamformer.
|
||||
|
||||
Notes
|
||||
-----
|
||||
.. versionadded:: 0.17
|
||||
"""
|
||||
|
||||
def copy(self):
|
||||
"""Copy the beamformer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
beamformer : instance of Beamformer
|
||||
A deep copy of the beamformer.
|
||||
"""
|
||||
return deepcopy(self)
|
||||
|
||||
def __repr__(self): # noqa: D105
|
||||
n_verts = sum(len(v) for v in self["vertices"])
|
||||
n_channels = len(self["ch_names"])
|
||||
if self["subject"] is None:
|
||||
subject = "unknown"
|
||||
else:
|
||||
subject = f'"{self["subject"]}"'
|
||||
out = "<Beamformer | {}, subject {}, {} vert, {} ch".format(
|
||||
self["kind"],
|
||||
subject,
|
||||
n_verts,
|
||||
n_channels,
|
||||
)
|
||||
if self["pick_ori"] is not None:
|
||||
out += f', {self["pick_ori"]} ori'
|
||||
if self["weight_norm"] is not None:
|
||||
out += f', {self["weight_norm"]} norm'
|
||||
if self.get("inversion") is not None:
|
||||
out += f', {self["inversion"]} inversion'
|
||||
if "rank" in self:
|
||||
out += f', rank {self["rank"]}'
|
||||
out += ">"
|
||||
return out
|
||||
|
||||
@verbose
|
||||
def save(self, fname, overwrite=False, verbose=None):
|
||||
"""Save the beamformer filter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fname : path-like
|
||||
The filename to use to write the HDF5 data.
|
||||
Should end in ``'-lcmv.h5'`` or ``'-dics.h5'``.
|
||||
%(overwrite)s
|
||||
%(verbose)s
|
||||
"""
|
||||
_, write_hdf5 = _import_h5io_funcs()
|
||||
|
||||
ending = f'-{self["kind"].lower()}.h5'
|
||||
check_fname(fname, self["kind"], (ending,))
|
||||
csd_orig = None
|
||||
try:
|
||||
if "csd" in self:
|
||||
csd_orig = self["csd"]
|
||||
self["csd"] = self["csd"].__getstate__()
|
||||
write_hdf5(fname, self, overwrite=overwrite, title="mnepython")
|
||||
finally:
|
||||
if csd_orig is not None:
|
||||
self["csd"] = csd_orig
|
||||
|
||||
|
||||
def read_beamformer(fname):
|
||||
"""Read a beamformer filter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fname : path-like
|
||||
The filename of the HDF5 file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
filter : instance of Beamformer
|
||||
The beamformer filter.
|
||||
"""
|
||||
read_hdf5, _ = _import_h5io_funcs()
|
||||
beamformer = read_hdf5(fname, title="mnepython")
|
||||
if "csd" in beamformer:
|
||||
beamformer["csd"] = CrossSpectralDensity(**beamformer["csd"])
|
||||
# h5io seems to cast `bool` to `int` on round-trip, probably a bug
|
||||
# we should fix at some point (if possible -- could be HDF5 limitation)
|
||||
for key in ("normalize_fwd", "is_free_ori", "is_ssp"):
|
||||
if key in beamformer:
|
||||
beamformer[key] = bool(beamformer[key])
|
||||
for key in ("data_cov", "noise_cov"):
|
||||
if beamformer.get(key) is not None:
|
||||
for pi, p in enumerate(beamformer[key]["projs"]):
|
||||
p = Projection(**p)
|
||||
p["active"] = bool(p["active"])
|
||||
beamformer[key]["projs"][pi] = p
|
||||
beamformer[key] = Covariance(
|
||||
*[
|
||||
beamformer[key].get(arg)
|
||||
for arg in (
|
||||
"data",
|
||||
"names",
|
||||
"bads",
|
||||
"projs",
|
||||
"nfree",
|
||||
"eig",
|
||||
"eigvec",
|
||||
"method",
|
||||
"loglik",
|
||||
)
|
||||
]
|
||||
)
|
||||
return Beamformer(beamformer)
|
||||
|
||||
|
||||
def _proj_whiten_data(M, proj, filters):
|
||||
if filters.get("is_ssp", True):
|
||||
# check whether data and filter projs match
|
||||
_check_proj_match(proj, filters)
|
||||
if filters["whitener"] is None:
|
||||
M = np.dot(filters["proj"], M)
|
||||
|
||||
if filters["whitener"] is not None:
|
||||
M = np.dot(filters["whitener"], M)
|
||||
return M
|
||||
648
mne/beamformer/_dics.py
Normal file
648
mne/beamformer/_dics.py
Normal file
@@ -0,0 +1,648 @@
|
||||
"""Dynamic Imaging of Coherent Sources (DICS)."""
|
||||
|
||||
# Authors: The MNE-Python contributors.
|
||||
# License: BSD-3-Clause
|
||||
# Copyright the MNE-Python contributors.
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .._fiff.pick import pick_channels, pick_info
|
||||
from ..channels import equalize_channels
|
||||
from ..forward import _subject_from_forward
|
||||
from ..minimum_norm.inverse import _check_depth, _check_reference, combine_xyz
|
||||
from ..rank import compute_rank
|
||||
from ..source_estimate import _get_src_type, _make_stc
|
||||
from ..time_frequency import EpochsTFR
|
||||
from ..time_frequency.tfr import _check_tfr_complex
|
||||
from ..utils import (
|
||||
_check_channels_spatial_filter,
|
||||
_check_one_ch_type,
|
||||
_check_option,
|
||||
_check_rank,
|
||||
_validate_type,
|
||||
logger,
|
||||
verbose,
|
||||
warn,
|
||||
)
|
||||
from ._compute_beamformer import (
|
||||
Beamformer,
|
||||
_check_src_type,
|
||||
_compute_beamformer,
|
||||
_compute_power,
|
||||
_prepare_beamformer_input,
|
||||
_proj_whiten_data,
|
||||
)
|
||||
|
||||
|
||||
@verbose
|
||||
def make_dics(
|
||||
info,
|
||||
forward,
|
||||
csd,
|
||||
reg=0.05,
|
||||
noise_csd=None,
|
||||
label=None,
|
||||
pick_ori=None,
|
||||
rank=None,
|
||||
weight_norm=None,
|
||||
reduce_rank=False,
|
||||
depth=1.0,
|
||||
real_filter=True,
|
||||
inversion="matrix",
|
||||
verbose=None,
|
||||
):
|
||||
"""Compute a Dynamic Imaging of Coherent Sources (DICS) spatial filter.
|
||||
|
||||
This is a beamformer filter that can be used to estimate the source power
|
||||
at a specific frequency range :footcite:`GrossEtAl2001`. It does this by
|
||||
constructing a spatial filter for each source point.
|
||||
The computation of these filters is very similar to those of the LCMV
|
||||
beamformer (:func:`make_lcmv`), but instead of operating on a covariance
|
||||
matrix, the CSD matrix is used. When applying these filters to a CSD matrix
|
||||
(see :func:`apply_dics_csd`), the source power can be estimated for each
|
||||
source point.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
%(info_not_none)s
|
||||
forward : instance of Forward
|
||||
Forward operator.
|
||||
csd : instance of CrossSpectralDensity
|
||||
The data cross-spectral density (CSD) matrices. A source estimate is
|
||||
performed for each frequency or frequency-bin defined in the CSD
|
||||
object.
|
||||
reg : float
|
||||
The regularization to apply to the cross-spectral density before
|
||||
computing the inverse.
|
||||
noise_csd : instance of CrossSpectralDensity | None
|
||||
Noise cross-spectral density (CSD) matrices. If provided, whitening
|
||||
will be done. The noise CSDs need to have been computed for the same
|
||||
frequencies as the data CSDs. Providing noise CSDs is mandatory if you
|
||||
mix sensor types, e.g. gradiometers with magnetometers or EEG with
|
||||
MEG.
|
||||
|
||||
.. versionadded:: 0.20
|
||||
label : Label | None
|
||||
Restricts the solution to a given label.
|
||||
%(pick_ori_bf)s
|
||||
%(rank_none)s
|
||||
|
||||
.. versionadded:: 0.17
|
||||
%(weight_norm)s
|
||||
|
||||
Defaults to ``None``, in which case no normalization is performed.
|
||||
%(reduce_rank)s
|
||||
%(depth)s
|
||||
real_filter : bool
|
||||
If ``True``, take only the real part of the cross-spectral-density
|
||||
matrices to compute real filters.
|
||||
|
||||
.. versionchanged:: 0.23
|
||||
Version 0.23 an earlier used ``real_filter=False`` as the default,
|
||||
as of version 0.24 ``True`` is the default.
|
||||
%(inversion_bf)s
|
||||
|
||||
.. versionchanged:: 0.21
|
||||
Default changed to ``'matrix'``.
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
filters : instance of Beamformer
|
||||
Dictionary containing filter weights from DICS beamformer.
|
||||
Contains the following keys:
|
||||
|
||||
'kind' : str
|
||||
The type of beamformer, in this case 'DICS'.
|
||||
'weights' : ndarray, shape (n_frequencies, n_weights)
|
||||
For each frequency, the filter weights of the beamformer.
|
||||
'csd' : instance of CrossSpectralDensity
|
||||
The data cross-spectral density matrices used to compute the
|
||||
beamformer.
|
||||
'ch_names' : list of str
|
||||
Channels used to compute the beamformer.
|
||||
'proj' : ndarray, shape (n_channels, n_channels)
|
||||
Projections used to compute the beamformer.
|
||||
'vertices' : list of ndarray
|
||||
Vertices for which the filter weights were computed.
|
||||
'n_sources' : int
|
||||
Number of source location for which the filter weight were
|
||||
computed.
|
||||
'subject' : str
|
||||
The subject ID.
|
||||
'pick-ori' : None | 'max-power' | 'normal' | 'vector'
|
||||
The orientation in which the beamformer filters were computed.
|
||||
'inversion' : 'single' | 'matrix'
|
||||
Whether the spatial filters were computed for each dipole
|
||||
separately or jointly for all dipoles at each vertex using a
|
||||
matrix inversion.
|
||||
'weight_norm' : None | 'unit-noise-gain'
|
||||
The normalization of the weights.
|
||||
'src_type' : str
|
||||
Type of source space.
|
||||
'source_nn' : ndarray, shape (n_sources, 3)
|
||||
For each source location, the surface normal.
|
||||
'is_free_ori' : bool
|
||||
Whether the filter was computed in a fixed direction
|
||||
(pick_ori='max-power', pick_ori='normal') or not.
|
||||
'whitener' : None | ndarray, shape (n_channels, n_channels)
|
||||
Whitening matrix, provided if whitening was applied to the
|
||||
covariance matrix and leadfield during computation of the
|
||||
beamformer weights.
|
||||
'max-power-ori' : ndarray, shape (n_sources, 3) | None
|
||||
When pick_ori='max-power', this fields contains the estimated
|
||||
direction of maximum power at each source location.
|
||||
|
||||
See Also
|
||||
--------
|
||||
apply_dics_csd
|
||||
|
||||
Notes
|
||||
-----
|
||||
The original reference is :footcite:`GrossEtAl2001`. See
|
||||
:footcite:`vanVlietEtAl2018` for a tutorial style paper on the topic.
|
||||
|
||||
The DICS beamformer is very similar to the LCMV (:func:`make_lcmv`)
|
||||
beamformer and many of the parameters are shared. However,
|
||||
:func:`make_dics` and :func:`make_lcmv` currently have different defaults
|
||||
for these parameters, which were settled on separately through extensive
|
||||
practical use case testing (but not necessarily exhaustive parameter space
|
||||
searching), and it remains to be seen how functionally interchangeable they
|
||||
could be.
|
||||
|
||||
The default setting reproduce the DICS beamformer as described in
|
||||
:footcite:`vanVlietEtAl2018`::
|
||||
|
||||
inversion='single', weight_norm=None, depth=1.
|
||||
|
||||
To use the :func:`make_lcmv` defaults, use::
|
||||
|
||||
inversion='matrix', weight_norm='unit-noise-gain-invariant', depth=None
|
||||
|
||||
For more information about ``real_filter``, see the
|
||||
supplemental information from :footcite:`HippEtAl2011`.
|
||||
|
||||
References
|
||||
----------
|
||||
.. footbibliography::
|
||||
""" # noqa: E501
|
||||
rank = _check_rank(rank)
|
||||
_check_option("pick_ori", pick_ori, [None, "vector", "normal", "max-power"])
|
||||
_check_option("inversion", inversion, ["single", "matrix"])
|
||||
_validate_type(weight_norm, (str, None), "weight_norm")
|
||||
|
||||
frequencies = [np.mean(freq_bin) for freq_bin in csd.frequencies]
|
||||
n_freqs = len(frequencies)
|
||||
|
||||
_, _, allow_mismatch = _check_one_ch_type("dics", info, forward, csd, noise_csd)
|
||||
# remove bads so that equalize_channels only keeps all good
|
||||
info = pick_info(info, pick_channels(info["ch_names"], [], info["bads"]))
|
||||
info, forward, csd = equalize_channels([info, forward, csd])
|
||||
|
||||
csd, noise_csd = _prepare_noise_csd(csd, noise_csd, real_filter)
|
||||
|
||||
depth = _check_depth(depth, "depth_sparse")
|
||||
if inversion == "single":
|
||||
depth["combine_xyz"] = False
|
||||
|
||||
(
|
||||
is_free_ori,
|
||||
info,
|
||||
proj,
|
||||
vertices,
|
||||
G,
|
||||
whitener,
|
||||
nn,
|
||||
orient_std,
|
||||
) = _prepare_beamformer_input(
|
||||
info,
|
||||
forward,
|
||||
label,
|
||||
pick_ori,
|
||||
noise_cov=noise_csd,
|
||||
rank=rank,
|
||||
pca=False,
|
||||
**depth,
|
||||
)
|
||||
|
||||
# Compute ranks
|
||||
csd_int_rank = []
|
||||
if not allow_mismatch:
|
||||
noise_rank = compute_rank(noise_csd, info=info, rank=rank)
|
||||
for i in range(len(frequencies)):
|
||||
csd_rank = compute_rank(
|
||||
csd.get_data(index=i, as_cov=True), info=info, rank=rank
|
||||
)
|
||||
if not allow_mismatch:
|
||||
for key in csd_rank:
|
||||
if key not in noise_rank or csd_rank[key] != noise_rank[key]:
|
||||
raise ValueError(
|
||||
f"{key} data rank ({csd_rank[key]}) did not match the noise "
|
||||
f"rank ({noise_rank.get(key, None)})"
|
||||
)
|
||||
csd_int_rank.append(sum(csd_rank.values()))
|
||||
|
||||
del noise_csd
|
||||
ch_names = list(info["ch_names"])
|
||||
|
||||
logger.info("Computing DICS spatial filters...")
|
||||
Ws = []
|
||||
max_oris = []
|
||||
for i, freq in enumerate(frequencies):
|
||||
if n_freqs > 1:
|
||||
logger.info(
|
||||
" computing DICS spatial filter at "
|
||||
f"{round(freq, 2)} Hz ({i + 1}/{n_freqs})"
|
||||
)
|
||||
|
||||
Cm = csd.get_data(index=i)
|
||||
|
||||
# XXX: Weird that real_filter happens *before* whitening, which could
|
||||
# make things complex again...?
|
||||
if real_filter:
|
||||
Cm = Cm.real
|
||||
|
||||
# compute spatial filter
|
||||
n_orient = 3 if is_free_ori else 1
|
||||
W, max_power_ori = _compute_beamformer(
|
||||
G,
|
||||
Cm,
|
||||
reg,
|
||||
n_orient,
|
||||
weight_norm,
|
||||
pick_ori,
|
||||
reduce_rank,
|
||||
rank=csd_int_rank[i],
|
||||
inversion=inversion,
|
||||
nn=nn,
|
||||
orient_std=orient_std,
|
||||
whitener=whitener,
|
||||
)
|
||||
Ws.append(W)
|
||||
max_oris.append(max_power_ori)
|
||||
|
||||
Ws = np.array(Ws)
|
||||
if pick_ori == "max-power":
|
||||
max_oris = np.array(max_oris)
|
||||
else:
|
||||
max_oris = None
|
||||
|
||||
src_type = _get_src_type(forward["src"], vertices)
|
||||
subject = _subject_from_forward(forward)
|
||||
is_free_ori = is_free_ori if pick_ori in [None, "vector"] else False
|
||||
n_sources = np.sum([len(v) for v in vertices])
|
||||
|
||||
filters = Beamformer(
|
||||
kind="DICS",
|
||||
weights=Ws,
|
||||
csd=csd,
|
||||
ch_names=ch_names,
|
||||
proj=proj,
|
||||
vertices=vertices,
|
||||
n_sources=n_sources,
|
||||
subject=subject,
|
||||
pick_ori=pick_ori,
|
||||
inversion=inversion,
|
||||
weight_norm=weight_norm,
|
||||
src_type=src_type,
|
||||
source_nn=forward["source_nn"].copy(),
|
||||
is_free_ori=is_free_ori,
|
||||
whitener=whitener,
|
||||
max_power_ori=max_oris,
|
||||
)
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
def _prepare_noise_csd(csd, noise_csd, real_filter):
|
||||
if noise_csd is not None:
|
||||
csd, noise_csd = equalize_channels([csd, noise_csd])
|
||||
# Use the same noise CSD for all frequencies
|
||||
if len(noise_csd.frequencies) > 1:
|
||||
noise_csd = noise_csd.mean()
|
||||
noise_csd = noise_csd.get_data(as_cov=True)
|
||||
if real_filter:
|
||||
noise_csd["data"] = noise_csd["data"].real
|
||||
return csd, noise_csd
|
||||
|
||||
|
||||
def _apply_dics(data, filters, info, tmin, tfr=False):
|
||||
"""Apply DICS spatial filter to data for source reconstruction."""
|
||||
if isinstance(data, np.ndarray) and data.ndim == (2 + tfr):
|
||||
data = [data]
|
||||
one_epoch = True
|
||||
else:
|
||||
one_epoch = False
|
||||
|
||||
Ws = filters["weights"]
|
||||
one_freq = len(Ws) == 1
|
||||
|
||||
subject = filters["subject"]
|
||||
# compatibility with 0.16, add src_type as None if not present:
|
||||
filters, warn_text = _check_src_type(filters)
|
||||
|
||||
for i, M in enumerate(data):
|
||||
if not one_epoch:
|
||||
logger.info(f"Processing epoch : {i + 1}")
|
||||
|
||||
# Apply SSPs
|
||||
if not tfr: # save computation, only compute once
|
||||
M_w = _proj_whiten_data(M, info["projs"], filters)
|
||||
|
||||
stcs = []
|
||||
for j, W in enumerate(Ws):
|
||||
if tfr: # must compute for each frequency
|
||||
M_w = _proj_whiten_data(M[:, j], info["projs"], filters)
|
||||
|
||||
# project to source space using beamformer weights
|
||||
sol = np.dot(W, M_w)
|
||||
|
||||
if filters["is_free_ori"] and filters["pick_ori"] != "vector":
|
||||
logger.info("combining the current components...")
|
||||
sol = combine_xyz(sol)
|
||||
|
||||
tstep = 1.0 / info["sfreq"]
|
||||
|
||||
stcs.append(
|
||||
_make_stc(
|
||||
sol,
|
||||
vertices=filters["vertices"],
|
||||
src_type=filters["src_type"],
|
||||
tmin=tmin,
|
||||
tstep=tstep,
|
||||
subject=subject,
|
||||
vector=(filters["pick_ori"] == "vector"),
|
||||
source_nn=filters["source_nn"],
|
||||
warn_text=warn_text,
|
||||
)
|
||||
)
|
||||
if one_freq:
|
||||
yield stcs[0]
|
||||
else:
|
||||
yield stcs
|
||||
|
||||
logger.info("[done]")
|
||||
|
||||
|
||||
@verbose
|
||||
def apply_dics(evoked, filters, verbose=None):
|
||||
"""Apply Dynamic Imaging of Coherent Sources (DICS) beamformer weights.
|
||||
|
||||
Apply Dynamic Imaging of Coherent Sources (DICS) beamformer weights
|
||||
on evoked data.
|
||||
|
||||
.. warning:: The result of this function is meant as an intermediate step
|
||||
for further processing (such as computing connectivity). If
|
||||
you are interested in estimating source time courses, use an
|
||||
LCMV beamformer (:func:`make_lcmv`, :func:`apply_lcmv`)
|
||||
instead. If you are interested in estimating spectral power at
|
||||
the source level, use :func:`apply_dics_csd`.
|
||||
.. warning:: This implementation has not been heavily tested so please
|
||||
report any issues or suggestions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
evoked : Evoked
|
||||
Evoked data to apply the DICS beamformer weights to.
|
||||
filters : instance of Beamformer
|
||||
DICS spatial filter (beamformer weights)
|
||||
Filter weights returned from :func:`make_dics`.
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
stc : SourceEstimate | VolSourceEstimate | list
|
||||
Source time courses. If the DICS beamformer has been computed for more
|
||||
than one frequency, a list is returned containing for each frequency
|
||||
the corresponding time courses.
|
||||
|
||||
See Also
|
||||
--------
|
||||
apply_dics_epochs
|
||||
apply_dics_tfr_epochs
|
||||
apply_dics_csd
|
||||
""" # noqa: E501
|
||||
_check_reference(evoked)
|
||||
|
||||
info = evoked.info
|
||||
data = evoked.data
|
||||
tmin = evoked.times[0]
|
||||
|
||||
sel = _check_channels_spatial_filter(evoked.ch_names, filters)
|
||||
data = data[sel]
|
||||
|
||||
stc = _apply_dics(data=data, filters=filters, info=info, tmin=tmin)
|
||||
|
||||
return next(stc)
|
||||
|
||||
|
||||
@verbose
|
||||
def apply_dics_epochs(epochs, filters, return_generator=False, verbose=None):
|
||||
"""Apply Dynamic Imaging of Coherent Sources (DICS) beamformer weights.
|
||||
|
||||
Apply Dynamic Imaging of Coherent Sources (DICS) beamformer weights
|
||||
on single trial data.
|
||||
|
||||
.. warning:: The result of this function is meant as an intermediate step
|
||||
for further processing (such as computing connectivity). If
|
||||
you are interested in estimating source time courses, use an
|
||||
LCMV beamformer (:func:`make_lcmv`, :func:`apply_lcmv`)
|
||||
instead. If you are interested in estimating spectral power at
|
||||
the source level, use :func:`apply_dics_csd`.
|
||||
.. warning:: This implementation has not been heavily tested so please
|
||||
report any issue or suggestions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
epochs : Epochs
|
||||
Single trial epochs.
|
||||
filters : instance of Beamformer
|
||||
DICS spatial filter (beamformer weights)
|
||||
Filter weights returned from :func:`make_dics`. The DICS filters must
|
||||
have been computed for a single frequency only.
|
||||
return_generator : bool
|
||||
Return a generator object instead of a list. This allows iterating
|
||||
over the stcs without having to keep them all in memory.
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
stc: list | generator of (SourceEstimate | VolSourceEstimate)
|
||||
The source estimates for all epochs.
|
||||
|
||||
See Also
|
||||
--------
|
||||
apply_dics
|
||||
apply_dics_tfr_epochs
|
||||
apply_dics_csd
|
||||
"""
|
||||
_check_reference(epochs)
|
||||
|
||||
if len(filters["weights"]) > 1:
|
||||
raise ValueError(
|
||||
"This function only works on DICS beamformer weights that have "
|
||||
"been computed for a single frequency. When calling make_dics(), "
|
||||
"make sure to use a CSD object with only a single frequency (or "
|
||||
"frequency-bin) defined."
|
||||
)
|
||||
|
||||
info = epochs.info
|
||||
tmin = epochs.times[0]
|
||||
|
||||
sel = _check_channels_spatial_filter(epochs.ch_names, filters)
|
||||
data = epochs.get_data(sel)
|
||||
|
||||
stcs = _apply_dics(data=data, filters=filters, info=info, tmin=tmin)
|
||||
|
||||
if not return_generator:
|
||||
stcs = list(stcs)
|
||||
|
||||
return stcs
|
||||
|
||||
|
||||
@verbose
|
||||
def apply_dics_tfr_epochs(epochs_tfr, filters, return_generator=False, verbose=None):
|
||||
"""Apply Dynamic Imaging of Coherent Sources (DICS) beamformer weights.
|
||||
|
||||
Apply Dynamic Imaging of Coherent Sources (DICS) beamformer weights
|
||||
on single trial time-frequency data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
epochs_tfr : EpochsTFR
|
||||
Single trial time-frequency epochs.
|
||||
filters : instance of Beamformer
|
||||
DICS spatial filter (beamformer weights)
|
||||
Filter weights returned from :func:`make_dics`.
|
||||
return_generator : bool
|
||||
Return a generator object instead of a list. This allows iterating
|
||||
over the stcs without having to keep them all in memory.
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
stcs : list of list of (SourceEstimate | VectorSourceEstimate | VolSourceEstimate)
|
||||
The source estimates for all epochs (outside list) and for
|
||||
all frequencies (inside list).
|
||||
|
||||
See Also
|
||||
--------
|
||||
apply_dics
|
||||
apply_dics_epochs
|
||||
apply_dics_csd
|
||||
""" # noqa E501
|
||||
_validate_type(epochs_tfr, EpochsTFR)
|
||||
_check_tfr_complex(epochs_tfr)
|
||||
|
||||
if filters["pick_ori"] == "vector":
|
||||
warn(
|
||||
"Using a vector solution to compute power will lead to "
|
||||
"inaccurate directions (only in the first quadrent) "
|
||||
"because power is a strictly positive (squared) metric. "
|
||||
"Using singular value decomposition (SVD) to determine "
|
||||
"the direction is not yet supported in MNE."
|
||||
)
|
||||
|
||||
sel = _check_channels_spatial_filter(epochs_tfr.ch_names, filters)
|
||||
data = epochs_tfr.data[:, sel, :, :]
|
||||
|
||||
stcs = _apply_dics(data, filters, epochs_tfr.info, epochs_tfr.tmin, tfr=True)
|
||||
if not return_generator:
|
||||
stcs = [[stc for stc in tfr_stcs] for tfr_stcs in stcs]
|
||||
return stcs
|
||||
|
||||
|
||||
@verbose
|
||||
def apply_dics_csd(csd, filters, verbose=None):
|
||||
"""Apply Dynamic Imaging of Coherent Sources (DICS) beamformer weights.
|
||||
|
||||
Apply a previously computed DICS beamformer to a cross-spectral density
|
||||
(CSD) object to estimate source power in time and frequency windows
|
||||
specified in the CSD object :footcite:`GrossEtAl2001`.
|
||||
|
||||
.. note:: Only power can computed from the cross-spectral density, not
|
||||
complex phase-amplitude, so vector DICS filters will be
|
||||
converted to scalar source estimates since power is strictly
|
||||
positive and so 3D directions cannot be combined meaningfully
|
||||
(the direction would be confined to the positive quadrant).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
csd : instance of CrossSpectralDensity
|
||||
The data cross-spectral density (CSD) matrices. A source estimate is
|
||||
performed for each frequency or frequency-bin defined in the CSD
|
||||
object.
|
||||
filters : instance of Beamformer
|
||||
DICS spatial filter (beamformer weights)
|
||||
Filter weights returned from `make_dics`.
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
stc : SourceEstimate
|
||||
Source power with frequency instead of time.
|
||||
frequencies : list of float
|
||||
The frequencies for which the source power has been computed. If the
|
||||
data CSD object defines frequency-bins instead of exact frequencies,
|
||||
the mean of each bin is returned.
|
||||
|
||||
See Also
|
||||
--------
|
||||
apply_dics
|
||||
apply_dics_epochs
|
||||
apply_dics_tfr_epochs
|
||||
|
||||
References
|
||||
----------
|
||||
.. footbibliography::
|
||||
""" # noqa: E501
|
||||
ch_names = filters["ch_names"]
|
||||
vertices = filters["vertices"]
|
||||
n_orient = 3 if filters["is_free_ori"] else 1
|
||||
subject = filters["subject"]
|
||||
whitener = filters["whitener"]
|
||||
n_sources = filters["n_sources"]
|
||||
|
||||
# If CSD is summed over multiple frequencies, take the average frequency
|
||||
frequencies = [np.mean(dfreq) for dfreq in csd.frequencies]
|
||||
n_freqs = len(frequencies)
|
||||
|
||||
source_power = np.zeros((n_sources, len(csd.frequencies)))
|
||||
|
||||
# Ensure the CSD is in the same order as the weights
|
||||
csd_picks = [csd.ch_names.index(ch) for ch in ch_names]
|
||||
|
||||
logger.info("Computing DICS source power...")
|
||||
for i, freq in enumerate(frequencies):
|
||||
if n_freqs > 1:
|
||||
logger.info(
|
||||
" applying DICS spatial filter at "
|
||||
f"{round(freq, 2)} Hz ({i + 1}/{n_freqs})"
|
||||
)
|
||||
|
||||
Cm = csd.get_data(index=i)
|
||||
Cm = Cm[csd_picks, :][:, csd_picks]
|
||||
W = filters["weights"][i]
|
||||
|
||||
# Whiten the CSD
|
||||
Cm = np.dot(whitener, np.dot(Cm, whitener.conj().T))
|
||||
|
||||
source_power[:, i] = _compute_power(Cm, W, n_orient)
|
||||
|
||||
logger.info("[done]")
|
||||
|
||||
# compatibility with 0.16, add src_type as None if not present:
|
||||
filters, warn_text = _check_src_type(filters)
|
||||
|
||||
return (
|
||||
_make_stc(
|
||||
source_power,
|
||||
vertices=vertices,
|
||||
src_type=filters["src_type"],
|
||||
tmin=0.0,
|
||||
tstep=1.0,
|
||||
subject=subject,
|
||||
warn_text=warn_text,
|
||||
),
|
||||
frequencies,
|
||||
)
|
||||
503
mne/beamformer/_lcmv.py
Normal file
503
mne/beamformer/_lcmv.py
Normal file
@@ -0,0 +1,503 @@
|
||||
"""Compute Linearly constrained minimum variance (LCMV) beamformer."""
|
||||
|
||||
# Authors: The MNE-Python contributors.
|
||||
# License: BSD-3-Clause
|
||||
# Copyright the MNE-Python contributors.
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .._fiff.meas_info import _simplify_info
|
||||
from .._fiff.pick import pick_channels_cov, pick_info
|
||||
from ..forward import _subject_from_forward
|
||||
from ..minimum_norm.inverse import _check_depth, _check_reference, combine_xyz
|
||||
from ..rank import compute_rank
|
||||
from ..source_estimate import _get_src_type, _make_stc
|
||||
from ..utils import (
|
||||
_check_channels_spatial_filter,
|
||||
_check_info_inv,
|
||||
_check_one_ch_type,
|
||||
logger,
|
||||
verbose,
|
||||
)
|
||||
from ._compute_beamformer import (
|
||||
Beamformer,
|
||||
_check_src_type,
|
||||
_compute_beamformer,
|
||||
_compute_power,
|
||||
_prepare_beamformer_input,
|
||||
_proj_whiten_data,
|
||||
)
|
||||
|
||||
|
||||
@verbose
|
||||
def make_lcmv(
|
||||
info,
|
||||
forward,
|
||||
data_cov,
|
||||
reg=0.05,
|
||||
noise_cov=None,
|
||||
label=None,
|
||||
pick_ori=None,
|
||||
rank="info",
|
||||
weight_norm="unit-noise-gain-invariant",
|
||||
reduce_rank=False,
|
||||
depth=None,
|
||||
inversion="matrix",
|
||||
verbose=None,
|
||||
):
|
||||
"""Compute LCMV spatial filter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
%(info_not_none)s
|
||||
Specifies the channels to include. Bad channels (in ``info['bads']``)
|
||||
are not used.
|
||||
forward : instance of Forward
|
||||
Forward operator.
|
||||
data_cov : instance of Covariance
|
||||
The data covariance.
|
||||
reg : float
|
||||
The regularization for the whitened data covariance.
|
||||
noise_cov : instance of Covariance
|
||||
The noise covariance. If provided, whitening will be done. Providing a
|
||||
noise covariance is mandatory if you mix sensor types, e.g.
|
||||
gradiometers with magnetometers or EEG with MEG.
|
||||
|
||||
.. note::
|
||||
If ``noise_cov`` is ``None`` and ``weight_norm='unit-noise-gain'``,
|
||||
the unit noise is assumed to be 1 in SI units, e.g., 1 T for
|
||||
magnetometers, 1 V for EEG, so resulting amplitudes will be tiny.
|
||||
Consider using :func:`mne.make_ad_hoc_cov` to provide a
|
||||
``noise_cov`` to set noise values that are more reasonable for
|
||||
neural data or using ``weight_norm='nai'`` for weight-normalized
|
||||
beamformer output that is scaled by a noise estimate.
|
||||
label : instance of Label
|
||||
Restricts the LCMV solution to a given label.
|
||||
%(pick_ori_bf)s
|
||||
|
||||
- ``'vector'``
|
||||
Keeps the currents for each direction separate
|
||||
%(rank_info)s
|
||||
%(weight_norm)s
|
||||
|
||||
Defaults to ``'unit-noise-gain-invariant'``.
|
||||
%(reduce_rank)s
|
||||
%(depth)s
|
||||
|
||||
.. versionadded:: 0.18
|
||||
%(inversion_bf)s
|
||||
|
||||
.. versionadded:: 0.21
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
filters : instance of Beamformer
|
||||
Dictionary containing filter weights from LCMV beamformer.
|
||||
Contains the following keys:
|
||||
|
||||
'kind' : str
|
||||
The type of beamformer, in this case 'LCMV'.
|
||||
'weights' : array
|
||||
The filter weights of the beamformer.
|
||||
'data_cov' : instance of Covariance
|
||||
The data covariance matrix used to compute the beamformer.
|
||||
'noise_cov' : instance of Covariance | None
|
||||
The noise covariance matrix used to compute the beamformer.
|
||||
'whitener' : None | ndarray, shape (n_channels, n_channels)
|
||||
Whitening matrix, provided if whitening was applied to the
|
||||
covariance matrix and leadfield during computation of the
|
||||
beamformer weights.
|
||||
'weight_norm' : str | None
|
||||
Type of weight normalization used to compute the filter
|
||||
weights.
|
||||
'pick-ori' : None | 'max-power' | 'normal' | 'vector'
|
||||
The orientation in which the beamformer filters were computed.
|
||||
'ch_names' : list of str
|
||||
Channels used to compute the beamformer.
|
||||
'proj' : array
|
||||
Projections used to compute the beamformer.
|
||||
'is_ssp' : bool
|
||||
If True, projections were applied prior to filter computation.
|
||||
'vertices' : list
|
||||
Vertices for which the filter weights were computed.
|
||||
'is_free_ori' : bool
|
||||
If True, the filter was computed with free source orientation.
|
||||
'n_sources' : int
|
||||
Number of source location for which the filter weight were
|
||||
computed.
|
||||
'src_type' : str
|
||||
Type of source space.
|
||||
'source_nn' : ndarray, shape (n_sources, 3)
|
||||
For each source location, the surface normal.
|
||||
'proj' : ndarray, shape (n_channels, n_channels)
|
||||
Projections used to compute the beamformer.
|
||||
'subject' : str
|
||||
The subject ID.
|
||||
'rank' : int
|
||||
The rank of the data covariance matrix used to compute the
|
||||
beamformer weights.
|
||||
'max-power-ori' : ndarray, shape (n_sources, 3) | None
|
||||
When pick_ori='max-power', this fields contains the estimated
|
||||
direction of maximum power at each source location.
|
||||
'inversion' : 'single' | 'matrix'
|
||||
Whether the spatial filters were computed for each dipole
|
||||
separately or jointly for all dipoles at each vertex using a
|
||||
matrix inversion.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The original reference is :footcite:`VanVeenEtAl1997`.
|
||||
|
||||
To obtain the Sekihara unit-noise-gain vector beamformer, you should use
|
||||
``weight_norm='unit-noise-gain', pick_ori='vector'`` followed by
|
||||
:meth:`vec_stc.project('pca', src) <mne.VectorSourceEstimate.project>`.
|
||||
|
||||
.. versionchanged:: 0.21
|
||||
The computations were extensively reworked, and the default for
|
||||
``weight_norm`` was set to ``'unit-noise-gain-invariant'``.
|
||||
|
||||
References
|
||||
----------
|
||||
.. footbibliography::
|
||||
"""
|
||||
# check number of sensor types present in the data and ensure a noise cov
|
||||
info = _simplify_info(info, keep=("proc_history",))
|
||||
noise_cov, _, allow_mismatch = _check_one_ch_type(
|
||||
"lcmv", info, forward, data_cov, noise_cov
|
||||
)
|
||||
# XXX we need this extra picking step (can't just rely on minimum norm's
|
||||
# because there can be a mismatch. Should probably add an extra arg to
|
||||
# _prepare_beamformer_input at some point (later)
|
||||
picks = _check_info_inv(info, forward, data_cov, noise_cov)
|
||||
info = pick_info(info, picks)
|
||||
data_rank = compute_rank(data_cov, rank=rank, info=info)
|
||||
noise_rank = compute_rank(noise_cov, rank=rank, info=info)
|
||||
for key in data_rank:
|
||||
if (
|
||||
key not in noise_rank or data_rank[key] != noise_rank[key]
|
||||
) and not allow_mismatch:
|
||||
raise ValueError(
|
||||
f"{key} data rank ({data_rank[key]}) did not match the noise rank ("
|
||||
f"{noise_rank.get(key, None)})"
|
||||
)
|
||||
del noise_rank
|
||||
rank = data_rank
|
||||
logger.info(f"Making LCMV beamformer with rank {rank}")
|
||||
del data_rank
|
||||
depth = _check_depth(depth, "depth_sparse")
|
||||
if inversion == "single":
|
||||
depth["combine_xyz"] = False
|
||||
|
||||
(
|
||||
is_free_ori,
|
||||
info,
|
||||
proj,
|
||||
vertno,
|
||||
G,
|
||||
whitener,
|
||||
nn,
|
||||
orient_std,
|
||||
) = _prepare_beamformer_input(
|
||||
info,
|
||||
forward,
|
||||
label,
|
||||
pick_ori,
|
||||
noise_cov=noise_cov,
|
||||
rank=rank,
|
||||
pca=False,
|
||||
**depth,
|
||||
)
|
||||
ch_names = list(info["ch_names"])
|
||||
|
||||
data_cov = pick_channels_cov(data_cov, include=ch_names)
|
||||
Cm = data_cov._get_square()
|
||||
if "estimator" in data_cov:
|
||||
del data_cov["estimator"]
|
||||
rank_int = sum(rank.values())
|
||||
del rank
|
||||
|
||||
# compute spatial filter
|
||||
n_orient = 3 if is_free_ori else 1
|
||||
W, max_power_ori = _compute_beamformer(
|
||||
G,
|
||||
Cm,
|
||||
reg,
|
||||
n_orient,
|
||||
weight_norm,
|
||||
pick_ori,
|
||||
reduce_rank,
|
||||
rank_int,
|
||||
inversion=inversion,
|
||||
nn=nn,
|
||||
orient_std=orient_std,
|
||||
whitener=whitener,
|
||||
)
|
||||
|
||||
# get src type to store with filters for _make_stc
|
||||
src_type = _get_src_type(forward["src"], vertno)
|
||||
|
||||
# get subject to store with filters
|
||||
subject_from = _subject_from_forward(forward)
|
||||
|
||||
# Is the computed beamformer a scalar or vector beamformer?
|
||||
is_free_ori = is_free_ori if pick_ori in [None, "vector"] else False
|
||||
is_ssp = bool(info["projs"])
|
||||
|
||||
filters = Beamformer(
|
||||
kind="LCMV",
|
||||
weights=W,
|
||||
data_cov=data_cov,
|
||||
noise_cov=noise_cov,
|
||||
whitener=whitener,
|
||||
weight_norm=weight_norm,
|
||||
pick_ori=pick_ori,
|
||||
ch_names=ch_names,
|
||||
proj=proj,
|
||||
is_ssp=is_ssp,
|
||||
vertices=vertno,
|
||||
is_free_ori=is_free_ori,
|
||||
n_sources=forward["nsource"],
|
||||
src_type=src_type,
|
||||
source_nn=forward["source_nn"].copy(),
|
||||
subject=subject_from,
|
||||
rank=rank_int,
|
||||
max_power_ori=max_power_ori,
|
||||
inversion=inversion,
|
||||
)
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
def _apply_lcmv(data, filters, info, tmin):
|
||||
"""Apply LCMV spatial filter to data for source reconstruction."""
|
||||
if isinstance(data, np.ndarray) and data.ndim == 2:
|
||||
data = [data]
|
||||
return_single = True
|
||||
else:
|
||||
return_single = False
|
||||
|
||||
W = filters["weights"]
|
||||
|
||||
for i, M in enumerate(data):
|
||||
if len(M) != len(filters["ch_names"]):
|
||||
raise ValueError("data and picks must have the same length")
|
||||
|
||||
if not return_single:
|
||||
logger.info(f"Processing epoch : {i + 1}")
|
||||
|
||||
M = _proj_whiten_data(M, info["projs"], filters)
|
||||
|
||||
# project to source space using beamformer weights
|
||||
vector = False
|
||||
if filters["is_free_ori"]:
|
||||
sol = np.dot(W, M)
|
||||
if filters["pick_ori"] == "vector":
|
||||
vector = True
|
||||
else:
|
||||
logger.info("combining the current components...")
|
||||
sol = combine_xyz(sol)
|
||||
else:
|
||||
# Linear inverse: do computation here or delayed
|
||||
if M.shape[0] < W.shape[0] and filters["pick_ori"] != "max-power":
|
||||
sol = (W, M)
|
||||
else:
|
||||
sol = np.dot(W, M)
|
||||
|
||||
tstep = 1.0 / info["sfreq"]
|
||||
|
||||
# compatibility with 0.16, add src_type as None if not present:
|
||||
filters, warn_text = _check_src_type(filters)
|
||||
|
||||
yield _make_stc(
|
||||
sol,
|
||||
vertices=filters["vertices"],
|
||||
tmin=tmin,
|
||||
tstep=tstep,
|
||||
subject=filters["subject"],
|
||||
vector=vector,
|
||||
source_nn=filters["source_nn"],
|
||||
src_type=filters["src_type"],
|
||||
warn_text=warn_text,
|
||||
)
|
||||
|
||||
logger.info("[done]")
|
||||
|
||||
|
||||
@verbose
|
||||
def apply_lcmv(evoked, filters, *, verbose=None):
|
||||
"""Apply Linearly Constrained Minimum Variance (LCMV) beamformer weights.
|
||||
|
||||
Apply Linearly Constrained Minimum Variance (LCMV) beamformer weights
|
||||
on evoked data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
evoked : Evoked
|
||||
Evoked data to invert.
|
||||
filters : instance of Beamformer
|
||||
LCMV spatial filter (beamformer weights).
|
||||
Filter weights returned from :func:`make_lcmv`.
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
stc : SourceEstimate | VolSourceEstimate | VectorSourceEstimate
|
||||
Source time courses.
|
||||
|
||||
See Also
|
||||
--------
|
||||
make_lcmv, apply_lcmv_raw, apply_lcmv_epochs, apply_lcmv_cov
|
||||
|
||||
Notes
|
||||
-----
|
||||
.. versionadded:: 0.18
|
||||
"""
|
||||
_check_reference(evoked)
|
||||
|
||||
info = evoked.info
|
||||
data = evoked.data
|
||||
tmin = evoked.times[0]
|
||||
|
||||
sel = _check_channels_spatial_filter(evoked.ch_names, filters)
|
||||
data = data[sel]
|
||||
|
||||
stc = _apply_lcmv(data=data, filters=filters, info=info, tmin=tmin)
|
||||
|
||||
return next(stc)
|
||||
|
||||
|
||||
@verbose
|
||||
def apply_lcmv_epochs(epochs, filters, *, return_generator=False, verbose=None):
|
||||
"""Apply Linearly Constrained Minimum Variance (LCMV) beamformer weights.
|
||||
|
||||
Apply Linearly Constrained Minimum Variance (LCMV) beamformer weights
|
||||
on single trial data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
epochs : Epochs
|
||||
Single trial epochs.
|
||||
filters : instance of Beamformer
|
||||
LCMV spatial filter (beamformer weights)
|
||||
Filter weights returned from :func:`make_lcmv`.
|
||||
return_generator : bool
|
||||
Return a generator object instead of a list. This allows iterating
|
||||
over the stcs without having to keep them all in memory.
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
stc: list | generator of (SourceEstimate | VolSourceEstimate)
|
||||
The source estimates for all epochs.
|
||||
|
||||
See Also
|
||||
--------
|
||||
make_lcmv, apply_lcmv_raw, apply_lcmv, apply_lcmv_cov
|
||||
"""
|
||||
_check_reference(epochs)
|
||||
|
||||
info = epochs.info
|
||||
tmin = epochs.times[0]
|
||||
|
||||
sel = _check_channels_spatial_filter(epochs.ch_names, filters)
|
||||
data = epochs.get_data(sel)
|
||||
stcs = _apply_lcmv(data=data, filters=filters, info=info, tmin=tmin)
|
||||
|
||||
if not return_generator:
|
||||
stcs = [s for s in stcs]
|
||||
|
||||
return stcs
|
||||
|
||||
|
||||
@verbose
|
||||
def apply_lcmv_raw(raw, filters, start=None, stop=None, *, verbose=None):
|
||||
"""Apply Linearly Constrained Minimum Variance (LCMV) beamformer weights.
|
||||
|
||||
Apply Linearly Constrained Minimum Variance (LCMV) beamformer weights
|
||||
on raw data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw : mne.io.Raw
|
||||
Raw data to invert.
|
||||
filters : instance of Beamformer
|
||||
LCMV spatial filter (beamformer weights).
|
||||
Filter weights returned from :func:`make_lcmv`.
|
||||
start : int
|
||||
Index of first time sample (index not time is seconds).
|
||||
stop : int
|
||||
Index of first time sample not to include (index not time is seconds).
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
stc : SourceEstimate | VolSourceEstimate
|
||||
Source time courses.
|
||||
|
||||
See Also
|
||||
--------
|
||||
make_lcmv, apply_lcmv_epochs, apply_lcmv, apply_lcmv_cov
|
||||
"""
|
||||
_check_reference(raw)
|
||||
|
||||
info = raw.info
|
||||
|
||||
sel = _check_channels_spatial_filter(raw.ch_names, filters)
|
||||
data, times = raw[sel, start:stop]
|
||||
tmin = times[0]
|
||||
|
||||
stc = _apply_lcmv(data=data, filters=filters, info=info, tmin=tmin)
|
||||
|
||||
return next(stc)
|
||||
|
||||
|
||||
@verbose
|
||||
def apply_lcmv_cov(data_cov, filters, verbose=None):
|
||||
"""Apply Linearly Constrained Minimum Variance (LCMV) beamformer weights.
|
||||
|
||||
Apply Linearly Constrained Minimum Variance (LCMV) beamformer weights
|
||||
to a data covariance matrix to estimate source power.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data_cov : instance of Covariance
|
||||
Data covariance matrix.
|
||||
filters : instance of Beamformer
|
||||
LCMV spatial filter (beamformer weights).
|
||||
Filter weights returned from :func:`make_lcmv`.
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
stc : SourceEstimate | VolSourceEstimate
|
||||
Source power.
|
||||
|
||||
See Also
|
||||
--------
|
||||
make_lcmv, apply_lcmv, apply_lcmv_epochs, apply_lcmv_raw
|
||||
"""
|
||||
sel = _check_channels_spatial_filter(data_cov.ch_names, filters)
|
||||
sel_names = [data_cov.ch_names[ii] for ii in sel]
|
||||
data_cov = pick_channels_cov(data_cov, sel_names)
|
||||
|
||||
n_orient = filters["weights"].shape[0] // filters["n_sources"]
|
||||
# Need to project and whiten along both dimensions
|
||||
data = _proj_whiten_data(data_cov["data"].T, data_cov["projs"], filters)
|
||||
data = _proj_whiten_data(data.T, data_cov["projs"], filters)
|
||||
del data_cov
|
||||
source_power = _compute_power(data, filters["weights"], n_orient)
|
||||
|
||||
# compatibility with 0.16, add src_type as None if not present:
|
||||
filters, warn_text = _check_src_type(filters)
|
||||
|
||||
return _make_stc(
|
||||
source_power,
|
||||
vertices=filters["vertices"],
|
||||
src_type=filters["src_type"],
|
||||
tmin=0.0,
|
||||
tstep=1.0,
|
||||
subject=filters["subject"],
|
||||
source_nn=filters["source_nn"],
|
||||
warn_text=warn_text,
|
||||
)
|
||||
315
mne/beamformer/_rap_music.py
Normal file
315
mne/beamformer/_rap_music.py
Normal file
@@ -0,0 +1,315 @@
|
||||
"""Compute a Recursively Applied and Projected MUltiple Signal Classification (RAP-MUSIC).""" # noqa
|
||||
|
||||
# Authors: The MNE-Python contributors.
|
||||
# License: BSD-3-Clause
|
||||
# Copyright the MNE-Python contributors.
|
||||
|
||||
import numpy as np
|
||||
from scipy import linalg
|
||||
|
||||
from .._fiff.pick import pick_channels_forward, pick_info
|
||||
from ..fixes import _safe_svd
|
||||
from ..forward import convert_forward_solution, is_fixed_orient
|
||||
from ..inverse_sparse.mxne_inverse import _make_dipoles_sparse
|
||||
from ..minimum_norm.inverse import _log_exp_var
|
||||
from ..utils import _check_info_inv, fill_doc, logger, verbose
|
||||
from ._compute_beamformer import _prepare_beamformer_input
|
||||
|
||||
|
||||
@fill_doc
|
||||
def _apply_rap_music(
|
||||
data, info, times, forward, noise_cov, n_dipoles=2, picks=None, use_trap=False
|
||||
):
|
||||
"""RAP-MUSIC or TRAP-MUSIC for evoked data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : array, shape (n_channels, n_times)
|
||||
Evoked data.
|
||||
%(info_not_none)s
|
||||
times : array
|
||||
Times.
|
||||
forward : instance of Forward
|
||||
Forward operator.
|
||||
noise_cov : instance of Covariance
|
||||
The noise covariance.
|
||||
n_dipoles : int
|
||||
The number of dipoles to estimate. The default value is 2.
|
||||
picks : list of int
|
||||
Caller ensures this is a list of int.
|
||||
use_trap : bool
|
||||
Use the TRAP-MUSIC variant if True (default False).
|
||||
|
||||
Returns
|
||||
-------
|
||||
dipoles : list of instances of Dipole
|
||||
The dipole fits.
|
||||
explained_data : array | None
|
||||
Data explained by the dipoles using a least square fitting with the
|
||||
selected active dipoles and their estimated orientation.
|
||||
"""
|
||||
info = pick_info(info, picks)
|
||||
del picks
|
||||
# things are much simpler if we avoid surface orientation
|
||||
align = forward["source_nn"].copy()
|
||||
if forward["surf_ori"] and not is_fixed_orient(forward):
|
||||
forward = convert_forward_solution(forward, surf_ori=False)
|
||||
is_free_ori, info, _, _, G, whitener, _, _ = _prepare_beamformer_input(
|
||||
info, forward, noise_cov=noise_cov, rank=None
|
||||
)
|
||||
forward = pick_channels_forward(forward, info["ch_names"], ordered=True)
|
||||
del info
|
||||
|
||||
# whiten the data (leadfield already whitened)
|
||||
M = np.dot(whitener, data)
|
||||
del data
|
||||
|
||||
_, eig_vectors = linalg.eigh(np.dot(M, M.T))
|
||||
phi_sig = eig_vectors[:, -n_dipoles:]
|
||||
|
||||
n_orient = 3 if is_free_ori else 1
|
||||
G.shape = (G.shape[0], -1, n_orient)
|
||||
gain = forward["sol"]["data"].copy()
|
||||
gain.shape = G.shape
|
||||
n_channels = G.shape[0]
|
||||
A = np.empty((n_channels, n_dipoles))
|
||||
gain_dip = np.empty((n_channels, n_dipoles))
|
||||
oris = np.empty((n_dipoles, 3))
|
||||
poss = np.empty((n_dipoles, 3))
|
||||
|
||||
G_proj = G.copy()
|
||||
phi_sig_proj = phi_sig.copy()
|
||||
|
||||
idxs = list()
|
||||
for k in range(n_dipoles):
|
||||
subcorr_max = -1.0
|
||||
source_idx, source_ori, source_pos = 0, [0, 0, 0], [0, 0, 0]
|
||||
for i_source in range(G.shape[1]):
|
||||
Gk = G_proj[:, i_source]
|
||||
subcorr, ori = _compute_subcorr(Gk, phi_sig_proj)
|
||||
if subcorr > subcorr_max:
|
||||
subcorr_max = subcorr
|
||||
source_idx = i_source
|
||||
source_ori = ori
|
||||
source_pos = forward["source_rr"][i_source]
|
||||
if n_orient == 3 and align is not None:
|
||||
surf_normal = forward["source_nn"][3 * i_source + 2]
|
||||
# make sure ori is aligned to the surface orientation
|
||||
source_ori *= np.sign(source_ori @ surf_normal) or 1.0
|
||||
if n_orient == 1:
|
||||
source_ori = forward["source_nn"][i_source]
|
||||
|
||||
idxs.append(source_idx)
|
||||
if n_orient == 3:
|
||||
Ak = np.dot(G[:, source_idx], source_ori)
|
||||
else:
|
||||
Ak = G[:, source_idx, 0]
|
||||
A[:, k] = Ak
|
||||
oris[k] = source_ori
|
||||
poss[k] = source_pos
|
||||
|
||||
logger.info(f"source {k + 1} found: p = {source_idx}")
|
||||
if n_orient == 3:
|
||||
logger.info("ori = {} {} {}".format(*tuple(oris[k])))
|
||||
|
||||
projection = _compute_proj(A[:, : k + 1])
|
||||
G_proj = np.einsum("ab,bso->aso", projection, G)
|
||||
phi_sig_proj = np.dot(projection, phi_sig)
|
||||
if use_trap:
|
||||
phi_sig_proj = phi_sig_proj[:, -(n_dipoles - k) :]
|
||||
del G, G_proj
|
||||
|
||||
sol = linalg.lstsq(A, M)[0]
|
||||
if n_orient == 3:
|
||||
X = sol[:, np.newaxis] * oris[:, :, np.newaxis]
|
||||
X.shape = (-1, len(times))
|
||||
else:
|
||||
X = sol
|
||||
|
||||
gain_active = gain[:, idxs]
|
||||
if n_orient == 3:
|
||||
gain_dip = (oris * gain_active).sum(-1)
|
||||
idxs = np.array(idxs)
|
||||
active_set = np.array([[3 * idxs, 3 * idxs + 1, 3 * idxs + 2]]).T.ravel()
|
||||
else:
|
||||
gain_dip = gain_active[:, :, 0]
|
||||
active_set = idxs
|
||||
gain_active = whitener @ gain_active.reshape(gain.shape[0], -1)
|
||||
assert gain_active.shape == (n_channels, X.shape[0])
|
||||
|
||||
explained_data = gain_dip @ sol
|
||||
M_estimate = whitener @ explained_data
|
||||
_log_exp_var(M, M_estimate)
|
||||
tstep = np.median(np.diff(times)) if len(times) > 1 else 1.0
|
||||
dipoles = _make_dipoles_sparse(
|
||||
X, active_set, forward, times[0], tstep, M, gain_active, active_is_idx=True
|
||||
)
|
||||
for dipole, ori in zip(dipoles, oris):
|
||||
signs = np.sign((dipole.ori * ori).sum(-1, keepdims=True))
|
||||
dipole.ori *= signs
|
||||
dipole.amplitude *= signs[:, 0]
|
||||
logger.info("[done]")
|
||||
return dipoles, explained_data
|
||||
|
||||
|
||||
def _compute_subcorr(G, phi_sig):
|
||||
"""Compute the subspace correlation."""
|
||||
Ug, Sg, Vg = _safe_svd(G, full_matrices=False)
|
||||
# Now we look at the actual rank of the forward fields
|
||||
# in G and handle the fact that it might be rank defficient
|
||||
# eg. when using MEG and a sphere model for which the
|
||||
# radial component will be truly 0.
|
||||
rank = np.sum(Sg > (Sg[0] * 1e-6))
|
||||
if rank == 0:
|
||||
return 0, np.zeros(len(G))
|
||||
rank = max(rank, 2) # rank cannot be 1
|
||||
Ug, Sg, Vg = Ug[:, :rank], Sg[:rank], Vg[:rank]
|
||||
tmp = np.dot(Ug.T.conjugate(), phi_sig)
|
||||
Uc, Sc, _ = _safe_svd(tmp, full_matrices=False)
|
||||
X = np.dot(Vg.T / Sg[None, :], Uc[:, 0]) # subcorr
|
||||
return Sc[0], X / np.linalg.norm(X)
|
||||
|
||||
|
||||
def _compute_proj(A):
|
||||
"""Compute the orthogonal projection operation for a manifold vector A."""
|
||||
U, _, _ = _safe_svd(A, full_matrices=False)
|
||||
return np.identity(A.shape[0]) - np.dot(U, U.T.conjugate())
|
||||
|
||||
|
||||
def _rap_music(evoked, forward, noise_cov, n_dipoles, return_residual, use_trap):
|
||||
"""RAP-/TRAP-MUSIC implementation."""
|
||||
info = evoked.info
|
||||
data = evoked.data
|
||||
times = evoked.times
|
||||
|
||||
picks = _check_info_inv(info, forward, data_cov=None, noise_cov=noise_cov)
|
||||
|
||||
data = data[picks]
|
||||
|
||||
dipoles, explained_data = _apply_rap_music(
|
||||
data, info, times, forward, noise_cov, n_dipoles, picks, use_trap
|
||||
)
|
||||
|
||||
if return_residual:
|
||||
residual = evoked.copy().pick([info["ch_names"][p] for p in picks])
|
||||
residual.data -= explained_data
|
||||
active_projs = [p for p in residual.info["projs"] if p["active"]]
|
||||
for p in active_projs:
|
||||
p["active"] = False
|
||||
residual.add_proj(active_projs, remove_existing=True)
|
||||
residual.apply_proj()
|
||||
return dipoles, residual
|
||||
else:
|
||||
return dipoles
|
||||
|
||||
|
||||
@verbose
|
||||
def rap_music(
|
||||
evoked,
|
||||
forward,
|
||||
noise_cov,
|
||||
n_dipoles=5,
|
||||
return_residual=False,
|
||||
*,
|
||||
verbose=None,
|
||||
):
|
||||
"""RAP-MUSIC source localization method.
|
||||
|
||||
Compute Recursively Applied and Projected MUltiple SIgnal Classification
|
||||
(RAP-MUSIC) :footcite:`MosherLeahy1999,MosherLeahy1996` on evoked data.
|
||||
|
||||
.. note:: The goodness of fit (GOF) of all the returned dipoles is the
|
||||
same and corresponds to the GOF of the full set of dipoles.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
evoked : instance of Evoked
|
||||
Evoked data to localize.
|
||||
forward : instance of Forward
|
||||
Forward operator.
|
||||
noise_cov : instance of Covariance
|
||||
The noise covariance.
|
||||
n_dipoles : int
|
||||
The number of dipoles to look for. The default value is 5.
|
||||
return_residual : bool
|
||||
If True, the residual is returned as an Evoked instance.
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
dipoles : list of instance of Dipole
|
||||
The dipole fits.
|
||||
residual : instance of Evoked
|
||||
The residual a.k.a. data not explained by the dipoles.
|
||||
Only returned if return_residual is True.
|
||||
|
||||
See Also
|
||||
--------
|
||||
mne.fit_dipole
|
||||
mne.beamformer.trap_music
|
||||
|
||||
Notes
|
||||
-----
|
||||
.. versionadded:: 0.9.0
|
||||
|
||||
References
|
||||
----------
|
||||
.. footbibliography::
|
||||
"""
|
||||
return _rap_music(evoked, forward, noise_cov, n_dipoles, return_residual, False)
|
||||
|
||||
|
||||
@verbose
|
||||
def trap_music(
|
||||
evoked,
|
||||
forward,
|
||||
noise_cov,
|
||||
n_dipoles=5,
|
||||
return_residual=False,
|
||||
*,
|
||||
verbose=None,
|
||||
):
|
||||
"""TRAP-MUSIC source localization method.
|
||||
|
||||
Compute Truncated Recursively Applied and Projected MUltiple SIgnal Classification
|
||||
(TRAP-MUSIC) :footcite:`Makela2018` on evoked data.
|
||||
|
||||
.. note:: The goodness of fit (GOF) of all the returned dipoles is the
|
||||
same and corresponds to the GOF of the full set of dipoles.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
evoked : instance of Evoked
|
||||
Evoked data to localize.
|
||||
forward : instance of Forward
|
||||
Forward operator.
|
||||
noise_cov : instance of Covariance
|
||||
The noise covariance.
|
||||
n_dipoles : int
|
||||
The number of dipoles to look for. The default value is 5.
|
||||
return_residual : bool
|
||||
If True, the residual is returned as an Evoked instance.
|
||||
%(verbose)s
|
||||
|
||||
Returns
|
||||
-------
|
||||
dipoles : list of instance of Dipole
|
||||
The dipole fits.
|
||||
residual : instance of Evoked
|
||||
The residual a.k.a. data not explained by the dipoles.
|
||||
Only returned if return_residual is True.
|
||||
|
||||
See Also
|
||||
--------
|
||||
mne.fit_dipole
|
||||
mne.beamformer.rap_music
|
||||
|
||||
Notes
|
||||
-----
|
||||
.. versionadded:: 1.4
|
||||
|
||||
References
|
||||
----------
|
||||
.. footbibliography::
|
||||
"""
|
||||
return _rap_music(evoked, forward, noise_cov, n_dipoles, return_residual, True)
|
||||
86
mne/beamformer/resolution_matrix.py
Normal file
86
mne/beamformer/resolution_matrix.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Compute resolution matrix for beamformers."""
|
||||
|
||||
# Authors: The MNE-Python contributors.
|
||||
# License: BSD-3-Clause
|
||||
# Copyright the MNE-Python contributors.
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .._fiff.pick import pick_channels, pick_channels_forward, pick_info
|
||||
from ..evoked import EvokedArray
|
||||
from ..utils import fill_doc, logger
|
||||
from ._lcmv import apply_lcmv
|
||||
|
||||
|
||||
@fill_doc
|
||||
def make_lcmv_resolution_matrix(filters, forward, info):
|
||||
"""Compute resolution matrix for LCMV beamformer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filters : instance of Beamformer
|
||||
Dictionary containing filter weights from LCMV beamformer
|
||||
(see mne.beamformer.make_lcmv).
|
||||
forward : instance of Forward
|
||||
Forward Solution with leadfield matrix.
|
||||
%(info_not_none)s Used to compute LCMV filters.
|
||||
|
||||
Returns
|
||||
-------
|
||||
resmat : array, shape (n_dipoles_lcmv, n_dipoles_fwd)
|
||||
Resolution matrix (filter matrix multiplied to leadfield from
|
||||
forward solution). Numbers of rows (n_dipoles_lcmv) and columns
|
||||
(n_dipoles_fwd) may differ by a factor depending on orientation
|
||||
constraints of filter and forward solution, respectively (e.g. factor 3
|
||||
for free dipole orientation versus factor 1 for scalar beamformers).
|
||||
"""
|
||||
# don't include bad channels from noise covariance matrix
|
||||
bads_filt = filters["noise_cov"]["bads"]
|
||||
ch_names = filters["noise_cov"]["names"]
|
||||
|
||||
# good channels
|
||||
ch_names = [c for c in ch_names if (c not in bads_filt)]
|
||||
|
||||
# adjust channels in forward solution
|
||||
forward = pick_channels_forward(forward, ch_names, ordered=True)
|
||||
|
||||
# get leadfield matrix from forward solution
|
||||
leadfield = forward["sol"]["data"]
|
||||
|
||||
# get the filter weights for beamformer as matrix
|
||||
filtmat = _get_matrix_from_lcmv(filters, forward, info)
|
||||
|
||||
# compute resolution matrix
|
||||
resmat = filtmat.dot(leadfield)
|
||||
|
||||
logger.info(f"Dimensions of LCMV resolution matrix: {resmat.shape}.")
|
||||
|
||||
return resmat
|
||||
|
||||
|
||||
def _get_matrix_from_lcmv(filters, forward, info, verbose=None):
|
||||
"""Get inverse matrix for LCMV beamformer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
invmat : array, shape (n_dipoles, n_channels)
|
||||
Inverse matrix associated with LCMV beamformer filters.
|
||||
"""
|
||||
# number of channels for identity matrix
|
||||
info = pick_info(info, pick_channels(info["ch_names"], filters["ch_names"]))
|
||||
n_chs = len(info["ch_names"])
|
||||
|
||||
# create identity matrix as input for inverse operator
|
||||
# set elements to zero for non-selected channels
|
||||
id_mat = np.eye(n_chs)
|
||||
|
||||
# convert identity matrix to evoked data type (pretending it's an epochs
|
||||
evo_ident = EvokedArray(id_mat, info=info, tmin=0.0)
|
||||
|
||||
# apply beamformer to identity matrix
|
||||
stc_lcmv = apply_lcmv(evo_ident, filters, verbose=verbose)
|
||||
|
||||
# turn source estimate into numpsy array
|
||||
invmat = stc_lcmv.data
|
||||
|
||||
return invmat
|
||||
Reference in New Issue
Block a user