Alternative versions: LabNotebook, Nbviewer
Format: straight-up
Overview¶
I've been doing a lot of work recently with cortical surface laplace-beltrami operator (LBO) eigenmodes. The LBO modes are a set of spherical harmonic-like basis functions that can be used to describe both the shape and the spatiotemporal dynamics of neural activity on the cortex.
There are nice tools from moo chung and martin reuter + arno klein, in matlab and python respectively for computing the LBO from arbitrary surface meshes, both of which work very well for human cortical surface data. Strangely, however, it appears that both of these tools fail on monkey (rhesus macaque) cortical surface meshes.
Initially the following notes were simply a report on this problem and essentially a plea for help. However I'm happy to report I've now found a relatively simple regulariation-based solution.
Notebook Setup¶
Define some variables
# put some system-specific variables in the namespace ('le' dict)
%run ~/set_localenv_vars.py
nb_name = 'macaque_lbo_modes_problem'
# this is where I look for things
outdir = '%s/notebooks/%s' %(le['data_dir'],nb_name)
# this is where things actually live
outdir_loc = '%s/%s' %('/mnt/titania-hecuba/mcintosh_lab/john/Data/notebooks', nb_name)
#outdir_loc = '%s/%s' %('/hecuba/mcintosh_lab/john/Data/notebooks', nb_name)
import os,sys
if not os.path.isdir(outdir_loc): os.makedirs(outdir_loc)
if not os.path.isdir(outdir): os.system('ln -s %s %s' %(outdir_loc,outdir))
!mkdir $outdir/pics
# freesurfer surfaces
fsav5_dir = '/software/freesurfer/subjects/fsaverage5'
fsav5_lhp_file = fsav5_dir + '/surf/lh.pial'
fsav5_lhc_file = fsav5_dir + '/surf/lh.curv'
# macaque datasets:
# 1. cmtk library example data
m1_f = '/mnt/rotmangrid-scratch2/mcintosh_lab/jgriffiths/Data/cmtk_data/macaca_mulatta_01.cff'
# 2. f99 macaque data (from gleb bezgin)
m2_f = '/mnt/rotmangrid-scratch2/mcintosh_lab/jgriffiths/Data/gleb_data/f99_vertices_faces.mat'
# 3. NIMH macaque template https://github.com/jms290/NMT/tree/master/NMT_v1.2
m3_f = '/alexandra/mcintosh_lab/john/Code/libraries_of_others/github/NMT/NMT_v1.2/lh.GM.gii'
mindboggle_dir = le['code_dir'] + '/libraries_of_others/github/mindboggle'
sys.path.append(mindboggle_dir)
# stuff for workdocs-cloudfiles
aws_key = 'drowssaperucesyreva'
aws_secret = '?teytidettopsuoyevah'
Importage
# Generic imports
import os,sys,glob,numpy as np,pandas
from scipy.sparse.linalg import eigsh,lobpcg
from scipy.io import loadmat
# Neuroimaging stuff
import nibabel as nib
from mindboggle.shapes.laplace_beltrami import computeAB
import cfflib
# Visualization stuff
%matplotlib inline
from matplotlib import pyplot as plt
from IPython.display import Image,display as d,clear_output
from nilearn.plotting import plot_surf_stat_map
# aws api and workdocs-cloudfiles stuff
sys.path.append(le['ipynb_workdocs_dir'])
from new_utils import nb_fig,cloudfiles_nb
Calico document extensions
%%javascript
IPython.load_extensions('calico-spell-check',
'calico-document-tools', 'calico-cell-tools');
Initialize aws api and workdocs-cloudfiles folder
cnb = cloudfiles_nb('aws', [aws_key,aws_secret])
cnb.initialize_folder(nb_name)
Go to output folder
os.chdir(outdir)
Ok, let's get cracking
Load data and look at surfaces¶
fsaverage 5 human data
fsav5_lhp_vtx,fsav5_lhp_tri = nib.freesurfer.read_geometry(fsav5_lhp_file)
macaque dataset 1
res_lh,res_rh = cfflib.load(m1_f).get_connectome_surface()
res_lh.load()
m1_vtx = res_lh.data.darrays[0].data
m1_tri = res_lh.data.darrays[1].data
macaque dataset 2
f2_mat = loadmat(m2_f, struct_as_record=False,squeeze_me=True)
m2_vtx = f2_mat['vertices_original']
m2_tri = f2_mat['faces']
m2_tri-=1
macaque dataset 3
img = nib.gifti.giftiio.read(m3_f)
m3_vtx = img.darrays[0].data
m3_tri = img.darrays[1].data
So we have one human cortical surface and three different macaque cortical surfaces. Let's take a look at these together:
Reference point: human brain LBO modes¶
Here is how the human brain cortical surface LBO modes look. This is what I've trying to get from the macaque data. Showing here modes 2-4, whic suffice to give the general picture of what we're looking for. The first mode is uniform and not very informative to look at, so we skip that one.
fsav5_lhp_A,fsav5_lhp_B = computeAB(fsav5_lhp_vtx,fsav5_lhp_tri)
fsav5_lhp_lbo_evals,fsav5_lhp_lbo_evecs = eigsh(fsav5_lhp_A,M=fsav5_lhp_B,sigma=0,k=10)
The problem: doesn't work for macaque¶
Ok so this is the error that we get when trying to compute LBO modes as above from macaque surfaces. The mindboggle computeAB
function seems to run ok, but the sparse eigendecomposition with eigsh
fails because A
is singular.
macaque dataset 1
m1_A,m1_B = computeAB(m1_vtx,m1_tri)
m1_lbo_evals,m1_lbo_evecs = eigsh(m1_A,M=m1_B,sigma=0,k=10)
macaque dataset 2
m2_A,m2_B = computeAB(m2_vtx,m2_tri)
m2_lbo_evals,m2_lbo_evecs = eigsh(m2_A,M=m2_B,sigma=0,k=10)
macaque dataset 3
m3_A,m3_B = computeAB(m3_vtx,m3_tri)
m3_lbo_evals,m3_lbo_evecs = eigsh(m3_A,M=m3_B,sigma=0,k=10)
Alternative computation 1: doesn't work¶
The mindboggle fem_laplacian.py
code has an alternative for when this kind of error is encountered. Here's a snippet:
# -----------------------------------------------------------
# Use the eigsh eigensolver:
# -----------------------------------------------------------
try :
# eigs is for nonsymmetric matrices while
# eigsh is for real-symmetric or complex-Hermitian matrices:
eigenvalues, eigenvectors = eigsh(A, k=spectrum_size,
M=B,sigma=-0.01)
spectrum = eigenvalues.tolist()
# -----------------------------------------------------------
# Use the lobpcg eigensolver:
# -----------------------------------------------------------
except RuntimeError:
if verbose:
print("eigsh() failed. Now try lobpcg.")
print("Warning: lobpcg can produce"
"different results from "
"Reuter (2006) shapeDNA-tria software.")
# Initial eigenvector values:
init_eigenvecs = np.random.random((A.shape[0],
spectrum_size))
# maxiter = 40 forces lobpcg to use 20 iterations.
# Strangely, largest=false finds largest eigenvalues
# and largest=True gives the smallest eigenvalues:
eigenvalues, eigenvectors = lobpcg(A, init_eigenvecs,B=B,
largest=True, maxiter=40)
...etc etc
Let's try this for the first macaque dataset:
spectrum_size = 5
A,B = computeAB(m1_vtx,m1_tri)
init_evecs = np.random.random((A.shape[0],
spectrum_size))
m1_lobpcg_lbo_evals,m1_lobpcg_lbo_evecs = lobpcg(A, init_evecs,B=B,
largest=True, maxiter=40)
Unfortunately the outputs of this computation are clearly not what we're after; which fortunately we can judge because we have some geometric intuitions about what LBO eigenmodes 'should' look like.
They should not look like that.
Also, using that on human brains doesn't look right either. So looks like the lobpcg
computation should be avoided for this particular application.
Alternative computation 2: also doesn't work¶
Second alternative I tried is moo chung's matlab code. Won't show that in full detail here; but it's pretty simple to use. Goes something like this:
load(f);
mac.vertices = vertices_orig;
mac.faces = faces;
[A, C] =FEM(mac);
[V, D] = eigs(C,A,999,'sm');
But this also fails at the eigedecomposition stage.
Solution: regularization¶
It's clear from the error message above RuntimeError: Factor is exactly singular
that there is a problem inverting the A matrix because it is singular and/or poorly conditioned. The error message from the matlab option (not shown) complains about the same thing.
The solution to this problem isn't too hard to find: the top google hit for the "python RuntimeError: Factor is exactly singular" has a comment reply that points to this stackoverflow post on improving a badly conditioned matrix.
The discussion there receommends adding a small constant c
to the diagonal of the matrix.
A simple way to find the best value of c
is simply to start with some small starting value that works (o.e. when added to the diagonal doesn't throw up the singular matrix error), and then suceessively reduce the value of c
and until the you start getting the error again.
Here's a dumb, very simple way to do that. I find the best (/ an acceptable) regularization factor by decreasing c
until the eigsh
call throws the error it was doing above
blah = 500
while blah > 100:
c = c/10.
print 'c=%s' %c
m1_A,m1_B = computeAB(m1_vtx,m1_tri)
m1_A_mod = m1_A.copy()
for a in np.arange(m1_A_mod.shape[0]): m1_A_mod[a,a] += c
m1_lbo_evals,m1_lbo_evecs = eigsh(m1_A_mod,M=m1_B,
sigma=0,k=10)
c = 1E-50
m1_A,m1_B = computeAB(m1_vtx,m1_tri)
m1_A_mod = m1_A.copy()
for a in np.arange(m1_A_mod.shape[0]): m1_A_mod[a,a] += c
m1_lbo_evals,m1_lbo_evecs = eigsh(m1_A_mod,M=m1_B,sigma=0,k=10)
c = 1E-50
m2_A,m2_B = computeAB(m2_vtx,m2_tri)
m2_A_mod = m2_A.copy()
for a in np.arange(m2_A_mod.shape[0]): m2_A_mod[a,a] += c
m2_lbo_evals,m2_lbo_evecs = eigsh(m2_A_mod,M=m2_B,sigma=0,k=10)
c = 1E-20
m3_A,m1_B = computeAB(m3_vtx,m3_tri)
m3_A_mod = m3_A.copy()
for a in np.arange(m3_A_mod.shape[0]): m3_A_mod[a,a] += c
m3_lbo_evals,m3_lbo_evecs = eigsh(m3_A_mod,M=m3_B,sigma=0,k=10)
Ok so in the third case there the modes aren't looking so good as the first two. Possibly that can be fixed with a bit more of a refine search for a good value of c
.
That aside, bonzer: we have functioning LBO decompositions for macaque cortical surface data :)