Repository: mne-tools/mne-python

Issue:
ENH: allow for spatial connectivity between two hemispheres
In volumetric source spaces you can get bi-lateral ROIs, blobs, and  connected clusters, not that easily with Freesurfer surfaces. However, it's an unphysiological prior to not allow bihemispheric connections in permutation clustering tests. With @banilo and @yuval-harpaz we were wondering about ways to connect medial vertices across hemispheres, maybe based on a minimum distance criterion. As an intermediate step it would be cool to support FS xhemi registrations to support mapping vertices between hemispheres. Input and ideas are welcome.

cc @Eric89GXL @mainakjas @agramfort 



Discussion:
Hi
I used fsaverage_sym from:
https://surfer.nmr.mgh.harvard.edu/fswiki/Xhemi
I read the spheres surfaces with matlab and looked for min distance there,
which made more sense than on pial.
I then looked for each of these pairs of dots (in lPair and rPair vectors
below) for their closes points on individual sphere surfaces.
The result seems reasonable but I did not validate it yet.

This is my code for looking for LR pairs on the template

sphere.lh=mne_read_surface('/usr/local/freesurfer/subjects/fsaverage_sym/surf/lh.sphere.left_right');
sphere.rh=mne_read_surface('/usr/local/freesurfer/subjects/fsaverage_sym/surf/rh.sphere.left_right');
for srci=1:10242

[~,lPair(srci)]=min(sum(abs(sphere.lh-repmat(sphere.rh(srci,:),length(sphere.lh),1)),2));
    prog(srci)
end
rPair=1:10242;

here is how I looked for pairs for individuals

```
   sub
```

_Sphere.lh=mne_read_surface(['/usr/local/freesurfer/subjects/',subj,'/surf/lh.sphere']);_
*
subSphere.rh=mne_read_surface(['/usr/local/freesurfer/subjects/',subj,'/surf/rh.sphere']);*
-        clear li ri*
  *
  %[v,i]=min(sum(abs(sphere.rh-repmat(subSphere.rh(1,:),length(sphere.lh),1)),2));*
-        for srci=1:10242*
  *
  [~,ri(srci)]=min(sum(abs(subSphere.rh-repmat(sphere.rh(srci,:),length(subSphere.rh),1)),2));*
  *
  [~,li(srci)]=min(sum(abs(subSphere.lh-repmat(sphere.lh(lPair(srci),:),length(subSphere.lh),1)),2));*
-            prog(srci)*
-        end*
-        disp(' ');*
-        if length(unique(li))<10242*
-            disp([subj,' has ',num2str(10242-length(unique(li))),'
  repeting matches for LH'])*
-        end*
-        if length(unique(ri))<10242*
-            disp([subj,' has ',num2str(10242-length(unique(ri))),'
  repeting matches for RH'])*
-        end*

On 14 July 2015 at 10:47, Denis A. Engemann notifications@github.com
wrote:

> In volumetric source spaces you can get bi-lateral ROIs, blobs, and
> connected clusters, not that easily with Freesurfer surfaces. However, it's
> an unphysiological prior to not allow bihemispheric connections in
> permutation clustering tests. With @banilo https://github.com/banilo
> and @yuval-harpaz https://github.com/yuval-harpaz we were wondering
> about ways to connect medial vertices across hemispheres, maybe based on a
> minimum distance criterion. As an intermediate step it would be cool to
> support FS xhemi registrations to support mapping vertices between
> hemispheres. Input and ideas are welcome.
> 
> cc @Eric89GXL https://github.com/Eric89GXL @mainakjas
> https://github.com/mainakjas @agramfort https://github.com/agramfort
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/mne-tools/mne-python/issues/2286.

## 

Yuval

Dr .Harpaz

BIU MEG lab http://faculty.biu.ac.il/~goldsa/index.html

A while back I used Euclidean distance for the medial wall labels, it's
pretty easy to combine that with the existing surface distances. Just
compute the distances between medial left vertices and medial right
vertices, and populate the correct entries in the existing distance matrix.

I think this can be done in a few lines with e.g. `cdist`. Okay to close this one as an mne-python issue, or do you think the API should live in mne-python?

> do you think the API should live in mne-python?

it should once we're confident that his does the job.

Can you come up with the desired API call? `get_euclidean_inter_hemi_neighbors(src, limit)` or so? Then your connectivity matrix would just be the surface one union the euclidean one.

Maybe `mne.connect_hemis(connectivity, src, limit)`

by the way, I played with corregistering left to right hemispheres some
more a while ago. I found that the simplest way is to corregister one
hemisphere of a subject to the other, and then on the inflated overlaping
surfaces (say the real LH and the flipped RH) to find nearest nodes

On 25 November 2015 at 16:03, Denis A. Engemann notifications@github.com
wrote:

> Maybe mne.connect_hemis(connectivity, src, limit)
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/mne-tools/mne-python/issues/2286#issuecomment-159615566
> .

## 

Yuval

Dr .Harpaz

BIU MEG lab http://faculty.biu.ac.il/~goldsa/index.html


Resolving patch:
diff --git a/mne/__init__.py b/mne/__init__.py
--- a/mne/__init__.py
+++ b/mne/__init__.py
@@ -50,6 +50,7 @@
                               spatial_src_connectivity,
                               spatial_tris_connectivity,
                               spatial_dist_connectivity,
+                              spatial_inter_hemi_connectivity,
                               spatio_temporal_src_connectivity,
                               spatio_temporal_tris_connectivity,
                               spatio_temporal_dist_connectivity,
diff --git a/mne/source_estimate.py b/mne/source_estimate.py
--- a/mne/source_estimate.py
+++ b/mne/source_estimate.py
@@ -1394,7 +1394,7 @@ def to_original_src(self, src_orig, subject_orig=None,
         """
         if self.subject is None:
             raise ValueError('stc.subject must be set')
-        src_orig = _ensure_src(src_orig)
+        src_orig = _ensure_src(src_orig, kind='surf')
         subject_orig = _ensure_src_subject(src_orig, subject_orig)
         data_idx, vertices = _get_morph_src_reordering(
             self.vertices, src_orig, subject_orig, self.subject, subjects_dir)
@@ -1835,10 +1835,7 @@ def plot_surface(self, src, subject=None, surface='inflated', hemi='lh',
         """
 
         # extract surface source spaces
-        src = _ensure_src(src)
-        surf = [s for s in src if s['type'] == 'surf']
-        if len(surf) != 2:
-            raise ValueError('Source space must contain exactly two surfaces.')
+        surf = _ensure_src(src, kind='surf')
 
         # extract surface source estimate
         data = self.data[:surf[0]['nuse'] + surf[1]['nuse']]
@@ -2322,7 +2319,7 @@ def spatio_temporal_src_connectivity(src, n_times, dist=None, verbose=None):
 
     Parameters
     ----------
-    src : source space
+    src : instance of SourceSpaces
         The source space.
     n_times : int
         Number of time instants.
@@ -2442,7 +2439,7 @@ def spatio_temporal_dist_connectivity(src, n_times, dist, verbose=None):
 
     Parameters
     ----------
-    src : source space
+    src : instance of SourceSpaces
         The source space must have distances between vertices computed, such
         that src['dist'] exists and is useful. This can be obtained using MNE
         with a call to mne_add_patch_info with the --dist option.
@@ -2482,7 +2479,7 @@ def spatial_src_connectivity(src, dist=None, verbose=None):
 
     Parameters
     ----------
-    src : source space
+    src : instance of SourceSpaces
         The source space.
     dist : float, or None
         Maximal geodesic distance (in m) between vertices in the
@@ -2526,7 +2523,7 @@ def spatial_dist_connectivity(src, dist, verbose=None):
 
     Parameters
     ----------
-    src : source space
+    src : instance of SourceSpaces
         The source space must have distances between vertices computed, such
         that src['dist'] exists and is useful. This can be obtained using MNE
         with a call to mne_add_patch_info with the --dist option.
@@ -2544,6 +2541,38 @@ def spatial_dist_connectivity(src, dist, verbose=None):
     return spatio_temporal_dist_connectivity(src, 1, dist)
 
 
+def spatial_inter_hemi_connectivity(src, dist, verbose=None):
+    """Get vertices on each hemisphere that are close to the other hemisphere
+
+    Parameters
+    ----------
+    src : instance of SourceSpaces
+        The source space. Must be surface type.
+    dist : float
+        Maximal Euclidean distance (in m) between vertices in one hemisphere
+        compared to the other to consider neighbors.
+    verbose : bool, str, int, or None
+        If not None, override default verbose level (see mne.verbose).
+
+    Returns
+    -------
+    connectivity : sparse COO matrix
+        The connectivity matrix describing the spatial graph structure.
+        Typically this should be combined (addititively) with another
+        existing intra-hemispheric connectivity matrix, e.g. computed
+        using geodesic distances.
+    """
+    from scipy.spatial.distance import cdist
+    src = _ensure_src(src, kind='surf')
+    conn = cdist(src[0]['rr'][src[0]['vertno']],
+                 src[1]['rr'][src[1]['vertno']])
+    conn = sparse.csr_matrix(conn <= dist, dtype=int)
+    empties = [sparse.csr_matrix((nv, nv), dtype=int) for nv in conn.shape]
+    conn = sparse.vstack([sparse.hstack([empties[0], conn]),
+                          sparse.hstack([conn.T, empties[1]])])
+    return conn
+
+
 @verbose
 def _get_connectivity_from_edges(edges, n_times, verbose=None):
     """Given edges sparse matrix, create connectivity matrix"""
diff --git a/mne/source_space.py b/mne/source_space.py
--- a/mne/source_space.py
+++ b/mne/source_space.py
@@ -2106,7 +2106,7 @@ def _get_solids(tri_rrs, fros):
 
 
 @verbose
-def _ensure_src(src, verbose=None):
+def _ensure_src(src, kind=None, verbose=None):
     """Helper to ensure we have a source space"""
     if isinstance(src, string_types):
         if not op.isfile(src):
@@ -2115,6 +2115,13 @@ def _ensure_src(src, verbose=None):
         src = read_source_spaces(src, verbose=False)
     if not isinstance(src, SourceSpaces):
         raise ValueError('src must be a string or instance of SourceSpaces')
+    if kind is not None:
+        if kind == 'surf':
+            surf = [s for s in src if s['type'] == 'surf']
+            if len(surf) != 2 or len(src) != 2:
+                raise ValueError('Source space must contain exactly two '
+                                 'surfaces.')
+            src = surf
     return src
 
 


Test patch:
diff --git a/mne/tests/test_source_estimate.py b/mne/tests/test_source_estimate.py
--- a/mne/tests/test_source_estimate.py
+++ b/mne/tests/test_source_estimate.py
@@ -12,11 +12,12 @@
 
 from mne.datasets import testing
 from mne import (stats, SourceEstimate, VolSourceEstimate, Label,
-                 read_source_spaces, MixedSourceEstimate)
-from mne import read_source_estimate, morph_data, extract_label_time_course
-from mne.source_estimate import (spatio_temporal_tris_connectivity,
-                                 spatio_temporal_src_connectivity,
-                                 compute_morph_matrix, grade_to_vertices,
+                 read_source_spaces, MixedSourceEstimate, read_source_estimate,
+                 morph_data, extract_label_time_course,
+                 spatio_temporal_tris_connectivity,
+                 spatio_temporal_src_connectivity,
+                 spatial_inter_hemi_connectivity)
+from mne.source_estimate import (compute_morph_matrix, grade_to_vertices,
                                  grade_to_tris)
 
 from mne.minimum_norm import read_inverse_operator
@@ -33,6 +34,8 @@
 fname_t1 = op.join(data_path, 'subjects', 'sample', 'mri', 'T1.mgz')
 fname_src = op.join(data_path, 'MEG', 'sample',
                     'sample_audvis_trunc-meg-eeg-oct-6-fwd.fif')
+fname_src_3 = op.join(data_path, 'subjects', 'sample', 'bem',
+                      'sample-oct-4-src.fif')
 fname_stc = op.join(data_path, 'MEG', 'sample', 'sample_audvis_trunc-meg')
 fname_smorph = op.join(data_path, 'MEG', 'sample',
                        'sample_audvis_trunc-meg')
@@ -45,6 +48,40 @@
 rng = np.random.RandomState(0)
 
 
+@testing.requires_testing_data
+def test_aaspatial_inter_hemi_connectivity():
+    """Test spatial connectivity between hemispheres"""
+    # trivial cases
+    conn = spatial_inter_hemi_connectivity(fname_src_3, 5e-6)
+    assert_equal(conn.data.size, 0)
+    conn = spatial_inter_hemi_connectivity(fname_src_3, 5e6)
+    assert_equal(conn.data.size, np.prod(conn.shape) // 2)
+    # actually interesting case (1cm), should be between 2 and 10% of verts
+    src = read_source_spaces(fname_src_3)
+    conn = spatial_inter_hemi_connectivity(src, 10e-3)
+    conn = conn.tocsr()
+    n_src = conn.shape[0]
+    assert_true(n_src * 0.02 < conn.data.size < n_src * 0.10)
+    assert_equal(conn[:src[0]['nuse'], :src[0]['nuse']].data.size, 0)
+    assert_equal(conn[-src[1]['nuse']:, -src[1]['nuse']:].data.size, 0)
+    c = (conn.T + conn) / 2. - conn
+    c.eliminate_zeros()
+    assert_equal(c.data.size, 0)
+    # check locations
+    upper_right = conn[:src[0]['nuse'], src[0]['nuse']:].toarray()
+    assert_equal(upper_right.sum(), conn.sum() // 2)
+    good_labels = ['S_pericallosal', 'Unknown', 'G_and_S_cingul-Mid-Post',
+                   'G_cuneus']
+    for hi, hemi in enumerate(('lh', 'rh')):
+        has_neighbors = src[hi]['vertno'][np.where(np.any(upper_right,
+                                                          axis=1 - hi))[0]]
+        labels = read_labels_from_annot('sample', 'aparc.a2009s', hemi,
+                                        subjects_dir=subjects_dir)
+        use_labels = [l.name[:-3] for l in labels
+                      if np.in1d(l.vertices, has_neighbors).any()]
+        assert_true(set(use_labels) - set(good_labels) == set())
+
+
 @slow_test
 @testing.requires_testing_data
 def test_volume_stc():