In this demo we show how to use LIMIX to build genetic models and perform different anlysis of single and multiple traits.
We consider a yeast dataset (Smith et al, 2008, PLoS Biology)
consisting of 109 individuals with 2,956 marker SNPs and expression levels for 5,493 in glucose and ethanol growth media respectively.
Such a dataset is ideal to showcase LIMIX and multiple trait analysis,
containing two different axis of multi-trait variation
(multiple genes and multiple environments).
In particular, in these demos,
we focus on the subset of the 11 genes in the Lysine Biosynthesis KEEG pathway considered in the paper.

We start discussing GWAS, multi locus models
and variance decomposition models for single trait analysis and then
we show how there models are extended for joint analysis of multiple traits.
Then we go on to discuss how the prediction tool made available by LIMIX
can be used to monitor overfitting and perform model selection.
Finally, we show how LIMIX can be used to build model that infer and account for hidden confounders
from high-dimensional phenotype data both in single trait and multi trait analysis.

# Setting up

```
%matplotlib inline
#%connect_info
```

```
import scipy as SP
import pylab as PL
from matplotlib import cm
import h5py
import pdb
import pandas as pd
SP.random.seed(0)
```

```
# import limix
import sys
sys.path.append('./../build/release.darwin/interfaces/python')
import limix.modules.varianceDecomposition as VAR
import limix.modules.qtl as QTL
import limix.modules.data as DATA
import limix.modules.genotype_reader as gr
import limix.modules.phenotype_reader as phr
import limix.modules.data_util as data_util
```

```
# temporary
from limix.utils.util import *
from limix.utils.plot import plot_manhattan
```

```
#genes from lysine biosynthesis pathway
lysine_group = ['YIL094C', 'YDL182W', 'YDL131W', 'YER052C', 'YBR115C', 'YDR158W',
                'YNR050C', 'YJR139C', 'YIR034C', 'YGL202W', 'YDR234W']
```

```
#import data
file_name = '../tutorials/data/smith_2008/smith08.hdf5'
geno_reader  = gr.genotype_reader_tables(file_name)
pheno_reader = phr.pheno_reader_tables(file_name)

data = DATA.QTLData(geno_reader=geno_reader,pheno_reader=pheno_reader)
#getting genotypes
snps = data.getGenotypes() #SNPs
position = data.getPos()
chromBounds = data_util.estCumPos(position=position,offset=100000)

# non-normalized and normalized sample relatedeness matrix
sample_relatedness_unnormalized = data.getCovariance(normalize=False)
sample_relatedness  = sample_relatedness_unnormalized/sample_relatedness_unnormalized.diagonal().mean()
```

```
# plot sample relatedeness matrix
plt = PL.subplot(1,1,1)
PL.title('genetic kinship')
PL.imshow(sample_relatedness,vmin=0,vmax=1,interpolation='none',cmap=cm.afmhot)
PL.colorbar(ticks=[0,0.5,1],orientation='horizontal')
plt.set_xticks([])
plt.set_yticks([])
```

# Single Trait Analysis

## Single Locus Analysis

### The Genetic Model

Indicating with $N$ the number of samples,
the standard LMM considered by LIMIX is
\begin{equation}
\mathbf{y} = \mathbf{F}\boldsymbol{\alpha} + \mathbf{x}\beta + \mathbf{g}+\boldsymbol{\psi},\;\;\;
\mathbf{g}\sim\mathcal{N}\left(\mathbf{0},\sigma_g^2\mathbf{R}\right),\;
\boldsymbol{\psi}\sim\mathcal{N}\left(\mathbf{0},\sigma_e^2\mathbf{I}_N\right)
\end{equation}
where
\begin{eqnarray}
\mathbf{y}   &=& \text{phenotype vector} \in \mathcal{R}^{N,1} \\
\mathbf{F}   &=& \text{matrix of $K$ covariates} \in \mathcal{R}^{N,K} \\
\boldsymbol{\alpha} &=& \text{effect of covariates} \in \mathcal{R}^{K,1} \\
\mathbf{x}   &=& \text{genetic profile of the SNP being tested} \in \mathcal{R}^{N,1} \\
\boldsymbol{\beta}   &=& \text{effect size of the SNP} \in \mathcal{R} \\
\mathbf{R}   &=& \text{sample relatedeness matrix} \in \mathcal{R}^{N,N} \\
\end{eqnarray}
Association between phenotypic changes and the genetic markers is tested by testing $\beta\neq0$.

The model can be rewritten using the delta-representation
\begin{equation}
\mathbf{y}\sim
\mathcal{N}\left(\mathbf{W}\boldsymbol{\alpha}+\mathbf{x}\beta,
\sigma_g^2\left(\mathbf{R}+\delta\mathbf{I}\right)\right)
\end{equation}
where $\delta={\sigma_e^2}/{\sigma_g^2}$ is the signal-to-noise ratio.
and is refitted SNP-by-SNP during the scan.

### Example: Single-trait eQTL mapping for gene YBR115C in environment 0

```
#create a complex query on the gene_ID and environment:
# select environment 0 for gene YBR115C
phenotype_query = "(gene_ID=='YBR115C') & (environment==0)"

# getting data
data_subsample = data.subsample_phenotypes(phenotype_query=phenotype_query,intersection=True)

#get variables we need from data
snps = data_subsample.getGenotypes(impute_missing=True)
phenotypes,sample_idx = data_subsample.getPhenotypes(phenotype_query=phenotype_query,intersection=True); assert sample_idx.all()

#set parameters for the analysis
N, P = phenotypes.shape

covs = None                 #covariates
searchDelta = False         #specify if delta should be optimized for each SNP
test="lrt"                  #specify type of statistical test

# Running the analysis
# when cov are not set (None), LIMIX considers an intercept (covs=SP.ones((N,1)))
lmm = QTL.test_lmm(snps=snps,pheno=phenotypes.values,K=sample_relatedness,covs=covs, test=test)

pvalues = lmm.getPv()       # 1xS vector of p-values (S=X.shape[1])
#convert P-values to a DataFrame for nice output writing:
pvalues = pd.DataFrame(data=pvalues.T,index=data_subsample.geno_ID,columns=phenotypes.columns)
pvalues = pd.concat([position,pvalues],join="outer",axis=1)

betas = lmm.getBetaSNP()    # 1xS vector of effect sizes (S=X.shape[1])
#convert betas to a DataFrame for nice output writing:
betas = pd.DataFrame(data=betas.T,index=data_subsample.geno_ID,columns=phenotypes.columns)
betas = pd.concat([position,pvalues],join="outer",axis=1)
```

    finished GWAS testing in 0.59 seconds

```
plt = PL.subplot(2,1,1)
plot_manhattan(plt,pvalues['pos_cum'],pvalues['YBR115C:0'],chromBounds)
```

### Example: Single-trait eQTL mapping for all genes in the pathways in environment 0

```
#create a complex query on the gene_ID and environment:
# select environment 0 for all genes in lysine_group
phenotype_query = "(gene_ID in %s) & (environment==0)" % str(lysine_group)

data_subsample = data.subsample_phenotypes(phenotype_query=phenotype_query,intersection=True)

#get variables we need from data
snps = data_subsample.getGenotypes(impute_missing=True)
phenotypes,sample_idx = data_subsample.getPhenotypes(phenotype_query=phenotype_query,intersection=True); assert sample_idx.all()

sample_relatedness = data_subsample.getCovariance()
position = data_subsample.getPos()

#set parameters for the analysis
N, G = phenotypes.shape

covs = None                 #covariates
searchDelta = False         #specify if delta should be optimized for each SNP
test="lrt"                  #specify type of statistical test

# Running the analysis
# when cov are not set (None), LIMIX considers an intercept (covs=SP.ones((N,1)))
lmm = QTL.test_lmm(snps=snps,pheno=phenotypes.values,K=sample_relatedness,covs=covs,test=test)

# get p-values
pvalues = lmm.getPv()       # 1xS vector of p-values (S=X.shape[1])
#convert P-values to a DataFrame for nice output writing:
pvalues = pd.DataFrame(data=pvalues.T,index=data_subsample.geno_ID,columns=phenotypes.columns)
pvalues = pd.concat([position,pvalues],join="outer",axis=1)

betas = lmm.getBetaSNP()    # 1xS vector of effect sizes (S=X.shape[1])
#convert betas to a DataFrame for nice output writing:
betas = pd.DataFrame(data=betas.T,index=data_subsample.geno_ID,columns=phenotypes.columns)
betas = pd.concat([position,pvalues],join="outer",axis=1)
```

    finished GWAS testing in 4.91 seconds

```
# plotting all manhattan plots
PL.figure(1,figsize=(10,16))
lim = -1.2*SP.log10(pv.min())
for g in range(G):
    plt = PL.subplot(G,2,g+1)
    PL.title(lysine_group[g])
    if g!=G-1:   xticklabels = False
    else:        xticklabels = True
    plot_manhattan(plt,pvalues['pos_cum'],pvalues[lysine_group[g]+":0"],chromBounds,lim=lim,xticklabels=xticklabels)
PL.tight_layout()
```

```
# plotting minimum pv across genes
pv_min = lmm.getPv().min(0)
plt = PL.subplot(2,1,1)
plot_manhattan(plt,pvalues['pos_cum'],pv_min,chromBounds)
```

## Multi locus Single Trait Model

### The Genetic Model

The model LIMIX emplys for standard GWAS can be extended to include multiple loci by step-wise forward selection.
This is done by performing the following two steps:
- a genome-wide scan is performed using the standard linear mixed model
- if the most associated marker has P-value lower than a certain threshold
the marker is added as covariate and the algorithm return to step 1, otherwise it stops here.

LIMIX allows the user to set
- a threshold either over the P-value or the Q-value of the most associated SNP as stopping criterion;
- a maximum number of iterations as stopping criterion;

### Example: Single-trait eQTL mapping for gene YBR115C in environment 0

```
# getting data
#create a complex query on the gene_ID and environment:
# select environment 0 for gene YBR115C
phenotype_query = "(gene_ID=='YBR115C') & (environment==0)"

data_subsample = data.subsample_phenotypes(phenotype_query=phenotype_query,intersection=True)

#get variables we need from data
snps = data_subsample.getGenotypes(impute_missing=True)
phenotypes,sample_idx = data_subsample.getPhenotypes(phenotype_query=phenotype_query,intersection=True); assert sample_idx.all()

sample_relatedness = data_subsample.getCovariance()
pos = data_subsample.getPos()

#set parameters for the analysis
N, G = phenotypes.shape

covs = None                 #covariates
searchDelta = False         #specify if delta should be optimized for each SNP
test="lrt"                  #specify type of statistical test

# Running the analysis
threshold = 0.01/snps.shape[1] # inclusion threshold on pvalues (Bonferroni corrected significance level of 1%)
maxiter = 10                # maximum number of iterations
lmm, RV = QTL.forward_lmm(snps,phenotypes.values,K=sample_relatedness,qvalues=False,threshold=threshold,maxiter=maxiter)
# RV contains:
# - iadded:   array of indices of SNPs included in order of inclusion
# - pvadded:  array of Pvalues obtained by the included SNPs in iteration before inclusion
# - pvall:   [maxiter x S] SP.array of Pvalues for all iterations
```

    finished GWAS testing in 0.63 seconds
    finished GWAS testing in 0.47 seconds

```
# TODO: plotting
```

## Variance Component Models

Here we show how the LIMIX variance decomposition module
can be used in genetic anlysis of variance for single trait analysis.

### The Genetic Model

The model considered by the LIMIX variance decomposition module is an extension of the genetic model
employed in standard GWAS in to include different random effects terms:
\begin{equation}
\mathbf{y} = \mathbf{F}\boldsymbol{\alpha} + \sum_{x}\mathbf{u}^{(x)} + \boldsymbol{\psi},\;\;\;\;
\mathbf{u}^{(x)}\sim\mathcal{N}\left(\mathbf{0},{\sigma^{(x)}}^2\mathbf{R}^{(x)}\right),\;
\boldsymbol{\Psi}\sim\mathcal{N}\left(\mathbf{0},\sigma_e^2\mathbf{I}_N\right)
\end{equation}
where
\begin{eqnarray}
\mathbf{y}   &=& \text{phenotype vector} \in \mathcal{R}^{N,1} \\
\mathbf{F}   &=& \text{matrix of $K$ covariates} \in \mathcal{R}^{N,K} \\
\boldsymbol{\alpha} &=& \text{effect of covariates} \in \mathcal{R}^{K,1} \\
\mathbf{R}^{(x)}   &=& \text{is the sample covariance matrix for contribution $x$} \in \mathcal{R}^{N,N} \\
\end{eqnarray}

If $\mathbf{R}$ is a genetic contribution from set of SNPs $\mathcal{S}$,
with a bit of abuse of notation we can write
$\mathbf{R}= \frac{1}{C}\mathbf{X}_{:,\,\mathcal{S}}{\mathbf{X}_{:,\,\mathcal{S}}}^T$
where $C=\frac{1}{N}\text{trace}\left(\mathbf{X}_{:,\,\mathcal{S}_i}{\mathbf{X}_{:,\,\mathcal{S}_i}}^T\right)$.

LIMIX allows the user to flexibly build the model by adding fixed and random effect terms.

### Example: Cis/Trans Variance Decomposition in environment 0

Here we use the LIMIX variance decomposition module to quantify the variability
in gene expression explained by proximal (cis) and distal (trans) genetic variation.
To do so, we build a linear mixed model with a fixed effect intercept,
two random effects for cis and trans genetic effects and a noise random effect:

\begin{equation}
\mathbf{y} = \mathbf{1}_N\mu + \mathbf{u}^{(cis)} + \mathbf{u}^{(trans)} + \boldsymbol{\psi},\;\;\;\;
\mathbf{u}^{(cis)}\sim\mathcal{N}\left(\mathbf{0},{\sigma^{(x)}}^2\mathbf{R}^{(cis)}\right),\;
\mathbf{u}^{(trans)}\sim\mathcal{N}\left(\mathbf{0},{\sigma^{(x)}}^2\mathbf{R}^{(trans)}\right),\;
\boldsymbol{\Psi}\sim\mathcal{N}\left(\mathbf{0},\sigma_e^2\mathbf{I}_N\right)
\end{equation}

where $\mathbf{R}^\text{(cis)}$ and $\mathbf{R}^\text{(trans)}$ are the local and distal relatedeness matrices,
built considering all SNPs in cis and trans (i.e., not in cis) respectively.
As cis region we considered a window of 500kb downstream and upstream the gene.

The gene-model is fitted to gene expression in environment 0 for
all genes in the Lysine Biosynthesis pathway and variance components are averaged thereafter to obtain pathway
based variance components.

```
# loop over genes and fit variance component model
var = []
genes_analyzed = []
K_all = data.getCovariance(normalize=False)

for gene_idx,gene in enumerate(lysine_group):
    #create a complex query on the gene_ID and environment:
    # select environment 0 for all genes in lysine_group
    phenotype_query = "(gene_ID=='%s') & (environment==0)" % gene
    print "running variance decomposition on %s" % phenotype_query
    data_subsample = data.subsample_phenotypes(phenotype_query=phenotype_query,intersection=True)

    #get variables we need from data
    phenotypes,sample_idx = data_subsample.getPhenotypes(phenotype_query=phenotype_query,intersection=True); assert sample_idx.all()
    if phenotypes.shape[1]==0:
        continue
    #snps = data_subsample.getGenotypes(impute_missing=True)
    pos_gene = data_subsample.pheno_reader.get_pos(phenotype_query=phenotype_query)
    K_cis = data_subsample.getCovariance(normalize=False,pos_start=pos_gene["start"][0],pos_end=pos_gene["end"][0],windowsize=5e5)
    K_trans = K_all-K_cis
    K_cis /= K_cis.diagonal().mean()
    K_trans /= K_trans.diagonal().mean()

    # variance component model
    vc = VAR.VarianceDecomposition(phenotypes.values)
    vc.addFixedEffect()
    vc.addRandomEffect(K=K_cis)
    vc.addRandomEffect(K=K_trans)
    vc.addRandomEffect(is_noise=True)
    vc.optimize()

    # get variances
    # vc.getVariances() returs a vector of variances explained
    # by the three random effects in order of addition (cis,trans,noise)
    _var = vc.getVarianceComps()

    var.append(_var)
    genes_analyzed.append(gene)
# concatenate in a unique matrix
var = SP.concatenate(var)
```

    running variance decomposition on (gene_ID=='YIL094C') & (environment==0)
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YDL182W') & (environment==0)
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YDL131W') & (environment==0)
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YER052C') & (environment==0)
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YBR115C') & (environment==0)
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YDR158W') & (environment==0)
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YNR050C') & (environment==0)
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YJR139C') & (environment==0)
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YIR034C') & (environment==0)
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YGL202W') & (environment==0)
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YDR234W') & (environment==0)
    Local minimum found in run 1 out of 10

```
#normalize variance component and average
var/=var.sum(1)[:,SP.newaxis]
var_mean = var.mean(0)
column_labels = ["cis","trans","noise"]
colors = ['Green','MediumBlue','Gray']
PL.pie(var_mean,labels=column_labels,autopct='%1.1f%%',colors=colors,shadow=True, startangle=0)
#convert var to a DataFrame for nice output writing:
var = pd.DataFrame(data=var,index=genes_analyzed,columns=column_labels)

#convert betas to a DataFrame for nice output writing:
var_mean = pd.DataFrame(data=var_mean[SP.newaxis,:],index=["lysine_biosynthesis"],columns=column_labels)
```

# Multi Trait Analysis

Denoting with $N$ and $P$ the number of samples and phenotypes in the analysis,
LIMIX models the $N \times P$ matrix-variate phenotype $\mathbf{Y}$
as a sum of fixed and random effects in a multivariate mixed model framework.

Single-trait models only consider the across-sample axis of variation and thus
- fixed effects need only specification of a sample design (denoted by $\mathbf{F}$ so far)
- random effects need only specification of a sample covariance matrix (denoted by $\mathbf{R}$ so far),
which describes the correlation across samples induced by the random effect

Conversely, multi-trait models also consider the across-trait axis of variation and thus
- fixed effects also require a sample design (denoted with $\mathbf{A}$ in the following)
- random effects also require a trait covariance matrix (denoted with $\mathbf{C}$ in the following),
which describes the correlation across traits induced by the random effect

Different forms of trait designs for fixed effects and trait covariances for random effects
reflect different hypothesis on how these contributions affect phenotypes
(e.g., shared and trait-specific effects).
In the following, we will show how LIMIX can be used for different analysis of multiple traits
wisely choosing trait designs and covariances.

We decided to start with multi-trait variance components models before going to
multi-trait GWAS analysis, including single-locus and multi-locus analysis.
In the next section we will be discussing more subtle problems as regularization and model selection.

## Variance decomposition

### Genetic Model

The general multi-trait linear mixed models considered in the LIMIX variance decomposition module can be written as
\begin{equation}
\mathbf{Y} = \sum_i\mathbf{F}_i\boldsymbol{W}_i\boldsymbol{A}^\text{(cov)}_i + \sum_{x}\mathbf{U}^{(x)} + \boldsymbol{\Psi},\;\;\;\;
\mathbf{U}^{(x)}\sim\text{MVN}\left(\mathbf{0},\mathbf{R}^{(x)},\mathbf{C}^{(x)}\right),\;
\boldsymbol{\Psi}\sim\text{MVN}\left(\mathbf{0},\mathbf{I}_N,\mathbf{C}^\text{(noise)}\right)
\end{equation}
where
\begin{eqnarray}
\mathbf{Y}   &=& \text{phenotype vector} \in \mathcal{R}^{N,P} \\
\mathbf{F}_i   &=& \text{sample designs for fixed effects} \in \mathcal{R}^{N,K} \\
\mathbf{W}_i   &=& \text{effect size matrix of covariates} \in \mathcal{R}^{K,L} \\
\mathbf{A}^\text{(cov)} &=& \text{trait design for the fixed effect} \in \mathcal{R}^{L,P} \\
\mathbf{R}^{(x)}   &=& \text{sample covariance matrix for contribution $x$} \in \mathcal{R}^{N,N} \\
\mathbf{C}^{(x)}   &=& \text{trait covariance matrix for contribution $x$} \in \mathcal{R}^{P,P} \\
\mathbf{C}^\text{(noise)}   &=& \text{residual trait covariance matrix} \in \mathcal{R}^{P,P} \\
\end{eqnarray}

The variance decomposition module in LIMIX allows the user to build the model by adding any number of fixed and random effects, specify different designs for fixed effects and choose covariance models for random effects. In case of only two random effects LIMIX uses speedups beaking the $O(N^3P^3)$ complexity in $O(N^3+P^3)$.

In the following we show two different example.
In the first example we construct interpretable trait covariances for random effects and build
a model that dissects gene expression variability across environmental and cis and trans genetic effects,
and their interactions (cis GxE, trans GxE).
In the second example we show how to use the fast implementation for a two-random-effect model,
one for genetic effects and the other for residuals.
Such a model is the natural extention to multi-trait analysis of the null model in single-trait GWAS
and it is the first step of any multi-trait GWAS analysis performed in LIMIX.

### Example 1: GxE variance decomposition for Lysine Biosynthesis

Indicating with $N$ and $E=2$ respectively the number of samples and the number of environments,
we considered for each gene a model consisting of
an envoronment-specific intercept term,
a random effect for cis genetic effects,
a random effect for trans genetic effects,
and a noise random effect term.

The model can be written
\begin{equation}
\mathbf{Y}=\mathbf{FWA}+\mathbf{U}^\text{(cis)}+\mathbf{U}^\text{(trans)}+\boldsymbol{\Psi},\;\;\;\;\;
\mathbf{U}^\text{(cis)}\sim\text{MVN}(\mathbf{0},\mathbf{R}^\text{(cis)},\mathbf{C}^\text{(cis)}),\;
\mathbf{U}^\text{(trans)}\sim\text{MVN}(\mathbf{0},\mathbf{R}^\text{(trans)},\mathbf{C}^\text{(trans)}),\;
\boldsymbol{\Psi}\sim\text{MVN}(\mathbf{0},\mathbf{I}_N,\mathbf{C}_n),
\end{equation}
where $\mathbf{R}^\text{(cis)}$ and $\mathbf{R}^\text{(trans)}$ are the local and distal kinships,
built considering all SNPs in cis and trans (i.e., not in cis) respectively.
As in the single-trait analysis, we considered as cis region
the region of 500kb downstream and upstream the gene.

To dissect persistent and specific variance compoents we considered a 'block+diagonal' form for $C^{(x)}$:
\begin{equation}
C^{(x)} =
{a^{(x)}}^2
\left[
\begin{array}{cc}
1 & 1\\
1 & 1
\end{array}
\right]
+
\left[
\begin{array}{cc}
{c^{(x)}_1}^2 & 0\\
0 & {c^{(x)}_2}^2
\end{array}
\right]
=
\left[
\begin{array}{cc}
{a^{(x)}}^2+{c^{(x)}_1}^2 & {a^{(x)}}^2\\
{a^{(x)}}^2 & {a^{(x)}}^2+{c^{(x)}_2}^2
\end{array}
\right]
\end{equation}
Variance of persistent and specific (GxE) effects from term $x$ are ${a^{(x)}}^2$ and $\frac{1}{2}\left({c_1^{(x)}}^2+{c_2^{(x)}}^2\right)$ while the variance explained by pure environmental shift can be calculated as $\text{var}\left(W\right)$.

```
# loop over genes and fit variance component model
var = []
genes_analyzed = []
K_all = data.getCovariance(normalize=False)

for gene_idx,gene in enumerate(lysine_group):
    #create a complex query on the gene_ID and environment:
    # select (all environments for) the current gene in lysine_group
    phenotype_query = "(gene_ID=='%s')" % gene
    print "running variance decomposition on %s" % phenotype_query
    data_subsample = data.subsample_phenotypes(phenotype_query=phenotype_query,intersection=True)

    #get variables we need from data
    phenotypes,sample_idx = data_subsample.getPhenotypes(phenotype_query=phenotype_query,intersection=True); assert sample_idx.all()
    if phenotypes.shape[1]==0:#gene not found
        continue
    #snps = data_subsample.getGenotypes(impute_missing=True)
    pos_gene = data_subsample.pheno_reader.get_pos(phenotype_query=phenotype_query)
    K_cis = data_subsample.getCovariance(normalize=False,pos_start=pos_gene["start"][0],pos_end=pos_gene["end"][0],windowsize=5e5)
    K_trans = K_all-K_cis
    K_cis /= K_cis.diagonal().mean()
    K_trans /= K_trans.diagonal().mean()

    # variance component model
    vc = VAR.VarianceDecomposition(phenotypes.values)
    vc.addFixedEffect()
    vc.addRandomEffect(K=K_cis,trait_covar_type='block_diag')
    vc.addRandomEffect(K=K_trans,trait_covar_type='block_diag')
    vc.addRandomEffect(is_noise=True,trait_covar_type='block_diag')
    vc.optimize()

    # environmental variance component
    vEnv = vc.getWeights().var()
    # cis and cisGxE variance components
    Ccis  = vc.getTraitCovar(0)
    a2cis = Ccis[0,1]
    c2cis = Ccis.diagonal().mean()-a2cis
    # trans and cisGxE variance components
    Ctrans  = vc.getTraitCovar(1)
    a2trans = Ctrans[0,1]
    c2trans = Ctrans.diagonal().mean()-a2trans
    # noise variance components
    Cnois = vc.getTraitCovar(2)
    vNois = Cnois.diagonal().mean()

    # Append
    var.append(SP.array([[vEnv,a2cis,c2cis,a2trans,c2trans,vNois]]))
    genes_analyzed.append(gene)
# concatenate in a unique matrix
var = SP.concatenate(var)
```

    running variance decomposition on (gene_ID=='YIL094C')
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YDL182W')
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YDL131W')
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YER052C')
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YBR115C')
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YDR158W')
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YNR050C')
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YJR139C')
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YIR034C')
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YGL202W')
    Local minimum found in run 1 out of 10
    running variance decomposition on (gene_ID=='YDR234W')
    Local minimum found in run 1 out of 10

```
#normalize variance component and average
var/=var.sum(1)[:,SP.newaxis]
var_mean = var.mean(0)
labels = ['env','cis','cisGxE','trans','transGxE','noise']
colors = ['Gold','DarkGreen','YellowGreen','DarkBlue','RoyalBlue','Gray']
PL.pie(var_mean,labels=labels,autopct='%1.1f%%',colors=colors,shadow=True, startangle=0)
```

### Example 2: fast variance decomposition

In this example we consider a model with only two random effects: one genetic and a noise term
and show how to perform fast inference to learn the trait covariance matrices.

We apply the model to the expression in environoment 0 of the
11 genes of the lysine biosynthesis pathway.

```
#create a complex query on the gene_ID and environment:
# select environment 0 for all genes in lysine_group
phenotype_query = "(gene_ID in %s) & (environment==0)" % str(lysine_group)

data_subsample = data.subsample_phenotypes(phenotype_query=phenotype_query,intersection=True)

#get variables we need from data
phenotypes,sample_idx = data_subsample.getPhenotypes(phenotype_query=phenotype_query,intersection=True); assert sample_idx.all()

sample_relatedness = data_subsample.getCovariance()
pos = data_subsample.getPos()

#set parameters for the analysis
N, G = phenotypes.shape

# variance component model
vc = VAR.VarianceDecomposition(phenotypes.values)
vc.addFixedEffect()
vc.addRandomEffect(K=sample_relatedness,trait_covar_type='lowrank_diag',rank=4)
vc.addRandomEffect(is_noise=True,trait_covar_type='lowrank_diag',rank=4)
vc.optimize()
# retrieve geno and noise covariance matrix
Cg = vc.getTraitCovar(0)
Cn = vc.getTraitCovar(1)
```

    Local minimum found in run 1 out of 10
    Local minimum found in run 1 out of 10

```
# plot trait covariance matrices
plt = PL.subplot(1,2,1)
PL.title('Geno trait covariance')
PL.imshow(Cg,vmin=-0.2,vmax=0.8,interpolation='none',cmap=cm.afmhot)
PL.colorbar(ticks=[-0.2,0.,0.4,0.8],orientation='horizontal')
plt.set_xticks([])
plt.set_yticks([])
plt = PL.subplot(1,2,2)
PL.title('Noise trait covariance')
PL.imshow(Cn,vmin=-0.2,vmax=0.8,interpolation='none',cmap=cm.afmhot)
PL.colorbar(ticks=[-0.2,0.,0.4,0.8],orientation='horizontal')
plt.set_xticks([])
plt.set_yticks([])
```

## Multi Trait GWAS

The genetic model considered by LIMIX for multivariate GWAS consists of
fixed effects for covariates,
a fixed effect for the SNP being tested,
a random effect for the polygenic effect
and a noisy random effect term.

The model can be written
\begin{equation}
\mathbf{Y}=
\sum_i\mathbf{F}_i\boldsymbol{W}_i\boldsymbol{A}^\text{(cov)}_i
+\mathbf{XBA}_1^\text{(snp)}+\mathbf{G}+\boldsymbol{\Psi},\;\;\;\;\;
\mathbf{G}\sim\text{MVN}(\mathbf{0},\mathbf{K},\mathbf{C}_g),\;
\boldsymbol{\Psi}\sim\text{MVN}(\mathbf{0},\mathbf{I}_N,\mathbf{C}_n),
\end{equation}
where
\begin{eqnarray}
\mathbf{Y}   &=& \text{matrix-variate phenotype} \in \mathcal{R}^{N,P} \\
\mathbf{F_i}   &=& \text{sample design for covariates} \in \mathcal{R}^{N,K} \\
\mathbf{W_i}   &=& \text{effect size matrix of covariates} \in \mathcal{R}^{K,L} \\
\mathbf{A_i}^\text{(cov)} &=& \text{trait design for the fixed effect} \in \mathcal{R}^{L,P} \\
\mathbf{X}   &=& \text{genetic profile of the SNP} \in \mathcal{R}^{N,1} \\
\mathbf{B}   &=& \text{effect sizes of the SNP} \in \mathcal{R}^{1,M} \\
\mathbf{A}_1^\text{(snp)} &=& \text{trait design for the fixed effect} \in \mathcal{R}^{M,P} \\
\mathbf{K}   &=& \text{sample relatedeness matrix} \in \mathcal{R}^{N,N} \\
\mathbf{C}_g &=& \text{genetic trait covariance matrix} \in \mathcal{R}^{P,P} \\
\mathbf{I}_N &=& \text{sample noise matrix} \in \mathcal{R}^{N,N} \\
\mathbf{C}_n &=& \text{noise trait covariance matrix} \in \mathcal{R}^{P,P}
\end{eqnarray}

Prior to any multi-trait GWAS analysis,
LIMIX learns the trait covariates matrices on the mixed model without the fixed effect from the SNP.
However, LIMIX allows the user to set refitting of the signal-to-noise ratio $\delta$ SNP-by-SNP during the GWAS
as in single-trait analysis.

In general, LIMIX tests for particular trait designs of the fixed effect
$\mathbf{A}_1^\text{(snp)}\neq\mathbf{A}_0^\text{(snp)}$.
As shown extensively below, specifying opportunately $\mathbf{A}_1^\text{(snp)}$ and $\mathbf{A}_0^\text{(snp)}$
different biological hypothesis can be tested.

### Any effect test

Association between any of the phenotypes and the genetic marker can be tested by setting
\begin{equation}
\mathbf{A}_1^\text{(snp)} = \mathbf{I}_P,\;\;\;
\mathbf{A}_0^\text{(snp)} = \mathbf{0}
\end{equation}
This is a $P$ degrees of freedom test.

In this context, multi-trait modelling is considered just to empower detection
of genetic loci while there is no interest in the specific design of the association.

#### Example: any effect test for gene YJR139C across environments

Here we test for genetic effects on expression of one gene
either in environment 0 or in environment 1.

```
# Get data
# select all environmens for gene YJR139C in lysine_group
phenotype_query = "(gene_ID == 'YJR139C')"

data_subsample = data.subsample_phenotypes(phenotype_query=phenotype_query,intersection=True)

#get variables we need from data
snps = data_subsample.getGenotypes(impute_missing=True)
phenotypes,sample_idx = data_subsample.getPhenotypes(phenotype_query=phenotype_query,intersection=True); assert sample_idx.all()

sample_relatedness = data_subsample.getCovariance()
pos = data_subsample.getPos()

#set parameters for the analysis
N, P = phenotypes.shape

covs = None                 #covariates
Acovs = None                #the design matrix for the covariates
Asnps = SP.eye(P)           #the design matrix for the SNPs
K1r = sample_relatedness    #the first sample-sample covariance matrix (non-noise)
K2r = SP.eye(N)             #the second sample-sample covariance matrix (noise)
K1c = None                  #the first phenotype-phenotype covariance matrix (non-noise)
K2c = None                  #the second phenotype-phenotype covariance matrix (noise)
covar_type = 'freeform'     #the type of covariance matrix to be estimated for unspecified covariances
searchDelta = False         #specify if delta should be optimized for each SNP
test="lrt"                  #specify type of statistical test

# Running the analysis
# when cov are not set (None), LIMIX considers an intercept (covs=SP.ones((N,1)))
lmm, pvalues = QTL.test_lmm_kronecker(snps,phenotypes.values,covs=covs,Acovs=Acovs,Asnps=Asnps,K1r=K1r,trait_covar_type=covar_type)

#convert P-values to a DataFrame for nice output writing:
pvalues = pd.DataFrame(data=pvalues.T,index=data_subsample.geno_ID,columns=['YJR139C'])
pvalues = pd.concat([pos,pvalues],join="outer",axis=1)
```

    .. Training the backgrond covariance with a GP model
    Local minimum found in run 1 out of 10
    Background model trained in 0.55 s

REMARKS:
- kronecker_lmm always considers Asnp0=0
thus only Asnp1 needs to be specified
- if trait covariance matrices (arguments K1c and K2c are not specified)
kronecker_lmm implements the variance decomposition module to estimate them.
In this case, the user can specify the trait covariance model to considered (covar_type and rank).

```
plt = PL.subplot(2,1,1)
plot_manhattan(plt,pvalues['pos_cum'],pvalues['YJR139C'],chromBounds)
```

### Common effect test

A common effect test is a 1 degree of freedom test and can be done by setting
\begin{equation}
\mathbf{A}_1^\text{(snp)} = \mathbf{1}_{1,P},\;\;\;
\mathbf{A}_0^\text{(snp)} = \mathbf{0}
\end{equation}

#### Example: common effect test for gene YJR139C across environments

```
# Get data
# select all environmens for gene YJR139C in lysine_group
phenotype_query = "(gene_ID == 'YJR139C')"

data_subsample = data.subsample_phenotypes(phenotype_query=phenotype_query,intersection=True)

#get variables we need from data
snps = data_subsample.getGenotypes(impute_missing=True)
phenotypes,sample_idx = data_subsample.getPhenotypes(phenotype_query=phenotype_query,intersection=True); assert sample_idx.all()

sample_relatedness = data_subsample.getCovariance()
pos = data_subsample.getPos()

#set parameters for the analysis
N, P = phenotypes.shape

covs = None                 #covariates
Acovs = None                #the design matrix for the covariates
Asnps = SP.ones((1,P))      #the design matrix for the SNPs
K1r = sample_relatedness    #the first sample-sample covariance matrix (non-noise)
K2r = SP.eye(N)             #the second sample-sample covariance matrix (noise)
K1c = None                  #the first phenotype-phenotype covariance matrix (non-noise)
K2c = None                  #the second phenotype-phenotype covariance matrix (noise)
covar_type = 'freeform'     #the type of covariance matrix to be estimated for unspecified covariances
searchDelta = False         #specify if delta should be optimized for each SNP
test="lrt"                  #specify type of statistical test

# Running the analysis
# when cov are not set (None), LIMIX considers an intercept (covs=SP.ones((N,1)))
lmm, pvalues = QTL.test_lmm_kronecker(snps,phenotypes.values,covs=covs,Acovs=Acovs,Asnps=Asnps,K1r=K1r,trait_covar_type=covar_type)

#convert P-values to a DataFrame for nice output writing:
pvalues = pd.DataFrame(data=pvalues.T,index=data_subsample.geno_ID,columns=['YJR139C'])
pvalues = pd.concat([pos,pvalues],join="outer",axis=1)
```

    .. Training the backgrond covariance with a GP model
    Local minimum found in run 1 out of 10
    Background model trained in 0.55 s

```
plt = PL.subplot(2,1,1)
plot_manhattan(plt,pvalues['pos_cum'],pvalues['YJR139C'],chromBounds)
```

### Specific effect test

For a specifc effect test for trait $p$
the alternative model is set to have both a common and a specific effect
for transcript $p$ from the SNP while the null model has only a common effect.

It is a 1 degree of freedom test and,
in the particular case of $P=3$ traits and for $p=0$, it can be done by setting
\begin{equation}
\mathbf{A}_1^\text{(snp)} =
\begin{pmatrix}
  1 & 0 & 0 \\
  1 & 1 & 1
 \end{pmatrix}
\;\;\;,
\mathbf{A}_0^\text{(snp)} = \mathbf{1}_{1,3}
\end{equation}

#### Example: specific effect test for gene YJR139C across environmentsExample

```
# select all environmens for gene YJR139C in lysine_group
phenotype_query = "(gene_ID == 'YJR139C')"

data_subsample = data.subsample_phenotypes(phenotype_query=phenotype_query,intersection=True)

#get variables we need from data
snps = data_subsample.getGenotypes(impute_missing=True)
phenotypes,sample_idx = data_subsample.getPhenotypes(phenotype_query=phenotype_query,intersection=True); assert sample_idx.all()

sample_relatedness = data_subsample.getCovariance()
pos = data_subsample.getPos()

#set parameters for the analysis
N, P = phenotypes.shape

covs = None                 #covariates
Acovs = None                #the design matrix for the covariates
Asnps0 = SP.ones((1,P))     #the null model design matrix for the SNPs
Asnps1 = SP.zeros((2,P))    #the alternative model design matrix for the SNPs
Asnps1[0,:] = 1.0
Asnps1[1,0] = 1.0
K1r = sample_relatedness    #the first sample-sample covariance matrix (non-noise)
K2r = SP.eye(N)             #the second sample-sample covariance matrix (noise)
K1c = None                  #the first phenotype-phenotype covariance matrix (non-noise)
K2c = None                  #the second phenotype-phenotype covariance matrix (noise)
covar_type = 'freeform'     #the type of covariance matrix to be estimated for unspecified covariances
searchDelta = False         #specify if delta should be optimized for each SNP
test="lrt"                  #specify type of statistical test

# Running the analysis
# when cov are not set (None), LIMIX considers an intercept (covs=SP.ones((N,1)))
pvalues = QTL.test_interaction_lmm_kronecker(snps=snps,phenos=phenotypes.values,covs=covs,Acovs=Acovs,Asnps1=Asnps1,Asnps0=Asnps0,K1r=K1r,K2r=K2r,K1c=K1c,K2c=K2c,trait_covar_type=covar_type,searchDelta=searchDelta)

#convert P-values to a DataFrame for nice output writing:
pvalues = pd.DataFrame(data=SP.concatenate(pvalues).T,index=data_subsample.geno_ID,columns=["specific","null_common","alternative_any"])
pvalues = pd.concat([pos,pvalues],join="outer",axis=1)

```

    .. Training the backgrond covariance with a GP model
    Local minimum found in run 1 out of 10
    Background model trained in 0.57 s

Simple_interaction_kronecker not only compares the alternative model (where the trait design of the SNP is Asnp=Asnp1)
versus the null model (where the trait design of the SNP is Asnp=Asnp0) but also compares
the alternative model and the null models versus the no association model (Asnp=0).
Three pvalues are then retured:
- pv for alternative vs null (specific effect test)
- pv for null vs noAssociation (common effect test)
- pv for alternative vs noAssociation (any effect test)

```
tests = ['Any effect test','Interaction effect test']
lim = -1.2*SP.log10(SP.array([pvalues['alternative_any'].min(),pvalues['null_common'].min(),pvalues['specific'].min()]).min())
plt = PL.subplot(2,1,1)
plot_manhattan(plt,pvalues['pos_cum'],pvalues['alternative_any'],chromBounds,colorS='k',colorNS='k',lim=lim,alphaNS=0.05)
plot_manhattan(plt,pvalues['pos_cum'],pvalues['null_common'],chromBounds,colorS='y',colorNS='y',lim=lim,alphaNS=0.05)
plot_manhattan(plt,pvalues['pos_cum'],pvalues['specific'],chromBounds,colorS='r',colorNS='r',lim=lim,alphaNS=0.05)
```

```

```

### Example: any, common and specific effect tests across environments for all genes in the pathway

```
pvalues_dict={}

for gene_idx,gene in enumerate(lysine_group):
    # select all environmens for gene YJR139C in lysine_group
    phenotype_query = "(gene_ID == '%s')" % gene
    print "running GxE GWAS tests on %s" % phenotype_query
    data_subsample = data.subsample_phenotypes(phenotype_query=phenotype_query,intersection=True)

    #get variables we need from data
    phenotypes,sample_idx = data_subsample.getPhenotypes(phenotype_query=phenotype_query,intersection=True); assert sample_idx.all()
    if phenotypes.shape[1]==0:
        continue

    snps = data_subsample.getGenotypes(impute_missing=True)

    sample_relatedness = data_subsample.getCovariance()
    pos = data_subsample.getPos()

    #set parameters for the analysis
    N, P = phenotypes.shape

    covs = None                 #covariates
    Acovs = None                #the design matrix for the covariates
    Asnps0 = SP.ones((1,P))     #the null model design matrix for the SNPs
    Asnps1 = SP.zeros((2,P))    #the alternative model design matrix for the SNPs
    Asnps1[0,:] = 1.0
    Asnps1[1,0] = 1.0
    K1r = sample_relatedness    #the first sample-sample covariance matrix (non-noise)
    K2r = SP.eye(N)             #the second sample-sample covariance matrix (noise)
    K1c = None                  #the first phenotype-phenotype covariance matrix (non-noise)
    K2c = None                  #the second phenotype-phenotype covariance matrix (noise)
    covar_type = 'freeform'     #the type of covariance matrix to be estimated for unspecified covariances
    searchDelta = False         #specify if delta should be optimized for each SNP
    test="lrt"                  #specify type of statistical test

    # Running the analysis
    # when cov are not set (None), LIMIX considers an intercept (covs=SP.ones((N,1)))
    pvalues = QTL.test_interaction_lmm_kronecker(snps=snps,phenos=phenotypes.values,covs=covs,Acovs=Acovs,Asnps1=Asnps1,Asnps0=Asnps0,K1r=K1r,K2r=K2r,K1c=K1c,K2c=K2c,trait_covar_type=covar_type,searchDelta=searchDelta)

    #convert P-values to a DataFrame for nice output writing:
    pvalues = pd.DataFrame(data=SP.concatenate(pvalues).T,index=data_subsample.geno_ID,columns=["specific","null_common","alternative_any"])
    pvalues = pd.concat([pos,pvalues],join="outer",axis=1)
    key = "pvalues_%s" % gene
    pvalues_dict[key] = pvalues

```

    running GxE GWAS tests on (gene_ID == 'YIL094C')
    .. Training the backgrond covariance with a GP model
    Local minimum found in run 1 out of 10
    Background model trained in 0.51 s
    running GxE GWAS tests on (gene_ID == 'YDL182W')
    .. Training the backgrond covariance with a GP model
    Local minimum found in run 1 out of 10
    Background model trained in 0.51 s
    running GxE GWAS tests on (gene_ID == 'YDL131W')
    .. Training the backgrond covariance with a GP model
    Local minimum found in run 1 out of 10
    Background model trained in 0.49 s
    running GxE GWAS tests on (gene_ID == 'YER052C')
    .. Training the backgrond covariance with a GP model
    Local minimum found in run 1 out of 10
    Background model trained in 0.56 s
    running GxE GWAS tests on (gene_ID == 'YBR115C')
    .. Training the backgrond covariance with a GP model
    Local minimum found in run 1 out of 10
    Background model trained in 1.27 s
    running GxE GWAS tests on (gene_ID == 'YDR158W')
    .. Training the backgrond covariance with a GP model
    Local minimum found in run 1 out of 10
    Background model trained in 0.50 s
    running GxE GWAS tests on (gene_ID == 'YNR050C')
    .. Training the backgrond covariance with a GP model
    Local minimum found in run 1 out of 10
    Background model trained in 0.53 s
    running GxE GWAS tests on (gene_ID == 'YJR139C')
    .. Training the backgrond covariance with a GP model
    Local minimum found in run 1 out of 10
    Background model trained in 0.52 s
    running GxE GWAS tests on (gene_ID == 'YIR034C')
    .. Training the backgrond covariance with a GP model
    Local minimum found in run 1 out of 10
    Background model trained in 0.45 s
    running GxE GWAS tests on (gene_ID == 'YGL202W')
    .. Training the backgrond covariance with a GP model
    Local minimum found in run 1 out of 10
    Background model trained in 0.50 s
    running GxE GWAS tests on (gene_ID == 'YDR234W')
    .. Training the backgrond covariance with a GP model
    Local minimum found in run 1 out of 10
    Background model trained in 0.50 s

```
# plotting all manhattan plots
PL.figure(1,figsize=(10,16))
lim = -1.2*SP.log10(SP.array([pv_any.min(),pv_comm.min(),pv_spec.min()]).min())
for g in range(G):
    plt = PL.subplot(G,2,g+1)
    PL.title(lysine_group[g])
    if g!=G-1:   xticklabels = False
    else:        xticklabels = True
    plot_manhattan(plt,cumPos,pv_any[g,:],chromBounds,colorS='k',colorNS='k',lim=lim,alphaNS=0.05,xticklabels=xticklabels)
    plot_manhattan(plt,cumPos,pv_comm[g,:],chromBounds,colorS='y',colorNS='y',lim=lim,alphaNS=0.05,xticklabels=xticklabels)
    plot_manhattan(plt,cumPos,pv_spec[g,:],chromBounds,colorS='r',colorNS='r',lim=lim,alphaNS=0.05,xticklabels=xticklabels)
PL.tight_layout()
```

All any-effect eQTL are explained by environmental persistent signal
with the exception of the peak on chromosome 2 for gene YNR050C that has
an almost significant specific component.
These results together with those obtained from the GxE variance decomposition analysis
suggest that the strong transGxE signal of the genes in the pathway is due to interaction
of the poligenic effect with the environment.

## Multi trait Multi locus

```

```

# Predictions and Model Selection

```
Y = data.getPhenotypes(geneIDs=lysine_group,environments=0)[0]
# variance component model
vc = VAR.VarianceDecomposition(Y)
vc.addFixedEffect()
vc.addRandomEffect(K=R,trait_covar_type='lowrank_id',rank=1)
vc.addRandomEffect(is_noise=True,trait_covar_type='lowrank_id',rank=1)
vc.optimize()
# retrieve geno and noise covariance matrix
Cg = vc.getTraitCovar(0)
Cn = vc.getTraitCovar(1)
```

    Local minimum found at iteration 0

```
PL.subplot(2,2,1)
PL.imshow(Cg)
PL.subplot(2,2,2)
PL.imshow(Cn)
```

```
Y = data.getPhenotypes(geneIDs=lysine_group,environments=0)[0]
# variance component model
vc = VAR.VarianceDecomposition(Y)
vc.addFixedEffect()
vc.addRandomEffect(K=R,trait_covar_type='lowrank_id',rank=1)
vc.addRandomEffect(is_noise=True,trait_covar_type='lowrank_id',rank=1)
vc.optimize(lambd=1e2)
# retrieve geno and noise covariance matrix
Cg = vc.getTraitCovar(0)
Cn = vc.getTraitCovar(1)
```

    No local minimum found for the tested initialization points

```
import temporary.varianceDecomposition as VAROLD
vc = VAR.CVarianceDecomposition(Y)
vc.addFixedEffect(SP.ones((Y.shape[0],1)))
vc.addMultiTraitTerm(K=R,covar_type='lowrank_id',rank=1)
vc.addMultiTraitTerm(is_noise=True,covar_type='lowrank_id',rank=1)
vc.findLocalOptimum(fast=True,lambd1=1e2)

```

# PANAMA

```

```

```

```

```

```


# Tutorial 1: Neural Rate Models
**Week 2, Day 4: Dynamic Networks**

**By Neuromatch Academy**

__Content creators:__ Qinglong Gu, Songtin Li, Arvind Kumar, John Murray, Julijana Gjorgjieva

__Content reviewers:__  Maryam Vaziri-Pashkam, Ella Batty, Lorenzo Fontolan, Richard Gao, Spiros Chavlis, Michael Waskom, Siddharth Suresh

**Our 2021 Sponsors, including Presenting Sponsor Facebook Reality Labs**

---
# Tutorial Objectives

*Estimated timing of tutorial: 1 hour, 25 minutes*

The brain is a complex system, not because it is composed of a large number of diverse types of neurons, but mainly because of how neurons are connected to each other. The brain is indeed a network of highly specialized neuronal networks.

The activity of a neural network constantly evolves in time. For this reason, neurons can be modeled as dynamical systems. The dynamical system approach is only one of the many modeling approaches that computational neuroscientists have developed (other points of view include information processing,  statistical models, etc.).

How the dynamics of neuronal networks affect the representation and processing of information in the brain is an open question. However, signatures of altered brain dynamics present in many brain diseases (e.g., in epilepsy or Parkinson's disease) tell us that it is crucial to study network activity dynamics if we want to understand the brain.

In this tutorial, we will simulate and study one of the simplest models of biological neuronal networks. Instead of modeling and simulating individual excitatory neurons (e.g., LIF models that you implemented yesterday), we will treat them as a single homogeneous population and approximate their dynamics using a single one-dimensional equation describing the evolution of their average spiking rate in time.

In this tutorial, we will learn how to build a firing rate model of a single population of excitatory neurons.

**Steps:**
- Write the equation for the firing rate dynamics of a 1D excitatory population.
- Visualize the response of the population as a function of parameters such as threshold level and gain, using the frequency-current (F-I) curve.
- Numerically simulate the dynamics of the excitatory population and find the fixed points of the system.

```python
# @title Tutorial slides

# @markdown These are the slides for the videos in all tutorials today
from IPython.display import IFrame
IFrame(src=f"https://mfr.ca-1.osf.io/render?url=https://osf.io/nvuty/?direct%26mode=render%26action=download%26mode=render", width=854, height=480)
```

---
# Setup

```python
# Imports

import matplotlib.pyplot as plt
import numpy as np
import scipy.optimize as opt       # root-finding algorithm
```

```python
# @title Figure Settings
import ipywidgets as widgets       # interactive display
%config InlineBackend.figure_format = 'retina'
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/master/nma.mplstyle")
```

```python
# @title Plotting Functions

def plot_fI(x, f):
  plt.figure(figsize=(6, 4))  # plot the figure
  plt.plot(x, f, 'k')
  plt.xlabel('x (a.u.)', fontsize=14)
  plt.ylabel('F(x)', fontsize=14)
  plt.show()

def plot_dr_r(r, drdt, x_fps=None):
  plt.figure()
  plt.plot(r, drdt, 'k')
  plt.plot(r, 0. * r, 'k--')
  if x_fps is not None:
    plt.plot(x_fps, np.zeros_like(x_fps), "ko", ms=12)
  plt.xlabel(r'$r$')
  plt.ylabel(r'$\frac{dr}{dt}$', fontsize=20)
  plt.ylim(-0.1, 0.1)

def plot_dFdt(x, dFdt):
  plt.figure()
  plt.plot(x, dFdt, 'r')
  plt.xlabel('x (a.u.)', fontsize=14)
  plt.ylabel('dF(x)', fontsize=14)
  plt.show()
```

---
# Section 1: Neuronal network dynamics

```python
# @title Video 1: Dynamic networks
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1dh411o7qJ", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="p848349hPyw", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

    Tab(children=(Output(), Output()), _titles={'0': 'Youtube', '1': 'Bilibili'})

This video covers how to model a network with a single population of neurons and introduces neural rate-based models. It overviews feedforward networks and defines the F-I (firing rate vs. input) curve.

## Section 1.1: Dynamics of a single excitatory population

 Click here for text recap of relevant part of video

Individual neurons respond by spiking. When we average the spikes of neurons in a population, we can define the average firing activity of the population. In this model, we are interested in how the population-averaged firing varies as a function of time and network parameters. Mathematically, we can describe the firing rate dynamic of a feed-forward network as:

\begin{align}
\tau \frac{dr}{dt} &= -r + F(I_{\text{ext}})  \quad\qquad (1)
\end{align}

$r(t)$ represents the average firing rate of the excitatory population at time $t$, $\tau$ controls the timescale of the evolution of the average firing rate, $I_{\text{ext}}$ represents the external input, and the transfer function $F(\cdot)$ (which can be related to f-I curve of individual neurons described in the next sections) represents the population activation function in response to all received inputs.

To start building the model, please execute the cell below to initialize the simulation parameters.

```python
# @markdown *Execute this cell to set default parameters for a single excitatory population model*

def default_pars_single(**kwargs):
  pars = {}

  # Excitatory parameters
  pars['tau'] = 1.     # Timescale of the E population [ms]
  pars['a'] = 1.2      # Gain of the E population
  pars['theta'] = 2.8  # Threshold of the E population

  # Connection strength
  pars['w'] = 0.  # E to E, we first set it to 0

  # External input
  pars['I_ext'] = 0.

  # simulation parameters
  pars['T'] = 20.       # Total duration of simulation [ms]
  pars['dt'] = .1       # Simulation time step [ms]
  pars['r_init'] = 0.2  # Initial value of E

  # External parameters if any
  pars.update(kwargs)

  # Vector of discretized time points [ms]
  pars['range_t'] = np.arange(0, pars['T'], pars['dt'])

  return pars

pars = default_pars_single()
print(pars)
```

    {'tau': 1.0, 'a': 1.2, 'theta': 2.8, 'w': 0.0, 'I_ext': 0.0, 'T': 20.0, 'dt': 0.1, 'r_init': 0.2, 'range_t': array([ 0. ,  0.1,  0.2,  0.3,  0.4,  0.5,  0.6,  0.7,  0.8,  0.9,  1. ,
            1.1,  1.2,  1.3,  1.4,  1.5,  1.6,  1.7,  1.8,  1.9,  2. ,  2.1,
            2.2,  2.3,  2.4,  2.5,  2.6,  2.7,  2.8,  2.9,  3. ,  3.1,  3.2,
            3.3,  3.4,  3.5,  3.6,  3.7,  3.8,  3.9,  4. ,  4.1,  4.2,  4.3,
            4.4,  4.5,  4.6,  4.7,  4.8,  4.9,  5. ,  5.1,  5.2,  5.3,  5.4,
            5.5,  5.6,  5.7,  5.8,  5.9,  6. ,  6.1,  6.2,  6.3,  6.4,  6.5,
            6.6,  6.7,  6.8,  6.9,  7. ,  7.1,  7.2,  7.3,  7.4,  7.5,  7.6,
            7.7,  7.8,  7.9,  8. ,  8.1,  8.2,  8.3,  8.4,  8.5,  8.6,  8.7,
            8.8,  8.9,  9. ,  9.1,  9.2,  9.3,  9.4,  9.5,  9.6,  9.7,  9.8,
            9.9, 10. , 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8, 10.9,
           11. , 11.1, 11.2, 11.3, 11.4, 11.5, 11.6, 11.7, 11.8, 11.9, 12. ,
           12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8, 12.9, 13. , 13.1,
           13.2, 13.3, 13.4, 13.5, 13.6, 13.7, 13.8, 13.9, 14. , 14.1, 14.2,
           14.3, 14.4, 14.5, 14.6, 14.7, 14.8, 14.9, 15. , 15.1, 15.2, 15.3,
           15.4, 15.5, 15.6, 15.7, 15.8, 15.9, 16. , 16.1, 16.2, 16.3, 16.4,
           16.5, 16.6, 16.7, 16.8, 16.9, 17. , 17.1, 17.2, 17.3, 17.4, 17.5,
           17.6, 17.7, 17.8, 17.9, 18. , 18.1, 18.2, 18.3, 18.4, 18.5, 18.6,
           18.7, 18.8, 18.9, 19. , 19.1, 19.2, 19.3, 19.4, 19.5, 19.6, 19.7,
           19.8, 19.9])}

You can now use:
- `pars = default_pars_single()` to get all the parameters.
- `pars = default_pars_single(T=T_sim, dt=time_step)` to set new simulation time and time step
- To update an existing parameter dictionary, use `pars['New_para'] = value`

Because `pars` is a dictionary, it can be passed to a function that requires individual parameters as arguments using `my_func(**pars)` syntax.

## Section 1.2: F-I curves

*Estimated timing to here from start of tutorial: 17 min*

 Click here for text recap of relevant part of video

In electrophysiology, a neuron is often characterized by its spike rate output in response to input currents. This is often called the **F-I** curve, denoting the output spike frequency (**F**) in response to different injected currents (**I**). We estimated this for an LIF neuron in yesterday's tutorial.

The transfer function $F(\cdot)$ in Equation $1$ represents the gain of the population as a function of the total input. The gain is often modeled as a sigmoidal function, i.e., more input drive leads to a nonlinear increase in the population firing rate. The output firing rate will eventually saturate for high input values.

A sigmoidal $F(\cdot)$ is parameterized by its gain $a$ and threshold $\theta$.

$$ F(x;a,\theta) = \frac{1}{1+\text{e}^{-a(x-\theta)}} - \frac{1}{1+\text{e}^{a\theta}}  \quad(2)$$

The argument $x$ represents the input to the population. Note that the second term is chosen so that $F(0;a,\theta)=0$.

Many other transfer functions (generally monotonic) can be also used. Examples are the rectified linear function $ReLU(x)$ or the hyperbolic tangent $tanh(x)$.

### Coding Exercise 1.2: Implement F-I curve

Let's first investigate the activation functions before simulating the dynamics of the entire population.

In this exercise, you will implement a sigmoidal **F-I** curve or transfer function $F(x)$, with gain $a$ and threshold level $\theta$ as parameters:

$$ F(x;a,\theta) = \frac{1}{1+\text{e}^{-a(x-\theta)}} - \frac{1}{1+\text{e}^{a\theta}}$$

```python
def F(x, a, theta):
  """
  Population activation function.

  Args:
    x (float): the population input
    a (float): the gain of the function
    theta (float): the threshold of the function

  Returns:
    float: the population activation response F(x) for input x
  """
  #################################################
  ## TODO for students: compute f = F(x) ##
  # Fill out function and remove
  # raise NotImplementedError("Student excercise: implement the f-I function")
  #################################################

  # Define the sigmoidal transfer function f = F(x)
  # first attempt:
  f = (1 / (1 + np.exp(-a * (x - theta)))) - (1 / (1 + np.exp(a * theta)))
  # cleaner way via given solution:
  # f = (1 + np.exp(-a * (x - theta)))**-1 - (1 + np.exp(a * theta))**-1

  return f

# Set parameters
pars = default_pars_single()  # get default parameters
x = np.arange(0, 10, .1)      # set the range of input

# Compute transfer function
f = F(x, pars['a'], pars['theta'])

# Visualize
plot_fI(x, f)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial1_Solution_04e84428.py)

*Example output:*

### Interactive Demo 1.2 : Parameter exploration of F-I curve
Here's an interactive demo that shows how the F-I curve changes for different values of the gain and threshold parameters.

1. How does the gain parameter ($a$) affect the F-I curve?
1. How does the threshold parameter ($\theta$) affect the F-I curve?

1. Gain seems to affect saturation slopeness?
2. Seems to affect at which x-value the line is steepest/saturates at?

```python

"""
1)  a determines the slope (gain) of the rising phase of the F-I curve
2) theta determines the input at which the function F(x) reaches its mid-value (0.5).
That is, theta shifts the F-I curve along the horizontal axis.
For our neurons we are using in this tutorial:
- a controls the gain of the neuron population
- theta controls the threshold at which the neuron population starts to respond
"""
```

    '\n1)  a determines the slope (gain) of the rising phase of the F-I curve\n2) theta determines the input at which the function F(x) reaches its mid-value (0.5).\nThat is, theta shifts the F-I curve along the horizontal axis.\nFor our neurons we are using in this tutorial:\n- a controls the gain of the neuron population\n- theta controls the threshold at which the neuron population starts to respond\n'

```python
# @title

# @markdown Make sure you execute this cell to enable the widget!

def interactive_plot_FI(a, theta):
  """
  Population activation function.

  Expecxts:
    a     : the gain of the function
    theta : the threshold of the function

  Returns:
    plot the F-I curve with give parameters
  """

  # set the range of input
  x = np.arange(0, 10, .1)
  plt.figure()
  plt.plot(x, F(x, a, theta), 'k')
  plt.xlabel('x (a.u.)', fontsize=14)
  plt.ylabel('F(x)', fontsize=14)
  plt.show()

_ = widgets.interact(interactive_plot_FI, a=(0.3, 3, 0.3), theta=(2, 4, 0.2))
```

    interactive(children=(FloatSlider(value=1.5, description='a', max=3.0, min=0.3, step=0.3), FloatSlider(value=3…

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial1_Solution_89e4146a.py)

## Section 1.3: Simulation scheme of E dynamics

*Estimated timing to here from start of tutorial: 27 min*

Because $F(\cdot)$ is a nonlinear function, the exact solution of our differential equation of population activity can not be determined via analytical methods. As we have seen before, we can use numerical methods, specifically the Euler method, to find the solution (that is, simulate the population activity).

```python
# @markdown *Execute this cell to enable the single population rate model simulator: `simulate_single`*

def simulate_single(pars):
  """
  Simulate an excitatory population of neurons

  Args:
    pars   : Parameter dictionary

  Returns:
    rE     : Activity of excitatory population (array)

  Example:
    pars = default_pars_single()
    r = simulate_single(pars)
  """

  # Set parameters
  tau, a, theta = pars['tau'], pars['a'], pars['theta']
  w = pars['w']
  I_ext = pars['I_ext']
  r_init = pars['r_init']
  dt, range_t = pars['dt'], pars['range_t']
  Lt = range_t.size

  # Initialize activity
  r = np.zeros(Lt)
  r[0] = r_init
  I_ext = I_ext * np.ones(Lt)

  # Update the E activity
  for k in range(Lt - 1):
      dr = dt / tau * (-r[k] + F(w * r[k] + I_ext[k], a, theta))
      r[k+1] = r[k] + dr

  return r

help(simulate_single)
```

    Help on function simulate_single in module __main__:

    simulate_single(pars)
        Simulate an excitatory population of neurons

        Args:
          pars   : Parameter dictionary

        Returns:
          rE     : Activity of excitatory population (array)

        Example:
          pars = default_pars_single()
          r = simulate_single(pars)

### Interactive Demo 1.3: Parameter Exploration of single population dynamics

Explore these dynamics of the population activity in this interactive demo.

1.  How does $r_{\text{sim}}(t)$ change with different $I_{\text{ext}}$ values?
2.  How does it change with different $\tau$ values?

Note that, $r_{\rm ana}(t)$ denotes the analytical solution - you will learn how this is computed in the next section.

"""
1) Weak inputs to the neurons eventually result in the activity converging to zero.
 Strong inputs to the neurons eventually result in the activity converging to max value
2) The time constant tau, does not affect the final response reached but it determines
the time the neurons take to reach to their fixed point.
""";

```python
# @title

# @markdown Make sure you execute this cell to enable the widget!

# get default parameters
pars = default_pars_single(T=20.)

def Myplot_E_diffI_difftau(I_ext, tau):
  # set external input and time constant
  pars['I_ext'] = I_ext
  pars['tau'] = tau

  # simulation
  r = simulate_single(pars)

  # Analytical Solution
  r_ana = (pars['r_init']
           + (F(I_ext, pars['a'], pars['theta'])
           - pars['r_init']) * (1. - np.exp(-pars['range_t'] / pars['tau'])))

  # plot
  plt.figure()
  plt.plot(pars['range_t'], r, 'b', label=r'$r_{\mathrm{sim}}$(t)', alpha=0.5,
           zorder=1)
  plt.plot(pars['range_t'], r_ana, 'b--', lw=5, dashes=(2, 2),
           label=r'$r_{\mathrm{ana}}$(t)', zorder=2)
  plt.plot(pars['range_t'],
           F(I_ext, pars['a'], pars['theta']) * np.ones(pars['range_t'].size),
           'k--', label=r'$F(I_{\mathrm{ext}})$')
  plt.xlabel('t (ms)', fontsize=16.)
  plt.ylabel('Activity r(t)', fontsize=16.)
  plt.legend(loc='best', fontsize=14.)
  plt.show()

_ = widgets.interact(Myplot_E_diffI_difftau, I_ext=(0.0, 10., 1.),
                     tau=(1., 5., 0.2))
```

    interactive(children=(FloatSlider(value=5.0, description='I_ext', max=10.0, step=1.0), FloatSlider(value=3.0, …

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial1_Solution_44a82c10.py)

## Think! 1.3: Finite activities

Above, we have numerically solved a system driven by a positive input. Yet, $r_E(t)$ either decays to zero or reaches a fixed non-zero value.

1. Why doesn't the solution of the system "explode" in a finite time? In other words, what guarantees that $r_E$(t) stays finite?
2. Which parameter would you change in order to increase the maximum value of the response?

**1) As the F-I curve is bounded between zero and one, the system doesn't explode. The f-curve guarantees this property

2) One way to increase the maximum response is to change the f-I curve. For
example, the ReLU is an unbounded function, and thus will increase the overall maximal
response of the network.**

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial1_Solution_86ae968b.py)

---
# Section 2: Fixed points of the single population system

*Estimated timing to here from start of tutorial: 45 min*

## Section 2.1: Finding fixed points

```python
# @title Video 2: Fixed point
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1v54y1v7Gr", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="Ox3ELd1UFyo", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

    Tab(children=(Output(), Output()), _titles={'0': 'Youtube', '1': 'Bilibili'})

This video introduces recurrent networks and how to derive their fixed points. It also introduces vector fields and phase planes in one dimension.

 Click here for text recap of video

We can now extend our feed-forward network to a recurrent network, governed by the equation:

\begin{align}
\tau \frac{dr}{dt} &= -r + F(w\cdot r + I_{\text{ext}})  \quad\qquad (3)
\end{align}
 where as before, $r(t)$ represents the average firing rate of the excitatory population at time $t$, $\tau$ controls the timescale of the evolution of the average firing rate, $I_{\text{ext}}$ represents the external input, and the transfer function $F(\cdot)$ (which can be related to f-I curve of individual neurons described in the next sections) represents the population activation function in response to all received inputs. Now we also have $w$ which denotes the strength (synaptic weight) of the recurrent input to the population.

As you varied the two parameters in the last Interactive Demo, you noticed that, while at first the system output quickly changes, with time, it reaches its maximum/minimum value and does not change anymore. The value eventually reached by the system is called the **steady state** of the system, or the **fixed point**. Essentially, in the steady states the derivative with respect to time of the activity ($r$) is zero, i.e. $\displaystyle \frac{dr}{dt}=0$.

We can find that the steady state of the Equation. (1) by setting $\displaystyle{\frac{dr}{dt}=0}$ and solve for $r$:

$$-r_{\text{steady}} + F(w\cdot r_{\text{steady}} + I_{\text{ext}};a,\theta) = 0, \qquad (4)$$

When it exists, the solution of Equation. (4) defines a **fixed point** of the dynamical system in Equation (3). Note that if $F(x)$ is nonlinear, it is not always possible to find an analytical solution, but the solution can be found via numerical simulations, as we will do later.

From the Interactive Demo, one could also notice that the value of $\tau$ influences how quickly the activity will converge to the steady state from its initial value.

In the specific case of $w=0$, we can also analytically compute  the solution of Equation (1) (i.e., the thick blue dashed line) and deduce the role of $\tau$ in determining the convergence to the fixed point:

$$\displaystyle{r(t) = \big{[}F(I_{\text{ext}};a,\theta) -r(t=0)\big{]} (1-\text{e}^{-\frac{t}{\tau}})} + r(t=0)$$ \\

We can now numerically calculate the fixed point with a root finding algorithm.

### Coding Exercise 2.1.1: Visualization of the fixed points

When it is not possible to find the solution for Equation (3) analytically, a graphical approach can be taken. To that end, it is useful to plot $\displaystyle{\frac{dr}{dt}}$ as a function of $r$. The values of $r$ for which the plotted function crosses zero on the y axis correspond to fixed points.

Here, let us, for example, set $w=5.0$ and $I^{\text{ext}}=0.5$. From Equation (3), you can obtain

$$\frac{dr}{dt} = [-r + F(w\cdot r + I^{\text{ext}})]\,/\,\tau $$

Then, plot the $dr/dt$ as a function of $r$, and check for the presence of fixed points.

```python
def compute_drdt(r, I_ext, w, a, theta, tau, **other_pars):
  """Given parameters, compute dr/dt as a function of r.

  Args:
    r (1D array) : Average firing rate of the excitatory population
    I_ext, w, a, theta, tau (numbers): Simulation parameters to use
    other_pars : Other simulation parameters are unused by this function

  Returns
    drdt function for each value of r
  """
  #########################################################################
  # TODO compute drdt and disable the error
  # raise NotImplementedError("Finish the compute_drdt function")
  #########################################################################

  # Calculate drdt
  # F(x, a, theta)
  # not x = w * r * I_ext
  drdt = ((-1 * r) + F(w * r + I_ext, a, theta)) / tau

  return drdt

# Define a vector of r values and the simulation parameters
r = np.linspace(0, 1, 1000)
pars = default_pars_single(I_ext=0.5, w=5)

# Compute dr/dt
drdt = compute_drdt(r, **pars)

# Visualize
plot_dr_r(r, drdt)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial1_Solution_c4108be6.py)

*Example output:*

### Coding Exercise 2.1.2: Numerical calculution of fixed points

We will now find the fixed points numerically. To do so, we need to specify initial values ($r_{\text{guess}}$) for the root-finding algorithm to start from. From the line $\displaystyle{\frac{dr}{dt}}$ plotted above in the last exercise, initial values can be chosen as a set of values close to where the line crosses zero on the y axis (real fixed point).

The next cell defines three helper functions that we will use:

- `my_fp_single(r_guess, **pars)` uses a root-finding algorithm to locate a fixed point near a given initial value
- `check_fp_single(x_fp, **pars)` verifies that the values of $r_{\rm fp}$ for which $\displaystyle{\frac{dr}{dt}} = 0$ are the true fixed points
- `my_fp_finder(r_guess_vector, **pars)` accepts an array of initial values and finds the same number of fixed points, using the above two functions

```python
# @markdown *Execute this cell to enable the fixed point functions*

def my_fp_single(r_guess, a, theta, w, I_ext, **other_pars):
  """
  Calculate the fixed point through drE/dt=0

  Args:
    r_guess  : Initial value used for scipy.optimize function
    a, theta, w, I_ext : simulation parameters

  Returns:
    x_fp    : value of fixed point
  """
  # define the right hand of E dynamics
  def my_WCr(x):
    r = x
    drdt = (-r + F(w * r + I_ext, a, theta))
    y = np.array(drdt)

    return y

  x0 = np.array(r_guess)
  x_fp = opt.root(my_WCr, x0).x.item()

  return x_fp

def check_fp_single(x_fp, a, theta, w, I_ext, mytol=1e-4, **other_pars):
  """
   Verify |dr/dt| < mytol

  Args:
    fp      : value of fixed point
    a, theta, w, I_ext: simulation parameters
    mytol   : tolerance, default as 10^{-4}

  Returns :
    Whether it is a correct fixed point: True/False
  """
  # calculate Equation(3)
  y = x_fp - F(w * x_fp + I_ext, a, theta)

  # Here we set tolerance as 10^{-4}
  return np.abs(y) < mytol

def my_fp_finder(pars, r_guess_vector, mytol=1e-4):
  """
  Calculate the fixed point(s) through drE/dt=0

  Args:
    pars    : Parameter dictionary
    r_guess_vector  : Initial values used for scipy.optimize function
    mytol   : tolerance for checking fixed point, default as 10^{-4}

  Returns:
    x_fps   : values of fixed points

  """
  x_fps = []
  correct_fps = []
  for r_guess in r_guess_vector:
    x_fp = my_fp_single(r_guess, **pars)
    if check_fp_single(x_fp, **pars, mytol=mytol):
      x_fps.append(x_fp)

  return x_fps

help(my_fp_finder)
```

    Help on function my_fp_finder in module __main__:

    my_fp_finder(pars, r_guess_vector, mytol=0.0001)
        Calculate the fixed point(s) through drE/dt=0

        Args:
          pars    : Parameter dictionary
          r_guess_vector  : Initial values used for scipy.optimize function
          mytol   : tolerance for checking fixed point, default as 10^{-4}

        Returns:
          x_fps   : values of fixed points

```python
# Set parameters
r = np.linspace(0, 1, 1000)
pars = default_pars_single(I_ext=0.5, w=5)

# Compute dr/dt
drdt = compute_drdt(r, **pars)

#############################################################################
# TODO for students:
# Define initial values close to the intersections of drdt and y=0
# (How many initial values? Hint: How many times do the two lines intersect?)
# Calculate the fixed point with these initial values and plot them
# raise NotImplementedError('student_exercise: find fixed points numerically')
#############################################################################

# Initial guesses for fixed points
r_guess_vector = [0.5] # eventually settles to where the line crosses 0 i.e. flips signs/direction

r_guess_vector = [0, 0.4, 0.9]

# Find fixed point numerically
x_fps = my_fp_finder(pars, r_guess_vector)

# Visualize
plot_dr_r(r, drdt, x_fps)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial1_Solution_1c599fac.py)

*Example output:*

### Interactive Demo 2.1: fixed points as a function of recurrent and external inputs.

You can now explore how the previous plot changes when the recurrent coupling $w$ and the external input $I_{\text{ext}}$ take different values. How does the number of fixed points change?

```python
# @title

# @markdown Make sure you execute this cell to enable the widget!

def plot_intersection_single(w, I_ext):
  # set your parameters
  pars = default_pars_single(w=w, I_ext=I_ext)

  # find fixed points
  r_init_vector = [0, .4, .9]
  x_fps = my_fp_finder(pars, r_init_vector)

  # plot
  r = np.linspace(0, 1., 1000)
  drdt = (-r + F(w * r + I_ext, pars['a'], pars['theta'])) / pars['tau']

  plot_dr_r(r, drdt, x_fps)

_ = widgets.interact(plot_intersection_single, w=(-1, 7, 0.2),
                     I_ext=(0, 3, 0.1))
```

    interactive(children=(FloatSlider(value=3.0, description='w', max=7.0, min=-1.0, step=0.2), FloatSlider(value=…

```python
# The fixed points of the single excitatory neuron population are determined by both
# recurrent connections w and external input I_ext. In a previous interactive demo
# we saw how the system showed two different steady-states when w = 0. But when w
# doe not equal 0, for some range of w the system shows three fixed points and the
# steady state depends on the initial conditions (i.e.
# r at time zero.).

```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial1_Solution_39f3d710.py)

## Section 2.2: Relationship between trajectories & fixed points

Let's examine the relationship between the population activity over time and the fixed points.

Here, let us first set $w=5.0$ and $I_{\text{ext}}=0.5$, and investigate the dynamics of $r(t)$ starting with different initial values $r(0) \equiv r_{\text{init}}$.

```python
# @markdown Execute to visualize dr/dt

def plot_intersection_single(w, I_ext):
  # set your parameters
  pars = default_pars_single(w=w, I_ext=I_ext)

  # find fixed points
  r_init_vector = [0, .4, .9]
  x_fps = my_fp_finder(pars, r_init_vector)

  # plot
  r = np.linspace(0, 1., 1000)
  drdt = (-r + F(w * r + I_ext, pars['a'], pars['theta'])) / pars['tau']

  plot_dr_r(r, drdt, x_fps)

plot_intersection_single(w = 5.0, I_ext = 0.5)
```

### Interactive Demo 2.2: dynamics as a function of the initial value

Let's now set $r_{\rm init}$ to a value of your choice in this demo. How does the solution change? What do you observe? How does that relate to the previous plot of $\frac{dr}{dt}$?

note: around above init r of 0.45 do i see system going towards 1 instead of towards 0

```python
# @markdown Make sure you execute this cell to enable the widget!

pars = default_pars_single(w=5.0, I_ext=0.5)

def plot_single_diffEinit(r_init):
  pars['r_init'] = r_init
  r = simulate_single(pars)

  plt.figure()
  plt.plot(pars['range_t'], r, 'b', zorder=1)
  plt.plot(0, r[0], 'bo', alpha=0.7, zorder=2)
  plt.xlabel('t (ms)', fontsize=16)
  plt.ylabel(r'$r(t)$', fontsize=16)
  plt.ylim(0, 1.0)
  plt.show()

_ = widgets.interact(plot_single_diffEinit, r_init=(0, 1, 0.02))
```

    interactive(children=(FloatSlider(value=0.0, description='r_init', max=1.0, step=0.02), Output()), _dom_classe…

```python
# There are three fixed points of the system for these values of
# w and I_ext. Choose the initial value in this demo and see in which direction
# the system output moves. When r_init is in the vicinity of the leftmost fixed points
# it moves towards the left most fixed point. When r_init is in the vicinity of the
# rightmost fixed points it moves towards the rightmost fixed point.
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial1_Solution_c73e9caf.py)

We will plot the trajectories of $r(t)$ with $r_{\text{init}} = 0.0, 0.1, 0.2,..., 0.9$.

```python
# @markdown Execute this cell to see the trajectories!

pars = default_pars_single()
pars['w'] = 5.0
pars['I_ext'] = 0.5

plt.figure(figsize=(8, 5))
for ie in range(10):
  pars['r_init'] = 0.1 * ie  # set the initial value
  r = simulate_single(pars)  # run the simulation

  # plot the activity with given initial
  plt.plot(pars['range_t'], r, 'b', alpha=0.1 + 0.1 * ie,
           label=r'r$_{\mathrm{init}}$=%.1f' % (0.1 * ie))

plt.xlabel('t (ms)')
plt.title('Two steady states?')
plt.ylabel(r'$r$(t)')
plt.legend(loc=[1.01, -0.06], fontsize=14)
plt.show()
```

We have three fixed points but only two steady states showing up - what's happening?

It turns out that the stability of the fixed points matters. If a fixed point is stable, a trajectory starting near that fixed point will stay close to that fixed point and converge to it (the steady state will equal the fixed point). If a fixed point is unstable, any trajectories starting close to it will diverge and go towards stable fixed points. In fact, the only way for a trajectory to reach a stable state at an unstable fixed point is if the initial value **exactly** equals the value of the fixed point.

### Think! 2.2: Stable vs unstable fixed points

Which of the fixed points for the model we've been examining in this section are stable vs unstable?

```python
# @markdown Execute to print fixed point values

# Initial guesses for fixed points
r_guess_vector = [0, .4, .9]

# Find fixed point numerically
x_fps = my_fp_finder(pars, r_guess_vector)

print(f'Our fixed points are {x_fps}')
```

    Our fixed points are [0.04153669901331739, 0.4471192240898344, 0.8997171538560865]

There are three fixed points of the system for these values of
w and I_ext. Choose the initial value in this demo and see in which direction
the system output moves. When r_init is in the vicinity of the leftmost fixed points
it moves towards the left most fixed point. When r_init is in the vicinity of the
rightmost fixed points it moves towards the rightmost fixed point.
""";

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial1_Solution_3d44c9a3.py)

We can simulate the trajectory if we start at the unstable fixed point: you can see that it remains at that fixed point (the red line below).

```python
# @markdown Execute to visualize trajectory starting at unstable fixed point
pars = default_pars_single()
pars['w'] = 5.0
pars['I_ext'] = 0.5

plt.figure(figsize=(8, 5))
for ie in range(10):
  pars['r_init'] = 0.1 * ie  # set the initial value
  r = simulate_single(pars)  # run the simulation

  # plot the activity with given initial
  plt.plot(pars['range_t'], r, 'b', alpha=0.1 + 0.1 * ie,
           label=r'r$_{\mathrm{init}}$=%.1f' % (0.1 * ie))

pars['r_init'] = x_fps[1]  # set the initial value
r = simulate_single(pars)  # run the simulation

# plot the activity with given initial
plt.plot(pars['range_t'], r, 'r', alpha=0.1 + 0.1 * ie,
          label=r'r$_{\mathrm{init}}$=%.4f' % (x_fps[1]))

plt.xlabel('t (ms)')
plt.title('Two steady states?')
plt.ylabel(r'$r$(t)')
plt.legend(loc=[1.01, -0.06], fontsize=14)
plt.show()
```

See Bonus Section 1 to cover how to determine the stability of fixed points in a quantitative way.

## Think! 2: Inhibitory populations

Throughout the tutorial, we have assumed $w> 0 $, i.e., we considered a single population of **excitatory** neurons. What do you think will be the behavior of a population of inhibitory neurons, i.e., where $w> 0$ is replaced by $w< 0$?

You can check this by going back the interactive demo 2.1 and set the
weight to w<0. You will notice that the system has only one fixed point and that
is at zero value. For this particular dynamics, the system will eventually converge
to zero. But try it out!

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial1_Solution_f3418e5f.py)

---
# Summary

*Estimated timing of tutorial: 1 hour, 25 minutes*

In this tutorial, we have investigated the dynamics of a rate-based single population of neurons.

We learned about:
- The effect of the input parameters and the time constant of the network on the dynamics of the population.
- How to find the fixed point(s) of the system.

We build on these concepts in the bonus material - check it out if you have time. You will learn:

- How to determine the stability of a fixed point by linearizing the system.
- How to add realistic inputs to our model.

---
# Bonus

---
## Bonus Section 1: Stability analysis via linearization of the dynamics

```python
# @title Video 3: Stability of fixed points
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1oA411e7eg", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="KKMlWWU83Jg", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

Here we will dive into the math of how to figure out the stability of a fixed point.

Just like in our equation for the feedforward network, a generic linear system
$$\frac{dx}{dt} = \lambda (x - b),$$
has a fixed point for $x=b$. The analytical solution of such a system can be found to be:
$$x(t) = b + \big{(} x(0) - b \big{)} \text{e}^{\lambda t}.$$
Now consider a small perturbation of the activity around the fixed point: $x(0) = b+ \epsilon$, where $|\epsilon| \ll 1$. Will the perturbation $\epsilon(t)$ grow with time or will it decay to the fixed point? The evolution of the perturbation with time can be written, using the analytical solution for $x(t)$, as:
 $$\epsilon (t) = x(t) - b = \epsilon \text{e}^{\lambda t}$$

- if $\lambda < 0$, $\epsilon(t)$ decays to zero, $x(t)$ will still converge to $b$ and the fixed point is "**stable**".

- if $\lambda > 0$, $\epsilon(t)$ grows with time, $x(t)$ will leave the fixed point $b$ exponentially, and the fixed point is, therefore, "**unstable**" .

Similar to what we did in the linear system above, in order to determine the stability of a fixed point $r^{*}$ of the excitatory population dynamics, we perturb Equation (1) around $r^{*}$ by $\epsilon$, i.e. $r = r^{*} + \epsilon$. We can plug in Equation (1) and obtain the equation determining the time evolution of the perturbation $\epsilon(t)$:

\begin{align}
\tau \frac{d\epsilon}{dt} \approx -\epsilon + w F'(w\cdot r^{*} + I_{\text{ext}};a,\theta) \epsilon
\end{align}

where $F'(\cdot)$ is the derivative of the transfer function $F(\cdot)$. We can rewrite the above equation as:

\begin{align}
\frac{d\epsilon}{dt} \approx \frac{\epsilon}{\tau }[-1 + w F'(w\cdot r^* + I_{\text{ext}};a,\theta)]
\end{align}

That is, as in the linear system above, the value of

$$\lambda = [-1+ wF'(w\cdot r^* + I_{\text{ext}};a,\theta)]/\tau \qquad (4)$$

determines whether the perturbation will grow or decay to zero, i.e., $\lambda$ defines the stability of the fixed point. This value is called the **eigenvalue** of the dynamical system.

The derivative of the sigmoid transfer function is:
\begin{align}
\frac{dF}{dx} & = \frac{d}{dx} (1+\exp\{-a(x-\theta)\})^{-1}  \\
& = a\exp\{-a(x-\theta)\} (1+\exp\{-a(x-\theta)\})^{-2}. \qquad (5)
\end{align}

We provide a helper function `dF` which computes this derivative.

```python
# @markdown Execute this cell to enable helper function `dF` and visualize derivative

def dF(x, a, theta):
  """
  Population activation function.

  Args:
    x     : the population input
    a     : the gain of the function
    theta : the threshold of the function

  Returns:
    dFdx  : the population activation response F(x) for input x
  """

  # Calculate the population activation
  dFdx = a * np.exp(-a * (x - theta)) * (1 + np.exp(-a * (x - theta)))**-2

  return dFdx

# Set parameters
pars = default_pars_single()  # get default parameters
x = np.arange(0, 10, .1)      # set the range of input

# Compute derivative of transfer function
df = dF(x, pars['a'], pars['theta'])

# Visualize
plot_dFdt(x, df)
```

### Bonus Coding Exercise 1: Compute eigenvalues

As discussed above, for the case with $w=5.0$ and $I_{\text{ext}}=0.5$, the system displays **three** fixed points. However, when we simulated the dynamics and varied the initial conditions $r_{\rm init}$, we could only obtain **two** steady states. In this exercise, we will now check the stability of each of the three fixed points by calculating the corresponding eigenvalues with the function `eig_single`. Check the sign of each eigenvalue (i.e., stability of each fixed point). How many of the fixed points are stable?

Note that the expression of the eigenvalue at fixed point $r^*$
$$\lambda = [-1+ wF'(w\cdot r^* + I_{\text{ext}};a,\theta)]/\tau$$

```python
def eig_single(fp, tau, a, theta, w, I_ext, **other_pars):
  """
  Args:
    fp   : fixed point r_fp
    tau, a, theta, w, I_ext : Simulation parameters

  Returns:
    eig : eigevalue of the linearized system
  """
  #####################################################################
  ## TODO for students: compute eigenvalue and disable the error
  raise NotImplementedError("Student excercise: compute the eigenvalue")
  ######################################################################
  # Compute the eigenvalue
  eig = ...

  return eig

# Find the eigenvalues for all fixed points
pars = default_pars_single(w=5, I_ext=.5)
r_guess_vector = [0, .4, .9]
x_fp = my_fp_finder(pars, r_guess_vector)

for fp in x_fp:
  eig_fp = eig_single(fp, **pars)
  print(f'Fixed point1 at {fp:.3f} with Eigenvalue={eig_fp:.3f}')
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W2D4_DynamicNetworks/solutions/W2D4_Tutorial1_Solution_dda11dcc.py)

**SAMPLE OUTPUT**

```
Fixed point1 at 0.042 with Eigenvalue=-0.583
Fixed point2 at 0.447 with Eigenvalue=0.498
Fixed point3 at 0.900 with Eigenvalue=-0.626
```

We can see that the first and third fixed points are stable (negative eigenvalues) and the second is unstable (positive eigenvalue) - as we expected!

---
## Bonus Section 2: Noisy input drives the transition between two stable states

As discussed in several previous tutorials, the Ornstein-Uhlenbeck (OU) process is usually used to generate a noisy input into the neuron. The OU input $\eta(t)$ follows:

$$\tau_\eta \frac{d}{dt}\eta(t) = -\eta (t) + \sigma_\eta\sqrt{2\tau_\eta}\xi(t)$$

Execute the following function `my_OU(pars, sig, myseed=False)` to generate an OU process.

```python
# @markdown Execute to get helper function `my_OU` and visualize OU process

def my_OU(pars, sig, myseed=False):
  """
  A functions that generates Ornstein-Uhlenback process

  Args:
    pars       : parameter dictionary
    sig        : noise amplitute
    myseed     : random seed. int or boolean

  Returns:
    I          : Ornstein-Uhlenbeck input current
  """

  # Retrieve simulation parameters
  dt, range_t = pars['dt'], pars['range_t']
  Lt = range_t.size
  tau_ou = pars['tau_ou']  # [ms]

  # set random seed
  if myseed:
      np.random.seed(seed=myseed)
  else:
      np.random.seed()

  # Initialize
  noise = np.random.randn(Lt)
  I_ou = np.zeros(Lt)
  I_ou[0] = noise[0] * sig

  # generate OU
  for it in range(Lt - 1):
    I_ou[it + 1] = (I_ou[it]
                    + dt / tau_ou * (0. - I_ou[it])
                    + np.sqrt(2 * dt / tau_ou) * sig * noise[it + 1])

  return I_ou

pars = default_pars_single(T=100)
pars['tau_ou'] = 1.  # [ms]
sig_ou = 0.1
I_ou = my_OU(pars, sig=sig_ou, myseed=2020)
plt.figure(figsize=(10, 4))
plt.plot(pars['range_t'], I_ou, 'r')
plt.xlabel('t (ms)')
plt.ylabel(r'$I_{\mathrm{OU}}$')
plt.show()
```

In the presence of two or more fixed points, noisy inputs can drive a transition between the fixed points! Here, we stimulate an E population for 1,000 ms applying OU inputs.

```python
# @markdown Execute this cell to simulate E population with OU inputs

pars = default_pars_single(T=1000)
pars['w'] = 5.0
sig_ou = 0.7
pars['tau_ou'] = 1.  # [ms]
pars['I_ext'] = 0.56 + my_OU(pars, sig=sig_ou, myseed=2020)

r = simulate_single(pars)

plt.figure(figsize=(10, 4))
plt.plot(pars['range_t'], r, 'b', alpha=0.8)
plt.xlabel('t (ms)')
plt.ylabel(r'$r(t)$')
plt.show()
```

You can see that the population activity is changing between fixed points (it goes up and down)!