JDG Lab Notebook

Replicating Spiegler et al. 2016

Alternative versions: LabNotebook, Nbviewer

Format: straight-up

Overview

The following are my notes on trying to reproduce, to various degrees of specificity, the simulations and analyses described in Spiegler et al. eNeuro 2016 "Selective Activation of Resting-State Networks following Focal Stimulation in a Connectome-Based Network Model of the Human Brain"

Notebook Setup

Define some variables

# put some system-specific variables in the namespace ('le' dict)
%run ~/set_localenv_vars.py

nb_name = 'replicating_spiegler2016'


# 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; 
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))

    # root path to tvb-library, tvb-data, etc. libraries. 
tvb_folder = le['code_dir'] + '/libraries_of_others/github'

tvb_dat_dir = tvb_folder + '/tvb-data'

ctx_file = tvb_dat_dir + '/tvb_data/surfaceData/cortex_16384.zip'
rm_file = tvb_dat_dir  + '/tvb_data/regionMapping/regionMapping_16k_76.bz2'
conn_file = tvb_dat_dir + '/tvb_data/connectivity/connectivity_76.zip'


# stuff for workdocs-cloudfiles

aws_key = 'drowssaperucesyreva'
aws_secret = '?teytidettopsuoyevah'  

Importage

# Generic imports

import os,sys,glob,h5py,itertools,multiprocessing,\
       numpy as np,pandas as pd
from datetime import datetime
from time import time
from scipy import optimize
from scipy.signal import hilbert
from scipy.sparse import bsr_matrix
from scipy.sparse.linalg import eigs as sp_eigs


# Visualization stuff

%matplotlib inline
from matplotlib import pyplot as plt
from matplotlib.pyplot import subplot
import matplotlib as mpl
import matplotlib.pyplot as pyplot
import matplotlib.colors
import matplotlib.ticker as ticker
import matplotlib.colors as colors
from matplotlib.tri import Triangulation
from matplotlib import cm
import matplotlib as mpl
from mpl_toolkits.mplot3d import Axes3D
from IPython.display import Image,display as d,clear_output
import seaborn as sns

from numpy import pi,cos,sin


# aws api and workdocs-cloudfiles stuff

sys.path.append(le['ipynb_workdocs_dir'])
from new_utils import nb_fig,cloudfiles_nb


# TVB stuff

nbso,nbse = sys.stdout,sys.stderr # hack part 1/2 to keep output printing properly
sys.path.append(tvb_folder +'/tvb-library')
sys.path.append(tvb_folder + '/tvb-data')
from tvb.simulator.lab import *
sys.stdout,sys.stderr = nbso,nbse  # ...hack part 2/2

from tvb.datatypes.cortex import Cortex
from tvb.datatypes.region_mapping import RegionMapping
from tvb.datatypes.projections import ProjectionMatrix


from copy import copy # seems that this needs to be done last otherwise it gets 
                      # replaced with numpy copy function

Define some functions

#Visualization things - see here https://johngriffiths.github.io/LabNotebook/mpl-surface-viz.html


def plot_surface_mpl(vtx,tri,data=None,rm=None,reorient='tvb',view='superior',
                     shaded=False,ax=None,figsize=(6,4), title=None,
                     lthr=None,uthr=None, nz_thr = 1E-20,
                     shade_kwargs = {'edgecolors': 'k', 'linewidth': 0.1,
                                     'alpha': None, 'cmap': 'coolwarm',
                                     'vmin': None, 'vmax': None}):
                        
  r"""Plot surfaces, surface patterns, and region patterns with matplotlib
    
  This is a general-use function for neuroimaging surface-based data, and 
  does not necessarily require construction of or interaction with tvb 
  datatypes. 

  See also:  plot_surface_mpl_mv



  Parameters
  ----------
  
  vtx           : N vertices x 3 array of surface vertex xyz coordinates 

  tri           : N faces x 3 array of surface faces

  data          : array of numbers to colour surface with. Can be either 
                  a pattern across surface vertices (N vertices x 1 array),
                  or a pattern across the surface's region mapping 
                  (N regions x 1 array), in which case the region mapping 
                  bust also be given as an argument. 
                  
  rm            : region mapping - N vertices x 1 array with (up to) N 
                  regions unique values; each element specifies which 
                  region the corresponding surface vertex is mapped to 

  reorient      : modify the vertex coordinate frame and/or orientation 
                  so that the same default rotations can subsequently be 
                  used for image views. The standard coordinate frame is 
                  xyz; i.e. first,second,third axis = left-right, 
                  front-back, and up-down, respectively. The standard 
                  starting orientation is axial view; i.e. looking down on
                  the brain in the x-y plane.
                  
                  Options: 

                    tvb (default)   : swaps the first 2 axes and applies a rotation
                                              
                    fs              : for the standard freesurfer (RAS) orientation; 
                                      e.g. fsaverage lh.orig. 
                                      No transformations needed for this; so is 
                                      gives same result as reorient=None

  view          : specify viewing angle. 
  
                  This can be done in one of two ways: by specifying a string 
                  corresponding to a standard viewing angle, or by providing 
                  a tuple or list of tuples detailing exact rotations to apply 
                  around each axis. 
                  
                  Standard view options are:
    
                  lh_lat / lh_med / rh_lat / rh_med / 
                  superior / inferior / posterior / anterior

                  (Note: if the surface contains both hemispheres, then medial 
                   surfaces will not be visible, so e.g. 'rh_med' will look the 
                   same as 'lh_lat')
                   
                  Arbitrary rotations can be specied by a tuple or a list of 
                  tuples, each with two elements, the first defining the axis 
                  to rotate around [0,1,2], the second specifying the angle in 
                  degrees. When a list is given the rotations are applied 
                  sequentially in the order given. 
                  
                  Example: rotations = [(0,45),(1,-45)] applies 45 degrees 
                  rotation around the first axis, followed by 45 degrees rotate 
                  around the second axis. 

  lthr/uthr     : lower/upper thresholds - set to zero any datapoints below / 
                  above these values
  
  nz_thr        : near-zero threshold - set to zero all datapoints with absolute 
                  values smaller than this number. Default is a very small 
                  number (1E-20), which unless your data has very small numbers, 
                  will only mask out actual zeros. 

  shade_kwargs  : dictionary specifiying shading options

                  Most relevant options (see matplotlib 'tripcolor' for full details):
                  
                    - 'shading'        (either 'gourand' or omit; 
                                        default is 'flat')
                    - 'edgecolors'     'k' = black is probably best
                    - 'linewidth'      0.1 works well; note that the visual 
                                       effect of this will depend on both the 
                                       surface density and the figure size 
                    - 'cmap'           colormap
                    - 'vmin'/'vmax'    scale colormap to these values
                    - 'alpha'          surface opacity
                  
  ax            : figure axis
  
  figsize       : figure size (ignore if ax provided)
  
  title         : text string to place above figure
  
  
  
                  
  Usage
  -----
       

  Basic freesurfer example:

  import nibabel as nib
  vtx,tri = nib.freesurfer.read_geometry('subjects/fsaverage/surf/lh.orig')
  plot_surface_mpl(vtx,tri,view='lh_lat',reorient='fs')



  Basic tvb example:
  
  ctx = cortex.Cortex.from_file(source_file = ctx_file,
                                region_mapping_file =rm_file)
  vtx,tri,rm = ctx.vertices,ctx.triangles,ctx.region_mapping
  conn = connectivity.Connectivity.from_file(conn_file); conn.configure()
  isrh_reg = conn.is_right_hemisphere(range(conn.number_of_regions))
  isrh_vtx = np.array([isrh_reg[r] for r in rm])
  dat = conn.tract_lengths[:,5]

  plot_surface_mpl(vtx=vtx,tri=tri,rm=rm,data=dat,view='inferior',title='inferior')

  fig, ax = plt.subplots()
  plot_surface_mpl(vtx=vtx,tri=tri,rm=rm,data=dat, view=[(0,-90),(1,55)],ax=ax,
                   title='lh angle',shade_kwargs={'shading': 'gouraud', 'cmap': 'rainbow'})

   
  """
    
  # Copy things to make sure we don't modify things 
  # in the namespace inadvertently. 
    
  vtx,tri = vtx.copy(),tri.copy()
  if data is not None: data = data.copy()

  # 1. Set the viewing angle 
  
  if reorient == 'tvb':
    # The tvb default brain has coordinates in the order 
    # yxz for some reason. So first change that:   
    vtx = np.array([vtx[:,1],vtx[:,0],vtx[:,2]]).T.copy()
    
    # Also need to reflect in the x axis
    vtx[:,0]*=-1

  # (reorient == 'fs' is same as reorient=None; so not strictly needed
  #  but is included for clarity)
   


  # ...get rotations for standard view options
    
  if   view == 'lh_lat'    : rots =  [(0,-90),(1,90)  ]
  elif view == 'lh_med'    : rots =  [(0,-90),(1,-90) ] 
  elif view == 'rh_lat'    : rots =  [(0,-90),(1,-90) ]
  elif view == 'rh_med'    : rots =  [(0,-90),(1,90)  ]
  elif view == 'superior'  : rots =   None
  elif view == 'inferior'  : rots =   (1,180)
  elif view == 'anterior'  : rots =   (0,-90)
  elif view == 'posterior' : rots =  [(0, -90),(1,180)]
  elif (type(view) == tuple) or (type(view) == list): rots = view 

  # (rh_lat is the default 'view' argument because no rotations are 
  #  for that one; so if no view is specified when the function is called, 
  #  the 'rh_lat' option is chose here and the surface is shown 'as is' 
                            
                            
  # ...apply rotations                          
     
  if rots is None: rotmat = np.eye(3)
  else:            rotmat = get_combined_rotation_matrix(rots)
  vtx = np.dot(vtx,rotmat)

                                    
      
  # 2. Sort out the data
                                    
                                    
  # ...if no data is given, plot a vector of 1s. 
  #    if using region data, create corresponding surface vector 
  if data is None: 
    data = np.ones(vtx.shape[0]) 
  elif data.shape[0] != vtx.shape[0]: 
    data = np.array([data[r] for r in rm])
    
  # ...apply thresholds
  if uthr: data *= (data < uthr)
  if lthr: data *= (data > lthr)
  data *= (np.abs(data) > nz_thr)

                                    
  # 3. Create the surface triangulation object 
  
  x,y,z = vtx.T
  tx,ty,tz = vtx[tri].mean(axis=1).T
  tr = Triangulation(x,y,tri[np.argsort(tz)])
                
  # 4. Make the figure 

  if ax is None: fig, ax = plt.subplots(figsize=figsize)  
  
  #if shade = 'gouraud': shade_opts['shade'] = 
  tc = ax.tripcolor(tr, np.squeeze(data), **shade_kwargs)
                        
  ax.set_aspect('equal')
  ax.axis('off')
    
  if title is not None: ax.set_title(title)
        
        
        
        
def plot_surface_mpl_mv(vtx=None,tri=None,data=None,rm=None,hemi=None,   # Option 1
                        vtx_lh=None,tri_lh=None,data_lh=None,rm_lh=None, # Option 2
                        vtx_rh=None,tri_rh=None,data_rh=None,rm_rh=None,
                        title=None,**kwargs):

  r"""Convenience wrapper on plot_surface_mpl for multiple views 
   
  This function calls plot_surface_mpl five times to give a complete 
  picture of a surface- or region-based spatial pattern. 

  As with plot_surface_mpl, this function is written so as to be 
  generally usable with neuroimaging surface-based data, and does not 
  require construction of of interaction with tvb datatype objects. 

  In order for the medial surfaces to be displayed properly, it is 
  necessary to separate the left and right hemispheres. This can be 
  done in one of two ways: 

  1. Provide single arrays for vertices, faces, data, and 
     region mappings, and addition provide arrays of indices for 
     each of these (vtx_inds,tr_inds,rm_inds) with 0/False 
     indicating left hemisphere vertices/faces/regions, and 1/True 
     indicating right hemisphere. 

     Note: this requires that 

  2. Provide separate vertices,faces,data,and region mappings for 
     each hemisphere (vtx_lh,tri_lh; vtx_rh,tri_rh,etc...)


 
  Parameters
  ----------

  (see also plot_surface_mpl parameters info for more details)

  (Option 1)

  vtx               :  surface vertices
 
  tri               : surface faces

  data              : spatial pattern to plot

  rm                : surface vertex to region mapping

  hemi              : hemisphere labels for each vertex
                      (1/True = right, 0/False = left) - 
      

  OR

  (Option 2)

  vtx_lh            : left hemisphere surface_vertices
  vtx_rh            : right ``      ``    ``     ``
  
  tri_lh            : left hemisphere surface faces 
  tri_rh            : right ``      ``    ``     ``

  data_lh          : left hemisphere surface_vertices
  data_rh          : right ``      ``    ``     ``

  rm_lh            : left hemisphere region_mapping
  rm_rh            : right ``      ``    ``     ``


  title            : title to show above middle plot
 
  kwargs           : additional tripcolor kwargs; see plot_surface_mpl

 

  Examples
  ----------

  # TVB default data

  # Plot one column of the region-based tract lengths 
  # connectivity matrix. The corresponding region is 
  # right auditory cortex ('rA1')

  ctx = cortex.Cortex.from_file(source_file = ctx_file,
                                region_mapping_file =rm_file)
  vtx,tri,rm = ctx.vertices,ctx.triangles,ctx.region_mapping
  conn = connectivity.Connectivity.from_file(conn_file); conn.configure()
  isrh_reg = conn.is_right_hemisphere(range(conn.number_of_regions))
  isrh_vtx = np.array([isrh_reg[r] for r in rm])
  dat = conn.tract_lengths[:,5]

  plot_surface_mpl_mv(vtx=vtx,tri=tri,rm=rm,data=dat,
                      hemi=isrh_vtx,title=u'rA1 \ntract length')

  plot_surface_mpl_mv(vtx=vtx,tri=tri,rm=rm,data=dat,
                    hemi=isrh_vtx,title=u'rA1 \ntract length',
                    shade_kwargs = {'shading': 'gouraud',
                                    'cmap': 'rainbow'}) 


  """
   

 
  if vtx is not None:                                    # Option 1
    tri_hemi = hemi[tri].any(axis=1)
    tri_lh,tri_rh = tri[tri_hemi==0],tri[tri_hemi==1]
  elif vtx_lh is not None:                               # Option 2
    vtx = np.vstack([vtx_lh,vtx_rh])
    tri = np.vstack([tri_lh,tri_rh+tri_lh.max()+1])

  if data_lh is not None:                                # Option 2
    data = np.hstack([data_lh,data_rh])
    
  if rm_lh is not None:                                  # Option 2 
    rm = np.hstack([rm_lh,rm_rh + rm_lh.max() + 1])
    
 

  # 2. Now do the plots for each view

  # (Note: for the single hemispheres we only need lh/rh arrays for the 
  #  faces (tri); the full vertices, region mapping, and data arrays
  #  can be given as arguments, they just won't be shown if they aren't 
  #  connected by the faces in tri )
  
  # LH lateral
  plot_surface_mpl(vtx,tri_lh,data=data,rm=rm,view='lh_lat',
                   ax=subplot(2,3,1),**kwargs)
    
  # LH medial
  plot_surface_mpl(vtx,tri_lh, data=data,rm=rm,view='lh_med',
                   ax=subplot(2,3,4),**kwargs)
    
  # RH lateral
  plot_surface_mpl(vtx,tri_rh, data=data,rm=rm,view='rh_lat',
                   ax=subplot(2,3,3),**kwargs)
    
  # RH medial
  plot_surface_mpl(vtx,tri_rh, data=data,rm=rm,view='rh_med',
                   ax=subplot(2,3,6),**kwargs)
    
  # Both superior
  plot_surface_mpl(vtx,tri, data=data,rm=rm,view='superior',
                   ax=subplot(1,3,2),title=title,**kwargs)
    
  plt.subplots_adjust(left=0.0, right=1.0, bottom=0.0,
                      top=1.0, wspace=0, hspace=0)    
    
    
    
    

def get_combined_rotation_matrix(rotations):
  '''Return a combined rotation matrix from a dictionary of rotations around 
     the x,y,or z axes'''
  rotmat = np.eye(3)
    
  if type(rotations) is tuple: rotations = [rotations] 
  for r in rotations:
    newrot = get_rotation_matrix(r[0],r[1])
    rotmat = np.dot(rotmat,newrot)
  return rotmat



def get_rotation_matrix(rotation_axis, deg):

  '''Return rotation matrix in the x,y,or z plane'''



  # (note make deg minus to change from anticlockwise to clockwise rotation)
  th = -deg * (pi/180) # convert degrees to radians

  if rotation_axis == 0:
    return np.array( [[    1,         0,         0    ],
                      [    0,      cos(th),   -sin(th)],
                      [    0,      sin(th),    cos(th)]])
  elif rotation_axis ==1:
    return np.array( [[   cos(th),    0,        sin(th)],
                      [    0,         1,          0    ],
                      [  -sin(th),    0,        cos(th)]])
  elif rotation_axis ==2:
    return np.array([[   cos(th),  -sin(th),     0    ],
                     [    sin(th),   cos(th),     0   ],
                     [     0,         0,          1   ]])

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

Notes

Some bullet points from Spiegler et al. methods:

Nodes and connectivity:

  • 190 nodes (74 cortical plus 116 subcortical)
  • each area contains between 29 and 683 nodes -> for a total of 8,192 nodes per hemisphere.
  • cortical parcellation is tvb default
  • vary ratio of heterogeneous to homogeneous connectivity
  • consider spatial range of homogeneous SC as a parameter varying between 10 and 41 mm.

  • transmission speed v = 6 ms−1

  • Each of the n = 116 considered subcortical areas is lumped to a single point in space S = ∪r∈R3 Ar with R3 = R(n) + 2m.

Dynamics:

  • FHN (?)
  • near criticality
  • Each node in the model is parameterized by γ to operate intrinsically at the same distance from the critical point if unconnected.'
  • A node shows zero activity or oscillation (∼42 Hz) in response to stimulation
  • The parameterization γ = 1.21 and ε = 12.3083 sets an isolated brain area close to a critical point, that is, an Andronov–Hopf bifurcation (sketched in Fig. 2) with a natural frequency of ∼42 Hz using a characteristic rate of η = 76.74 s−1.
  • The connectivity-weighted input determines criticality by working against inherent energy dissipation (i.e., stable focus) toward the bifurcation. So that the bifurcation was not passed, both homogeneous and heterogeneous SC, g(x) and C (‖ x − X′ ‖ /v), are normalized to unity maximum in-strength across time delays by (1) ∫ dx g(x) = 1 and (2)
  • Heun's method; time step of 40 μs for 1 second/realization of one of the following factors: each of the 190 stimulation sites, SC balance, α = {0.0, 0.2, 0.4, 0.6, 0.8, 1.0}, and homogeneous spreading, σ/mm ∈ N: 10 ≤ σ/mm ≤ 41.
  • implementation is verified by the algebraic solution of an isolated node
  • Because of the finite edge lengths in the mesh, the spatial range of the homogeneous SC should not fall below 6.627 mm for the −3 dB cutoff of spatial frequencies with respect to their magnitude (Spiegler and Jirsa, 2013, their Table 7). The lower bound of the spatial range of σ = 10 mm for the homogeneous Gaussian connectivity kernel causes a loss of at least 20% of spatial information (mainly short range), which corresponds to a −7.13274 dB cutoff (Spiegler and Jirsa, 2013, their Fig. 3A)

Stimulation:

  • systematically stimulate each of the 190 areas with a large range of parameter values (for the ratio and the spatial range), resulting in a total of all 37,620 simulation trials.
  • network model is set so that criticality is never reached by normalizing the SC to unity maximum in-strength so that activity cannot be amplified through the SC
  • All network nodes of a brain area are constantly stimulated for a period of the characteristic time of the nodes, η−1, to evoke damped oscillations with a maximum magnitude of one.
  • The stimulation response of an isolated node is subtracted from the response of stimulated nodes in the network.
  • A principal component analysis (PCA) was performed using the covariance matrix among the 16,500 nodes.
  • The period of 0.5 s of data after 0.5 s of stimulus onset (estimated by the cellular automaton) was decomposed.
  • For further analysis, up to three principal components (i.e., orthogonal) are considered that cover >99% of variance across conditions.

Time series analysis:

  • At a given time point, we extract the amplitude of the envelope for the 16,500 nodes (the 16,384 cortical nodes and the 116 subcortical ones), which we normalize to 1.
  • We calculate the covariance among the 16,500 time series (the 16,384 cortical nodes and the 116 subcortical ones) for a time window centered at 750 ms and then perform a PCA to extract the subnetworks capturing >99% of the activity.

  • focus on the time-delayed interaction among the cerebral areas in the cellular automaton, because homogenous SC is instantaneous

  • brain areas are either activate or inactive

  • The temporal decomposition of the heterogeneous SC according to the transmission times gives rules for changing the state of cells over time.

  • The cellular automaton is initialized from the overall inactive state. An activation of a cell triggers a cascade of activation in time until no more cells get activated.

  • In this manner, 190 characteristic activation cascades emerged, each by stimulation, that is, activation of a single cell.

  • The time that the cellular automaton enters the steady state across all stimulation estimates the transient period from the time delays in the heterogeneous SC.

  • This estimate of the cellular automaton was then used to set the starting time for decomposing the simulated data of the full model (Eqs. 1, 2).

Adapting 2D oscillator to spiegler eqs

The nodal dynamics in Speigeler et al. are given as

$$E(\Psi(x,t)) = \eta \begin{bmatrix} \psi_2(x,t) & - \gamma \psi_1(x,t) & - \psi_1^3(x,t) \\ & \epsilon \psi_1(x,t) & \\ \end{bmatrix} $$

The TVB generic 2D oscillator equation is

\begin{eqnarray} \dot{V} && = d \, \tau (-f V^3 + e V^2 + g V + \alpha W + \gamma I) \\ \dot{W} && = \dfrac{d}{\tau}\,\,(c V^2 + b V - \beta W + a) \end{eqnarray}

Note that the $\gamma$ parameter in the Spiegler et al. notation is not the same as the $\gamma$ parameter in the TVB code notation.

So we have the following correspondences in notation between Spiegler et al. and the G2dO in the tvb code:

Spiegler G2dO Description
$\psi_1$ $V$ Fast state variable
$\psi_2$ $W$ Slow state variable
$\eta$ $d$ Rate constant
$\gamma$ $(-)g$ Coefficient for linear self-feedback term in first state variable
$\epsilon$ $b$ Coefficient for gain of first state variable input to second state variable

It can be seen from the above that the Spiegler et al. system is a special case of the G2dO system with the following components removed:

  • quadratic dependence on $V$ in the first term
  • quadratic dependence on $V$ linear dependence on $W$, and constant offset in second term
  • timescale separation between the two variables ($\tau$)

We can therefore recover the Spiegler et al. system from the G2dO system with the following parameter settings :

$c = \alpha = 0 $

$f = \tau = \gamma = 1$,

and, as indicated in the above table, matching the $\eta$, $\gamma$, and $\epsilon$ Spiegler parameters to the $d$, $-g$, and $b$ G2dO parameters, respectively.

A few more notes:

  • Note the sign change for $g$; in the Spiegler equations the linear term in the first variable is subtracted; wherease in G2dO equations it is added. So the G2dO $g$ parameter should be a sign-flipped Spiegler $\gamma$ parameter.
  • One might think from just the above that we actually want to remove the $I$ term from the G2dO equation by setting its $\gamma $ to 0. And for the isolated node example here, that would be correct. But for the network models we come to later, inputs from other nodes actually enter through the $I$ term. So we certainly don't want to set $\gamma$ to 0.

Let's put these parameters into a dictionary for general use:

# ('_sp' refers to the variable symbol as used in Spiegler et al; 
#   so as to avoid ambiguity)


gamma_sp = 1.21
epsilon_sp =  12.3083

# is this the correct? 
# - units in Spiegler are S^-1. 
# - default value for g2do d is 0.02
# - so this gives 0.07674
eta_sp = (1/1000.) * 76.74    #eta_sp = 76.74 # 1. # 76.74 ##1. # 76.74 # 1.



sp_g2do_params = dict(d = eta_sp,
                      tau = 1.,
                      f = 1.,
                      e = 0., 
                      g = -gamma_sp,
                      alpha = 1., 
                      gamma = 1., 
                      c = 0.,
                      b= -epsilon_sp, # should not be negative? LOOKS LIKE IT AL COMES DOWN TO THIS PARAM
                      beta = 0,
                      a = 0.)

Visualizing isolated node dynamics

The TVB code allows us to investigate isolated node dynamics relatively easy, without having to run full network simulations, using the stationary_trajectory method of model objects. Here is a simple example:

# <!-- collapse=False-->


# Run sims

# Specify model with Spiegler parameters
sp_g2do = models.Generic2dOscillator(**sp_g2do_params)
#sp_g2do.d = (1/1000.)*eta_sp

# Evolve system for 1000 sytems, step duration 0.1ms
time,dat = sp_g2do.stationary_trajectory(n_step=5000,dt=0.1)

# Put sim results into a pandas dataframe
V_dat = np.squeeze(dat[:,0,:,:])
W_dat = np.squeeze(dat[:,1,:,:])
df_VW = pd.DataFrame([V_dat,W_dat],
                     index=['V', 'W'], 
                     columns=time).T



# Make 2 quick plots:

f = outdir + '/pics/single_node_phase_plane_dynamics.png'

fig, ax = plt.subplots(ncols=2, figsize=(12,3))

# V and W vs. time
df_VW.plot(ax=ax[0])

# V vs. W trajectory
ymax = df_VW.W.max() + df_VW.W.std()*1.5
ymin = df_VW.W.min() - df_VW.W.std()*1.5
xmax = df_VW.V.max() + df_VW.V.std()*1.5
xmin = df_VW.V.min() - df_VW.V.std()*1.5
df_VW.plot(x='V', y='W', legend=False,ax=ax[1],
           xlim=[xmin,xmax],ylim=[ymin,ymax],alpha=0.5);



plt.savefig(f, bbox_inches='tight')
plt.close()


# Embed in LN
cap = ''; label = 'Single node phase plane dynamics'
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500))
d(im)
broken link Single node phase plane dynamics.

By default the stationary trajectory method, like the full simulation functions, will choose random initial values for the state variables, within a specified range. We can think of the transient at the start of the time series due to these initial conditions as a kind of stimulus.

If we make several of these plots as a function of the bifurcation parameter, manually specify the same initial conditions each time, and plot in 3D, we can get something close to the bifurcation diagram in Figure 2a of Spiegler et al.

f = outdir + '/pics/Spiegler_fig2a_replication.png'


# First run the model for the parameter values and 
# put into a pandas dataframe

bif_params = [-2 , -0.5 ,1.]# 1.]

res = {}

for b in bif_params:

  mod = models.Generic2dOscillator(**sp_g2do_params)
  mod.g = b

  initstim = np.array([2.,0.])[:,np.newaxis]
    
  time,dat = mod.stationary_trajectory(n_step=5000,dt=0.1,initial_conditions=initstim)
    
    
  V_dat = np.squeeze(dat[:,0,:,:])
  W_dat = np.squeeze(dat[:,1,:,:])
  df_VW = pd.DataFrame([V_dat,W_dat],
                       index=['V', 'W'], 
                       columns=time).T
  
  res[b] = df_VW
    

df_VWb = pd.concat(res);
df_VWb.index.names = ['g', 'time']


# Now make the 3D plot

# (note that matplotlib's 3d plotting doesn't get the order of the elements quite right
# (maybe use plotly or something to get a better version of that))

mpl.rcParams['legend.fontsize'] = 15;

fig = plt.figure();
ax = fig.gca(projection='3d');

for b_it,b in enumerate(bif_params):

  z = df_VWb.ix[b].W.values
  y = df_VWb.ix[b].V.values
  x = np.ones(y.shape[0])*(0.2*b_it)  

  ax.plot(x,y,z, label='$\gamma = %s $' %b);#,alpha=0.5,linewidth=0.5)

    
# Draw at thick black line through the first two
# ...and  a dashed black line through the third one
ax.plot([-0.1,0.3],[0,0],[0,0],c='k', alpha=0.3);
ax.plot([0.3,0.6],[0,0],[0,0],c='k', alpha=0.3, linestyle='--');


ax.view_init(30, 250); # angle)

#ax.axis('off')
ax.legend();
plt.show();


fig.savefig(f, bbox_inches='tight');
plt.close();
plt.clf();
clear_output();
# <!-- collapse=False-->

# Embed in LN
cap = ''; label = 'Spielger Fig 2a replication'
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500));
d(im)
broken link Spielger Fig 2a replication.

(Observant people will notice that the 3D view here isn't really necessary because the system is only evolving on a 2D manifold (V vs. W), and the bifurcation parameter isn't being changed continuously. So the 3D views are literally just a series of stacked snapshots of a 2D system)

Here are the corresponding time series plots, as per the bottom part of Fig. 2a in the paper.

# <!-- collapse=False-->


# Make fig
f = outdir + '/pics/Spiegler_fig2b_replication.png'

fig, ax = plt.subplots(ncols=3, figsize=(12,3))
for b_it, b in enumerate(bif_params):  df_VWb.ix[b].ix[:150].plot(ax=ax[b_it],ylim=[-5.5,5.5])    
plt.tight_layout() 

plt.savefig(f, bbox_inches='tight')
plt.close()


# Embed in LN
cap = ''; label = 'Spielger Fig 2b replication'
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500))
d(im)
broken link Spielger Fig 2b replication.

(Note that unlike the paper we're plotting both the fast and slow variables here. The plots in the paper correspond to just the 'V' variables here).

A word of caution when exploring these kind of patterns: sometimes it is a little tricky to tell, visually at least, whether the sytem is settling to a fixed point, or settling on a very small amplitude oscillation. In particular, things like the axis scales (esp. when looking at different regimes with vastly different amplitude ranges alongside each other), run time (beware very short runs), etc. can be misleading.

In the above examples we actually know that the critical point is exactly $\gamma=0$, and this can be verified analytically. But this is a rather special case.

Region-based simulations

Now we set up the full stimulated network simulations. Note that these are region-based (i.e. only 76 nodes), unlike the full 16000 node surface simulations done in Spiegler et al. Some (more restricted) explorations of surface simulations are described below.

Additionally, there are a number of other details of the methodology in Spiegler et al. that are yet to be added in here. Specifically, the following are still to do:

  • time-integrated scaling of connectivity weights
  • stimulation duration defined by characteristic time constant from $\eta$ parameter
  • stimulus amplitude dependence on $\eta$
  • cellular automaton
  • covariance matrix extraction

With that caveat, let's dive in:

Connectivity

As noted, Spiegler et al. do something funky with their connectivity weights that I haven't yet attempted to implement. So we simply use the default connectivity for now:

conn = connectivity.Connectivity(load_default=True)

Stimulus

In liu of the adaptive stimulation duration, here for now we just use a single short pulse, timed to come in after the initial transient has settled (1500ms).

First get the node indices for the region we want to stimulate:

stim_node_names = ['rPFCCL']
stim_node_inds = []
for s in stim_node_names:
  for r_it,r in enumerate(conn.region_labels): 
    if s == r: 
      stim_node_inds.append(r_it)
print stim_node_inds
[17]

Now plot the stimulus pattern

# <!-- collapse=False-->
f = outdir + '/pics/stimulus_pattern.png'

eqn_t = equations.PulseTrain()
eqn_t.parameters['onset'] = 1.5e3
eqn_t.parameters['T'] = 500.0
eqn_t.parameters['tau'] = 50.0

# configure stimulus spatial pattern
stim_weight = 0.1
weighting = np.zeros((76, ))
weighting[stim_node_inds] = stim_weight

stimulus = patterns.StimuliRegion(
    temporal=eqn_t,
    connectivity=conn,
    weight=weighting)


#Configure space and time
stimulus.configure_space()
stimulus.configure_time(np.arange(0., 2e3, 2**-4))

#And take a look
plot_pattern(stimulus)


plt.savefig(f, bbox_inches='tight')
plt.close()



# Embed figure
cap = ''; label = 'stimulus pattern'
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500))
d(im)
broken link stimulus pattern.

Run sim function

Now we define a function that runs a short simulation, with the flexibility of modifying various details such as connectivity, model, and stimulus parameters, as needed:

def run_sim(g2do_params,conn=None,
            sim_len = 2e3,dt = 0.1,
            tau = 2,lca=0.01,
            tavg_per = 1.0,
            stim_node_inds=[17],
            stim_eqn_onset =1.5e3,
            stim_eqn_T = 500.0,
            stim_eqn_tau = 50.0,
            stim_weight = 0.1):

  if conn == None:
    conn = connectivity.Connectivity(load_default=True)
    
  model = models.Generic2dOscillator(**g2do_params)
  cpl=coupling.Linear(a=lca)
  solver=integrators.HeunDeterministic(dt=dt)
  mons=(monitors.TemporalAverage(period=tavg_per),)
    
  eqn_t = equations.PulseTrain()
  eqn_t.parameters['onset'] = stim_eqn_onset  
  eqn_t.parameters['T'] = stim_eqn_T
  eqn_t.parameters['tau'] = stim_eqn_tau

  weighting = np.zeros((76, ))
  weighting[stim_node_inds] = stim_weight

  stimulus = patterns.StimuliRegion(temporal=eqn_t,
                                    connectivity=conn,
                                    weight=weighting)
  stimulus.configure_space()
  stimulus.configure_time(np.arange(0., 3e3, 2**-4))
    
  sim = simulator.Simulator(model = model,connectivity=conn,coupling=cpl,
                            integrator=solver, 
                            monitors=mons,
                            stimulus=stimulus)
  sim.configure()

  (tavg_time, tavg_data),  = sim.run(simulation_length=sim_len) 
    
  df_tavg = pd.DataFrame(np.squeeze(tavg_data),index=tavg_time)
  df_tavg.index.names = ['t']
  df_tavg.columns.names= ['node']
  df_tavg.columns = conn.region_labels    
    
  return df_tavg

Test runs

Now do some test runs with this function.

First, using the parameters identified as matching those of Spiegler et al:

# <!-- collapse=False-->


# Run simulation
df_tavg_sp1 = run_sim(sp_g2do_params,lca=0.1,sim_len=2e3)


# Make the figure
f = outdir + '/pics/df_tavg_sp1_ts.png'
fig, ax = plt.subplots(nrows=2, figsize=(12,6))
df_tavg_sp1.plot(legend=False,ax=ax[0])
df_tavg_sp1.ix[1450:1650].plot(legend=False,ax=ax[1])
plt.savefig(f, bbox_inches='tight')
plt.close()


# Embed in LN
cap = ''; label = ''
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500))
d(im)
broken link .

The top row in the above figure is the full simulation time series. You can see the stimulation as a little blip around 1500ms. The second row is zoomed in to 200ms around that blip point. In the second figure one can immediately see a problem with this run: there is very little effect of the stimulaion, outside the of the stimulated node - the blue one with the large inflections. That is, we aren't seeing much by way of propagation.

I'm not yet exactly sure of the reason for this, but in the meantime, I find we do get a nice stimation affect it we set the $\beta$ parameter in g2do to 1:

# <!-- collapse=False-->


# Run sim
ps = copy(sp_g2do_params) # copy cos we don't want to change beta in this dict
ps['beta'] = 1
df_tavg_sp2 = run_sim(ps,lca=0.1,sim_len=2e3)


# Make figure
f = outdir + '/pics/df_tavg_sp2_ts.png'
fig, ax = plt.subplots(nrows=2, figsize=(12,6))
df_tavg_sp2.plot(legend=False,ax=ax[0])
df_tavg_sp2.ix[1450:1650].plot(legend=False,ax=ax[1])
plt.savefig(f, bbox_inches='tight')
plt.close()

# Embed in LN
cap = ''; label = ''
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500))
d(im)
broken link .

You can see here now that in addition to the effect on the stimulated node, there is some additional activity above the baseline (pre-stimulus) level. We can't see that very clearly here, but that doesn't matter; we'll look in lots of detail later.

While we're at it, let's take a look at a few other modifications from the original spiegler parameters:

Spiegler params with e = 3

# <!-- collapse=False-->
f = outdir + '/pics/df_tavg_sp3_ts.png'


# Run simulation
sp = copy(sp_g2do_params)
sp['e'] = 3.
df_tavg_sp3 = run_sim(ps,lca=0.1,sim_len=2e3)


# Make figure
f = outdir + '/pics/df_tavg_sp3_ts.png'
fig, ax = plt.subplots(nrows=2, figsize=(12,6))
df_tavg_sp3.plot(legend=False,ax=ax[0])
df_tavg_sp3.ix[1450:1650].plot(legend=False,ax=ax[1])
plt.savefig(f, bbox_inches='tight')
plt.close()

# Embed in LN
cap = ''; label = ''
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500))
d(im)
broken link .

Spiegler params with beta = 1 and e = 3

# <!-- collapse=False-->


# Run sim

ps = copy(sp_g2do_params)
ps['beta'] = 1.
ps['e'] = 3.
df_tavg_sp4 = run_sim(ps,lca=0.1,sim_len=2e3)


#Make fig
f = outdir + '/pics/df_tavg_sp4_ts.png'
fig, ax = plt.subplots(nrows=2, figsize=(12,6))
df_tavg_sp4.plot(legend=False,ax=ax[0])
df_tavg_sp4.ix[1450:1650].plot(legend=False,ax=ax[1])
plt.savefig(f, bbox_inches='tight')
plt.close()

# Embed in LN`
cap = ''; label = ''
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500))
d(im)
broken link .

Heatmaps are a handy way of visualizing activity propagation, which help to remove the visual clutter of overlaid lines in the above plots.

You need to get the colour scaling right though; in this case we want to set it to within the range of the second, lower amplitude group of inflections in the above figures; something like -0.01 to 0.01. If we allow default colour scaling, all we will see is the big inflections of the stimulated node, and very little else.

# <!-- collapse=False-->

#Make fig
f = outdir + '/pics/df_tavg_sp2_heatmap.png'
fig, ax = plt.subplots(figsize=(12,12))
sns.heatmap(df_tavg_sp2.ix[1490:1650].T, ax=ax,
            vmin=-0.01,vmax=0.01,xticklabels='')
plt.savefig(f, bbox_inches='tight')
plt.close()

# Embed in LN`
cap = ''; label = ''
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500))
d(im)
broken link .

Another handy way of visualizing propagtion is on a brain surface.

# These are some visualization things necessary for the brain surface plots
ctx = cortex.Cortex.from_file(source_file = ctx_file,
                              region_mapping_file =rm_file)
vtx,tri,rm = ctx.vertices,ctx.triangles,ctx.region_mapping
conn = connectivity.Connectivity.from_file(conn_file); conn.configure()
isrh_reg = conn.is_right_hemisphere(range(conn.number_of_regions))
isrh_vtx = np.array([isrh_reg[r] for r in rm])
dat = conn.tract_lengths[:,0] # 'dat' is one column of the region-based tract lengths 
                              # connectivity matrix. The corresponding region is 
                              # right auditory cortex ('rA1')
# <!-- collapse=False-->
f = outdir + '/pics/df_tavg_sp2_brainsurf.png'


# Make fig
fig, ax = plt.subplots(ncols=10, nrows=2,figsize=(15,3))
cmap = cm.Reds
cmap.set_under(color='w')

kws = {'edgecolors': 'k', 'vmin': 0., 'cmap': cmap, 
       'vmax': 0.02, 'alpha': None, 'linewidth': 0.01}

ts = [1500.5, 1520.5,1540.5,1560.5,1580.5,1600.5,1650.5,1700.5,1750.5,1800.5]

for t_it,t in enumerate(ts):

  dat = df_tavg_sp2.ix[t].abs().values
    
  plot_surface_mpl(vtx=vtx,tri=tri,data=dat,rm=rm,ax=ax[0][t_it],
                   shade_kwargs=kws,
                   view='rh_lat')
    
  plot_surface_mpl(vtx=vtx,tri=tri,data=dat,rm=rm,ax=ax[1][t_it],
                   shade_kwargs=kws,
                   view='superior')
    
    
  ax[0][t_it].set_title('t=%1.1fms' %t)
    
        
plt.savefig(f, bbox_inches='tight')
plt.close()


# Embed in LN`
cap = ''; label = ''
im = nb_fig(f,label,cap,cnb,show_fignum=False,size=(800,500))
d(im)
broken link .

Time series envelopes and covariance matrix modes

in progress...

Run for all nodes

Now loop through and do this stimulating each node in the network in turn.

# Run sims
roi_names = conn.region_labels
roi_inds = range(roi_names.shape[0])

stim_roisims = {}
sp = copy(sp_g2do_params)
sp['beta'] = 1.

for roi_name,roi_ind in zip(roi_names,roi_inds):
    
  df = run_sim(sp,stim_node_inds = [roi_ind],
               lca=0.1,sim_len=2000)
  stim_roisims[roi_name] = df
    
    
# Collect into a single dataframe
df_stimroisims = pd.concat(stim_roisims)
df_stimroisims.index.names = ['stim_roi', 't']
df_stimroisims.columns.names = ['roi']

# Same to file
df_stimroisims.to_pickle(outdir + '/df_stimroisims.pkl')

Surface-based simulations

in progress...