513 lines
16 KiB
Python
513 lines
16 KiB
Python
# Authors: The MNE-Python contributors.
|
|
# License: BSD-3-Clause
|
|
# Copyright the MNE-Python contributors.
|
|
|
|
import numpy as np
|
|
|
|
from ._fiff.constants import FIFF
|
|
from ._fiff.open import fiff_open
|
|
from ._fiff.pick import _picks_to_idx, pick_types, pick_types_forward
|
|
from ._fiff.proj import (
|
|
Projection,
|
|
_has_eeg_average_ref_proj,
|
|
_read_proj,
|
|
_write_proj,
|
|
make_eeg_average_ref_proj,
|
|
make_projector,
|
|
)
|
|
from ._fiff.write import start_and_end_file
|
|
from .cov import _check_n_samples
|
|
from .epochs import Epochs
|
|
from .event import make_fixed_length_events
|
|
from .fixes import _safe_svd
|
|
from .forward import _subject_from_forward, convert_forward_solution, is_fixed_orient
|
|
from .parallel import parallel_func
|
|
from .source_estimate import _make_stc
|
|
from .utils import (
|
|
_check_fname,
|
|
_check_option,
|
|
_validate_type,
|
|
check_fname,
|
|
logger,
|
|
verbose,
|
|
)
|
|
|
|
|
|
@verbose
|
|
def read_proj(fname, verbose=None):
|
|
"""Read projections from a FIF file.
|
|
|
|
Parameters
|
|
----------
|
|
fname : path-like
|
|
The name of file containing the projections vectors. It should end with
|
|
``-proj.fif`` or ``-proj.fif.gz``.
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
projs : list of Projection
|
|
The list of projection vectors.
|
|
|
|
See Also
|
|
--------
|
|
write_proj
|
|
"""
|
|
check_fname(
|
|
fname, "projection", ("-proj.fif", "-proj.fif.gz", "_proj.fif", "_proj.fif.gz")
|
|
)
|
|
|
|
ff, tree, _ = fiff_open(fname)
|
|
with ff as fid:
|
|
projs = _read_proj(fid, tree)
|
|
return projs
|
|
|
|
|
|
@verbose
|
|
def write_proj(fname, projs, *, overwrite=False, verbose=None):
|
|
"""Write projections to a FIF file.
|
|
|
|
Parameters
|
|
----------
|
|
fname : path-like
|
|
The name of file containing the projections vectors. It should end with
|
|
``-proj.fif`` or ``-proj.fif.gz``.
|
|
projs : list of Projection
|
|
The list of projection vectors.
|
|
%(overwrite)s
|
|
|
|
.. versionadded:: 1.0
|
|
%(verbose)s
|
|
|
|
.. versionadded:: 1.0
|
|
|
|
See Also
|
|
--------
|
|
read_proj
|
|
"""
|
|
fname = _check_fname(fname, overwrite=overwrite)
|
|
check_fname(
|
|
fname, "projection", ("-proj.fif", "-proj.fif.gz", "_proj.fif", "_proj.fif.gz")
|
|
)
|
|
with start_and_end_file(fname) as fid:
|
|
_write_proj(fid, projs)
|
|
|
|
|
|
@verbose
|
|
def _compute_proj(
|
|
data, info, n_grad, n_mag, n_eeg, desc_prefix, meg="separate", verbose=None
|
|
):
|
|
_validate_type(n_grad, "numeric", "n_grad", "float or int")
|
|
_validate_type(n_mag, "numeric", "n_grad", "float or int")
|
|
_validate_type(n_eeg, "numeric", "n_eeg", "float or int")
|
|
for n_, n_name in ((n_grad, "n_grad"), (n_mag, "n_mag"), (n_eeg, "n_eeg")):
|
|
if n_ < 0:
|
|
raise ValueError(
|
|
f"Argument '{n_name}' must be either a positive integer or a float "
|
|
f"between 0 and 1. '{n_}' is invalid."
|
|
)
|
|
_check_option("meg", meg, ("separate", "combined"))
|
|
if meg == "combined":
|
|
if n_grad != n_mag:
|
|
raise ValueError(
|
|
f"n_grad ({n_grad}) must be equal to n_mag ({n_mag}) when using "
|
|
"meg='combined'."
|
|
)
|
|
ch_types = ("meg", "eeg")
|
|
n_vectors = (n_grad, n_eeg)
|
|
kinds = ("meg", "eeg")
|
|
else:
|
|
ch_types = ("grad", "mag", "eeg")
|
|
n_vectors = (n_grad, n_mag, n_eeg)
|
|
kinds = ("planar", "axial", "eeg")
|
|
|
|
projs = []
|
|
for ch_type, n_vector, kind in zip(ch_types, n_vectors, kinds):
|
|
# select channels to use
|
|
try:
|
|
idx = _picks_to_idx(info, ch_type, with_ref_meg=False, exclude="bads")
|
|
except ValueError:
|
|
logger.info("No channels '%s' found. Skipping.", ch_type)
|
|
continue
|
|
names = [info["ch_names"][k] for k in idx]
|
|
|
|
data_ = data[idx][:, idx] # data is the covariance matrix: U * S**2 * Ut
|
|
U, Sexp2, _ = _safe_svd(data_, full_matrices=False)
|
|
exp_var = Sexp2 / Sexp2.sum()
|
|
|
|
# select vectors to use
|
|
if 0 < n_vector < 1:
|
|
n_vector = np.searchsorted(np.cumsum(exp_var), n_vector, "left") + 1
|
|
U = U[:, :n_vector]
|
|
exp_var = exp_var[:n_vector]
|
|
|
|
# create projectors
|
|
for k, (u, var) in enumerate(zip(U.T, exp_var)):
|
|
proj_data = dict(
|
|
col_names=names,
|
|
row_names=None,
|
|
data=u[np.newaxis, :],
|
|
nrow=1,
|
|
ncol=u.size,
|
|
)
|
|
desc = f"{kind}-{desc_prefix}-PCA-{k + 1:02d}"
|
|
logger.info(f"Adding projection: {desc} (exp var={100 * float(var):0.1f}%)")
|
|
proj = Projection(
|
|
active=False,
|
|
data=proj_data,
|
|
desc=desc,
|
|
kind=FIFF.FIFFV_PROJ_ITEM_FIELD,
|
|
explained_var=var,
|
|
)
|
|
projs.append(proj)
|
|
return projs
|
|
|
|
|
|
@verbose
|
|
def compute_proj_epochs(
|
|
epochs,
|
|
n_grad=2,
|
|
n_mag=2,
|
|
n_eeg=2,
|
|
n_jobs=None,
|
|
desc_prefix=None,
|
|
meg="separate",
|
|
verbose=None,
|
|
):
|
|
"""Compute SSP (signal-space projection) vectors on epoched data.
|
|
|
|
%(compute_ssp)s
|
|
|
|
Parameters
|
|
----------
|
|
epochs : instance of Epochs
|
|
The epochs containing the artifact.
|
|
%(n_proj_vectors)s
|
|
%(n_jobs)s
|
|
Number of jobs to use to compute covariance.
|
|
desc_prefix : str | None
|
|
The description prefix to use. If None, one will be created based on
|
|
the event_id, tmin, and tmax.
|
|
meg : str
|
|
Can be ``'separate'`` (default) or ``'combined'`` to compute projectors
|
|
for magnetometers and gradiometers separately or jointly.
|
|
If ``'combined'``, ``n_mag == n_grad`` is required and the number of
|
|
projectors computed for MEG will be ``n_mag``.
|
|
|
|
.. versionadded:: 0.18
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
projs: list of Projection
|
|
List of projection vectors.
|
|
|
|
See Also
|
|
--------
|
|
compute_proj_raw, compute_proj_evoked
|
|
"""
|
|
# compute data covariance
|
|
data = _compute_cov_epochs(epochs, n_jobs)
|
|
event_id = epochs.event_id
|
|
if event_id is None or len(list(event_id.keys())) == 0:
|
|
event_id = "0"
|
|
elif len(event_id.keys()) == 1:
|
|
event_id = str(list(event_id.values())[0])
|
|
else:
|
|
event_id = "Multiple-events"
|
|
if desc_prefix is None:
|
|
desc_prefix = f"{event_id}-{epochs.tmin:<.3f}-{epochs.tmax:<.3f}"
|
|
return _compute_proj(data, epochs.info, n_grad, n_mag, n_eeg, desc_prefix, meg=meg)
|
|
|
|
|
|
def _compute_cov_epochs(epochs, n_jobs, *, log_drops=False):
|
|
"""Compute epochs covariance."""
|
|
parallel, p_fun, n_jobs = parallel_func(np.dot, n_jobs)
|
|
n_start = len(epochs.events)
|
|
data = parallel(p_fun(e, e.T) for e in epochs)
|
|
n_epochs = len(data)
|
|
if n_epochs == 0:
|
|
raise RuntimeError("No good epochs found")
|
|
if log_drops:
|
|
logger.info(f"Dropped {n_start - n_epochs}/{n_start} epochs")
|
|
|
|
n_chan, n_samples = epochs.info["nchan"], len(epochs.times)
|
|
_check_n_samples(n_samples * n_epochs, n_chan)
|
|
data = sum(data)
|
|
return data
|
|
|
|
|
|
@verbose
|
|
def compute_proj_evoked(
|
|
evoked, n_grad=2, n_mag=2, n_eeg=2, desc_prefix=None, meg="separate", verbose=None
|
|
):
|
|
"""Compute SSP (signal-space projection) vectors on evoked data.
|
|
|
|
%(compute_ssp)s
|
|
|
|
Parameters
|
|
----------
|
|
evoked : instance of Evoked
|
|
The Evoked obtained by averaging the artifact.
|
|
%(n_proj_vectors)s
|
|
desc_prefix : str | None
|
|
The description prefix to use. If None, one will be created based on
|
|
tmin and tmax.
|
|
|
|
.. versionadded:: 0.17
|
|
meg : str
|
|
Can be ``'separate'`` (default) or ``'combined'`` to compute projectors
|
|
for magnetometers and gradiometers separately or jointly.
|
|
If ``'combined'``, ``n_mag == n_grad`` is required and the number of
|
|
projectors computed for MEG will be ``n_mag``.
|
|
|
|
.. versionadded:: 0.18
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
projs : list of Projection
|
|
List of projection vectors.
|
|
|
|
See Also
|
|
--------
|
|
compute_proj_raw, compute_proj_epochs
|
|
"""
|
|
data = np.dot(evoked.data, evoked.data.T) # compute data covariance
|
|
if desc_prefix is None:
|
|
desc_prefix = f"{evoked.times[0]:<.3f}-{evoked.times[-1]:<.3f}"
|
|
return _compute_proj(data, evoked.info, n_grad, n_mag, n_eeg, desc_prefix, meg=meg)
|
|
|
|
|
|
@verbose
|
|
def compute_proj_raw(
|
|
raw,
|
|
start=0,
|
|
stop=None,
|
|
duration=1,
|
|
n_grad=2,
|
|
n_mag=2,
|
|
n_eeg=0,
|
|
reject=None,
|
|
flat=None,
|
|
n_jobs=None,
|
|
meg="separate",
|
|
verbose=None,
|
|
):
|
|
"""Compute SSP (signal-space projection) vectors on continuous data.
|
|
|
|
%(compute_ssp)s
|
|
|
|
Parameters
|
|
----------
|
|
raw : instance of Raw
|
|
A raw object to use the data from.
|
|
start : float
|
|
Time (in seconds) to start computing SSP.
|
|
stop : float | None
|
|
Time (in seconds) to stop computing SSP. None will go to the end of the file.
|
|
duration : float | None
|
|
Duration (in seconds) to chunk data into for SSP
|
|
If duration is ``None``, data will not be chunked.
|
|
%(n_proj_vectors)s
|
|
reject : dict | None
|
|
Epoch PTP rejection threshold used if ``duration != None``. See `~mne.Epochs`.
|
|
flat : dict | None
|
|
Epoch flatness rejection threshold used if ``duration != None``. See
|
|
`~mne.Epochs`.
|
|
%(n_jobs)s
|
|
Number of jobs to use to compute covariance.
|
|
meg : str
|
|
Can be ``'separate'`` (default) or ``'combined'`` to compute projectors
|
|
for magnetometers and gradiometers separately or jointly.
|
|
If ``'combined'``, ``n_mag == n_grad`` is required and the number of
|
|
projectors computed for MEG will be ``n_mag``.
|
|
|
|
.. versionadded:: 0.18
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
projs: list of Projection
|
|
List of projection vectors.
|
|
|
|
See Also
|
|
--------
|
|
compute_proj_epochs, compute_proj_evoked
|
|
"""
|
|
if duration is not None:
|
|
duration = np.round(duration * raw.info["sfreq"]) / raw.info["sfreq"]
|
|
events = make_fixed_length_events(raw, 999, start, stop, duration)
|
|
picks = pick_types(
|
|
raw.info, meg=True, eeg=True, eog=True, ecg=True, emg=True, exclude="bads"
|
|
)
|
|
epochs = Epochs(
|
|
raw,
|
|
events,
|
|
None,
|
|
tmin=0.0,
|
|
tmax=duration - 1.0 / raw.info["sfreq"],
|
|
picks=picks,
|
|
reject=reject,
|
|
flat=flat,
|
|
baseline=None,
|
|
proj=False,
|
|
)
|
|
data = _compute_cov_epochs(epochs, n_jobs, log_drops=True)
|
|
info = epochs.info
|
|
if not stop:
|
|
stop = raw.n_times / raw.info["sfreq"]
|
|
else:
|
|
# convert to sample indices
|
|
start = max(raw.time_as_index(start)[0], 0)
|
|
stop = raw.time_as_index(stop)[0] if stop else raw.n_times
|
|
stop = min(stop, raw.n_times)
|
|
data, times = raw[:, start:stop]
|
|
_check_n_samples(stop - start, data.shape[0])
|
|
data = np.dot(data, data.T) # compute data covariance
|
|
info = raw.info
|
|
# convert back to times
|
|
start = start / raw.info["sfreq"]
|
|
stop = stop / raw.info["sfreq"]
|
|
|
|
desc_prefix = f"Raw-{start:<.3f}-{stop:<.3f}"
|
|
projs = _compute_proj(data, info, n_grad, n_mag, n_eeg, desc_prefix, meg=meg)
|
|
return projs
|
|
|
|
|
|
@verbose
|
|
def sensitivity_map(
|
|
fwd, projs=None, ch_type="grad", mode="fixed", exclude=(), *, verbose=None
|
|
):
|
|
"""Compute sensitivity map.
|
|
|
|
Such maps are used to know how much sources are visible by a type
|
|
of sensor, and how much projections shadow some sources.
|
|
|
|
Parameters
|
|
----------
|
|
fwd : Forward
|
|
The forward operator.
|
|
projs : list
|
|
List of projection vectors.
|
|
ch_type : ``'grad'`` | ``'mag'`` | ``'eeg'``
|
|
The type of sensors to use.
|
|
mode : str
|
|
The type of sensitivity map computed. See manual. Should be ``'free'``,
|
|
``'fixed'``, ``'ratio'``, ``'radiality'``, ``'angle'``,
|
|
``'remaining'``, or ``'dampening'`` corresponding to the argument
|
|
``--map 1, 2, 3, 4, 5, 6, 7`` of the command ``mne_sensitivity_map``.
|
|
exclude : list of str | str
|
|
List of channels to exclude. If empty do not exclude any (default).
|
|
If ``'bads'``, exclude channels in ``fwd['info']['bads']``.
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
stc : SourceEstimate | VolSourceEstimate
|
|
The sensitivity map as a SourceEstimate or VolSourceEstimate instance
|
|
for visualization.
|
|
|
|
Notes
|
|
-----
|
|
When mode is ``'fixed'`` or ``'free'``, the sensitivity map is normalized
|
|
by its maximum value.
|
|
"""
|
|
# check strings
|
|
_check_option("ch_type", ch_type, ["eeg", "grad", "mag"])
|
|
_check_option(
|
|
"mode",
|
|
mode,
|
|
["free", "fixed", "ratio", "radiality", "angle", "remaining", "dampening"],
|
|
)
|
|
|
|
# check forward
|
|
if is_fixed_orient(fwd, orig=True):
|
|
raise ValueError("fwd should must be computed with free orientation")
|
|
|
|
# limit forward (this will make a copy of the data for us)
|
|
if ch_type == "eeg":
|
|
fwd = pick_types_forward(fwd, meg=False, eeg=True, exclude=exclude)
|
|
else:
|
|
fwd = pick_types_forward(fwd, meg=ch_type, eeg=False, exclude=exclude)
|
|
|
|
convert_forward_solution(
|
|
fwd, surf_ori=True, force_fixed=False, copy=False, verbose=False
|
|
)
|
|
assert fwd["surf_ori"] and not is_fixed_orient(fwd)
|
|
|
|
gain = fwd["sol"]["data"]
|
|
|
|
# Make sure EEG has average
|
|
if ch_type == "eeg":
|
|
if projs is None or not _has_eeg_average_ref_proj(fwd["info"], projs=projs):
|
|
eeg_ave = [make_eeg_average_ref_proj(fwd["info"])]
|
|
else:
|
|
eeg_ave = []
|
|
projs = eeg_ave if projs is None else projs + eeg_ave
|
|
|
|
# Construct the projector
|
|
residual_types = ["angle", "remaining", "dampening"]
|
|
if projs is not None:
|
|
proj, ncomp, U = make_projector(
|
|
projs, fwd["sol"]["row_names"], include_active=True
|
|
)
|
|
# do projection for most types
|
|
if mode not in residual_types:
|
|
gain = np.dot(proj, gain)
|
|
elif ncomp == 0:
|
|
raise RuntimeError(
|
|
"No valid projectors found for channel type "
|
|
f"{ch_type}, cannot compute {mode}"
|
|
)
|
|
# can only run the last couple methods if there are projectors
|
|
elif mode in residual_types:
|
|
raise ValueError(f"No projectors used, cannot compute {mode}")
|
|
|
|
_, n_dipoles = gain.shape
|
|
n_locations = n_dipoles // 3
|
|
del n_dipoles
|
|
sensitivity_map = np.empty(n_locations)
|
|
|
|
for k in range(n_locations):
|
|
gg = gain[:, 3 * k : 3 * (k + 1)] # noqa: E203
|
|
if mode != "fixed":
|
|
s = _safe_svd(gg, full_matrices=False, compute_uv=False)
|
|
if mode == "free":
|
|
sensitivity_map[k] = s[0]
|
|
else:
|
|
gz = np.linalg.norm(gg[:, 2]) # the normal component
|
|
if mode == "fixed":
|
|
sensitivity_map[k] = gz
|
|
elif mode == "ratio":
|
|
sensitivity_map[k] = gz / s[0]
|
|
elif mode == "radiality":
|
|
sensitivity_map[k] = 1.0 - (gz / s[0])
|
|
else:
|
|
if mode == "angle":
|
|
co = np.linalg.norm(np.dot(gg[:, 2], U))
|
|
sensitivity_map[k] = co / gz
|
|
else:
|
|
p = np.linalg.norm(np.dot(proj, gg[:, 2]))
|
|
if mode == "remaining":
|
|
sensitivity_map[k] = p / gz
|
|
elif mode == "dampening":
|
|
sensitivity_map[k] = 1.0 - p / gz
|
|
else:
|
|
raise ValueError(f"Unknown mode type (got {mode})")
|
|
|
|
# only normalize fixed and free methods
|
|
if mode in ["fixed", "free"]:
|
|
sensitivity_map /= np.max(sensitivity_map)
|
|
|
|
subject = _subject_from_forward(fwd)
|
|
vertices = [s["vertno"] for s in fwd["src"]]
|
|
return _make_stc(
|
|
sensitivity_map[:, np.newaxis],
|
|
vertices,
|
|
fwd["src"].kind,
|
|
tmin=0.0,
|
|
tstep=1.0,
|
|
subject=subject,
|
|
)
|