"""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) `. .. 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, )