504 lines
15 KiB
Python
504 lines
15 KiB
Python
"""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,
|
|
)
|