"""Operations on ``mne.Forward``, ``mne.SourceSpaces`` and mapping between vertices and leadfield indices."""
# Author: Julia Jurkowska
import mne
import os
import numpy as np
from scipy.spatial import cKDTree
import mvpure_py
from ._utils import (
_check_hemi_param,
_check_hemi_and_vertices_matching
)
[docs]
def subset_forward(old_fwd: mne.Forward,
localized,
vertices: list[int] = None,
hemi: str = "both") -> mne.Forward:
"""
Subset ``old_fwd`` (mne.Forward) for it to contain information only for certain vertices' numbers.
Parameters
-----------
old_fwd: mne.Forward
``mne.Forward`` to get subset from
hemi: str
hemisphere(s) containing the dipoles that are expected to be present in ``mne.Forward`` subset
Returns
-----------
mne.Forward: subset of mne.Forward for given vertices
"""
if localized is not None:
new_fwd = _subset_forward_by_localized(old_fwd, localized, hemi)
elif vertices is not None:
new_fwd = _subset_forward_by_vertices(old_fwd, vertices, hemi)
return new_fwd
def _subset_forward_by_localized(old_fwd: mne.Forward, localized, #: mvpure_py.Localized,
hemi: str = "both") -> mne.Forward:
"""
Subset mne.Forward for it to contain information only for certain dipoles
that were previously localized via mvpure.localizer localizers
Parameters
-----------
old_fwd: mne.Forward
mne.Forward to et subset from
localized: mvpure_py.Localized
instance with information about localized sources
hemi: str
hemisphere(s) containing the dipoles that are expected to be present in mne.Forward subset
Returns
-----------
mne.Forward: subset of mne.Forward for given vertices
"""
# create a copy of old_fwd as a draft for new one
new_fwd = old_fwd.copy()
new_fwd['nsource'] = localized['nsource']
new_fwd['sol']['ncol'] = localized['nsource']
new_fwd['sol']['data'] = localized['leadfield']
new_fwd['source_rr'] = old_fwd['source_rr'][list(localized['sources']), :]
new_fwd['source_nn'] = old_fwd['source_nn'][list(localized['sources']), :]
# transform source ordinal number (leadfield index) to vertice number
trans_sources = transform_leadfield_indices_to_vertices(localized['sources'],
old_fwd['src'],
hemi=hemi,
include_mapping=False)
new_fwd['src'] = _subset_src(old_fwd['src'], trans_sources, hemi)
return new_fwd
def _subset_forward_by_vertices(old_fwd: mne.Forward, vertices: list[int], hemi: str = "both") -> mne.Forward:
"""
Subset mne.Forward for it to contain information only for certain dipoles
that were previously localized via mvpure.localizer localizators
Parameters:
-----------
old_fwd: mne.Forward
mne.Forward to et subset from
vertices: list[int]
list of vertices indices to include in mne.Forward subset
hemi: str
hemisphere(s) containing the dipoles that are expected to be present in mne.Forward subset
Returns:
-----------
mne.Forward: subset of mne.Forward for given vertices
"""
# create a copy of old_fwd as a draft for new one
new_fwd = old_fwd.copy()
new_fwd['nsource'] = len(vertices)
new_fwd['sol']['ncol'] = len(vertices)
lf_idx = transform_vertices_to_leadfield_indices(vertices, old_fwd['src'])
new_fwd['sol']['data'] = old_fwd['leadfield'][:, lf_idx]
new_fwd['source_rr'] = old_fwd['source_rr'][lf_idx, :]
new_fwd['source_nn'] = old_fwd['source_nn'][lf_idx, :]
new_fwd['src'] = _subset_src(old_fwd['src'], vertices, hemi)
return new_fwd
def _subset_src(old_src: mne.SourceSpaces,
vertices: list[int] | list[list[int]],
hemi: str) -> mne.SourceSpaces:
"""
Subset mne.SourceSpaces for it to contain information only for certain vertices' numbers.
Parameters:
-----------
old_src: mne.SourceSpaces
source space to get subset from
vertices: list[int] | list[list[int]]
vertices to be included in subset of mne.SourceSpaces
If hemi == 'both' ```vertices``` should be list containing two lists with integers.
If hemi == 'lh' or hemi == 'rh' ```vertices``` should be a list of integers.
hemi: str (Options: 'both', 'rh', 'lh')
hemispheres from which vertices are.
Returns:
-----------
mne.SourceSpaces: subset of mne.SourceSpaces for given vertices
"""
# check correctness of 'hemi' parameter an its consistence to 'vertices' parameter
_check_hemi_param(hemi)
_check_hemi_and_vertices_matching(hemi, vertices)
# create a copy of old_src as a draft for new one
new_src = old_src.copy()
# iterate through hemispheres
for i in range(len(vertices)):
new_src[i]['nuse'] = len(vertices[i]) if len(vertices) == 1 else len(vertices)
new_src[i]['inuse'] = np.where(np.isin(np.arange(len(old_src[i]['inuse'])),
vertices[i]), 1, 0)
new_src[i]['vertno'] = np.sort(vertices[i])
return new_src
[docs]
def map_vertices_to_fsaverage(locs,
hemi: list[str] | str,
surf: str = "white"):
"""
Map vertices from subject-specific surface to fsaverage coordinates for one or both hemispheres.
This function wraps `_map_vertices_to_fsaverage_for_hemi` and allows mapping vertices
for either a single hemisphere ('lh' or 'rh') or both hemispheres simultaneously.
Parameters
----------
locs : mvpure_py.Localized
``mvpure_py.Localized`` object to map vertices from.
After mapping, each vertex in the specified hemisphere(s) will have a new key
``"morphed_to_fs"`` storing the corresponding fsaverage vertex index.
hemi : list[str] | str
Which hemisphere to map.
surf : str (Default: 'white')
Surface which coordinates use during mapping.
Raises
-------
ValueError
If ``hemi`` is not one of ['lh', 'rh', 'both'].
"""
_check_hemi_param(hemi)
if hemi == "both":
hemi = ["lh", "rh"]
elif hemi in ["lh", "rh"]:
hemi = [hemi]
else:
raise ValueError("Only possible options for 'hemi' parameter are: ['lh', 'rh', 'both'].")
for h in hemi:
_map_vertices_to_fsaverage_for_hemi(locs=locs, hemi=h, surf=surf)
def _map_vertices_to_fsaverage_for_hemi(locs, hemi: str, surf: str):
"""
Map vertices from a subject-specific brain surface to the fsaverage surface for a given hemisphere.
This function:
- takes vertices from mvpure_py.Localized object for a specific subject and hemisphere,
- converts their coordinates to MNI space,
- finds the closest corresponding vertices on the fsaverage surface.
Parameters
----------
locs : mvpure_py.Localized
mvpure_py.Localized object containing vertex information and subject metadata.
hemi : str
Hemisphere to process. Needs to be 'lh' or 'rh'.
surf : str
Surface type to use for fsaverage mapping.
Notes
----------
- The mapping is hemisphere-specific and assumes fsaverage has a standard FreeSurfer directory structure.
"""
# Read vertices from mvpure_py.Localized object (only for given hemi)
vert = [vert for vert in list(locs['vertices'].keys()) if locs['vertices'][vert]['hemi'] == hemi]
# If there are no vertices for given hemisphere - return None
if len(vert) == 0:
return None
hemis_int = 0 if hemi == 'lh' else 1
# Load the target fsaverage surface
surf_path = os.path.join(locs['subjects_dir'], 'fsaverage', 'surf', f'{hemi}.{surf}')
fs_vertices, _ = mne.read_surface(surf_path)
fs_vertex_indices = np.arange(fs_vertices.shape[0]) # from 0 to (n_vertices - 1)
# Convert vertices from subject_from into MNI coordinates
mni_coords = mne.vertex_to_mni(vert,
hemis=hemis_int,
subject=locs['subject'],
subjects_dir=locs['subjects_dir'])
# Convert vertices from fsaverage into MNI coordinates
fs_mni_coords = mne.vertex_to_mni(fs_vertex_indices,
hemis=hemis_int,
subject="fsaverage",
subjects_dir=locs['subjects_dir'])
# Using KDTree to find the closest fsaverage vertex for each input MNI coordinates
tree = cKDTree(fs_mni_coords)
_, idx = tree.query(mni_coords)
mapped_vertices = fs_vertex_indices[idx]
# Add to instance
for i, v in enumerate(vert):
locs['vertices'][v]["morphed_to_fs"] = int(mapped_vertices[i])