<commit_before># -*- coding: utf-8 -*-
# Authors: <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#
# License: BSD (3-clause)

from collections import Counter
from copy import deepcopy
import datetime
from io import BytesIO
import operator
from textwrap import shorten

import numpy as np
from scipy import linalg

from .pick import channel_type, pick_channels, pick_info
from .constants import FIFF
from .open import fiff_open
from .tree import dir_tree_find
from .tag import read_tag, find_tag, _coord_dict
from .proj import _read_proj, _write_proj, _uniquify_projs, _normalize_proj
from .ctf_comp import read_ctf_comp, write_ctf_comp
from .write import (start_file, end_file, start_block, end_block,
                    write_string, write_dig_points, write_float, write_int,
                    write_coord_trans, write_ch_info, write_name_list,
                    write_julian, write_float_matrix, write_id, DATE_NONE)
from .proc_history import _read_proc_history, _write_proc_history
from ..transforms import invert_transform, Transform, _coord_frame_name
from ..utils import (logger, verbose, warn, object_diff, _validate_type,
                     _stamp_to_dt, _dt_to_stamp, _pl, _is_numeric)
from ._digitization import (_format_dig_points, _dig_kind_proper,
                            _dig_kind_rev, _dig_kind_ints, _read_dig_fif)
from ._digitization import write_dig as _dig_write_dig
from .compensator import get_current_comp

b = bytes  # alias

_kind_dict = dict(
    eeg=(FIFF.FIFFV_EEG_CH, FIFF.FIFFV_COIL_EEG, FIFF.FIFF_UNIT_V),
    mag=(FIFF.FIFFV_MEG_CH, FIFF.FIFFV_COIL_VV_MAG_T3, FIFF.FIFF_UNIT_T),
    grad=(FIFF.FIFFV_MEG_CH, FIFF.FIFFV_COIL_VV_PLANAR_T1, FIFF.FIFF_UNIT_T_M),
    ref_meg=(FIFF.FIFFV_REF_MEG_CH, FIFF.FIFFV_COIL_VV_MAG_T3,
             FIFF.FIFF_UNIT_T),
    misc=(FIFF.FIFFV_MISC_CH, FIFF.FIFFV_COIL_NONE, FIFF.FIFF_UNIT_NONE),
    stim=(FIFF.FIFFV_STIM_CH, FIFF.FIFFV_COIL_NONE, FIFF.FIFF_UNIT_V),
    eog=(FIFF.FIFFV_EOG_CH, FIFF.FIFFV_COIL_NONE, FIFF.FIFF_UNIT_V),
    ecg=(FIFF.FIFFV_ECG_CH, FIFF.FIFFV_COIL_NONE, FIFF.FIFF_UNIT_V),
    emg=(FIFF.FIFFV_EMG_CH, FIFF.FIFFV_COIL_NONE, FIFF.FIFF_UNIT_V),
    seeg=(FIFF.FIFFV_SEEG_CH, FIFF.FIFFV_COIL_EEG, FIFF.FIFF_UNIT_V),
    bio=(FIFF.FIFFV_BIO_CH, FIFF.FIFFV_COIL_NONE, FIFF.FIFF_UNIT_V),
    ecog=(FIFF.FIFFV_ECOG_CH, FIFF.FIFFV_COIL_EEG, FIFF.FIFF_UNIT_V),
    fnirs_raw=(FIFF.FIFFV_FNIRS_CH, FIFF.FIFFV_COIL_FNIRS_RAW,
               FIFF.FIFF_UNIT_V),
    fnirs_od=(FIFF.FIFFV_FNIRS_CH, FIFF.FIFFV_COIL_FNIRS_OD,
              FIFF.FIFF_UNIT_NONE),
    hbo=(FIFF.FIFFV_FNIRS_CH, FIFF.FIFFV_COIL_FNIRS_HBO, FIFF.FIFF_UNIT_MOL),
    hbr=(FIFF.FIFFV_FNIRS_CH, FIFF.FIFFV_COIL_FNIRS_HBR, FIFF.FIFF_UNIT_MOL),
    csd=(FIFF.FIFFV_EEG_CH, FIFF.FIFFV_COIL_EEG_CSD, FIFF.FIFF_UNIT_V_M2),
)


def _get_valid_units():
    """Get valid units according to the International System of Units (SI).

    The International System of Units (SI, [1]_) is the default system for
    describing units in the Brain Imaging Data Structure (BIDS). For more
    information, see the BIDS specification [2]_ and the appendix "Units"
    therein.

    References
    ----------
    [1] .. https://en.wikipedia.org/wiki/International_System_of_Units
    [2] .. https://bids-specification.readthedocs.io/en/stable/

    """
    valid_prefix_names = ['yocto', 'zepto', 'atto', 'femto', 'pico', 'nano',
                          'micro', 'milli', 'centi', 'deci', 'deca', 'hecto',
                          'kilo', 'mega', 'giga', 'tera', 'peta', 'exa',
                          'zetta', 'yotta']
    valid_prefix_symbols = ['y', 'z', 'a', 'f', 'p', 'n', u'µ', 'm', 'c', 'd',
                            'da', 'h', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
    valid_unit_names = ['metre', 'kilogram', 'second', 'ampere', 'kelvin',
                        'mole', 'candela', 'radian', 'steradian', 'hertz',
                        'newton', 'pascal', 'joule', 'watt', 'coulomb', 'volt',
                        'farad', 'ohm', 'siemens', 'weber', 'tesla', 'henry',
                        'degree Celsius', 'lumen', 'lux', 'becquerel', 'gray',
                        'sievert', 'katal']
    valid_unit_symbols = ['m', 'kg', 's', 'A', 'K', 'mol', 'cd', 'rad', 'sr',
                          'Hz', 'N', 'Pa', 'J', 'W', 'C', 'V', 'F', u'Ω', 'S',
                          'Wb', 'T', 'H', u'°C', 'lm', 'lx', 'Bq', 'Gy', 'Sv',
                          'kat']

    # Valid units are all possible combinations of either prefix name or prefix
    # symbol together with either unit name or unit symbol. E.g., nV for
    # nanovolt
    valid_units = []
    valid_units += ([''.join([prefix, unit]) for prefix in valid_prefix_names
                     for unit in valid_unit_names])
    valid_units += ([''.join([prefix, unit]) for prefix in valid_prefix_names
                     for unit in valid_unit_symbols])
    valid_units += ([''.join([prefix, unit]) for prefix in valid_prefix_symbols
                     for unit in valid_unit_names])
    valid_units += ([''.join([prefix, unit]) for prefix in valid_prefix_symbols
                     for unit in valid_unit_symbols])

    # units are also valid without a prefix
    valid_units += valid_unit_names
    valid_units += valid_unit_symbols

    # we also accept "n/a" as a unit, which is the default missing value in
    # BIDS
    valid_units += ["n/a"]

    return tuple(valid_units)


def _unique_channel_names(ch_names):
    """Ensure unique channel names."""
    FIFF_CH_NAME_MAX_LENGTH = 15
    unique_ids = np.unique(ch_names, return_index=True)[1]
    if len(unique_ids) != len(ch_names):
        dups = {ch_names[x]
                for x in np.setdiff1d(range(len(ch_names)), unique_ids)}
        warn('Channel names are not unique, found duplicates for: '
             '%s. Applying running numbers for duplicates.' % dups)
        for ch_stem in dups:
            overlaps = np.where(np.array(ch_names) == ch_stem)[0]
            # We need an extra character since we append '-'.
            # np.ceil(...) is the maximum number of appended digits.
            n_keep = (FIFF_CH_NAME_MAX_LENGTH - 1 -
                      int(np.ceil(np.log10(len(overlaps)))))
            n_keep = min(len(ch_stem), n_keep)
            ch_stem = ch_stem[:n_keep]
            for idx, ch_idx in enumerate(overlaps):
                ch_name = ch_stem + '-%s' % idx
                if ch_name not in ch_names:
                    ch_names[ch_idx] = ch_name
                else:
                    raise ValueError('Adding a running number for a '
                                     'duplicate resulted in another '
                                     'duplicate name %s' % ch_name)

    return ch_names


class MontageMixin(object):
    """Mixin for Montage setting."""

    @verbose
    def set_montage(self, montage, match_case=True,
                    on_missing='raise', verbose=None):
        """Set EEG sensor configuration and head digitization.

        Parameters
        ----------
        %(montage)s
        %(match_case)s
        %(on_missing_montage)s
        %(verbose_meth)s

        Returns
        -------
        inst : instance of Raw | Epochs | Evoked
            The instance.

        Notes
        -----
        Operates in place.
        """
        # How to set up a montage to old named fif file (walk through example)
        # https://gist.github.com/massich/f6a9f4799f1fbeb8f5e8f8bc7b07d3df

        from ..channels.montage import _set_montage
        info = self if isinstance(self, Info) else self.info
        _set_montage(info, montage, match_case, on_missing)
        return self


# XXX Eventually this should be de-duplicated with the MNE-MATLAB stuff...
class Info(dict, MontageMixin):
    """Measurement information.

    This data structure behaves like a dictionary. It contains all metadata
    that is available for a recording. However, its keys are restricted to
    those provided by the
    `FIF format specification <https://github.com/mne-tools/fiff-constants>`__,
    so new entries should not be manually added.

    The only entries that should be manually changed by the user are
    ``info['bads']`` and ``info['description']``. All other entries should
    be considered read-only, though they can be modified by various MNE-Python
    functions or methods (which have safeguards to ensure all fields remain in
    sync).

    This class should not be instantiated directly. To create a measurement
    information strucure, use :func:`mne.create_info`.

    Attributes
    ----------
    acq_pars : str | None
        MEG system acquition parameters.
        See :class:`mne.AcqParserFIF` for details.
    acq_stim : str | None
        MEG system stimulus parameters.
    bads : list of str
        List of bad (noisy/broken) channels, by name. These channels will by
        default be ignored by many processing steps.
    ch_names : list of str
        The names of the channels.
    chs : list of dict
        A list of channel information dictionaries, one per channel.
        See Notes for more information.
    comps : list of dict
        CTF software gradient compensation data.<commit_msg>Clearer info docstring (#7832)<commit_after># -*- coding: utf-8 -*-
# Authors: <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#
# License: BSD (3-clause)

from collections import Counter
from copy import deepcopy
import datetime
from io import BytesIO
import operator
from textwrap import shorten

import numpy as np
from scipy import linalg

from .pick import channel_type, pick_channels, pick_info
from .constants import FIFF
from .open import fiff_open
from .tree import dir_tree_find
from .tag import read_tag, find_tag, _coord_dict
from .proj import _read_proj, _write_proj, _uniquify_projs, _normalize_proj
from .ctf_comp import read_ctf_comp, write_ctf_comp
from .write import (start_file, end_file, start_block, end_block,
                    write_string, write_dig_points, write_float, write_int,
                    write_coord_trans, write_ch_info, write_name_list,
                    write_julian, write_float_matrix, write_id, DATE_NONE)
from .proc_history import _read_proc_history, _write_proc_history
from ..transforms import invert_transform, Transform, _coord_frame_name
from ..utils import (logger, verbose, warn, object_diff, _validate_type,
                     _stamp_to_dt, _dt_to_stamp, _pl, _is_numeric)
from ._digitization import (_format_dig_points, _dig_kind_proper,
                            _dig_kind_rev, _dig_kind_ints, _read_dig_fif)
from ._digitization import write_dig as _dig_write_dig
from .compensator import get_current_comp

b = bytes  # alias

_kind_dict = dict(
    eeg=(FIFF.FIFFV_EEG_CH, FIFF.FIFFV_COIL_EEG, FIFF.FIFF_UNIT_V),
    mag=(FIFF.FIFFV_MEG_CH, FIFF.FIFFV_COIL_VV_MAG_T3, FIFF.FIFF_UNIT_T),
    grad=(FIFF.FIFFV_MEG_CH, FIFF.FIFFV_COIL_VV_PLANAR_T1, FIFF.FIFF_UNIT_T_M),
    ref_meg=(FIFF.FIFFV_REF_MEG_CH, FIFF.FIFFV_COIL_VV_MAG_T3,
             FIFF.FIFF_UNIT_T),
    misc=(FIFF.FIFFV_MISC_CH, FIFF.FIFFV_COIL_NONE, FIFF.FIFF_UNIT_NONE),
    stim=(FIFF.FIFFV_STIM_CH, FIFF.FIFFV_COIL_NONE, FIFF.FIFF_UNIT_V),
    eog=(FIFF.FIFFV_EOG_CH, FIFF.FIFFV_COIL_NONE, FIFF.FIFF_UNIT_V),
    ecg=(FIFF.FIFFV_ECG_CH, FIFF.FIFFV_COIL_NONE, FIFF.FIFF_UNIT_V),
    emg=(FIFF.FIFFV_EMG_CH, FIFF.FIFFV_COIL_NONE, FIFF.FIFF_UNIT_V),
    seeg=(FIFF.FIFFV_SEEG_CH, FIFF.FIFFV_COIL_EEG, FIFF.FIFF_UNIT_V),
    bio=(FIFF.FIFFV_BIO_CH, FIFF.FIFFV_COIL_NONE, FIFF.FIFF_UNIT_V),
    ecog=(FIFF.FIFFV_ECOG_CH, FIFF.FIFFV_COIL_EEG, FIFF.FIFF_UNIT_V),
    fnirs_raw=(FIFF.FIFFV_FNIRS_CH, FIFF.FIFFV_COIL_FNIRS_RAW,
               FIFF.FIFF_UNIT_V),
    fnirs_od=(FIFF.FIFFV_FNIRS_CH, FIFF.FIFFV_COIL_FNIRS_OD,
              FIFF.FIFF_UNIT_NONE),
    hbo=(FIFF.FIFFV_FNIRS_CH, FIFF.FIFFV_COIL_FNIRS_HBO, FIFF.FIFF_UNIT_MOL),
    hbr=(FIFF.FIFFV_FNIRS_CH, FIFF.FIFFV_COIL_FNIRS_HBR, FIFF.FIFF_UNIT_MOL),
    csd=(FIFF.FIFFV_EEG_CH, FIFF.FIFFV_COIL_EEG_CSD, FIFF.FIFF_UNIT_V_M2),
)


def _get_valid_units():
    """Get valid units according to the International System of Units (SI).

    The International System of Units (SI, [1]_) is the default system for
    describing units in the Brain Imaging Data Structure (BIDS). For more
    information, see the BIDS specification [2]_ and the appendix "Units"
    therein.

    References
    ----------
    [1] .. https://en.wikipedia.org/wiki/International_System_of_Units
    [2] .. https://bids-specification.readthedocs.io/en/stable/

    """
    valid_prefix_names = ['yocto', 'zepto', 'atto', 'femto', 'pico', 'nano',
                          'micro', 'milli', 'centi', 'deci', 'deca', 'hecto',
                          'kilo', 'mega', 'giga', 'tera', 'peta', 'exa',
                          'zetta', 'yotta']
    valid_prefix_symbols = ['y', 'z', 'a', 'f', 'p', 'n', u'µ', 'm', 'c', 'd',
                            'da', 'h', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
    valid_unit_names = ['metre', 'kilogram', 'second', 'ampere', 'kelvin',
                        'mole', 'candela', 'radian', 'steradian', 'hertz',
                        'newton', 'pascal', 'joule', 'watt', 'coulomb', 'volt',
                        'farad', 'ohm', 'siemens', 'weber', 'tesla', 'henry',
                        'degree Celsius', 'lumen', 'lux', 'becquerel', 'gray',
                        'sievert', 'katal']
    valid_unit_symbols = ['m', 'kg', 's', 'A', 'K', 'mol', 'cd', 'rad', 'sr',
                          'Hz', 'N', 'Pa', 'J', 'W', 'C', 'V', 'F', u'Ω', 'S',
                          'Wb', 'T', 'H', u'°C', 'lm', 'lx', 'Bq', 'Gy', 'Sv',
                          'kat']

    # Valid units are all possible combinations of either prefix name or prefix
    # symbol together with either unit name or unit symbol. E.g., nV for
    # nanovolt
    valid_units = []
    valid_units += ([''.join([prefix, unit]) for prefix in valid_prefix_names
                     for unit in valid_unit_names])
    valid_units += ([''.join([prefix, unit]) for prefix in valid_prefix_names
                     for unit in valid_unit_symbols])
    valid_units += ([''.join([prefix, unit]) for prefix in valid_prefix_symbols
                     for unit in valid_unit_names])
    valid_units += ([''.join([prefix, unit]) for prefix in valid_prefix_symbols
                     for unit in valid_unit_symbols])

    # units are also valid without a prefix
    valid_units += valid_unit_names
    valid_units += valid_unit_symbols

    # we also accept "n/a" as a unit, which is the default missing value in
    # BIDS
    valid_units += ["n/a"]

    return tuple(valid_units)


def _unique_channel_names(ch_names):
    """Ensure unique channel names."""
    FIFF_CH_NAME_MAX_LENGTH = 15
    unique_ids = np.unique(ch_names, return_index=True)[1]
    if len(unique_ids) != len(ch_names):
        dups = {ch_names[x]
                for x in np.setdiff1d(range(len(ch_names)), unique_ids)}
        warn('Channel names are not unique, found duplicates for: '
             '%s. Applying running numbers for duplicates.' % dups)
        for ch_stem in dups:
            overlaps = np.where(np.array(ch_names) == ch_stem)[0]
            # We need an extra character since we append '-'.
            # np.ceil(...) is the maximum number of appended digits.
            n_keep = (FIFF_CH_NAME_MAX_LENGTH - 1 -
                      int(np.ceil(np.log10(len(overlaps)))))
            n_keep = min(len(ch_stem), n_keep)
            ch_stem = ch_stem[:n_keep]
            for idx, ch_idx in enumerate(overlaps):
                ch_name = ch_stem + '-%s' % idx
                if ch_name not in ch_names:
                    ch_names[ch_idx] = ch_name
                else:
                    raise ValueError('Adding a running number for a '
                                     'duplicate resulted in another '
                                     'duplicate name %s' % ch_name)

    return ch_names


class MontageMixin(object):
    """Mixin for Montage setting."""

    @verbose
    def set_montage(self, montage, match_case=True,
                    on_missing='raise', verbose=None):
        """Set EEG sensor configuration and head digitization.

        Parameters
        ----------
        %(montage)s
        %(match_case)s
        %(on_missing_montage)s
        %(verbose_meth)s

        Returns
        -------
        inst : instance of Raw | Epochs | Evoked
            The instance.

        Notes
        -----
        Operates in place.
        """
        # How to set up a montage to old named fif file (walk through example)
        # https://gist.github.com/massich/f6a9f4799f1fbeb8f5e8f8bc7b07d3df

        from ..channels.montage import _set_montage
        info = self if isinstance(self, Info) else self.info
        _set_montage(info, montage, match_case, on_missing)
        return self


# XXX Eventually this should be de-duplicated with the MNE-MATLAB stuff...
class Info(dict, MontageMixin):
    """Measurement information.

    This data structure behaves like a dictionary. It contains all metadata
    that is available for a recording. However, its keys are restricted to
    those provided by the
    `FIF format specification <https://github.com/mne-tools/fiff-constants>`__,
    so new entries should not be manually added.

    .. warning:: The only entries that should be manually changed by the user
                 are ``info['bads']`` and ``info['description']``. All other
                 entries should be considered read-only, though they can be
                 modified by various MNE-Python functions or methods (which
                 have safeguards to ensure all fields remain in sync).

    This class should not be instantiated directly. To create a measurement
    information structure, use :func:`mne.create_info`.

    Attributes
    ----------
    acq_pars : str | None
        MEG system acquition parameters.
        See :class:`mne.AcqParserFIF` for details.
    acq_stim : str | None
        MEG system stimulus parameters.
    bads : list of str
        List of bad (noisy/broken) channels, by name. These channels will by
        default be ignored by many processing steps.
    ch_names : list of str
        The names of the channels.
    chs : list of dict
        A list of channel information dictionaries, one per channel.
        See Notes for more information.
    comps : list of dict
        CTF software gradient compensation data.
{'fraction_non_alphanumeric': 0.11163818662066735, 'fraction_numerical': 0.003773381488868525, 'mean_word_length': 2.5526618153964, 'pattern_counts': {'":': 0, '<': 29, '<?xml version=': 0, '>': 29, 'https://': 8, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7444370', 'n_tokens_mistral': 6390, 'n_tokens_neox': 6127, 'n_words': 1623}