<commit_before># Authors: <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#
# License: Simplified BSD

from collections import OrderedDict
from copy import deepcopy
import os.path as op
import re

import numpy as np

from ..defaults import HEAD_SIZE_DEFAULT
from .._freesurfer import get_mni_fiducials
from ..viz import plot_montage
from ..transforms import (apply_trans, get_ras_to_neuromag_trans, _sph_to_cart,
                          _topo_to_sph, _frame_to_str, Transform,
                          _verbose_frames, _fit_matched_points,
                          _quat_to_affine, _ensure_trans)
from ..io._digitization import (_count_points_by_type,
                                _get_dig_eeg, _make_dig_points, write_dig,
                                _read_dig_fif, _format_dig_points,
                                _get_fid_coords, _coord_frame_const,
                                _get_data_as_dict_from_dig)
from ..io.meas_info import create_info
from ..io.open import fiff_open
from ..io.pick import pick_types, _picks_to_idx, channel_type
from ..io.constants import FIFF, CHANNEL_LOC_ALIASES
from ..utils import (warn, copy_function_doc_to_method_doc, _pl, verbose,
                     _check_option, _validate_type, _check_fname, _on_missing,
                     fill_doc, _docdict)

from ._dig_montage_utils import _read_dig_montage_egi
from ._dig_montage_utils import _parse_brainvision_dig_montage

_BUILT_IN_MONTAGES = [
    'EGI_256',
    'GSN-HydroCel-128', 'GSN-HydroCel-129', 'GSN-HydroCel-256',
    'GSN-HydroCel-257', 'GSN-HydroCel-32', 'GSN-HydroCel-64_1.0',
    'GSN-HydroCel-65_1.0',
    'biosemi128', 'biosemi16', 'biosemi160', 'biosemi256',
    'biosemi32', 'biosemi64',
    'easycap-M1', 'easycap-M10',
    'mgh60', 'mgh70',
    'standard_1005', 'standard_1020', 'standard_alphabetic',
    'standard_postfixed', 'standard_prefixed', 'standard_primed',
    'artinis-octamon', 'artinis-brite23'
]


def _check_get_coord_frame(dig):
    dig_coord_frames = sorted(set(d['coord_frame'] for d in dig))
    if len(dig_coord_frames) != 1:
        raise RuntimeError(
            'Only a single coordinate frame in dig is supported, got '
            f'{dig_coord_frames}')
    return _frame_to_str[dig_coord_frames.pop()] if dig_coord_frames else None


def get_builtin_montages():
    """Get a list of all builtin montages.

    Returns
    -------
    montages : list
        Names of all builtin montages that can be used by
        :func:`make_standard_montage`.
    """
    return _BUILT_IN_MONTAGES


def make_dig_montage(ch_pos=None, nasion=None, lpa=None, rpa=None,
                     hsp=None, hpi=None, coord_frame='unknown'):
    r"""Make montage from arrays.

    Parameters
    ----------
    ch_pos : dict | None
        Dictionary of channel positions. Keys are channel names and values
        are 3D coordinates - array of shape (3,) - in native digitizer space
        in m.
    nasion : None | array, shape (3,)
        The position of the nasion fiducial point.
        This point is assumed to be in the native digitizer space in m.
    lpa : None | array, shape (3,)
        The position of the left periauricular fiducial point.
        This point is assumed to be in the native digitizer space in m.
    rpa : None | array, shape (3,)
        The position of the right periauricular fiducial point.
        This point is assumed to be in the native digitizer space in m.
    hsp : None | array, shape (n_points, 3)
        This corresponds to an array of positions of the headshape points in
        3d. These points are assumed to be in the native digitizer space in m.
    hpi : None | array, shape (n_hpi, 3)
        This corresponds to an array of HPI points in the native digitizer
        space. They only necessary if computation of a ``compute_dev_head_t``
        is True.
    coord_frame : str
        The coordinate frame of the points. Usually this is ``'unknown'``
        for native digitizer space.
        Other valid values are: ``'head'``, ``'meg'``, ``'mri'``,
        ``'mri_voxel'``, ``'mri_tal'``, ``'ras'``, ``'fs_tal'``,
        ``'ctf_head'``, and ``'ctf_meg'``.

        .. note::
            For custom montages without fiducials, this parameter must be set
            to ``'head'``.

    Returns
    -------
    montage : instance of DigMontage
        The montage.

    See Also
    --------
    DigMontage
    read_dig_captrak
    read_dig_egi
    read_dig_fif
    read_dig_localite
    read_dig_polhemus_isotrak
    """
    _validate_type(ch_pos, (dict, None), 'ch_pos')
    if ch_pos is None:
        ch_names = None
    else:
        ch_names = list(ch_pos)
    dig = _make_dig_points(
        nasion=nasion, lpa=lpa, rpa=rpa, hpi=hpi, extra_points=hsp,
        dig_ch_pos=ch_pos, coord_frame=coord_frame
    )

    return DigMontage(dig=dig, ch_names=ch_names)


class DigMontage(object):
    """Montage for digitized electrode and headshape position data.

    .. warning:: Montages are typically created using one of the helper
                 functions in the ``See Also`` section below instead of
                 instantiating this class directly.

    Parameters
    ----------
    dig : list of dict
        The object containing all the dig points.
    ch_names : list of str
        The names of the EEG channels.

    See Also
    --------
    read_dig_captrak
    read_dig_dat
    read_dig_egi
    read_dig_fif
    read_dig_hpts
    read_dig_localite
    read_dig_polhemus_isotrak
    make_dig_montage

    Notes
    -----
    .. versionadded:: 0.9.0
    """

    def __init__(self, *, dig=None, ch_names=None):
        dig = list() if dig is None else dig
        _validate_type(item=dig, types=list, item_name='dig')
        ch_names = list() if ch_names is None else ch_names
        n_eeg = sum([1 for d in dig if d['kind'] == FIFF.FIFFV_POINT_EEG])
        if n_eeg != len(ch_names):
            raise ValueError(
                'The number of EEG channels (%d) does not match the number'
                ' of channel names provided (%d)' % (n_eeg, len(ch_names))
            )

        self.dig = dig
        self.ch_names = ch_names

    def __repr__(self):
        """Return string representation."""
        n_points = _count_points_by_type(self.dig)
        return ('<DigMontage | {extra:d} extras (headshape), {hpi:d} HPIs,'
                ' {fid:d} fiducials, {eeg:d} channels>').format(**n_points)

    @copy_function_doc_to_method_doc(plot_montage)
    def plot(self, scale_factor=20, show_names=True, kind='topomap', show=True,
             sphere=None, verbose=None):
        return plot_montage(self, scale_factor=scale_factor,
                            show_names=show_names, kind=kind, show=show,
                            sphere=sphere)

    @fill_doc
    def rename_channels(self, mapping, allow_duplicates=False):
        """Rename the channels.

        Parameters
        ----------
        %(mapping_rename_channels_duplicates)s

        Returns
        -------
        inst : instance of DigMontage
            The instance. Operates in-place.
        """
        from .channels import rename_channels
        temp_info = create_info(list(self._get_ch_pos()), 1000., 'eeg')
        rename_channels(temp_info, mapping, allow_duplicates)
        self.ch_names = temp_info['ch_names']

    @verbose
    def save(self, fname, *, overwrite=False, verbose=None):
        """Save digitization points to FIF.

        Parameters
        ----------
        fname : str
            The filename to use. Should end in .fif or .fif.gz.
        %(overwrite)s
        %(verbose)s
        """
        coord_frame = _check_get_coord_frame(self.dig)
        write_dig(fname, self.dig, coord_frame, overwrite=overwrite)

    def __iadd__(self, other):
        """Add two DigMontages in place.

        Notes
        -----
        Two DigMontages can only be added if there are no duplicated ch_names
        and if fiducials are present they should share the same coordinate
        system and location values.
        """
        def is_fid_defined(fid):
            return not(
                fid.nasion is None and fid.lpa is None and fid.rpa is None
            )

        # Check for none duplicated ch_names
        ch_names_intersection = set(self.ch_names).intersection(other.ch_names)
        if ch_names_intersection:
            raise RuntimeError((
                "Cannot add two DigMontage objects if they contain duplicated"
                " channel names. Duplicated channel(s) found: {}."
            ).format(
                ', '.join(['%r' % v for v in sorted(ch_names_intersection)])
            ))

        # Check for unique matching fiducials
        self_fid, self_coord = _get_fid_coords(self.dig)
        other_fid, other_coord = _get_fid_coords(other.dig)

        if is_fid_defined(self_fid) and is_fid_defined(other_fid):
            if self_coord != other_coord:
                raise RuntimeError('Cannot add two DigMontage objects if '
                                   'fiducial locations are not in the same '
                                   'coordinate system.')

            for kk in self_fid:
                if not np.array_equal(self_fid[kk], other_fid[kk]):
                    raise RuntimeError('Cannot add two DigMontage objects if '
                                       'fiducial locations do not match '
                                       '(%s)' % kk)

            # keep self
            self.dig = _format_dig_points(
                self.dig + [d for d in other.dig
                            if d['kind'] != FIFF.FIFFV_POINT_CARDINAL]
            )
        else:
            self.dig = _format_dig_points(self.dig + other.dig)

        self.ch_names += other.ch_names
        return self

    def copy(self):
        """Copy the DigMontage object.

        Returns
        -------
        dig : instance of DigMontage
            The copied DigMontage instance.
        """
        return deepcopy(self)

    def __add__(self, other):
        """Add two DigMontages."""
        out = self.copy()
        out += other
        return out

    def _get_ch_pos(self):
        pos = [d['r'] for d in _get_dig_eeg(self.dig)]
        assert len(self.ch_names) == len(pos)
        return OrderedDict(zip(self.ch_names, pos))

    def _get_dig_names(self):
        NAMED_KIND = (FIFF.FIFFV_POINT_EEG,)
        is_eeg = np.array([d['kind'] in NAMED_KIND for d in self.dig])
        assert len(self.ch_names) == is_eeg.sum()
        dig_names = [None] * len(self.dig)
        for ch_name_idx, dig_idx in enumerate(np.where(is_eeg)[0]):
            dig_names[dig_idx] = self.ch_names[ch_name_idx]

        return dig_names

    def get_positions(self):
        """Get all channel and fiducial positions.

        Returns
        -------
        positions : dict
            A dictionary of the positions for channels (``ch_pos``),
            coordinate frame (``coord_frame``), nasion (``nasion``),
            left preauricular point (``lpa``),
            right preauricular point (``rpa``),
            Head Shape Polhemus (``hsp``), and
            Head Position Indicator(``hpi``).
            E.g.::

                {
                    'ch_pos': {'EEG061': [0, 0, 0]},
                    'nasion': [0, 0, 1],
                    'coord_frame': 'mni_tal',
                    'lpa': [0, 1, 0],
                    'rpa': [1, 0, 0],
                    'hsp': None,
                    'hpi': None
                }
        """
        # get channel positions as dict
        ch_pos = self._get_ch_pos()

        # get coordframe and fiducial coordinates
        montage_bunch = _get_data_as_dict_from_dig(self.dig)
        coord_frame = _frame_to_str.get(montage_bunch.coord_frame)

        # return dictionary
        positions = dict(
            ch_pos=ch_pos,
            coord_frame=coord_frame,
            nasion=montage_bunch.nasion,
            lpa=montage_bunch.lpa,
            rpa=montage_bunch.rpa,
            hsp=montage_bunch.hsp,
            hpi=montage_bunch.hpi,
        )
        return positions

    @verbose
    def apply_trans(self, trans, verbose=None):
        """Apply a transformation matrix to the montage.

        Parameters
        ----------
        trans : instance of mne.transforms.Transform
            The transformation matrix to be applied.
        %(verbose)s
        """
        _validate_type(trans, Transform, 'trans')
        coord_frame = self.get_positions()['coord_frame']
        trans = _ensure_trans(trans, fro=coord_frame, to=trans['to'])
        for d in self.dig:
            d['r'] = apply_trans(trans, d['r'])
            d['coord_frame'] = trans['to']

    @verbose
    def add_estimated_fiducials(self, subject, subjects_dir=None,
                                verbose=None):
        """Estimate fiducials based on FreeSurfer ``fsaverage`` subject.

        This takes a montage with the ``mri`` coordinate frame,
        corresponding to the FreeSurfer RAS (xyz in the volume) T1w
        image of the specific subject. It will call
        :func:`mne.coreg.get_mni_fiducials` to estimate LPA, RPA and
        Nasion fiducial points.

        Parameters
        ----------
        %(subject)s
        %(subjects_dir)s
        %(verbose)s

        Returns
        -------
        inst : instance of DigMontage
            The instance, modified in-place.

        See Also
        --------
        :ref:`tut-source-alignment`

        Notes
        -----
        Since MNE uses the FIF data structure, it relies on the ``head``
        coordinate frame. Any coordinate frame can be transformed
        to ``head`` if the fiducials (i.e. LPA, RPA and Nasion) are
        defined. One can use this function to estimate those fiducials
        and then use ``montage.get_native_head_t()`` to get the
        head <-> MRI transform.
        """
        # get coordframe and fiducial coordinates
        montage_bunch = _get_data_as_dict_from_dig(self.dig)

        # get the coordinate frame and check that it's MRI
        if montage_bunch.coord_frame != FIFF.FIFFV_COORD_MRI:
            raise RuntimeError(
                f'Montage should be in the "mri" coordinate frame '
                f'to use `add_estimated_fiducials`. The current coordinate '
                f'frame is {montage_bunch.coord_frame}')

        # estimate LPA, nasion, RPA from FreeSurfer fsaverage
        fids_mri = list(get_mni_fiducials(subject, subjects_dir))

        # add those digpoints to front of montage
        self.dig = fids_mri + self.dig
        return self

    @verbose
    def add_mni_fiducials(self, subjects_dir=None, verbose=None):
        """Add fiducials to a montage in MNI space.

        Parameters
        ----------
        %(subjects_dir)s
        %(verbose)s
<commit_msg>fix typo [skip azp] [skip actions] (#10409)<commit_after># Authors: <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#          <NAME> <<EMAIL>>
#
# License: Simplified BSD

from collections import OrderedDict
from copy import deepcopy
import os.path as op
import re

import numpy as np

from ..defaults import HEAD_SIZE_DEFAULT
from .._freesurfer import get_mni_fiducials
from ..viz import plot_montage
from ..transforms import (apply_trans, get_ras_to_neuromag_trans, _sph_to_cart,
                          _topo_to_sph, _frame_to_str, Transform,
                          _verbose_frames, _fit_matched_points,
                          _quat_to_affine, _ensure_trans)
from ..io._digitization import (_count_points_by_type,
                                _get_dig_eeg, _make_dig_points, write_dig,
                                _read_dig_fif, _format_dig_points,
                                _get_fid_coords, _coord_frame_const,
                                _get_data_as_dict_from_dig)
from ..io.meas_info import create_info
from ..io.open import fiff_open
from ..io.pick import pick_types, _picks_to_idx, channel_type
from ..io.constants import FIFF, CHANNEL_LOC_ALIASES
from ..utils import (warn, copy_function_doc_to_method_doc, _pl, verbose,
                     _check_option, _validate_type, _check_fname, _on_missing,
                     fill_doc, _docdict)

from ._dig_montage_utils import _read_dig_montage_egi
from ._dig_montage_utils import _parse_brainvision_dig_montage

_BUILT_IN_MONTAGES = [
    'EGI_256',
    'GSN-HydroCel-128', 'GSN-HydroCel-129', 'GSN-HydroCel-256',
    'GSN-HydroCel-257', 'GSN-HydroCel-32', 'GSN-HydroCel-64_1.0',
    'GSN-HydroCel-65_1.0',
    'biosemi128', 'biosemi16', 'biosemi160', 'biosemi256',
    'biosemi32', 'biosemi64',
    'easycap-M1', 'easycap-M10',
    'mgh60', 'mgh70',
    'standard_1005', 'standard_1020', 'standard_alphabetic',
    'standard_postfixed', 'standard_prefixed', 'standard_primed',
    'artinis-octamon', 'artinis-brite23'
]


def _check_get_coord_frame(dig):
    dig_coord_frames = sorted(set(d['coord_frame'] for d in dig))
    if len(dig_coord_frames) != 1:
        raise RuntimeError(
            'Only a single coordinate frame in dig is supported, got '
            f'{dig_coord_frames}')
    return _frame_to_str[dig_coord_frames.pop()] if dig_coord_frames else None


def get_builtin_montages():
    """Get a list of all builtin montages.

    Returns
    -------
    montages : list
        Names of all builtin montages that can be used by
        :func:`make_standard_montage`.
    """
    return _BUILT_IN_MONTAGES


def make_dig_montage(ch_pos=None, nasion=None, lpa=None, rpa=None,
                     hsp=None, hpi=None, coord_frame='unknown'):
    r"""Make montage from arrays.

    Parameters
    ----------
    ch_pos : dict | None
        Dictionary of channel positions. Keys are channel names and values
        are 3D coordinates - array of shape (3,) - in native digitizer space
        in m.
    nasion : None | array, shape (3,)
        The position of the nasion fiducial point.
        This point is assumed to be in the native digitizer space in m.
    lpa : None | array, shape (3,)
        The position of the left periauricular fiducial point.
        This point is assumed to be in the native digitizer space in m.
    rpa : None | array, shape (3,)
        The position of the right periauricular fiducial point.
        This point is assumed to be in the native digitizer space in m.
    hsp : None | array, shape (n_points, 3)
        This corresponds to an array of positions of the headshape points in
        3d. These points are assumed to be in the native digitizer space in m.
    hpi : None | array, shape (n_hpi, 3)
        This corresponds to an array of HPI points in the native digitizer
        space. They only necessary if computation of a ``compute_dev_head_t``
        is True.
    coord_frame : str
        The coordinate frame of the points. Usually this is ``'unknown'``
        for native digitizer space.
        Other valid values are: ``'head'``, ``'meg'``, ``'mri'``,
        ``'mri_voxel'``, ``'mri_tal'``, ``'ras'``, ``'fs_tal'``,
        ``'ctf_head'``, and ``'ctf_meg'``.

        .. note::
            For custom montages without fiducials, this parameter must be set
            to ``'head'``.

    Returns
    -------
    montage : instance of DigMontage
        The montage.

    See Also
    --------
    DigMontage
    read_dig_captrak
    read_dig_egi
    read_dig_fif
    read_dig_localite
    read_dig_polhemus_isotrak
    """
    _validate_type(ch_pos, (dict, None), 'ch_pos')
    if ch_pos is None:
        ch_names = None
    else:
        ch_names = list(ch_pos)
    dig = _make_dig_points(
        nasion=nasion, lpa=lpa, rpa=rpa, hpi=hpi, extra_points=hsp,
        dig_ch_pos=ch_pos, coord_frame=coord_frame
    )

    return DigMontage(dig=dig, ch_names=ch_names)


class DigMontage(object):
    """Montage for digitized electrode and headshape position data.

    .. warning:: Montages are typically created using one of the helper
                 functions in the ``See Also`` section below instead of
                 instantiating this class directly.

    Parameters
    ----------
    dig : list of dict
        The object containing all the dig points.
    ch_names : list of str
        The names of the EEG channels.

    See Also
    --------
    read_dig_captrak
    read_dig_dat
    read_dig_egi
    read_dig_fif
    read_dig_hpts
    read_dig_localite
    read_dig_polhemus_isotrak
    make_dig_montage

    Notes
    -----
    .. versionadded:: 0.9.0
    """

    def __init__(self, *, dig=None, ch_names=None):
        dig = list() if dig is None else dig
        _validate_type(item=dig, types=list, item_name='dig')
        ch_names = list() if ch_names is None else ch_names
        n_eeg = sum([1 for d in dig if d['kind'] == FIFF.FIFFV_POINT_EEG])
        if n_eeg != len(ch_names):
            raise ValueError(
                'The number of EEG channels (%d) does not match the number'
                ' of channel names provided (%d)' % (n_eeg, len(ch_names))
            )

        self.dig = dig
        self.ch_names = ch_names

    def __repr__(self):
        """Return string representation."""
        n_points = _count_points_by_type(self.dig)
        return ('<DigMontage | {extra:d} extras (headshape), {hpi:d} HPIs,'
                ' {fid:d} fiducials, {eeg:d} channels>').format(**n_points)

    @copy_function_doc_to_method_doc(plot_montage)
    def plot(self, scale_factor=20, show_names=True, kind='topomap', show=True,
             sphere=None, verbose=None):
        return plot_montage(self, scale_factor=scale_factor,
                            show_names=show_names, kind=kind, show=show,
                            sphere=sphere)

    @fill_doc
    def rename_channels(self, mapping, allow_duplicates=False):
        """Rename the channels.

        Parameters
        ----------
        %(mapping_rename_channels_duplicates)s

        Returns
        -------
        inst : instance of DigMontage
            The instance. Operates in-place.
        """
        from .channels import rename_channels
        temp_info = create_info(list(self._get_ch_pos()), 1000., 'eeg')
        rename_channels(temp_info, mapping, allow_duplicates)
        self.ch_names = temp_info['ch_names']

    @verbose
    def save(self, fname, *, overwrite=False, verbose=None):
        """Save digitization points to FIF.

        Parameters
        ----------
        fname : str
            The filename to use. Should end in .fif or .fif.gz.
        %(overwrite)s
        %(verbose)s
        """
        coord_frame = _check_get_coord_frame(self.dig)
        write_dig(fname, self.dig, coord_frame, overwrite=overwrite)

    def __iadd__(self, other):
        """Add two DigMontages in place.

        Notes
        -----
        Two DigMontages can only be added if there are no duplicated ch_names
        and if fiducials are present they should share the same coordinate
        system and location values.
        """
        def is_fid_defined(fid):
            return not(
                fid.nasion is None and fid.lpa is None and fid.rpa is None
            )

        # Check for none duplicated ch_names
        ch_names_intersection = set(self.ch_names).intersection(other.ch_names)
        if ch_names_intersection:
            raise RuntimeError((
                "Cannot add two DigMontage objects if they contain duplicated"
                " channel names. Duplicated channel(s) found: {}."
            ).format(
                ', '.join(['%r' % v for v in sorted(ch_names_intersection)])
            ))

        # Check for unique matching fiducials
        self_fid, self_coord = _get_fid_coords(self.dig)
        other_fid, other_coord = _get_fid_coords(other.dig)

        if is_fid_defined(self_fid) and is_fid_defined(other_fid):
            if self_coord != other_coord:
                raise RuntimeError('Cannot add two DigMontage objects if '
                                   'fiducial locations are not in the same '
                                   'coordinate system.')

            for kk in self_fid:
                if not np.array_equal(self_fid[kk], other_fid[kk]):
                    raise RuntimeError('Cannot add two DigMontage objects if '
                                       'fiducial locations do not match '
                                       '(%s)' % kk)

            # keep self
            self.dig = _format_dig_points(
                self.dig + [d for d in other.dig
                            if d['kind'] != FIFF.FIFFV_POINT_CARDINAL]
            )
        else:
            self.dig = _format_dig_points(self.dig + other.dig)

        self.ch_names += other.ch_names
        return self

    def copy(self):
        """Copy the DigMontage object.

        Returns
        -------
        dig : instance of DigMontage
            The copied DigMontage instance.
        """
        return deepcopy(self)

    def __add__(self, other):
        """Add two DigMontages."""
        out = self.copy()
        out += other
        return out

    def _get_ch_pos(self):
        pos = [d['r'] for d in _get_dig_eeg(self.dig)]
        assert len(self.ch_names) == len(pos)
        return OrderedDict(zip(self.ch_names, pos))

    def _get_dig_names(self):
        NAMED_KIND = (FIFF.FIFFV_POINT_EEG,)
        is_eeg = np.array([d['kind'] in NAMED_KIND for d in self.dig])
        assert len(self.ch_names) == is_eeg.sum()
        dig_names = [None] * len(self.dig)
        for ch_name_idx, dig_idx in enumerate(np.where(is_eeg)[0]):
            dig_names[dig_idx] = self.ch_names[ch_name_idx]

        return dig_names

    def get_positions(self):
        """Get all channel and fiducial positions.

        Returns
        -------
        positions : dict
            A dictionary of the positions for channels (``ch_pos``),
            coordinate frame (``coord_frame``), nasion (``nasion``),
            left preauricular point (``lpa``),
            right preauricular point (``rpa``),
            Head Shape Polhemus (``hsp``), and
            Head Position Indicator(``hpi``).
            E.g.::

                {
                    'ch_pos': {'EEG061': [0, 0, 0]},
                    'nasion': [0, 0, 1],
                    'coord_frame': 'mni_tal',
                    'lpa': [0, 1, 0],
                    'rpa': [1, 0, 0],
                    'hsp': None,
                    'hpi': None
                }
        """
        # get channel positions as dict
        ch_pos = self._get_ch_pos()

        # get coordframe and fiducial coordinates
        montage_bunch = _get_data_as_dict_from_dig(self.dig)
        coord_frame = _frame_to_str.get(montage_bunch.coord_frame)

        # return dictionary
        positions = dict(
            ch_pos=ch_pos,
            coord_frame=coord_frame,
            nasion=montage_bunch.nasion,
            lpa=montage_bunch.lpa,
            rpa=montage_bunch.rpa,
            hsp=montage_bunch.hsp,
            hpi=montage_bunch.hpi,
        )
        return positions

    @verbose
    def apply_trans(self, trans, verbose=None):
        """Apply a transformation matrix to the montage.

        Parameters
        ----------
        trans : instance of mne.transforms.Transform
            The transformation matrix to be applied.
        %(verbose)s
        """
        _validate_type(trans, Transform, 'trans')
        coord_frame = self.get_positions()['coord_frame']
        trans = _ensure_trans(trans, fro=coord_frame, to=trans['to'])
        for d in self.dig:
            d['r'] = apply_trans(trans, d['r'])
            d['coord_frame'] = trans['to']

    @verbose
    def add_estimated_fiducials(self, subject, subjects_dir=None,
                                verbose=None):
        """Estimate fiducials based on FreeSurfer ``fsaverage`` subject.

        This takes a montage with the ``mri`` coordinate frame,
        corresponding to the FreeSurfer RAS (xyz in the volume) T1w
        image of the specific subject. It will call
        :func:`mne.coreg.get_mni_fiducials` to estimate LPA, RPA and
        Nasion fiducial points.

        Parameters
        ----------
        %(subject)s
        %(subjects_dir)s
        %(verbose)s

        Returns
        -------
        inst : instance of DigMontage
            The instance, modified in-place.

        See Also
        --------
        :ref:`tut-source-alignment`

        Notes
        -----
        Since MNE uses the FIF data structure, it relies on the ``head``
        coordinate frame. Any coordinate frame can be transformed
        to ``head`` if the fiducials (i.e. LPA, RPA and Nasion) are
        defined. One can use this function to estimate those fiducials
        and then use ``mne.channels.compute_native_head_t(montage)``
        to get the head <-> MRI transform.
        """
        # get coordframe and fiducial coordinates
        montage_bunch = _get_data_as_dict_from_dig(self.dig)

        # get the coordinate frame and check that it's MRI
        if montage_bunch.coord_frame != FIFF.FIFFV_COORD_MRI:
            raise RuntimeError(
                f'Montage should be in the "mri" coordinate frame '
                f'to use `add_estimated_fiducials`. The current coordinate '
                f'frame is {montage_bunch.coord_frame}')

        # estimate LPA, nasion, RPA from FreeSurfer fsaverage
        fids_mri = list(get_mni_fiducials(subject, subjects_dir))

        # add those digpoints to front of montage
        self.dig = fids_mri + self.dig
        return self

    @verbose
    def add_mni_fiducials(self, subjects_dir=None, verbose=None):
        """Add fiducials to a montage in MNI space.

        Parameters
        ----------
        %(subjects_dir)s
        %(verbose)s

{'fraction_non_alphanumeric': 0.10026560424966799, 'fraction_numerical': 0.006341301460823373, 'mean_word_length': 2.0084898122253296, 'pattern_counts': {'":': 0, '<': 67, '<?xml version=': 0, '>': 67, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5251045', 'n_tokens_mistral': 9515, 'n_tokens_neox': 9045, 'n_words': 2498}