initial commit

This commit is contained in:
2025-08-19 09:13:22 -07:00
parent 28464811d6
commit 0977a3e14d
820 changed files with 1003358 additions and 2 deletions

View File

@@ -0,0 +1,5 @@
# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.
from .fieldtrip import read_evoked_fieldtrip, read_epochs_fieldtrip, read_raw_fieldtrip

View File

@@ -0,0 +1,185 @@
# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.
import numpy as np
from ...epochs import EpochsArray
from ...evoked import EvokedArray
from ...utils import _check_fname, _import_pymatreader_funcs
from ..array.array import RawArray
from .utils import (
_create_event_metadata,
_create_events,
_create_info,
_set_tmin,
_validate_ft_struct,
)
def read_raw_fieldtrip(fname, info, data_name="data") -> RawArray:
"""Load continuous (raw) data from a FieldTrip preprocessing structure.
This function expects to find single trial raw data (FT_DATATYPE_RAW) in
the structure data_name is pointing at.
.. warning:: FieldTrip does not normally store the original information
concerning channel location, orientation, type etc. It is
therefore **highly recommended** to provide the info field.
This can be obtained by reading the original raw data file
with MNE functions (without preload). The returned object
contains the necessary info field.
Parameters
----------
fname : path-like
Path and filename of the ``.mat`` file containing the data.
info : dict or None
The info dict of the raw data file corresponding to the data to import.
If this is set to None, limited information is extracted from the
FieldTrip structure.
data_name : str
Name of heading dict/variable name under which the data was originally
saved in MATLAB.
Returns
-------
raw : instance of RawArray
A Raw Object containing the loaded data.
See :class:`mne.io.Raw` for documentation of attributes and methods.
See Also
--------
mne.io.Raw : Documentation of attributes and methods of RawArray.
"""
read_mat = _import_pymatreader_funcs("FieldTrip I/O")
fname = _check_fname(fname, overwrite="read", must_exist=True)
ft_struct = read_mat(fname, ignore_fields=["previous"], variable_names=[data_name])
# load data and set ft_struct to the heading dictionary
ft_struct = ft_struct[data_name]
_validate_ft_struct(ft_struct)
info = _create_info(ft_struct, info) # create info structure
data = np.array(ft_struct["trial"]) # create the main data array
if data.ndim > 2:
data = np.squeeze(data)
if data.ndim == 1:
data = data[np.newaxis, ...]
if data.ndim != 2:
raise RuntimeError(
"The data you are trying to load does not seem to be raw data"
)
raw = RawArray(data, info) # create an MNE RawArray
return raw
def read_epochs_fieldtrip(
fname, info, data_name="data", trialinfo_column=0
) -> EpochsArray:
"""Load epoched data from a FieldTrip preprocessing structure.
This function expects to find epoched data in the structure data_name is
pointing at.
.. warning:: Only epochs with the same amount of channels and samples are
supported!
.. warning:: FieldTrip does not normally store the original information
concerning channel location, orientation, type etc. It is
therefore **highly recommended** to provide the info field.
This can be obtained by reading the original raw data file
with MNE functions (without preload). The returned object
contains the necessary info field.
Parameters
----------
fname : path-like
Path and filename of the ``.mat`` file containing the data.
info : dict or None
The info dict of the raw data file corresponding to the data to import.
If this is set to None, limited information is extracted from the
FieldTrip structure.
data_name : str
Name of heading dict/ variable name under which the data was originally
saved in MATLAB.
trialinfo_column : int
Column of the trialinfo matrix to use for the event codes.
Returns
-------
epochs : instance of EpochsArray
An EpochsArray containing the loaded data.
"""
read_mat = _import_pymatreader_funcs("FieldTrip I/O")
ft_struct = read_mat(fname, ignore_fields=["previous"], variable_names=[data_name])
# load data and set ft_struct to the heading dictionary
ft_struct = ft_struct[data_name]
_validate_ft_struct(ft_struct)
info = _create_info(ft_struct, info) # create info structure
data = np.array(ft_struct["trial"]) # create the epochs data array
events = _create_events(ft_struct, trialinfo_column)
if events is not None:
metadata = _create_event_metadata(ft_struct)
else:
metadata = None
tmin = _set_tmin(ft_struct) # create start time
epochs = EpochsArray(
data=data, info=info, tmin=tmin, events=events, metadata=metadata, proj=False
)
return epochs
def read_evoked_fieldtrip(fname, info, comment=None, data_name="data"):
"""Load evoked data from a FieldTrip timelocked structure.
This function expects to find timelocked data in the structure data_name is
pointing at.
.. warning:: FieldTrip does not normally store the original information
concerning channel location, orientation, type etc. It is
therefore **highly recommended** to provide the info field.
This can be obtained by reading the original raw data file
with MNE functions (without preload). The returned object
contains the necessary info field.
Parameters
----------
fname : path-like
Path and filename of the ``.mat`` file containing the data.
info : dict or None
The info dict of the raw data file corresponding to the data to import.
If this is set to None, limited information is extracted from the
FieldTrip structure.
comment : str
Comment on dataset. Can be the condition.
data_name : str
Name of heading dict/ variable name under which the data was originally
saved in MATLAB.
Returns
-------
evoked : instance of EvokedArray
An EvokedArray containing the loaded data.
"""
read_mat = _import_pymatreader_funcs("FieldTrip I/O")
ft_struct = read_mat(fname, ignore_fields=["previous"], variable_names=[data_name])
ft_struct = ft_struct[data_name]
_validate_ft_struct(ft_struct)
info = _create_info(ft_struct, info) # create info structure
data_evoked = ft_struct["avg"] # create evoked data
evoked = EvokedArray(data_evoked, info, comment=comment)
return evoked

367
mne/io/fieldtrip/utils.py Normal file
View File

@@ -0,0 +1,367 @@
# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.
import numpy as np
from ..._fiff._digitization import DigPoint, _ensure_fiducials_head
from ..._fiff.constants import FIFF
from ..._fiff.meas_info import create_info
from ..._fiff.pick import pick_info
from ...transforms import rotation3d_align_z_axis
from ...utils import _check_pandas_installed, warn
_supported_megs = ["neuromag306"]
_unit_dict = {
"m": 1,
"cm": 1e-2,
"mm": 1e-3,
"V": 1,
"mV": 1e-3,
"uV": 1e-6,
"T": 1,
"T/m": 1,
"T/cm": 1e2,
}
NOINFO_WARNING = (
"Importing FieldTrip data without an info dict from the "
"original file. Channel locations, orientations and types "
"will be incorrect. The imported data cannot be used for "
"source analysis, channel interpolation etc."
)
def _validate_ft_struct(ft_struct):
"""Run validation checks on the ft_structure."""
if isinstance(ft_struct, list):
raise RuntimeError("Loading of data in cell arrays is not supported")
def _create_info(ft_struct, raw_info):
"""Create MNE info structure from a FieldTrip structure."""
if raw_info is None:
warn(NOINFO_WARNING)
sfreq = _set_sfreq(ft_struct)
ch_names = ft_struct["label"]
if raw_info:
info = raw_info.copy()
missing_channels = set(ch_names) - set(info["ch_names"])
if missing_channels:
warn(
"The following channels are present in the FieldTrip data "
f"but cannot be found in the provided info: {missing_channels}.\n"
"These channels will be removed from the resulting data!"
)
missing_chan_idx = [ch_names.index(ch) for ch in missing_channels]
new_chs = [ch for ch in ch_names if ch not in missing_channels]
ch_names = new_chs
ft_struct["label"] = ch_names
if "trial" in ft_struct:
ft_struct["trial"] = _remove_missing_channels_from_trial(
ft_struct["trial"], missing_chan_idx
)
if "avg" in ft_struct:
if ft_struct["avg"].ndim == 2:
ft_struct["avg"] = np.delete(
ft_struct["avg"], missing_chan_idx, axis=0
)
with info._unlock():
info["sfreq"] = sfreq
ch_idx = [info["ch_names"].index(ch) for ch in ch_names]
pick_info(info, ch_idx, copy=False)
else:
info = create_info(ch_names, sfreq)
chs, dig = _create_info_chs_dig(ft_struct)
with info._unlock(update_redundant=True):
info.update(chs=chs, dig=dig)
return info
def _remove_missing_channels_from_trial(trial, missing_chan_idx):
if isinstance(trial, list):
for idx_trial in range(len(trial)):
trial[idx_trial] = _remove_missing_channels_from_trial(
trial[idx_trial], missing_chan_idx
)
elif isinstance(trial, np.ndarray):
if trial.ndim == 2:
trial = np.delete(trial, missing_chan_idx, axis=0)
else:
raise ValueError(
'"trial" field of the FieldTrip structure has an unknown format.'
)
return trial
def _create_info_chs_dig(ft_struct):
"""Create the chs info field from the FieldTrip structure."""
all_channels = ft_struct["label"]
ch_defaults = dict(
coord_frame=FIFF.FIFFV_COORD_UNKNOWN,
cal=1.0,
range=1.0,
unit_mul=FIFF.FIFF_UNITM_NONE,
loc=np.array([0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]),
unit=FIFF.FIFF_UNIT_V,
)
try:
elec = ft_struct["elec"]
except KeyError:
elec = None
try:
grad = ft_struct["grad"]
except KeyError:
grad = None
if elec is None and grad is None:
warn(
"The supplied FieldTrip structure does not have an elec or grad "
"field. No channel locations will extracted and the kind of "
"channel might be inaccurate."
)
if "chanpos" not in (elec or grad or {"chanpos": None}):
raise RuntimeError(
"This file was created with an old version of FieldTrip. You can "
"convert the data to the new version by loading it into FieldTrip "
"and applying ft_selectdata with an empty cfg structure on it. "
"Otherwise you can supply the Info field."
)
chs = list()
dig = list()
counter = 0
for idx_chan, cur_channel_label in enumerate(all_channels):
cur_ch = ch_defaults.copy()
cur_ch["ch_name"] = cur_channel_label
cur_ch["logno"] = idx_chan + 1
cur_ch["scanno"] = idx_chan + 1
if elec and cur_channel_label in elec["label"]:
cur_ch = _process_channel_eeg(cur_ch, elec)
assert cur_ch["coord_frame"] == FIFF.FIFFV_COORD_HEAD
# Ref gets ident=0 and we don't have it, so start at 1
counter += 1
d = DigPoint(
r=cur_ch["loc"][:3],
coord_frame=FIFF.FIFFV_COORD_HEAD,
kind=FIFF.FIFFV_POINT_EEG,
ident=counter,
)
dig.append(d)
elif grad and cur_channel_label in grad["label"]:
cur_ch = _process_channel_meg(cur_ch, grad)
else:
if cur_channel_label.startswith("EOG"):
cur_ch["kind"] = FIFF.FIFFV_EOG_CH
cur_ch["coil_type"] = FIFF.FIFFV_COIL_EEG
elif cur_channel_label.startswith("ECG"):
cur_ch["kind"] = FIFF.FIFFV_ECG_CH
cur_ch["coil_type"] = FIFF.FIFFV_COIL_EEG_BIPOLAR
elif cur_channel_label.startswith("STI"):
cur_ch["kind"] = FIFF.FIFFV_STIM_CH
cur_ch["coil_type"] = FIFF.FIFFV_COIL_NONE
else:
warn(
f"Cannot guess the correct type of channel {cur_channel_label}. "
"Making it a MISC channel."
)
cur_ch["kind"] = FIFF.FIFFV_MISC_CH
cur_ch["coil_type"] = FIFF.FIFFV_COIL_NONE
chs.append(cur_ch)
_ensure_fiducials_head(dig)
return chs, dig
def _set_sfreq(ft_struct):
"""Set the sample frequency."""
try:
sfreq = ft_struct["fsample"]
except KeyError:
try:
time = ft_struct["time"]
except KeyError:
raise ValueError("No Source for sfreq found")
else:
t1, t2 = float(time[0]), float(time[1])
sfreq = 1 / (t2 - t1)
try:
sfreq = float(sfreq)
except TypeError:
warn(
"FieldTrip structure contained multiple sample rates, trying the "
f"first of:\n{sfreq} Hz"
)
sfreq = float(sfreq.ravel()[0])
return sfreq
def _set_tmin(ft_struct):
"""Set the start time before the event in evoked data if possible."""
times = ft_struct["time"]
time_check = all(times[i][0] == times[i - 1][0] for i, x in enumerate(times))
if time_check:
tmin = times[0][0]
else:
raise RuntimeError(
"Loading data with non-uniform times per epoch is not supported"
)
return tmin
def _create_events(ft_struct, trialinfo_column):
"""Create an event matrix from the FieldTrip structure."""
if "trialinfo" not in ft_struct:
return None
event_type = ft_struct["trialinfo"]
event_number = range(len(event_type))
if trialinfo_column < 0:
raise ValueError("trialinfo_column must be positive")
available_ti_cols = 1
if event_type.ndim == 2:
available_ti_cols = event_type.shape[1]
if trialinfo_column > (available_ti_cols - 1):
raise ValueError(
"trialinfo_column is higher than the amount of columns in trialinfo."
)
event_trans_val = np.zeros(len(event_type))
if event_type.ndim == 2:
event_type = event_type[:, trialinfo_column]
events = (
np.vstack([np.array(event_number), event_trans_val, event_type]).astype("int").T
)
return events
def _create_event_metadata(ft_struct):
"""Create event metadata from trialinfo."""
pandas = _check_pandas_installed(strict=False)
if not pandas:
warn(
"The Pandas library is not installed. Not returning the original "
"trialinfo matrix as metadata."
)
return None
metadata = pandas.DataFrame(ft_struct["trialinfo"])
return metadata
def _process_channel_eeg(cur_ch, elec):
"""Convert EEG channel from FieldTrip to MNE.
Parameters
----------
cur_ch: dict
Channel specific dictionary to populate.
elec: dict
elec dict as loaded from the FieldTrip structure
Returns
-------
cur_ch: dict
The original dict (cur_ch) with the added information
"""
all_labels = np.asanyarray(elec["label"])
chan_idx_in_elec = np.where(all_labels == cur_ch["ch_name"])[0][0]
position = np.squeeze(elec["chanpos"][chan_idx_in_elec, :])
# chanunit = elec['chanunit'][chan_idx_in_elec] # not used/needed yet
position_unit = elec["unit"]
position = position * _unit_dict[position_unit]
cur_ch["loc"] = np.hstack((position, np.zeros((9,))))
cur_ch["unit"] = FIFF.FIFF_UNIT_V
cur_ch["kind"] = FIFF.FIFFV_EEG_CH
cur_ch["coil_type"] = FIFF.FIFFV_COIL_EEG
cur_ch["coord_frame"] = FIFF.FIFFV_COORD_HEAD
return cur_ch
def _process_channel_meg(cur_ch, grad):
"""Convert MEG channel from FieldTrip to MNE.
Parameters
----------
cur_ch: dict
Channel specific dictionary to populate.
grad: dict
grad dict as loaded from the FieldTrip structure
Returns
-------
dict: The original dict (cur_ch) with the added information
"""
all_labels = np.asanyarray(grad["label"])
chan_idx_in_grad = np.where(all_labels == cur_ch["ch_name"])[0][0]
gradtype = grad["type"]
chantype = grad["chantype"][chan_idx_in_grad]
position_unit = grad["unit"]
position = np.squeeze(grad["chanpos"][chan_idx_in_grad, :])
position = position * _unit_dict[position_unit]
if gradtype == "neuromag306" and "tra" in grad and "coilpos" in grad:
# Try to regenerate original channel pos.
idx_in_coilpos = np.where(grad["tra"][chan_idx_in_grad, :] != 0)[0]
cur_coilpos = grad["coilpos"][idx_in_coilpos, :]
cur_coilpos = cur_coilpos * _unit_dict[position_unit]
cur_coilori = grad["coilori"][idx_in_coilpos, :]
if chantype == "megmag":
position = cur_coilpos[0] - 0.0003 * cur_coilori[0]
if chantype == "megplanar":
tmp_pos = cur_coilpos - 0.0003 * cur_coilori
position = np.average(tmp_pos, axis=0)
original_orientation = np.squeeze(grad["chanori"][chan_idx_in_grad, :])
try:
orientation = rotation3d_align_z_axis(original_orientation).T
except AssertionError:
orientation = np.eye(3)
assert orientation.shape == (3, 3)
orientation = orientation.flatten()
# chanunit = grad['chanunit'][chan_idx_in_grad] # not used/needed yet
cur_ch["loc"] = np.hstack((position, orientation))
cur_ch["kind"] = FIFF.FIFFV_MEG_CH
if chantype == "megmag":
cur_ch["coil_type"] = FIFF.FIFFV_COIL_POINT_MAGNETOMETER
cur_ch["unit"] = FIFF.FIFF_UNIT_T
elif chantype == "megplanar":
cur_ch["coil_type"] = FIFF.FIFFV_COIL_VV_PLANAR_T1
cur_ch["unit"] = FIFF.FIFF_UNIT_T_M
elif chantype == "refmag":
cur_ch["coil_type"] = FIFF.FIFFV_COIL_MAGNES_REF_MAG
cur_ch["unit"] = FIFF.FIFF_UNIT_T
elif chantype == "refgrad":
cur_ch["coil_type"] = FIFF.FIFFV_COIL_MAGNES_REF_GRAD
cur_ch["unit"] = FIFF.FIFF_UNIT_T
elif chantype == "meggrad":
cur_ch["coil_type"] = FIFF.FIFFV_COIL_AXIAL_GRAD_5CM
cur_ch["unit"] = FIFF.FIFF_UNIT_T
else:
raise RuntimeError(f"Unexpected coil type: {chantype}.")
cur_ch["coord_frame"] = FIFF.FIFFV_COORD_HEAD
return cur_ch