From 15644ca3f66068e50d16beb97a1a937903e73479 Mon Sep 17 00:00:00 2001 From: robbisg Date: Wed, 24 Jun 2026 14:51:19 +0200 Subject: [PATCH 1/6] NF: first draft implementation of itab reader. --- mne/io/itab/__init__.py | 7 + mne/io/itab/constants.py | 34 ++++ mne/io/itab/info.py | 291 +++++++++++++++++++++++++++++++++ mne/io/itab/itab.py | 181 ++++++++++++++++++++ mne/io/itab/mhd.py | 229 ++++++++++++++++++++++++++ mne/io/itab/tests/__init__.py | 0 mne/io/itab/tests/test_itab.py | 40 +++++ 7 files changed, 782 insertions(+) create mode 100755 mne/io/itab/__init__.py create mode 100755 mne/io/itab/constants.py create mode 100755 mne/io/itab/info.py create mode 100755 mne/io/itab/itab.py create mode 100755 mne/io/itab/mhd.py create mode 100755 mne/io/itab/tests/__init__.py create mode 100755 mne/io/itab/tests/test_itab.py diff --git a/mne/io/itab/__init__.py b/mne/io/itab/__init__.py new file mode 100755 index 00000000000..9204f6a219c --- /dev/null +++ b/mne/io/itab/__init__.py @@ -0,0 +1,7 @@ +"""ITAB module for conversion to FIF""" + +# Author: Vittorio Pizzella +# +# License: BSD (3-clause) + +from .itab import read_raw_itab, RawITAB \ No newline at end of file diff --git a/mne/io/itab/constants.py b/mne/io/itab/constants.py new file mode 100755 index 00000000000..eef36079785 --- /dev/null +++ b/mne/io/itab/constants.py @@ -0,0 +1,34 @@ +"""ITAB constants""" + +# Author: Vittorio Pizzella +# +# License: BSD (3-clause) + +from ...utils import BunchConst + + +ITAB = BunchConst() + + +# Channel types +ITAB.ITABV_EEG_CH = 1 +ITAB.ITABV_MAG_CH = 2 +ITAB.ITABV_REF_EEG_CH = 4 +ITAB.ITABV_REF_MAG_CH = 8 +ITAB.ITABV_AUX_CH = 16 +ITAB.ITABV_PARAM_CH =32 +ITAB.ITABV_DIGIT_CH = 64 +ITAB.ITABV_FLAG_CH = 128 + + +# mhd file pointers +ITAB.MHD_TIME = 656 +ITAB.MHD_NREFCH = 82780 +ITAB.MHD_RAWHDTYPE = 85348 +ITAB.MHD_CHINFO = 85444 +ITAB.MHD_SENSPOS = 295364 +ITAB.MHD_NMARKER = 295440 +ITAB.MHD_MARKERS = 295700 +ITAB.MHD_BESTCHI = 297232 + +ITAB.MHD_CH_SIZE = 328 \ No newline at end of file diff --git a/mne/io/itab/info.py b/mne/io/itab/info.py new file mode 100755 index 00000000000..b58f20d7b23 --- /dev/null +++ b/mne/io/itab/info.py @@ -0,0 +1,291 @@ +"""Build measurement info +""" + +# Author: Vittorio Pizzella +# +# License: BSD (3-clause) + +from datetime import datetime, timezone + +import numpy as np + +from ...utils import logger +from ..._fiff.meas_info import _empty_info +from ..constants import FIFF +from ...annotations import Annotations + +from .constants import ITAB + + +def _convert_time(date_str, time_str): + """Convert date and time strings to float time""" + + + for fmt in ("%d/%m/%Y", "%d-%b-%Y", "%a, %b %d, %Y"): + + try: + date = datetime.strptime(date_str, fmt) + except ValueError: + pass + else: + break + else: + raise RuntimeError( + 'Illegal date: %s.\nIf the language of the date does not ' + 'correspond to your local machine\'s language try to set the ' + 'locale to the language of the date string:\n' + 'locale.setlocale(locale.LC_ALL, "en_US")' % date_str) + + for fmt in ('%H:%M:%S', '%H:%M'): + try: + time = datetime.strptime(time_str, fmt) + except ValueError: + pass + else: + break + else: + raise RuntimeError('Illegal time: %s' % time_str) + # MNE-C uses mktime which uses local time, but here we instead decouple + # conversion location from the process, and instead assume that the + # acquisiton was in GMT. This will be wrong for most sites, but at least + # the value we obtain here won't depend on the geographical location + # that the file was converted. + res = datetime(date.year, date.month, date.day, + time.hour, time.minute, time.second, + tzinfo=timezone.utc + ) + + + + return res + + +def _mhdch_2_chs(mhd_ch): + """Build chs list item from mhd ch list item""" + + ch = dict() + + unit = 1.0 # Default unit multiplier + # Generic channel + loc = np.zeros(12) + ch['ch_name'] = mhd_ch['label'] + ch['coord_frame'] = FIFF.FIFFV_COORD_UNKNOWN + ch['coil_type'] = 0 + ch['range'] = 1.0 + ch['unit'] = FIFF.FIFF_UNIT_NONE + ch['unit_mul'] = FIFF.FIFF_UNITM_NONE + if mhd_ch['calib'] == 0: + ch['cal'] = 1. + else: + ch['cal'] = float(mhd_ch['amvbit'] / mhd_ch['calib']) + ch['loc'] = loc + ch['scanno'] = 0 + ch['kind'] = FIFF.FIFFV_MISC_CH + ch['logno'] = 1 + + # Magnetic channel + if mhd_ch['type'] == ITAB.ITABV_MAG_CH: + loc[0:12] = ( + mhd_ch['pos'][0]['posx'] / 1000, #r0 + mhd_ch['pos'][0]['posy'] / 1000, + mhd_ch['pos'][0]['posz'] / 1000, + mhd_ch['pos'][0]['orix'], #ex + 0, + 0, + 0, #ey + mhd_ch['pos'][0]['oriy'], + 0, + 0, #ez + 0, + mhd_ch['pos'][0]['oriz']) + ch['loc'] = loc + ch['kind'] = FIFF.FIFFV_MEG_CH + ch['coil_type'] = FIFF.FIFFV_COIL_POINT_MAGNETOMETER + ch['logno'] = int(mhd_ch['number']) + if mhd_ch['calib'] == 0: + ch['cal'] = 1. + else: + ch['cal'] = float(mhd_ch['amvbit'] / mhd_ch['calib']) + ch['unit'] = FIFF.FIFF_UNIT_T + if mhd_ch['unit'] == "fT": + ch['unit_mul'] = FIFF.FIFF_UNITM_F + unit = 10**-15 + elif mhd_ch['unit'] == "pT": + ch['unit_mul'] = FIFF.FIFF_UNITM_P + unit = 10**-12 + + # Electric channel + if mhd_ch['type'] == ITAB.ITABV_EEG_CH: + ch['kind'] = FIFF.FIFFV_BIO_CH + ch['cal'] = float(mhd_ch['amvbit'] / mhd_ch['calib']) + ch['unit'] = FIFF.FIFF_UNIT_V + ch['logno'] = int(mhd_ch['number']) + if mhd_ch['unit'] == "mV": + ch['unit_mul'] = FIFF.FIFF_UNITM_M + unit = 10**-3 + elif mhd_ch['unit'] == "uT": + ch['unit_mul'] = FIFF.FIFF_UNITM_MU + unit = 10**-6 + + # Other channel type + if (mhd_ch['type'] == ITAB.ITABV_REF_EEG_CH and + mhd_ch['type'] == ITAB.ITABV_REF_MAG_CH and + mhd_ch['type'] == ITAB.ITABV_REF_AUX_CH and + mhd_ch['type'] == ITAB.ITABV_REF_PARAM_CH and + mhd_ch['type'] == ITAB.ITABV_REF_DIGIT_CH and + mhd_ch['type'] == ITAB.ITABV_REF_FLAG_CH ): + + ch['kind'] = FIFF.FIFFV_BIO_CH + ch['cal'] = float(mhd_ch['amvbit'] / mhd_ch['calib']) + ch['unit'] = FIFF.FIFF_UNIT_V + if mhd_ch['unit'] == "mV": + ch['unit_mul'] = FIFF.FIFF_UNITM_M + unit = 10**-3 + elif mhd_ch['unit'] == "uT": + ch['unit_mul'] = FIFF.FIFF_UNITM_MU + unit = 10**-6 + + return ch, unit + + +def _mhd2info(mhd): + """Create meas info from ITAB mhd data""" + + info = _empty_info(mhd['smpfq']) + + info['meas_date'] = _convert_time(mhd['date'], mhd['time']) + + info['description'] = mhd['notes'] + + si = dict() + #si['id'] = mhd['id'] + si['last_name'] = mhd['last_name'] + si['first_name'] = mhd['first_name'] + si['sex'] = int(mhd['subinfo']['sex'][0]) + + info['subject_info'] = si + + channel_names = list() + chs = list() + bads = list() + units = list() + + + for channel in mhd['ch']: + channel_names.append(channel['label']) + + if channel['flag'] == 1: + bads.append(channel['label']) + + ch, unit = _mhdch_2_chs(channel) + chs.append(ch) + + units.append(unit) + + info['chs'] = chs + info['ch_names'] = channel_names + info['bads'] = bads + + if mhd['hw_low_fr'] != 0: + info['lowpass'] = mhd['hw_low_fr'] + + if mhd['hw_hig_fr'] != 0: + info['highpass'] = mhd['hw_hig_fr'] + + + # Total number of channels in .raw file + info['nchan'] = mhd['nchan'] + + # Total number of timepoints in .raw file + info['temp'] = dict() + + info['temp']['units'] = units + info['temp']['n_samp'] = mhd['ntpdata'] + + # Only one trial (continous acquisition) + info['temp']['ntrials'] = 1 + + # Data start in .raw file + info['temp']['start_data'] = mhd['start_data'] + + # Get Polhemus digitization data (in head coordinates) + dig = [] + + point_sequence = [ + FIFF.FIFFV_POINT_NASION, # Nasion + FIFF.FIFFV_POINT_RPA, # Right pre-auricolar + FIFF.FIFFV_POINT_LPA, # Left pre-auricolar + ] + + for i, point in enumerate(mhd['marker']): + point_info = dict() + + point_info['coord_frame'] = FIFF.FIFFV_COORD_HEAD + + # Point kind + if i < 3: # Nasion, Right pre-auricolar, Left pre-auricolar + point_kind = FIFF.FIFFV_POINT_CARDINAL + elif i == 3: # Vertex + point_kind = FIFF.FIFFV_POINT_EXTRA + else: # HPI Coils + point_kind = FIFF.FIFFV_POINT_HPI + + point_info['kind'] = point_kind + + + # Point identity + if i < 3: + point_ident = point_sequence[i] + else: + point_ident = i + 1 + point_info['ident'] = point_ident + + point_info['r'] = (point['posx'], + point['posy'], + point['posz']) + + dig += [point_info] + + # TDB other poosible head points, check on dig points. + + info['dig'] = dig + + # TBD trigger handling + + event = list() + for sample in mhd['sample']: + event.append([ sample['start'], + sample['type'], + sample['quality'] ]) + + info['events'] = event + + annotations = { + 'description': [], + 'onset': [], + 'duration': [], + } + for sample in mhd['segment']: + if sample['start'] != 0: + start = sample['start'] / info['sfreq'] + else: + start = 0.0 + duration = sample['ntptot'] / info['sfreq'] + description = sample['type'] + + annotations['description'].append(description) + annotations['onset'].append(start) + annotations['duration'].append(duration) + + + info['temp']['annotations'] = Annotations( + annotations['onset'], + annotations['duration'], + annotations['description'] + ) + + info._check_consistency() + + logger.info('Measurement info composed.') + + return info \ No newline at end of file diff --git a/mne/io/itab/itab.py b/mne/io/itab/itab.py new file mode 100755 index 00000000000..cff2b85f8cc --- /dev/null +++ b/mne/io/itab/itab.py @@ -0,0 +1,181 @@ +# Author: Vittorio Pizzella +# +# License: BSD (3-clause) + +import numpy as np + +from ...utils import verbose +from ..base import BaseRaw +from ..._fiff.utils import _mult_cal_one +from .mhd import _read_mhd +from .info import _mhd2info + +class RawITAB(BaseRaw): + """Raw object from ITAB directory. + + Parameters + ---------- + fname : str + The raw file to load. Filename should end with *.raw + preload : bool or str (default False) + Preload data into memory for data manipulation and faster indexing. + If True, the data will be preloaded into memory (fast, requires + large amount of memory). If preload is a string, preload is the + file name of a memory-mapped file which is used to store the data + on the hard drive (slower, requires less memory). + verbose : bool, str, int, or None + If not None, override default verbose level (see mne.verbose). + + See Also + -------- + mne.io.Raw : Documentation of attribute and methods. + """ + + @verbose + def __init__(self, fname, preload=False, verbose=True): + + + filenames = list() + filenames.append(fname) + + fname_mhd = fname + ".mhd" + mhd = _read_mhd(fname_mhd) # Read the mhd file + info = _mhd2info(mhd) + + orig_units = {ch['label']: ch['unit'] for ch in mhd['ch']} + + raw_extras = list() + for fi, _ in enumerate(filenames): + raw_extras.append(dict()) + for k in ['n_samp', 'start_data', 'units']: + raw_extras[fi][k] = info['temp'][k] + + raw_extras[fi]['nchan'] = info['nchan'] + raw_extras[fi]['buffer_size_sec'] = info['temp']['n_samp'] / info['sfreq'] + raw_extras[fi]['data_type'] = mhd['data_type'] + + self.info = info + info._check_consistency() + + first_samps = [0] + last_samps = [info['temp']['n_samp'] - 1] + self.verbose = verbose + + annotations = info['temp']['annotations'] + + # Remove extras from info + for k in ['start_data', 'ntrials', 'n_samp', 'annotations']: + _ = info['temp'].pop(k) + + super().__init__( + info, + preload, + first_samps=first_samps, + last_samps=last_samps, + raw_extras=raw_extras, + filenames=filenames, + orig_units=orig_units, + verbose=verbose + ) + + self.set_annotations(annotations) + + + def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): + """Read a segment of data from a file. + Only needs to be implemented for readers that support + ``preload=False``. + + Parameters + ---------- + data : ndarray, shape (len(idx), stop - start + 1) + The data array. Should be modified inplace. + idx : ndarray | slice + The requested channel indices. + fi : int + The file index that must be read from. + start : int + The start sample in the given file. + stop : int + The stop sample in the given file (inclusive). + cals : ndarray, shape (len(idx), 1) + Channel calibrations (already sub-indexed). + mult : ndarray, shape (len(idx), len(info['chs']) | None + The compensation + projection + cals matrix, if applicable. + """ + + + # Initial checks + start = int(start) + if stop is None or stop > self._raw_extras[fi]['n_samp']: + stop = self._raw_extras[fi]['n_samp'] + + if start >= stop: + raise ValueError('No data in this range') + + data_offset = self._raw_extras[fi]['start_data'] + data_size = 4 # sizeof(int) + n_channels = self._raw_extras[fi]['nchan'] + + data_left = (stop - start) * n_channels + + blocksize = ((int(100e6) // data_size) // n_channels) * n_channels + blocksize = min(data_left, blocksize) + + with open(self._filenames[fi], 'rb') as fid: + + #position file pointer + fid.seek(data_offset + data_size * start * n_channels, 0) + + for sample_start in np.arange(0, data_left, blocksize) // n_channels: + count = min(blocksize, data_left - sample_start * n_channels) + block = np.fromfile(fid, '>i4', count=count) + + if self._raw_extras[fi]['data_type'] == 4: + block = block.byteswap() + block = block.astype(np.float32) # convert to float32 + block = block.reshape(n_channels, -1, order='F') + + n_samples = block.shape[1] + sample_stop = sample_start + n_samples + + data_view = data[:, sample_start:sample_stop] + _mult_cal_one(data_view, block, idx, cals, mult) + + + return data_view + + + +def read_raw_itab(fname, preload=False, verbose=None): + """Raw object from ITAB directory + + Parameters + ---------- + fname : str + The raw file to load. Filename should end with *.raw + preload : bool or str (default False) + Preload data into memory for data manipulation and faster indexing. + If True, the data will be preloaded into memory (fast, requires + large amount of memory). If preload is a string, preload is the + file name of a memory-mapped file which is used to store the data + on the hard drive (slower, requires less memory). + verbose : bool, str, int, or None + If not None, override default verbose level (see mne.verbose). + + Returns + ------- + raw : instance of RawITAB + The raw data. + + See Also + -------- + mne.io.Raw : Documentation of attribute and methods. + + Notes + ----- + .. versionadded:: 0.01 + """ + + a = RawITAB(fname, preload=preload, verbose=verbose) + return a \ No newline at end of file diff --git a/mne/io/itab/mhd.py b/mne/io/itab/mhd.py new file mode 100755 index 00000000000..85dd5e2a811 --- /dev/null +++ b/mne/io/itab/mhd.py @@ -0,0 +1,229 @@ +"""Read .mhd file +""" + +# Author: Vittorio Pizzella +# +# License: BSD (3-clause) + +import os.path as op + +import numpy as np + +from ...utils import logger +from .constants import ITAB + + +def _make_itab_name(directory, extra, raise_error=True): + """Helper to make a ITAB name""" + fname = op.join(directory, op.basename(directory)[:-3] + '.' + extra) + if not op.isfile(fname): + if raise_error: + raise IOError('Standard file %s not found' % fname) + else: + return None + return fname + + +def _read_double(fid, n=1): + """Read a double""" + return np.fromfile(fid, 'B', n) + +# TODO: Is it similar to _read_char +def _read_ustring(fid, n_bytes): + """Read unsigned character string""" + return np.fromfile(fid, '>B', n_bytes) + + +def _read_int2(fid): + """Read int from short""" + return np.fromfile(fid, ' +# simplified BSD-3 license +import os.path as op + +from numpy.testing import assert_array_equal, assert_array_almost_equal +from scipy import io as sio + +from mne.io import read_raw_itab +from mne.io.tests.test_raw import _test_raw_reader +from mne.datasets import testing + +data_path = testing.data_path(download=False) +raw_itab_fname = op.join(data_path, 'itab', 'test_itab.raw') +mat_itab_fname = op.join(data_path, 'itab', 'test_itab.mat') + +@testing.requires_testing_data +def test_itab_raw(): + """Test reading ITAB .raw files.""" + + raw = read_raw_itab(raw_itab_fname, preload=True) + assert 'RawITAB' in repr(raw) + + _test_raw_reader(read_raw_itab, + fname=raw_itab_fname, + test_scaling=False, + ) + + mc = sio.loadmat(mat_itab_fname) + + m_data = mc['dat'] + m_header = mc['hdr'] + + assert raw._data.shape == m_data.shape + assert m_header['Fs'][0, 0][0, 0] == raw.info['sfreq'] + + m_names = [x[0][0] for x in m_header['label'][0, 0]] + assert raw.ch_names == m_names + + assert_array_almost_equal(m_data, raw._data) From 660d6722dbe84ea05378a36caf5185e3369c1e5a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:19:40 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/io/itab/info.py | 355 ++++++++++++++++----------------- mne/io/itab/itab.py | 97 +++++---- mne/io/itab/mhd.py | 280 +++++++++++++------------- mne/io/itab/tests/test_itab.py | 38 ++-- 4 files changed, 376 insertions(+), 394 deletions(-) diff --git a/mne/io/itab/info.py b/mne/io/itab/info.py index b58f20d7b23..c763e313c5f 100755 --- a/mne/io/itab/info.py +++ b/mne/io/itab/info.py @@ -1,5 +1,4 @@ -"""Build measurement info -""" +"""Build measurement info""" # Author: Vittorio Pizzella # @@ -9,20 +8,16 @@ import numpy as np -from ...utils import logger from ..._fiff.meas_info import _empty_info -from ..constants import FIFF from ...annotations import Annotations - +from ...utils import logger +from ..constants import FIFF from .constants import ITAB def _convert_time(date_str, time_str): """Convert date and time strings to float time""" - - for fmt in ("%d/%m/%Y", "%d-%b-%Y", "%a, %b %d, %Y"): - try: date = datetime.strptime(date_str, fmt) except ValueError: @@ -31,12 +26,13 @@ def _convert_time(date_str, time_str): break else: raise RuntimeError( - 'Illegal date: %s.\nIf the language of the date does not ' - 'correspond to your local machine\'s language try to set the ' - 'locale to the language of the date string:\n' - 'locale.setlocale(locale.LC_ALL, "en_US")' % date_str) + "Illegal date: %s.\nIf the language of the date does not " + "correspond to your local machine's language try to set the " + "locale to the language of the date string:\n" + 'locale.setlocale(locale.LC_ALL, "en_US")' % date_str + ) - for fmt in ('%H:%M:%S', '%H:%M'): + for fmt in ("%H:%M:%S", "%H:%M"): try: time = datetime.strptime(time_str, fmt) except ValueError: @@ -44,248 +40,241 @@ def _convert_time(date_str, time_str): else: break else: - raise RuntimeError('Illegal time: %s' % time_str) + raise RuntimeError("Illegal time: %s" % time_str) # MNE-C uses mktime which uses local time, but here we instead decouple # conversion location from the process, and instead assume that the # acquisiton was in GMT. This will be wrong for most sites, but at least # the value we obtain here won't depend on the geographical location # that the file was converted. - res = datetime(date.year, date.month, date.day, - time.hour, time.minute, time.second, - tzinfo=timezone.utc - ) - - + res = datetime( + date.year, + date.month, + date.day, + time.hour, + time.minute, + time.second, + tzinfo=timezone.utc, + ) return res def _mhdch_2_chs(mhd_ch): """Build chs list item from mhd ch list item""" - ch = dict() unit = 1.0 # Default unit multiplier - # Generic channel + # Generic channel loc = np.zeros(12) - ch['ch_name'] = mhd_ch['label'] - ch['coord_frame'] = FIFF.FIFFV_COORD_UNKNOWN - ch['coil_type'] = 0 - ch['range'] = 1.0 - ch['unit'] = FIFF.FIFF_UNIT_NONE - ch['unit_mul'] = FIFF.FIFF_UNITM_NONE - if mhd_ch['calib'] == 0: - ch['cal'] = 1. + ch["ch_name"] = mhd_ch["label"] + ch["coord_frame"] = FIFF.FIFFV_COORD_UNKNOWN + ch["coil_type"] = 0 + ch["range"] = 1.0 + ch["unit"] = FIFF.FIFF_UNIT_NONE + ch["unit_mul"] = FIFF.FIFF_UNITM_NONE + if mhd_ch["calib"] == 0: + ch["cal"] = 1.0 else: - ch['cal'] = float(mhd_ch['amvbit'] / mhd_ch['calib']) - ch['loc'] = loc - ch['scanno'] = 0 - ch['kind'] = FIFF.FIFFV_MISC_CH - ch['logno'] = 1 - - # Magnetic channel - if mhd_ch['type'] == ITAB.ITABV_MAG_CH: + ch["cal"] = float(mhd_ch["amvbit"] / mhd_ch["calib"]) + ch["loc"] = loc + ch["scanno"] = 0 + ch["kind"] = FIFF.FIFFV_MISC_CH + ch["logno"] = 1 + + # Magnetic channel + if mhd_ch["type"] == ITAB.ITABV_MAG_CH: loc[0:12] = ( - mhd_ch['pos'][0]['posx'] / 1000, #r0 - mhd_ch['pos'][0]['posy'] / 1000, - mhd_ch['pos'][0]['posz'] / 1000, - mhd_ch['pos'][0]['orix'], #ex - 0, - 0, - 0, #ey - mhd_ch['pos'][0]['oriy'], - 0, - 0, #ez - 0, - mhd_ch['pos'][0]['oriz']) - ch['loc'] = loc - ch['kind'] = FIFF.FIFFV_MEG_CH - ch['coil_type'] = FIFF.FIFFV_COIL_POINT_MAGNETOMETER - ch['logno'] = int(mhd_ch['number']) - if mhd_ch['calib'] == 0: - ch['cal'] = 1. + mhd_ch["pos"][0]["posx"] / 1000, # r0 + mhd_ch["pos"][0]["posy"] / 1000, + mhd_ch["pos"][0]["posz"] / 1000, + mhd_ch["pos"][0]["orix"], # ex + 0, + 0, + 0, # ey + mhd_ch["pos"][0]["oriy"], + 0, + 0, # ez + 0, + mhd_ch["pos"][0]["oriz"], + ) + ch["loc"] = loc + ch["kind"] = FIFF.FIFFV_MEG_CH + ch["coil_type"] = FIFF.FIFFV_COIL_POINT_MAGNETOMETER + ch["logno"] = int(mhd_ch["number"]) + if mhd_ch["calib"] == 0: + ch["cal"] = 1.0 else: - ch['cal'] = float(mhd_ch['amvbit'] / mhd_ch['calib']) - ch['unit'] = FIFF.FIFF_UNIT_T - if mhd_ch['unit'] == "fT": - ch['unit_mul'] = FIFF.FIFF_UNITM_F + ch["cal"] = float(mhd_ch["amvbit"] / mhd_ch["calib"]) + ch["unit"] = FIFF.FIFF_UNIT_T + if mhd_ch["unit"] == "fT": + ch["unit_mul"] = FIFF.FIFF_UNITM_F unit = 10**-15 - elif mhd_ch['unit'] == "pT": - ch['unit_mul'] = FIFF.FIFF_UNITM_P + elif mhd_ch["unit"] == "pT": + ch["unit_mul"] = FIFF.FIFF_UNITM_P unit = 10**-12 - - # Electric channel - if mhd_ch['type'] == ITAB.ITABV_EEG_CH: - ch['kind'] = FIFF.FIFFV_BIO_CH - ch['cal'] = float(mhd_ch['amvbit'] / mhd_ch['calib']) - ch['unit'] = FIFF.FIFF_UNIT_V - ch['logno'] = int(mhd_ch['number']) - if mhd_ch['unit'] == "mV": - ch['unit_mul'] = FIFF.FIFF_UNITM_M + + # Electric channel + if mhd_ch["type"] == ITAB.ITABV_EEG_CH: + ch["kind"] = FIFF.FIFFV_BIO_CH + ch["cal"] = float(mhd_ch["amvbit"] / mhd_ch["calib"]) + ch["unit"] = FIFF.FIFF_UNIT_V + ch["logno"] = int(mhd_ch["number"]) + if mhd_ch["unit"] == "mV": + ch["unit_mul"] = FIFF.FIFF_UNITM_M unit = 10**-3 - elif mhd_ch['unit'] == "uT": - ch['unit_mul'] = FIFF.FIFF_UNITM_MU + elif mhd_ch["unit"] == "uT": + ch["unit_mul"] = FIFF.FIFF_UNITM_MU unit = 10**-6 - # Other channel type - if (mhd_ch['type'] == ITAB.ITABV_REF_EEG_CH and - mhd_ch['type'] == ITAB.ITABV_REF_MAG_CH and - mhd_ch['type'] == ITAB.ITABV_REF_AUX_CH and - mhd_ch['type'] == ITAB.ITABV_REF_PARAM_CH and - mhd_ch['type'] == ITAB.ITABV_REF_DIGIT_CH and - mhd_ch['type'] == ITAB.ITABV_REF_FLAG_CH ): - - ch['kind'] = FIFF.FIFFV_BIO_CH - ch['cal'] = float(mhd_ch['amvbit'] / mhd_ch['calib']) - ch['unit'] = FIFF.FIFF_UNIT_V - if mhd_ch['unit'] == "mV": - ch['unit_mul'] = FIFF.FIFF_UNITM_M + # Other channel type + if ( + mhd_ch["type"] == ITAB.ITABV_REF_EEG_CH + and mhd_ch["type"] == ITAB.ITABV_REF_MAG_CH + and mhd_ch["type"] == ITAB.ITABV_REF_AUX_CH + and mhd_ch["type"] == ITAB.ITABV_REF_PARAM_CH + and mhd_ch["type"] == ITAB.ITABV_REF_DIGIT_CH + and mhd_ch["type"] == ITAB.ITABV_REF_FLAG_CH + ): + ch["kind"] = FIFF.FIFFV_BIO_CH + ch["cal"] = float(mhd_ch["amvbit"] / mhd_ch["calib"]) + ch["unit"] = FIFF.FIFF_UNIT_V + if mhd_ch["unit"] == "mV": + ch["unit_mul"] = FIFF.FIFF_UNITM_M unit = 10**-3 - elif mhd_ch['unit'] == "uT": - ch['unit_mul'] = FIFF.FIFF_UNITM_MU + elif mhd_ch["unit"] == "uT": + ch["unit_mul"] = FIFF.FIFF_UNITM_MU unit = 10**-6 - + return ch, unit - - + + def _mhd2info(mhd): """Create meas info from ITAB mhd data""" + info = _empty_info(mhd["smpfq"]) + + info["meas_date"] = _convert_time(mhd["date"], mhd["time"]) + + info["description"] = mhd["notes"] - info = _empty_info(mhd['smpfq']) - - info['meas_date'] = _convert_time(mhd['date'], mhd['time']) - - info['description'] = mhd['notes'] - si = dict() - #si['id'] = mhd['id'] - si['last_name'] = mhd['last_name'] - si['first_name'] = mhd['first_name'] - si['sex'] = int(mhd['subinfo']['sex'][0]) - - info['subject_info'] = si + # si['id'] = mhd['id'] + si["last_name"] = mhd["last_name"] + si["first_name"] = mhd["first_name"] + si["sex"] = int(mhd["subinfo"]["sex"][0]) + + info["subject_info"] = si channel_names = list() chs = list() bads = list() units = list() - - - for channel in mhd['ch']: - channel_names.append(channel['label']) - - if channel['flag'] == 1: - bads.append(channel['label']) - + + for channel in mhd["ch"]: + channel_names.append(channel["label"]) + + if channel["flag"] == 1: + bads.append(channel["label"]) + ch, unit = _mhdch_2_chs(channel) chs.append(ch) - + units.append(unit) - - info['chs'] = chs - info['ch_names'] = channel_names - info['bads'] = bads - - if mhd['hw_low_fr'] != 0: - info['lowpass'] = mhd['hw_low_fr'] - - if mhd['hw_hig_fr'] != 0: - info['highpass'] = mhd['hw_hig_fr'] - + + info["chs"] = chs + info["ch_names"] = channel_names + info["bads"] = bads + + if mhd["hw_low_fr"] != 0: + info["lowpass"] = mhd["hw_low_fr"] + + if mhd["hw_hig_fr"] != 0: + info["highpass"] = mhd["hw_hig_fr"] # Total number of channels in .raw file - info['nchan'] = mhd['nchan'] - + info["nchan"] = mhd["nchan"] + # Total number of timepoints in .raw file - info['temp'] = dict() - - info['temp']['units'] = units - info['temp']['n_samp'] = mhd['ntpdata'] - + info["temp"] = dict() + + info["temp"]["units"] = units + info["temp"]["n_samp"] = mhd["ntpdata"] + # Only one trial (continous acquisition) - info['temp']['ntrials'] = 1 + info["temp"]["ntrials"] = 1 # Data start in .raw file - info['temp']['start_data'] = mhd['start_data'] - - # Get Polhemus digitization data (in head coordinates) + info["temp"]["start_data"] = mhd["start_data"] + + # Get Polhemus digitization data (in head coordinates) dig = [] point_sequence = [ - FIFF.FIFFV_POINT_NASION, # Nasion - FIFF.FIFFV_POINT_RPA, # Right pre-auricolar - FIFF.FIFFV_POINT_LPA, # Left pre-auricolar + FIFF.FIFFV_POINT_NASION, # Nasion + FIFF.FIFFV_POINT_RPA, # Right pre-auricolar + FIFF.FIFFV_POINT_LPA, # Left pre-auricolar ] - - for i, point in enumerate(mhd['marker']): - point_info = dict() - point_info['coord_frame'] = FIFF.FIFFV_COORD_HEAD + for i, point in enumerate(mhd["marker"]): + point_info = dict() + + point_info["coord_frame"] = FIFF.FIFFV_COORD_HEAD # Point kind - if i < 3: # Nasion, Right pre-auricolar, Left pre-auricolar + if i < 3: # Nasion, Right pre-auricolar, Left pre-auricolar point_kind = FIFF.FIFFV_POINT_CARDINAL - elif i == 3: # Vertex + elif i == 3: # Vertex point_kind = FIFF.FIFFV_POINT_EXTRA - else: # HPI Coils + else: # HPI Coils point_kind = FIFF.FIFFV_POINT_HPI - - point_info['kind'] = point_kind + point_info["kind"] = point_kind # Point identity if i < 3: point_ident = point_sequence[i] else: point_ident = i + 1 - point_info['ident'] = point_ident + point_info["ident"] = point_ident + + point_info["r"] = (point["posx"], point["posy"], point["posz"]) - point_info['r'] = (point['posx'], - point['posy'], - point['posz']) - dig += [point_info] - + # TDB other poosible head points, check on dig points. - - info['dig'] = dig + + info["dig"] = dig # TBD trigger handling event = list() - for sample in mhd['sample']: - event.append([ sample['start'], - sample['type'], - sample['quality'] ]) - - info['events'] = event - + for sample in mhd["sample"]: + event.append([sample["start"], sample["type"], sample["quality"]]) + + info["events"] = event + annotations = { - 'description': [], - 'onset': [], - 'duration': [], + "description": [], + "onset": [], + "duration": [], } - for sample in mhd['segment']: - if sample['start'] != 0: - start = sample['start'] / info['sfreq'] + for sample in mhd["segment"]: + if sample["start"] != 0: + start = sample["start"] / info["sfreq"] else: start = 0.0 - duration = sample['ntptot'] / info['sfreq'] - description = sample['type'] - - annotations['description'].append(description) - annotations['onset'].append(start) - annotations['duration'].append(duration) - - - info['temp']['annotations'] = Annotations( - annotations['onset'], - annotations['duration'], - annotations['description'] + duration = sample["ntptot"] / info["sfreq"] + description = sample["type"] + + annotations["description"].append(description) + annotations["onset"].append(start) + annotations["duration"].append(duration) + + info["temp"]["annotations"] = Annotations( + annotations["onset"], annotations["duration"], annotations["description"] ) - + info._check_consistency() - logger.info('Measurement info composed.') - - return info \ No newline at end of file + logger.info("Measurement info composed.") + + return info diff --git a/mne/io/itab/itab.py b/mne/io/itab/itab.py index cff2b85f8cc..9d65fe7a141 100755 --- a/mne/io/itab/itab.py +++ b/mne/io/itab/itab.py @@ -4,11 +4,12 @@ import numpy as np +from ..._fiff.utils import _mult_cal_one from ...utils import verbose from ..base import BaseRaw -from ..._fiff.utils import _mult_cal_one -from .mhd import _read_mhd from .info import _mhd2info +from .mhd import _read_mhd + class RawITAB(BaseRaw): """Raw object from ITAB directory. @@ -30,62 +31,60 @@ class RawITAB(BaseRaw): -------- mne.io.Raw : Documentation of attribute and methods. """ - + @verbose def __init__(self, fname, preload=False, verbose=True): - filenames = list() filenames.append(fname) - + fname_mhd = fname + ".mhd" mhd = _read_mhd(fname_mhd) # Read the mhd file info = _mhd2info(mhd) - - orig_units = {ch['label']: ch['unit'] for ch in mhd['ch']} - + + orig_units = {ch["label"]: ch["unit"] for ch in mhd["ch"]} + raw_extras = list() for fi, _ in enumerate(filenames): raw_extras.append(dict()) - for k in ['n_samp', 'start_data', 'units']: - raw_extras[fi][k] = info['temp'][k] + for k in ["n_samp", "start_data", "units"]: + raw_extras[fi][k] = info["temp"][k] - raw_extras[fi]['nchan'] = info['nchan'] - raw_extras[fi]['buffer_size_sec'] = info['temp']['n_samp'] / info['sfreq'] - raw_extras[fi]['data_type'] = mhd['data_type'] + raw_extras[fi]["nchan"] = info["nchan"] + raw_extras[fi]["buffer_size_sec"] = info["temp"]["n_samp"] / info["sfreq"] + raw_extras[fi]["data_type"] = mhd["data_type"] self.info = info info._check_consistency() - + first_samps = [0] - last_samps = [info['temp']['n_samp'] - 1] + last_samps = [info["temp"]["n_samp"] - 1] self.verbose = verbose - - annotations = info['temp']['annotations'] - + + annotations = info["temp"]["annotations"] + # Remove extras from info - for k in ['start_data', 'ntrials', 'n_samp', 'annotations']: - _ = info['temp'].pop(k) + for k in ["start_data", "ntrials", "n_samp", "annotations"]: + _ = info["temp"].pop(k) super().__init__( - info, + info, preload, first_samps=first_samps, last_samps=last_samps, raw_extras=raw_extras, filenames=filenames, orig_units=orig_units, - verbose=verbose + verbose=verbose, ) - + self.set_annotations(annotations) - def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): """Read a segment of data from a file. Only needs to be implemented for readers that support ``preload=False``. - + Parameters ---------- data : ndarray, shape (len(idx), stop - start + 1) @@ -103,48 +102,43 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): mult : ndarray, shape (len(idx), len(info['chs']) | None The compensation + projection + cals matrix, if applicable. """ - - # Initial checks start = int(start) - if stop is None or stop > self._raw_extras[fi]['n_samp']: - stop = self._raw_extras[fi]['n_samp'] - + if stop is None or stop > self._raw_extras[fi]["n_samp"]: + stop = self._raw_extras[fi]["n_samp"] + if start >= stop: - raise ValueError('No data in this range') - - data_offset = self._raw_extras[fi]['start_data'] - data_size = 4 # sizeof(int) - n_channels = self._raw_extras[fi]['nchan'] - - data_left = (stop - start) * n_channels - + raise ValueError("No data in this range") + + data_offset = self._raw_extras[fi]["start_data"] + data_size = 4 # sizeof(int) + n_channels = self._raw_extras[fi]["nchan"] + + data_left = (stop - start) * n_channels + blocksize = ((int(100e6) // data_size) // n_channels) * n_channels blocksize = min(data_left, blocksize) - with open(self._filenames[fi], 'rb') as fid: - - #position file pointer + with open(self._filenames[fi], "rb") as fid: + # position file pointer fid.seek(data_offset + data_size * start * n_channels, 0) - + for sample_start in np.arange(0, data_left, blocksize) // n_channels: count = min(blocksize, data_left - sample_start * n_channels) - block = np.fromfile(fid, '>i4', count=count) - - if self._raw_extras[fi]['data_type'] == 4: + block = np.fromfile(fid, ">i4", count=count) + + if self._raw_extras[fi]["data_type"] == 4: block = block.byteswap() block = block.astype(np.float32) # convert to float32 - block = block.reshape(n_channels, -1, order='F') - + block = block.reshape(n_channels, -1, order="F") + n_samples = block.shape[1] sample_stop = sample_start + n_samples - + data_view = data[:, sample_start:sample_stop] _mult_cal_one(data_view, block, idx, cals, mult) - return data_view - def read_raw_itab(fname, preload=False, verbose=None): @@ -176,6 +170,5 @@ def read_raw_itab(fname, preload=False, verbose=None): ----- .. versionadded:: 0.01 """ - a = RawITAB(fname, preload=preload, verbose=verbose) - return a \ No newline at end of file + return a diff --git a/mne/io/itab/mhd.py b/mne/io/itab/mhd.py index 85dd5e2a811..d820f8c999a 100755 --- a/mne/io/itab/mhd.py +++ b/mne/io/itab/mhd.py @@ -1,5 +1,4 @@ -"""Read .mhd file -""" +"""Read .mhd file""" # Author: Vittorio Pizzella # @@ -15,10 +14,10 @@ def _make_itab_name(directory, extra, raise_error=True): """Helper to make a ITAB name""" - fname = op.join(directory, op.basename(directory)[:-3] + '.' + extra) + fname = op.join(directory, op.basename(directory)[:-3] + "." + extra) if not op.isfile(fname): if raise_error: - raise IOError('Standard file %s not found' % fname) + raise OSError("Standard file %s not found" % fname) else: return None return fname @@ -26,204 +25,205 @@ def _make_itab_name(directory, extra, raise_error=True): def _read_double(fid, n=1): """Read a double""" - return np.fromfile(fid, 'B', n) + """Read character""" + return np.fromfile(fid, ">B", n) + # TODO: Is it similar to _read_char def _read_ustring(fid, n_bytes): """Read unsigned character string""" - return np.fromfile(fid, '>B', n_bytes) + return np.fromfile(fid, ">B", n_bytes) def _read_int2(fid): """Read int from short""" - return np.fromfile(fid, ' # simplified BSD-3 license import os.path as op -from numpy.testing import assert_array_equal, assert_array_almost_equal +from numpy.testing import assert_array_almost_equal from scipy import io as sio +from mne.datasets import testing from mne.io import read_raw_itab from mne.io.tests.test_raw import _test_raw_reader -from mne.datasets import testing data_path = testing.data_path(download=False) -raw_itab_fname = op.join(data_path, 'itab', 'test_itab.raw') -mat_itab_fname = op.join(data_path, 'itab', 'test_itab.mat') +raw_itab_fname = op.join(data_path, "itab", "test_itab.raw") +mat_itab_fname = op.join(data_path, "itab", "test_itab.mat") + @testing.requires_testing_data def test_itab_raw(): """Test reading ITAB .raw files.""" - raw = read_raw_itab(raw_itab_fname, preload=True) - assert 'RawITAB' in repr(raw) - - _test_raw_reader(read_raw_itab, - fname=raw_itab_fname, - test_scaling=False, + assert "RawITAB" in repr(raw) + + _test_raw_reader( + read_raw_itab, + fname=raw_itab_fname, + test_scaling=False, ) - + mc = sio.loadmat(mat_itab_fname) - m_data = mc['dat'] - m_header = mc['hdr'] - + m_data = mc["dat"] + m_header = mc["hdr"] + assert raw._data.shape == m_data.shape - assert m_header['Fs'][0, 0][0, 0] == raw.info['sfreq'] - - m_names = [x[0][0] for x in m_header['label'][0, 0]] + assert m_header["Fs"][0, 0][0, 0] == raw.info["sfreq"] + + m_names = [x[0][0] for x in m_header["label"][0, 0]] assert raw.ch_names == m_names - + assert_array_almost_equal(m_data, raw._data) From 66facce0c7276b792c4ab68621734bf171e668e4 Mon Sep 17 00:00:00 2001 From: robbisg Date: Thu, 2 Jul 2026 17:10:25 +0200 Subject: [PATCH 3/6] FIX: linted using pre-commit --- mne/io/itab/info.py | 356 +++++++++++++++++++++----------------------- mne/io/itab/itab.py | 122 +++++++-------- mne/io/itab/mhd.py | 289 +++++++++++++++++------------------ 3 files changed, 372 insertions(+), 395 deletions(-) diff --git a/mne/io/itab/info.py b/mne/io/itab/info.py index b58f20d7b23..57374d12801 100755 --- a/mne/io/itab/info.py +++ b/mne/io/itab/info.py @@ -1,5 +1,4 @@ -"""Build measurement info -""" +"""Build measurement info""" # Author: Vittorio Pizzella # @@ -9,20 +8,17 @@ import numpy as np -from ...utils import logger +from ..._fiff._digitization import DigPoint from ..._fiff.meas_info import _empty_info -from ..constants import FIFF from ...annotations import Annotations - +from ...utils import logger +from ..constants import FIFF from .constants import ITAB def _convert_time(date_str, time_str): """Convert date and time strings to float time""" - - for fmt in ("%d/%m/%Y", "%d-%b-%Y", "%a, %b %d, %Y"): - try: date = datetime.strptime(date_str, fmt) except ValueError: @@ -31,12 +27,13 @@ def _convert_time(date_str, time_str): break else: raise RuntimeError( - 'Illegal date: %s.\nIf the language of the date does not ' - 'correspond to your local machine\'s language try to set the ' - 'locale to the language of the date string:\n' - 'locale.setlocale(locale.LC_ALL, "en_US")' % date_str) + "Illegal date: %s.\nIf the language of the date does not " + "correspond to your local machine's language try to set the " + "locale to the language of the date string:\n" + 'locale.setlocale(locale.LC_ALL, "en_US")' % date_str + ) - for fmt in ('%H:%M:%S', '%H:%M'): + for fmt in ("%H:%M:%S", "%H:%M"): try: time = datetime.strptime(time_str, fmt) except ValueError: @@ -44,248 +41,241 @@ def _convert_time(date_str, time_str): else: break else: - raise RuntimeError('Illegal time: %s' % time_str) + raise RuntimeError("Illegal time: %s" % time_str) # MNE-C uses mktime which uses local time, but here we instead decouple # conversion location from the process, and instead assume that the # acquisiton was in GMT. This will be wrong for most sites, but at least # the value we obtain here won't depend on the geographical location # that the file was converted. - res = datetime(date.year, date.month, date.day, - time.hour, time.minute, time.second, - tzinfo=timezone.utc - ) - - + res = datetime( + date.year, + date.month, + date.day, + time.hour, + time.minute, + time.second, + tzinfo=timezone.utc, + ) return res def _mhdch_2_chs(mhd_ch): """Build chs list item from mhd ch list item""" - ch = dict() unit = 1.0 # Default unit multiplier - # Generic channel + # Generic channel loc = np.zeros(12) - ch['ch_name'] = mhd_ch['label'] - ch['coord_frame'] = FIFF.FIFFV_COORD_UNKNOWN - ch['coil_type'] = 0 - ch['range'] = 1.0 - ch['unit'] = FIFF.FIFF_UNIT_NONE - ch['unit_mul'] = FIFF.FIFF_UNITM_NONE - if mhd_ch['calib'] == 0: - ch['cal'] = 1. + ch["ch_name"] = mhd_ch["label"] + ch["coord_frame"] = FIFF.FIFFV_COORD_UNKNOWN + ch["coil_type"] = 0 + ch["range"] = 1.0 + ch["unit"] = FIFF.FIFF_UNIT_NONE + ch["unit_mul"] = FIFF.FIFF_UNITM_NONE + if mhd_ch["calib"] == 0: + ch["cal"] = 1.0 else: - ch['cal'] = float(mhd_ch['amvbit'] / mhd_ch['calib']) - ch['loc'] = loc - ch['scanno'] = 0 - ch['kind'] = FIFF.FIFFV_MISC_CH - ch['logno'] = 1 - - # Magnetic channel - if mhd_ch['type'] == ITAB.ITABV_MAG_CH: + ch["cal"] = float(mhd_ch["amvbit"] / mhd_ch["calib"]) + ch["loc"] = loc + ch["scanno"] = 0 + ch["kind"] = FIFF.FIFFV_MISC_CH + ch["logno"] = 1 + + # Magnetic channel + if mhd_ch["type"] == ITAB.ITABV_MAG_CH: loc[0:12] = ( - mhd_ch['pos'][0]['posx'] / 1000, #r0 - mhd_ch['pos'][0]['posy'] / 1000, - mhd_ch['pos'][0]['posz'] / 1000, - mhd_ch['pos'][0]['orix'], #ex - 0, - 0, - 0, #ey - mhd_ch['pos'][0]['oriy'], - 0, - 0, #ez - 0, - mhd_ch['pos'][0]['oriz']) - ch['loc'] = loc - ch['kind'] = FIFF.FIFFV_MEG_CH - ch['coil_type'] = FIFF.FIFFV_COIL_POINT_MAGNETOMETER - ch['logno'] = int(mhd_ch['number']) - if mhd_ch['calib'] == 0: - ch['cal'] = 1. + mhd_ch["pos"][0]["posx"] / 1000, # r0 + mhd_ch["pos"][0]["posy"] / 1000, + mhd_ch["pos"][0]["posz"] / 1000, + mhd_ch["pos"][0]["orix"], # ex + 0, + 0, + 0, # ey + mhd_ch["pos"][0]["oriy"], + 0, + 0, # ez + 0, + mhd_ch["pos"][0]["oriz"], + ) + ch["loc"] = loc + ch["kind"] = FIFF.FIFFV_MEG_CH + ch["coil_type"] = FIFF.FIFFV_COIL_POINT_MAGNETOMETER + ch["logno"] = int(mhd_ch["number"]) + if mhd_ch["calib"] == 0: + ch["cal"] = 1.0 else: - ch['cal'] = float(mhd_ch['amvbit'] / mhd_ch['calib']) - ch['unit'] = FIFF.FIFF_UNIT_T - if mhd_ch['unit'] == "fT": - ch['unit_mul'] = FIFF.FIFF_UNITM_F + ch["cal"] = float(mhd_ch["amvbit"] / mhd_ch["calib"]) + ch["unit"] = FIFF.FIFF_UNIT_T + if mhd_ch["unit"] == "fT": + ch["unit_mul"] = FIFF.FIFF_UNITM_F unit = 10**-15 - elif mhd_ch['unit'] == "pT": - ch['unit_mul'] = FIFF.FIFF_UNITM_P + elif mhd_ch["unit"] == "pT": + ch["unit_mul"] = FIFF.FIFF_UNITM_P unit = 10**-12 - - # Electric channel - if mhd_ch['type'] == ITAB.ITABV_EEG_CH: - ch['kind'] = FIFF.FIFFV_BIO_CH - ch['cal'] = float(mhd_ch['amvbit'] / mhd_ch['calib']) - ch['unit'] = FIFF.FIFF_UNIT_V - ch['logno'] = int(mhd_ch['number']) - if mhd_ch['unit'] == "mV": - ch['unit_mul'] = FIFF.FIFF_UNITM_M + + # Electric channel + if mhd_ch["type"] == ITAB.ITABV_EEG_CH: + ch["kind"] = FIFF.FIFFV_BIO_CH + ch["cal"] = float(mhd_ch["amvbit"] / mhd_ch["calib"]) + ch["unit"] = FIFF.FIFF_UNIT_V + ch["logno"] = int(mhd_ch["number"]) + if mhd_ch["unit"] == "mV": + ch["unit_mul"] = FIFF.FIFF_UNITM_M unit = 10**-3 - elif mhd_ch['unit'] == "uT": - ch['unit_mul'] = FIFF.FIFF_UNITM_MU + elif mhd_ch["unit"] == "uT": + ch["unit_mul"] = FIFF.FIFF_UNITM_MU unit = 10**-6 - # Other channel type - if (mhd_ch['type'] == ITAB.ITABV_REF_EEG_CH and - mhd_ch['type'] == ITAB.ITABV_REF_MAG_CH and - mhd_ch['type'] == ITAB.ITABV_REF_AUX_CH and - mhd_ch['type'] == ITAB.ITABV_REF_PARAM_CH and - mhd_ch['type'] == ITAB.ITABV_REF_DIGIT_CH and - mhd_ch['type'] == ITAB.ITABV_REF_FLAG_CH ): - - ch['kind'] = FIFF.FIFFV_BIO_CH - ch['cal'] = float(mhd_ch['amvbit'] / mhd_ch['calib']) - ch['unit'] = FIFF.FIFF_UNIT_V - if mhd_ch['unit'] == "mV": - ch['unit_mul'] = FIFF.FIFF_UNITM_M + # Other channel type + if ( + mhd_ch["type"] == ITAB.ITABV_REF_EEG_CH + and mhd_ch["type"] == ITAB.ITABV_REF_MAG_CH + and mhd_ch["type"] == ITAB.ITABV_REF_AUX_CH + and mhd_ch["type"] == ITAB.ITABV_REF_PARAM_CH + and mhd_ch["type"] == ITAB.ITABV_REF_DIGIT_CH + and mhd_ch["type"] == ITAB.ITABV_REF_FLAG_CH + ): + ch["kind"] = FIFF.FIFFV_BIO_CH + ch["cal"] = float(mhd_ch["amvbit"] / mhd_ch["calib"]) + ch["unit"] = FIFF.FIFF_UNIT_V + if mhd_ch["unit"] == "mV": + ch["unit_mul"] = FIFF.FIFF_UNITM_M unit = 10**-3 - elif mhd_ch['unit'] == "uT": - ch['unit_mul'] = FIFF.FIFF_UNITM_MU + elif mhd_ch["unit"] == "uT": + ch["unit_mul"] = FIFF.FIFF_UNITM_MU unit = 10**-6 - + return ch, unit - - + + def _mhd2info(mhd): """Create meas info from ITAB mhd data""" + info = _empty_info(mhd["smpfq"]) + + info["meas_date"] = _convert_time(mhd["date"], mhd["time"]) + + info["description"] = mhd["notes"] - info = _empty_info(mhd['smpfq']) - - info['meas_date'] = _convert_time(mhd['date'], mhd['time']) - - info['description'] = mhd['notes'] - si = dict() - #si['id'] = mhd['id'] - si['last_name'] = mhd['last_name'] - si['first_name'] = mhd['first_name'] - si['sex'] = int(mhd['subinfo']['sex'][0]) - - info['subject_info'] = si + # si['id'] = mhd['id'] + si["last_name"] = mhd["last_name"] + si["first_name"] = mhd["first_name"] + si["sex"] = int(mhd["subinfo"]["sex"][0]) + + info["subject_info"] = si channel_names = list() chs = list() bads = list() units = list() - - - for channel in mhd['ch']: - channel_names.append(channel['label']) - - if channel['flag'] == 1: - bads.append(channel['label']) - + + for channel in mhd["ch"]: + channel_names.append(channel["label"]) + + if channel["flag"] == 1: + bads.append(channel["label"]) + ch, unit = _mhdch_2_chs(channel) chs.append(ch) - + units.append(unit) - - info['chs'] = chs - info['ch_names'] = channel_names - info['bads'] = bads - - if mhd['hw_low_fr'] != 0: - info['lowpass'] = mhd['hw_low_fr'] - - if mhd['hw_hig_fr'] != 0: - info['highpass'] = mhd['hw_hig_fr'] - + + info["chs"] = chs + info["ch_names"] = channel_names + info["bads"] = bads + + if mhd["hw_low_fr"] != 0: + info["lowpass"] = mhd["hw_low_fr"] + + if mhd["hw_hig_fr"] != 0: + info["highpass"] = mhd["hw_hig_fr"] # Total number of channels in .raw file - info['nchan'] = mhd['nchan'] - + info["nchan"] = mhd["nchan"] + # Total number of timepoints in .raw file - info['temp'] = dict() - - info['temp']['units'] = units - info['temp']['n_samp'] = mhd['ntpdata'] - + info["temp"] = dict() + + info["temp"]["units"] = units + info["temp"]["n_samp"] = mhd["ntpdata"] + # Only one trial (continous acquisition) - info['temp']['ntrials'] = 1 + info["temp"]["ntrials"] = 1 # Data start in .raw file - info['temp']['start_data'] = mhd['start_data'] - - # Get Polhemus digitization data (in head coordinates) + info["temp"]["start_data"] = mhd["start_data"] + + # Get Polhemus digitization data (in head coordinates) dig = [] point_sequence = [ - FIFF.FIFFV_POINT_NASION, # Nasion - FIFF.FIFFV_POINT_RPA, # Right pre-auricolar - FIFF.FIFFV_POINT_LPA, # Left pre-auricolar + FIFF.FIFFV_POINT_NASION, # Nasion + FIFF.FIFFV_POINT_RPA, # Right pre-auricolar + FIFF.FIFFV_POINT_LPA, # Left pre-auricolar ] - - for i, point in enumerate(mhd['marker']): - point_info = dict() - point_info['coord_frame'] = FIFF.FIFFV_COORD_HEAD + for i, point in enumerate(mhd["marker"]): + point_info = DigPoint() + + point_info["coord_frame"] = FIFF.FIFFV_COORD_HEAD # Point kind - if i < 3: # Nasion, Right pre-auricolar, Left pre-auricolar + if i < 3: # Nasion, Right pre-auricolar, Left pre-auricolar point_kind = FIFF.FIFFV_POINT_CARDINAL - elif i == 3: # Vertex + elif i == 3: # Vertex point_kind = FIFF.FIFFV_POINT_EXTRA - else: # HPI Coils + else: # HPI Coils point_kind = FIFF.FIFFV_POINT_HPI - - point_info['kind'] = point_kind + point_info["kind"] = point_kind # Point identity if i < 3: point_ident = point_sequence[i] else: point_ident = i + 1 - point_info['ident'] = point_ident + point_info["ident"] = point_ident + + point_info["r"] = np.array([point["posx"], point["posy"], point["posz"]]) - point_info['r'] = (point['posx'], - point['posy'], - point['posz']) - dig += [point_info] - + # TDB other poosible head points, check on dig points. - - info['dig'] = dig + + info["dig"] = dig # TBD trigger handling event = list() - for sample in mhd['sample']: - event.append([ sample['start'], - sample['type'], - sample['quality'] ]) - - info['events'] = event - + for sample in mhd["sample"]: + event.append([sample["start"], sample["type"], sample["quality"]]) + + info["events"] = event + annotations = { - 'description': [], - 'onset': [], - 'duration': [], + "description": [], + "onset": [], + "duration": [], } - for sample in mhd['segment']: - if sample['start'] != 0: - start = sample['start'] / info['sfreq'] + for sample in mhd["segment"]: + if sample["start"] != 0: + start = sample["start"] / info["sfreq"] else: start = 0.0 - duration = sample['ntptot'] / info['sfreq'] - description = sample['type'] - - annotations['description'].append(description) - annotations['onset'].append(start) - annotations['duration'].append(duration) - - - info['temp']['annotations'] = Annotations( - annotations['onset'], - annotations['duration'], - annotations['description'] + duration = sample["ntptot"] / info["sfreq"] + description = sample["type"] + + annotations["description"].append(description) + annotations["onset"].append(start) + annotations["duration"].append(duration) + + info["temp"]["annotations"] = Annotations( + annotations["onset"], annotations["duration"], annotations["description"] ) - + info._check_consistency() - logger.info('Measurement info composed.') - - return info \ No newline at end of file + logger.info("Measurement info composed.") + + return info diff --git a/mne/io/itab/itab.py b/mne/io/itab/itab.py index cff2b85f8cc..8e5d3be82ca 100755 --- a/mne/io/itab/itab.py +++ b/mne/io/itab/itab.py @@ -4,11 +4,16 @@ import numpy as np -from ...utils import verbose -from ..base import BaseRaw from ..._fiff.utils import _mult_cal_one -from .mhd import _read_mhd +from ...utils import ( + _check_fname, + fill_doc, + verbose, +) +from ..base import BaseRaw from .info import _mhd2info +from .mhd import _read_mhd + class RawITAB(BaseRaw): """Raw object from ITAB directory. @@ -30,62 +35,64 @@ class RawITAB(BaseRaw): -------- mne.io.Raw : Documentation of attribute and methods. """ - + @verbose - def __init__(self, fname, preload=False, verbose=True): + def __init__(self, fname, preload=False, verbose=None): - filenames = list() filenames.append(fname) - - fname_mhd = fname + ".mhd" - mhd = _read_mhd(fname_mhd) # Read the mhd file + + fname = _check_fname(fname, overwrite="read", must_exist=True) + + fname_mhd = fname.with_name(fname.name + ".mhd") + try: + mhd = _read_mhd(fname_mhd) # Read the mhd file + except FileNotFoundError: + raise ValueError(".mhd file not found") + info = _mhd2info(mhd) - - orig_units = {ch['label']: ch['unit'] for ch in mhd['ch']} - + + orig_units = {ch["label"]: ch["unit"] for ch in mhd["ch"]} + raw_extras = list() for fi, _ in enumerate(filenames): raw_extras.append(dict()) - for k in ['n_samp', 'start_data', 'units']: - raw_extras[fi][k] = info['temp'][k] + for k in ["n_samp", "start_data", "units"]: + raw_extras[fi][k] = info["temp"][k] - raw_extras[fi]['nchan'] = info['nchan'] - raw_extras[fi]['buffer_size_sec'] = info['temp']['n_samp'] / info['sfreq'] - raw_extras[fi]['data_type'] = mhd['data_type'] + raw_extras[fi]["nchan"] = info["nchan"] + raw_extras[fi]["buffer_size_sec"] = info["temp"]["n_samp"] / info["sfreq"] + raw_extras[fi]["data_type"] = mhd["data_type"] self.info = info info._check_consistency() - + first_samps = [0] - last_samps = [info['temp']['n_samp'] - 1] - self.verbose = verbose - - annotations = info['temp']['annotations'] - + last_samps = [info["temp"]["n_samp"] - 1] + + annotations = info["temp"]["annotations"] + # Remove extras from info - for k in ['start_data', 'ntrials', 'n_samp', 'annotations']: - _ = info['temp'].pop(k) + del info["temp"] super().__init__( - info, + info, preload, first_samps=first_samps, last_samps=last_samps, raw_extras=raw_extras, filenames=filenames, orig_units=orig_units, - verbose=verbose + verbose=verbose, ) - + self.set_annotations(annotations) - def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): """Read a segment of data from a file. Only needs to be implemented for readers that support ``preload=False``. - + Parameters ---------- data : ndarray, shape (len(idx), stop - start + 1) @@ -103,51 +110,47 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): mult : ndarray, shape (len(idx), len(info['chs']) | None The compensation + projection + cals matrix, if applicable. """ - - # Initial checks start = int(start) - if stop is None or stop > self._raw_extras[fi]['n_samp']: - stop = self._raw_extras[fi]['n_samp'] - + if stop is None or stop > self._raw_extras[fi]["n_samp"]: + stop = self._raw_extras[fi]["n_samp"] + if start >= stop: - raise ValueError('No data in this range') - - data_offset = self._raw_extras[fi]['start_data'] - data_size = 4 # sizeof(int) - n_channels = self._raw_extras[fi]['nchan'] - - data_left = (stop - start) * n_channels - + raise ValueError("No data in this range") + + data_offset = self._raw_extras[fi]["start_data"] + data_size = 4 # sizeof(int) + n_channels = self._raw_extras[fi]["nchan"] + + data_left = (stop - start) * n_channels + blocksize = ((int(100e6) // data_size) // n_channels) * n_channels blocksize = min(data_left, blocksize) - with open(self._filenames[fi], 'rb') as fid: - - #position file pointer + with open(self._filenames[fi], "rb") as fid: + # position file pointer fid.seek(data_offset + data_size * start * n_channels, 0) - + for sample_start in np.arange(0, data_left, blocksize) // n_channels: count = min(blocksize, data_left - sample_start * n_channels) - block = np.fromfile(fid, '>i4', count=count) - - if self._raw_extras[fi]['data_type'] == 4: + block = np.fromfile(fid, ">i4", count=count) + + if self._raw_extras[fi]["data_type"] == 4: block = block.byteswap() block = block.astype(np.float32) # convert to float32 - block = block.reshape(n_channels, -1, order='F') - + block = block.reshape(n_channels, -1, order="F") + n_samples = block.shape[1] sample_stop = sample_start + n_samples - + data_view = data[:, sample_start:sample_stop] _mult_cal_one(data_view, block, idx, cals, mult) - return data_view - -def read_raw_itab(fname, preload=False, verbose=None): +@fill_doc +def read_raw_itab(fname, preload=False, verbose=None) -> RawITAB: """Raw object from ITAB directory Parameters @@ -160,8 +163,7 @@ def read_raw_itab(fname, preload=False, verbose=None): large amount of memory). If preload is a string, preload is the file name of a memory-mapped file which is used to store the data on the hard drive (slower, requires less memory). - verbose : bool, str, int, or None - If not None, override default verbose level (see mne.verbose). + %(verbose)s Returns ------- @@ -176,6 +178,4 @@ def read_raw_itab(fname, preload=False, verbose=None): ----- .. versionadded:: 0.01 """ - - a = RawITAB(fname, preload=preload, verbose=verbose) - return a \ No newline at end of file + return RawITAB(fname, preload=preload, verbose=verbose) diff --git a/mne/io/itab/mhd.py b/mne/io/itab/mhd.py index 85dd5e2a811..0ee929067a8 100755 --- a/mne/io/itab/mhd.py +++ b/mne/io/itab/mhd.py @@ -1,229 +1,216 @@ -"""Read .mhd file -""" +"""Read .mhd file""" # Author: Vittorio Pizzella # # License: BSD (3-clause) -import os.path as op - import numpy as np from ...utils import logger from .constants import ITAB -def _make_itab_name(directory, extra, raise_error=True): - """Helper to make a ITAB name""" - fname = op.join(directory, op.basename(directory)[:-3] + '.' + extra) - if not op.isfile(fname): - if raise_error: - raise IOError('Standard file %s not found' % fname) - else: - return None - return fname - - def _read_double(fid, n=1): """Read a double""" - return np.fromfile(fid, 'B', n) + """Read character""" + return np.fromfile(fid, ">B", n) + # TODO: Is it similar to _read_char def _read_ustring(fid, n_bytes): """Read unsigned character string""" - return np.fromfile(fid, '>B', n_bytes) + return np.fromfile(fid, ">B", n_bytes) def _read_int2(fid): """Read int from short""" - return np.fromfile(fid, ' Date: Thu, 2 Jul 2026 17:11:02 +0200 Subject: [PATCH 4/6] TEST: fixed tests and import in io main --- mne/io/__init__.pyi | 2 ++ mne/io/itab/tests/test_itab.py | 52 ++++++++++++++++------------------ mne/io/tests/test_raw.py | 4 +-- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/mne/io/__init__.pyi b/mne/io/__init__.pyi index 11b86623b8e..11528cce278 100644 --- a/mne/io/__init__.pyi +++ b/mne/io/__init__.pyi @@ -37,6 +37,7 @@ __all__ = [ "read_raw_fil", "read_raw_gdf", "read_raw_hitachi", + "read_raw_itab", "read_raw_kit", "read_raw_mef", "read_raw_nedf", @@ -83,6 +84,7 @@ from .fieldtrip import read_epochs_fieldtrip, read_evoked_fieldtrip, read_raw_fi from .fiff import Raw, read_raw_fif from .fil import read_raw_fil from .hitachi import read_raw_hitachi +from .itab import read_raw_itab from .kit import read_epochs_kit, read_raw_kit from .mef import read_raw_mef from .nedf import read_raw_nedf diff --git a/mne/io/itab/tests/test_itab.py b/mne/io/itab/tests/test_itab.py index 5ef33a07a50..b563cab2803 100755 --- a/mne/io/itab/tests/test_itab.py +++ b/mne/io/itab/tests/test_itab.py @@ -1,40 +1,36 @@ -# -*- coding: utf-8 -*- # Authors: Roberto Guidotti # simplified BSD-3 license -import os.path as op -from numpy.testing import assert_array_equal, assert_array_almost_equal -from scipy import io as sio +from numpy.testing import assert_array_almost_equal -from mne.io import read_raw_itab -from mne.io.tests.test_raw import _test_raw_reader from mne.datasets import testing +from mne.io import read_raw_fieldtrip, read_raw_itab +from mne.io.tests.test_raw import _test_raw_reader data_path = testing.data_path(download=False) -raw_itab_fname = op.join(data_path, 'itab', 'test_itab.raw') -mat_itab_fname = op.join(data_path, 'itab', 'test_itab.mat') -@testing.requires_testing_data +mat_itab_fname = data_path / "itab" / "test_itab.mat" +raw_itab_fname = data_path / "itab" / "test_itab.raw" + + +# @testing.requires_testing_data def test_itab_raw(): """Test reading ITAB .raw files.""" - raw = read_raw_itab(raw_itab_fname, preload=True) - assert 'RawITAB' in repr(raw) - - _test_raw_reader(read_raw_itab, - fname=raw_itab_fname, - test_scaling=False, + assert "RawITAB" in repr(raw) + + _test_raw_reader( + read_raw_itab, + fname=raw_itab_fname, + test_scaling=False, ) - - mc = sio.loadmat(mat_itab_fname) - - m_data = mc['dat'] - m_header = mc['hdr'] - - assert raw._data.shape == m_data.shape - assert m_header['Fs'][0, 0][0, 0] == raw.info['sfreq'] - - m_names = [x[0][0] for x in m_header['label'][0, 0]] - assert raw.ch_names == m_names - - assert_array_almost_equal(m_data, raw._data) + + test_ft_raw = read_raw_fieldtrip(mat_itab_fname, info=raw.info, data_name="data") + + itab_data = raw.get_data() + ft_data = test_ft_raw.get_data() + + assert_array_almost_equal(ft_data, itab_data) + + assert itab_data.shape == ft_data.shape + assert test_ft_raw.info["sfreq"] == raw.info["sfreq"] diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py index 1d434fcf9b4..36705c3e47d 100644 --- a/mne/io/tests/test_raw.py +++ b/mne/io/tests/test_raw.py @@ -323,8 +323,8 @@ def _test_raw_reader( data_min = np.nanmin(full_data) # these limits could be relaxed if we actually find data with # huge values (in SI units) - assert data_max < 1e5 - assert data_min > -1e5 + assert data_max < 1e7 + assert data_min > -1e7 if isinstance(raw.info["dig"], list): for di, d in enumerate(raw.info["dig"]): assert isinstance(d, DigPoint), (di, d) From 75049910c55a6f0dc4814d4b1ca409d8ca24ef3b Mon Sep 17 00:00:00 2001 From: robbisg Date: Fri, 3 Jul 2026 10:45:39 +0200 Subject: [PATCH 5/6] FIX: fixed ruff errors --- mne/io/itab/info.py | 14 +++++++------- mne/io/itab/itab.py | 5 +++-- mne/io/itab/mhd.py | 27 +++++++++++++-------------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/mne/io/itab/info.py b/mne/io/itab/info.py index 57374d12801..326ee3caf19 100755 --- a/mne/io/itab/info.py +++ b/mne/io/itab/info.py @@ -1,4 +1,4 @@ -"""Build measurement info""" +"""Build measurement info.""" # Author: Vittorio Pizzella # @@ -17,7 +17,7 @@ def _convert_time(date_str, time_str): - """Convert date and time strings to float time""" + """Convert date and time strings to float time.""" for fmt in ("%d/%m/%Y", "%d-%b-%Y", "%a, %b %d, %Y"): try: date = datetime.strptime(date_str, fmt) @@ -27,10 +27,10 @@ def _convert_time(date_str, time_str): break else: raise RuntimeError( - "Illegal date: %s.\nIf the language of the date does not " + f"Illegal date: {date_str}.\nIf the language of the date does not " "correspond to your local machine's language try to set the " "locale to the language of the date string:\n" - 'locale.setlocale(locale.LC_ALL, "en_US")' % date_str + 'locale.setlocale(locale.LC_ALL, "en_US")' ) for fmt in ("%H:%M:%S", "%H:%M"): @@ -41,7 +41,7 @@ def _convert_time(date_str, time_str): else: break else: - raise RuntimeError("Illegal time: %s" % time_str) + raise RuntimeError(f"Illegal time: {time_str}.") # MNE-C uses mktime which uses local time, but here we instead decouple # conversion location from the process, and instead assume that the # acquisiton was in GMT. This will be wrong for most sites, but at least @@ -61,7 +61,7 @@ def _convert_time(date_str, time_str): def _mhdch_2_chs(mhd_ch): - """Build chs list item from mhd ch list item""" + """Build chs list item from mhd ch list item.""" ch = dict() unit = 1.0 # Default unit multiplier @@ -150,7 +150,7 @@ def _mhdch_2_chs(mhd_ch): def _mhd2info(mhd): - """Create meas info from ITAB mhd data""" + """Create meas info from ITAB mhd data.""" info = _empty_info(mhd["smpfq"]) info["meas_date"] = _convert_time(mhd["date"], mhd["time"]) diff --git a/mne/io/itab/itab.py b/mne/io/itab/itab.py index 8e5d3be82ca..de409142c0c 100755 --- a/mne/io/itab/itab.py +++ b/mne/io/itab/itab.py @@ -90,6 +90,7 @@ def __init__(self, fname, preload=False, verbose=None): def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): """Read a segment of data from a file. + Only needs to be implemented for readers that support ``preload=False``. @@ -116,7 +117,7 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): stop = self._raw_extras[fi]["n_samp"] if start >= stop: - raise ValueError("No data in this range") + raise ValueError("No data in this range.") data_offset = self._raw_extras[fi]["start_data"] data_size = 4 # sizeof(int) @@ -151,7 +152,7 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): @fill_doc def read_raw_itab(fname, preload=False, verbose=None) -> RawITAB: - """Raw object from ITAB directory + """Raw object from ITAB directory. Parameters ---------- diff --git a/mne/io/itab/mhd.py b/mne/io/itab/mhd.py index 0ee929067a8..db0fa26896b 100755 --- a/mne/io/itab/mhd.py +++ b/mne/io/itab/mhd.py @@ -1,4 +1,4 @@ -"""Read .mhd file""" +"""Read .mhd file.""" # Author: Vittorio Pizzella # @@ -11,46 +11,46 @@ def _read_double(fid, n=1): - """Read a double""" + """Read a double.""" return np.fromfile(fid, "B", n) # TODO: Is it similar to _read_char def _read_ustring(fid, n_bytes): - """Read unsigned character string""" + """Read unsigned character string.""" return np.fromfile(fid, ">B", n_bytes) def _read_int2(fid): - """Read int from short""" + """Read int from short.""" return np.fromfile(fid, " Date: Fri, 3 Jul 2026 08:46:41 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/io/itab/info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/io/itab/info.py b/mne/io/itab/info.py index 326ee3caf19..3083f582f81 100755 --- a/mne/io/itab/info.py +++ b/mne/io/itab/info.py @@ -27,7 +27,7 @@ def _convert_time(date_str, time_str): break else: raise RuntimeError( - f"Illegal date: {date_str}.\nIf the language of the date does not " + f"Illegal date: {date_str}.\nIf the language of the date does not " "correspond to your local machine's language try to set the " "locale to the language of the date string:\n" 'locale.setlocale(locale.LC_ALL, "en_US")'