337 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			337 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# Authors: The MNE-Python contributors.
 | 
						|
# License: BSD-3-Clause
 | 
						|
# Copyright the MNE-Python contributors.
 | 
						|
 | 
						|
import json
 | 
						|
import pathlib
 | 
						|
 | 
						|
import numpy as np
 | 
						|
 | 
						|
from ..._fiff._digitization import _make_dig_points
 | 
						|
from ..._fiff.constants import FIFF
 | 
						|
from ..._fiff.meas_info import _empty_info
 | 
						|
from ..._fiff.utils import _read_segments_file
 | 
						|
from ..._fiff.write import get_new_file_id
 | 
						|
from ...transforms import Transform, apply_trans, get_ras_to_neuromag_trans
 | 
						|
from ...utils import _check_fname, fill_doc, verbose, warn
 | 
						|
from ..base import BaseRaw
 | 
						|
from .sensors import (
 | 
						|
    _get_plane_vectors,
 | 
						|
    _get_pos_units,
 | 
						|
    _refine_sensor_orientation,
 | 
						|
    _size2units,
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
@verbose
 | 
						|
def read_raw_fil(
 | 
						|
    binfile, precision="single", preload=False, *, verbose=None
 | 
						|
) -> "RawFIL":
 | 
						|
    """Raw object from FIL-OPMEG formatted data.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    binfile : path-like
 | 
						|
        Path to the MEG data binary (ending in ``'_meg.bin'``).
 | 
						|
    precision : str, optional
 | 
						|
        How is the data represented? ``'single'`` if 32-bit or ``'double'`` if
 | 
						|
        64-bit (default is single).
 | 
						|
    %(preload)s
 | 
						|
    %(verbose)s
 | 
						|
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    raw : instance of RawFIL
 | 
						|
        The raw data.
 | 
						|
        See :class:`mne.io.Raw` for documentation of attributes and methods.
 | 
						|
 | 
						|
    See Also
 | 
						|
    --------
 | 
						|
    mne.io.Raw : Documentation of attributes and methods of RawFIL.
 | 
						|
    """
 | 
						|
    return RawFIL(binfile, precision=precision, preload=preload)
 | 
						|
 | 
						|
 | 
						|
@fill_doc
 | 
						|
class RawFIL(BaseRaw):
 | 
						|
    """Raw object from FIL-OPMEG formatted data.
 | 
						|
 | 
						|
    Parameters
 | 
						|
    ----------
 | 
						|
    binfile : path-like
 | 
						|
        Path to the MEG data binary (ending in ``'_meg.bin'``).
 | 
						|
    precision : str, optional
 | 
						|
        How is the data represented? ``'single'`` if 32-bit or
 | 
						|
        ``'double'`` if 64-bit (default is single).
 | 
						|
    %(preload)s
 | 
						|
 | 
						|
    Returns
 | 
						|
    -------
 | 
						|
    raw : instance of RawFIL
 | 
						|
        The raw data.
 | 
						|
        See :class:`mne.io.Raw` for documentation of attributes and methods.
 | 
						|
 | 
						|
    See Also
 | 
						|
    --------
 | 
						|
    mne.io.Raw : Documentation of attributes and methods of RawFIL.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, binfile, precision="single", preload=False):
 | 
						|
        if precision == "single":
 | 
						|
            dt = np.dtype(">f")
 | 
						|
            bps = 4
 | 
						|
        else:
 | 
						|
            dt = np.dtype(">d")
 | 
						|
            bps = 8
 | 
						|
 | 
						|
        sample_info = dict()
 | 
						|
        sample_info["dt"] = dt
 | 
						|
        sample_info["bps"] = bps
 | 
						|
 | 
						|
        files = _get_file_names(binfile)
 | 
						|
 | 
						|
        chans = _from_tsv(files["chans"])
 | 
						|
        nchans = len(chans["name"])
 | 
						|
        nsamples = _determine_nsamples(files["bin"], nchans, precision) - 1
 | 
						|
        sample_info["nsamples"] = nsamples
 | 
						|
 | 
						|
        raw_extras = list()
 | 
						|
        raw_extras.append(sample_info)
 | 
						|
 | 
						|
        chans["pos"] = [None] * nchans
 | 
						|
        chans["ori"] = [None] * nchans
 | 
						|
        if files["positions"].is_file():
 | 
						|
            chanpos = _from_tsv(files["positions"])
 | 
						|
            nlocs = len(chanpos["name"])
 | 
						|
            for ii in range(0, nlocs):
 | 
						|
                idx = chans["name"].index(chanpos["name"][ii])
 | 
						|
                tmp = np.array(
 | 
						|
                    [chanpos["Px"][ii], chanpos["Py"][ii], chanpos["Pz"][ii]]
 | 
						|
                )
 | 
						|
                chans["pos"][idx] = tmp.astype(np.float64)
 | 
						|
                tmp = np.array(
 | 
						|
                    [chanpos["Ox"][ii], chanpos["Oy"][ii], chanpos["Oz"][ii]]
 | 
						|
                )
 | 
						|
                chans["ori"][idx] = tmp.astype(np.float64)
 | 
						|
        else:
 | 
						|
            warn("No sensor position information found.")
 | 
						|
 | 
						|
        with open(files["meg"]) as fid:
 | 
						|
            meg = json.load(fid)
 | 
						|
        info = _compose_meas_info(meg, chans)
 | 
						|
 | 
						|
        super().__init__(
 | 
						|
            info,
 | 
						|
            preload,
 | 
						|
            filenames=[files["bin"]],
 | 
						|
            raw_extras=raw_extras,
 | 
						|
            last_samps=[nsamples],
 | 
						|
            orig_format=precision,
 | 
						|
        )
 | 
						|
 | 
						|
        if files["coordsystem"].is_file():
 | 
						|
            with open(files["coordsystem"]) as fid:
 | 
						|
                csys = json.load(fid)
 | 
						|
            hc = csys["HeadCoilCoordinates"]
 | 
						|
 | 
						|
            for key in hc:
 | 
						|
                if key.lower() == "lpa":
 | 
						|
                    lpa = np.asarray(hc[key])
 | 
						|
                elif key.lower() == "rpa":
 | 
						|
                    rpa = np.asarray(hc[key])
 | 
						|
                elif key.lower().startswith("nas"):
 | 
						|
                    nas = np.asarray(hc[key])
 | 
						|
                else:
 | 
						|
                    warn(f"{key} is not a valid fiducial name!")
 | 
						|
 | 
						|
            size = np.linalg.norm(nas - rpa)
 | 
						|
            unit, sf = _size2units(size)
 | 
						|
            # TODO: These are not guaranteed to exist and could lead to a
 | 
						|
            # confusing error message, should fix later
 | 
						|
            lpa /= sf
 | 
						|
            rpa /= sf
 | 
						|
            nas /= sf
 | 
						|
 | 
						|
            t = get_ras_to_neuromag_trans(nas, lpa, rpa)
 | 
						|
 | 
						|
            # transform fiducial points
 | 
						|
            nas = apply_trans(t, nas)
 | 
						|
            lpa = apply_trans(t, lpa)
 | 
						|
            rpa = apply_trans(t, rpa)
 | 
						|
 | 
						|
            with self.info._unlock():
 | 
						|
                self.info["dig"] = _make_dig_points(
 | 
						|
                    nasion=nas, lpa=lpa, rpa=rpa, coord_frame="meg"
 | 
						|
                )
 | 
						|
        else:
 | 
						|
            warn(
 | 
						|
                "No fiducials found in files, defaulting sensor array to "
 | 
						|
                "FIFFV_COORD_DEVICE, this may cause problems later!"
 | 
						|
            )
 | 
						|
            t = np.eye(4)
 | 
						|
 | 
						|
        with self.info._unlock():
 | 
						|
            self.info["dev_head_t"] = Transform(
 | 
						|
                FIFF.FIFFV_COORD_DEVICE, FIFF.FIFFV_COORD_HEAD, t
 | 
						|
            )
 | 
						|
 | 
						|
    def _read_segment_file(self, data, idx, fi, start, stop, cals, mult):
 | 
						|
        """Read a chunk of raw data."""
 | 
						|
        si = self._raw_extras[fi]
 | 
						|
        _read_segments_file(
 | 
						|
            self, data, idx, fi, start, stop, cals, mult, dtype=si["dt"]
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
def _convert_channel_info(chans):
 | 
						|
    """Convert the imported _channels.tsv into the chs element of raw.info."""
 | 
						|
    nmeg = nstim = nmisc = nref = 0
 | 
						|
 | 
						|
    if not all(p is None for p in chans["pos"]):
 | 
						|
        _, sf = _get_pos_units(chans["pos"])
 | 
						|
 | 
						|
    chs = list()
 | 
						|
    for ii in range(len(chans["name"])):
 | 
						|
        ch = dict(
 | 
						|
            scanno=ii + 1,
 | 
						|
            range=1.0,
 | 
						|
            cal=1.0,
 | 
						|
            loc=np.full(12, np.nan),
 | 
						|
            unit_mul=FIFF.FIFF_UNITM_NONE,
 | 
						|
            ch_name=chans["name"][ii],
 | 
						|
            coil_type=FIFF.FIFFV_COIL_NONE,
 | 
						|
        )
 | 
						|
        chs.append(ch)
 | 
						|
 | 
						|
        # create the channel information
 | 
						|
        if chans["pos"][ii] is not None:
 | 
						|
            r0 = chans["pos"][ii].copy() / sf  # mm to m
 | 
						|
            ez = chans["ori"][ii].copy()
 | 
						|
            ez = ez / np.linalg.norm(ez)
 | 
						|
            ex, ey = _get_plane_vectors(ez)
 | 
						|
            ch["loc"] = np.concatenate([r0, ex, ey, ez])
 | 
						|
 | 
						|
        if chans["type"][ii] == "MEGMAG":
 | 
						|
            nmeg += 1
 | 
						|
            ch.update(
 | 
						|
                logno=nmeg,
 | 
						|
                coord_frame=FIFF.FIFFV_COORD_DEVICE,
 | 
						|
                kind=FIFF.FIFFV_MEG_CH,
 | 
						|
                unit=FIFF.FIFF_UNIT_T,
 | 
						|
                coil_type=FIFF.FIFFV_COIL_QUSPIN_ZFOPM_MAG2,
 | 
						|
            )
 | 
						|
        elif chans["type"][ii] == "MEGREFMAG":
 | 
						|
            nref += 1
 | 
						|
            ch.update(
 | 
						|
                logno=nref,
 | 
						|
                coord_frame=FIFF.FIFFV_COORD_UNKNOWN,
 | 
						|
                kind=FIFF.FIFFV_REF_MEG_CH,
 | 
						|
                unit=FIFF.FIFF_UNIT_T,
 | 
						|
                coil_type=FIFF.FIFFV_COIL_QUSPIN_ZFOPM_MAG2,
 | 
						|
            )
 | 
						|
        elif chans["type"][ii] == "TRIG":
 | 
						|
            nstim += 1
 | 
						|
            ch.update(
 | 
						|
                logno=nstim,
 | 
						|
                coord_frame=FIFF.FIFFV_COORD_UNKNOWN,
 | 
						|
                kind=FIFF.FIFFV_STIM_CH,
 | 
						|
                unit=FIFF.FIFF_UNIT_V,
 | 
						|
            )
 | 
						|
        else:
 | 
						|
            nmisc += 1
 | 
						|
            ch.update(
 | 
						|
                logno=nmisc,
 | 
						|
                coord_frame=FIFF.FIFFV_COORD_UNKNOWN,
 | 
						|
                kind=FIFF.FIFFV_MISC_CH,
 | 
						|
                unit=FIFF.FIFF_UNIT_NONE,
 | 
						|
            )
 | 
						|
 | 
						|
        # set the calibration based on the units - MNE expects T units for meg
 | 
						|
        # and V for eeg
 | 
						|
        if chans["units"][ii] == "fT":
 | 
						|
            ch.update(cal=1e-15)
 | 
						|
        elif chans["units"][ii] == "pT":
 | 
						|
            ch.update(cal=1e-12)
 | 
						|
        elif chans["units"][ii] == "nT":
 | 
						|
            ch.update(cal=1e-9)
 | 
						|
        elif chans["units"][ii] == "mV":
 | 
						|
            ch.update(cal=1e3)
 | 
						|
        elif chans["units"][ii] == "uV":
 | 
						|
            ch.update(cal=1e6)
 | 
						|
 | 
						|
    return chs
 | 
						|
 | 
						|
 | 
						|
def _compose_meas_info(meg, chans):
 | 
						|
    """Create info structure."""
 | 
						|
    info = _empty_info(meg["SamplingFrequency"])
 | 
						|
    # Collect all the necessary data from the structures read
 | 
						|
    info["meas_id"] = get_new_file_id()
 | 
						|
    tmp = _convert_channel_info(chans)
 | 
						|
    info["chs"] = _refine_sensor_orientation(tmp)
 | 
						|
    info["line_freq"] = meg["PowerLineFrequency"]
 | 
						|
    info._update_redundant()
 | 
						|
    info["bads"] = _read_bad_channels(chans)
 | 
						|
    info._unlocked = False
 | 
						|
    return info
 | 
						|
 | 
						|
 | 
						|
def _determine_nsamples(bin_fname, nchans, precision):
 | 
						|
    """Identify how many temporal samples in a dataset."""
 | 
						|
    bsize = bin_fname.stat().st_size
 | 
						|
    if precision == "single":
 | 
						|
        bps = 4
 | 
						|
    else:
 | 
						|
        bps = 8
 | 
						|
    nsamples = int(bsize / (nchans * bps))
 | 
						|
    return nsamples
 | 
						|
 | 
						|
 | 
						|
def _read_bad_channels(chans):
 | 
						|
    """Check _channels.tsv file to look for premarked bad channels."""
 | 
						|
    bads = list()
 | 
						|
    for ii in range(0, len(chans["status"])):
 | 
						|
        if chans["status"][ii] == "bad":
 | 
						|
            bads.append(chans["name"][ii])
 | 
						|
    return bads
 | 
						|
 | 
						|
 | 
						|
def _from_tsv(fname, dtypes=None):
 | 
						|
    """Read a tsv file into a dict (which we know is ordered)."""
 | 
						|
    data = np.loadtxt(
 | 
						|
        fname, dtype=str, delimiter="\t", ndmin=2, comments=None, encoding="utf-8-sig"
 | 
						|
    )
 | 
						|
    column_names = data[0, :]
 | 
						|
    info = data[1:, :]
 | 
						|
    data_dict = dict()
 | 
						|
    if dtypes is None:
 | 
						|
        dtypes = [str] * info.shape[1]
 | 
						|
    if not isinstance(dtypes, (list, tuple)):
 | 
						|
        dtypes = [dtypes] * info.shape[1]
 | 
						|
    if not len(dtypes) == info.shape[1]:
 | 
						|
        raise ValueError(
 | 
						|
            f"dtypes length mismatch. Provided: {len(dtypes)}, "
 | 
						|
            f"Expected: {info.shape[1]}"
 | 
						|
        )
 | 
						|
    for i, name in enumerate(column_names):
 | 
						|
        data_dict[name] = info[:, i].astype(dtypes[i]).tolist()
 | 
						|
    return data_dict
 | 
						|
 | 
						|
 | 
						|
def _get_file_names(binfile):
 | 
						|
    """Guess the filenames based on predicted suffixes."""
 | 
						|
    binfile = pathlib.Path(
 | 
						|
        _check_fname(binfile, overwrite="read", must_exist=True, name="fname")
 | 
						|
    )
 | 
						|
    if not (binfile.suffix == ".bin" and binfile.stem.endswith("_meg")):
 | 
						|
        raise ValueError(f"File must be a filename ending in _meg.bin, got {binfile}")
 | 
						|
    files = dict()
 | 
						|
    dir_ = binfile.parent
 | 
						|
    root = binfile.stem[:-4]  # no _meg
 | 
						|
    files["bin"] = dir_ / (root + "_meg.bin")
 | 
						|
    files["meg"] = dir_ / (root + "_meg.json")
 | 
						|
    files["chans"] = dir_ / (root + "_channels.tsv")
 | 
						|
    files["positions"] = dir_ / (root + "_positions.tsv")
 | 
						|
    files["coordsystem"] = dir_ / (root + "_coordsystem.json")
 | 
						|
    return files
 |