The notebook shows anlaysis of a SMART-seq2 dataset, where we start with per-cell bam files. It shows how to estimate gene-relative velocity (with different pooling options), as well as how to estimate gene-relative velocity.

Data loading

Load the velocyto package:

library(velocyto.R)

Load the data and genome annotations:

(this block will note evalaute by default, as it takes a while)

# if you want to run this, please download bam files (and genes.refFlat) from
# http://pklab.med.harvard.edu/velocyto/chromaffin/bams.tar
# and extract it ("tar xvf bams.tar") in the working directory.
# note: the file is fairly large - 5.6 GB! 
path <- "data/e12.5.bams"
files <- system(paste('find',path,'-name "*unique.bam" -print'),intern=T)
names(files) <- gsub(".*\\/(.*)_unique.bam","\\1",files)
# parse gene annotation, annotate bam file reads
dat <- read.smartseq2.bams(files,"data/genes.refFlat",n.cores=40)

(instead, we read in the resulting structure from the rds file)

dat <- readRDS(url("http://pklab.med.harvard.edu/velocyto/chromaffin/dat.rds"))

Read in cell cluster assignment and tSNE embedding used in the Furlan et al. (Science’17).

cell.colors <- readRDS(url("http://pklab.med.harvard.edu/velocyto/chromaffin/cell.colors.rds"))
emb <- readRDS(url("http://pklab.med.harvard.edu/velocyto/chromaffin/embedding.rds"))

Gene filtering

Spliced expression magnitude distribution across genes:

hist(log10(rowSums(dat$emat)+1),col='wheat',xlab='log10[ number of reads + 1]',main='number of reads per gene')

Set up expression matrices, filtering genes to leave those that exceed some pre-defined g to the average expression magnitude

# exonic read (spliced) expression matrix
emat <- dat$emat;
# intronic read (unspliced) expression matrix
nmat <- dat$iomat;
# spanning read (intron+exon) expression matrix
smat <- dat$smat;
# filter expression matrices based on some minimum max-cluster averages
emat <- filter.genes.by.cluster.expression(emat,cell.colors,min.max.cluster.average = 5)
nmat <- filter.genes.by.cluster.expression(nmat,cell.colors,min.max.cluster.average = 1)
smat <- filter.genes.by.cluster.expression(smat,cell.colors,min.max.cluster.average = 0.5)
# look at the resulting gene set
str(intersect(intersect(rownames(emat),rownames(nmat)),rownames(smat)))
 chr [1:5920] "Dmd" "Rbfox1" "Dlg2" "Auts2" "Ctnna2" "Rbms3" "Gpc6" "Pard3b" "Dpyd" "Hdac9" "Celf2" "Erc2" ...

Several variants of velocity estimates using gene-relative model

We’ll start with what is perhaps the most robust estimate, that combines cell kNN pooling with the gamma fit based on an extreme quantiles:

Using min/max quantile fit, in which case gene-specific offsets do not require spanning read (smat) fit. Here the fit is based on the top/bottom 2% of cells (by spliced expression magnitude)

rvel.qf <- gene.relative.velocity.estimates(emat,nmat,deltaT=1,kCells = 5,fit.quantile = 0.02)
calculating cell knn ... done
calculating convolved matrices ... done
fitting gamma coefficients ... done. succesfful fit for 8646 genes
filtered out 1335 out of 8646 genes due to low nmat-emat correlation
filtered out 968 out of 7311 genes due to low nmat-emat slope
calculating RNA velocity shift ... done
calculating extrapolated cell state ... done

We visualize the velocities by projecting observed and extrapolated cells onto the first 5 PCs:

pca.velocity.plot(rvel.qf,nPcs=5,plot.cols=2,cell.colors=ac(cell.colors,alpha=0.7),cex=1.2,pcount=0.1,pc.multipliers=c(1,-1,-1,-1,-1))
log ... pca ... pc multipliers ... delta norm ... done

Fitting of individual genes can be visualized using “show.gene” option. To save time, we’ll pass previously-calculated velocity (rvel.qf) to save calculation time:

# define custom pallet for expression magnitude
gene.relative.velocity.estimates(emat,nmat,deltaT=1,kCells = 5,fit.quantile = 0.02,old.fit=rvel.qf,show.gene='Chga',cell.emb=emb,cell.colors=cell.colors)
calculating convolved matrices ... done
[1] 1

Alternatively, we calculate gene-relative velocity, using k=5 cell kNN pooling, but now using entire range of expression to determine slope gamma, and using spanning reads (smat) to fit the gene offsets.

rvel <- gene.relative.velocity.estimates(emat,nmat,smat=smat,deltaT=1,kCells = 5, min.nmat.emat.slope = 0.1, min.nmat.smat.correlation = 0.1)
calculating cell knn ... done
calculating convolved matrices ... done
fitting smat-based offsets ... done
fitting gamma coefficients ... done. succesfful fit for 5920 genes
filtered out 574 out of 5920 genes due to low nmat-smat correlation
filtered out 589 out of 5346 genes due to low nmat-emat correlation
filtered out 546 out of 4757 genes due to low nmat-emat slope
calculating RNA velocity shift ... done
calculating extrapolated cell state ... done

We can visualize the velocity in PCA space:

pca.velocity.plot(rvel,nPcs=5,plot.cols=2,cell.colors=ac(cell.colors,alpha=0.7),cex=1.2,pcount=0.1,pc.multipliers=c(1,-1,1,1,1))
log ... pca ... pc multipliers ... delta norm ... done

Here we calculate the most basic version of velocity estimates, using relative gamma fit, without cell kNN smoothing:

rvel1 <- gene.relative.velocity.estimates(emat,nmat,deltaT=1,deltaT2 = 1,kCells = 1)
fitting gamma coefficients ... done. succesfful fit for 8646 genes
filtered out 820 out of 8646 genes due to low nmat-emat correlation
filtered out 496 out of 7826 genes due to low nmat-emat slope
calculating RNA velocity shift ... done
calculating extrapolated cell state ... done
pca.velocity.plot(rvel1,nPcs=5,plot.cols=2,cell.colors=ac(cell.colors,alpha=0.7),cex=1.2,pcount=0.1,pc.multipliers=c(1,-1,1,1,1))
log ... pca ... pc multipliers ... delta norm ... done

Velocity estimate based on gene structure

Genome-wide model fit:

# start with unfiltered matrices, as we can use more genes in these types of estimates
emat <- dat$emat; nmat <- dat$iomat; smat <- dat$smat;
emat <- filter.genes.by.cluster.expression(emat,cell.colors,min.max.cluster.average = 7)
gvel <- global.velcoity.estimates(emat, nmat, rvel, dat$base.df, smat=smat, deltaT=1, kCells=5, kGenes = 15, kGenes.trim = 5, min.gene.cells = 0, min.gene.conuts = 500)
filtered out 5 out of 9298 genes due to low emat levels
filtered out 1103 out of 9293 genes due to insufficient exonic or intronic lengths
filtered out 165 out of 8190 genes due to excessive nascent counts
using relative slopes for 3752 genes to fit structure-based model ... 67.3% deviance explained.
predicting gamma ... done
refitting offsets ... calculating cell knn ... done
calculating convolved matrices ... done
fitting smat-based offsets ... done
fitting gamma coefficients ... done. succesfful fit for 7912 genes
filtered out 930 out of 7893 genes due to low nmat-smat correlation
filtered out 911 out of 6963 genes due to low nmat-emat correlation
filtered out 388 out of 6052 genes due to low nmat-emat slope
calculating RNA velocity shift ... done
calculating extrapolated cell state ... done
re-estimated offsets for 6963 out of 8025 genes
calculating convolved matrices ... done
calculating gene knn ... done
estimating M values ... adjusting mval offsets ... re-estimating gamma ... done
calculating RNA velocity shift ... done
calculating extrapolated cell state ... done
pca.velocity.plot(gvel,nPcs=5,plot.cols=2,cell.colors=ac(cell.colors,alpha=0.7),cex=1.2,pcount=0.1,pc.multipliers=c(1,-1,-1,1,1))
log ... pca ... pc multipliers ... delta norm ... done

Or in tSNE space

#pdf(file='tsne.shift.plots.pdf',height=6,width=12)
par(mfrow=c(1,2), mar = c(2.5,2.5,2.5,1.5), mgp = c(2,0.65,0), cex = 0.85);
x <- tSNE.velocity.plot(rvel,nPcs=15,cell.colors=cell.colors,cex=0.9,perplexity=200,norm.nPcs=NA,pcount=0.1,scale='log',do.par=F)
rescaling ... log ... pca ... delta norm ... tSNE ...Read the 768 x 15 data matrix successfully!
OpenMP is working...
Using no_dims = 2, perplexity = 200.000000, and theta = 0.500000
Computing input similarities...
Normalizing input...
Building tree...
Done in 1.25 seconds (sparsity = 0.904921)!
Learning embedding...
Iteration 50: error is 45.122118 (50 iterations in 1.55 seconds)
Iteration 100: error is 45.122118 (50 iterations in 1.79 seconds)
Iteration 150: error is 45.122114 (50 iterations in 1.90 seconds)
Iteration 200: error is 45.121998 (50 iterations in 1.83 seconds)
Iteration 250: error is 45.118757 (50 iterations in 1.84 seconds)
Iteration 300: error is 0.147369 (50 iterations in 1.85 seconds)
Iteration 350: error is 0.145243 (50 iterations in 1.89 seconds)
Iteration 400: error is 0.144270 (50 iterations in 1.92 seconds)
Iteration 450: error is 0.143748 (50 iterations in 1.90 seconds)
Iteration 500: error is 0.143296 (50 iterations in 1.89 seconds)
Iteration 550: error is 0.143195 (50 iterations in 1.88 seconds)
Iteration 600: error is 0.143089 (50 iterations in 2.04 seconds)
Iteration 650: error is 0.143074 (50 iterations in 1.97 seconds)
Iteration 700: error is 0.143206 (50 iterations in 1.92 seconds)
Iteration 750: error is 0.143037 (50 iterations in 1.53 seconds)
Iteration 800: error is 0.143136 (50 iterations in 1.50 seconds)
Iteration 850: error is 0.143409 (50 iterations in 1.42 seconds)
Iteration 900: error is 0.143326 (50 iterations in 1.82 seconds)
Iteration 950: error is 0.143302 (50 iterations in 2.03 seconds)
Iteration 1000: error is 0.143286 (50 iterations in 1.95 seconds)
Fitting performed in 36.42 seconds.
delta norm ... done
x <- tSNE.velocity.plot(gvel,nPcs=15,cell.colors=cell.colors,cex=0.9,perplexity=200,norm.nPcs=NA,pcount=0.1,scale='log',do.par=F)
rescaling ... log ... pca ... delta norm ... tSNE ...Read the 768 x 15 data matrix successfully!
OpenMP is working...
Using no_dims = 2, perplexity = 200.000000, and theta = 0.500000
Computing input similarities...
Normalizing input...
Building tree...
Done in 1.46 seconds (sparsity = 0.905338)!
Learning embedding...
Iteration 50: error is 45.060497 (50 iterations in 1.77 seconds)
Iteration 100: error is 45.060497 (50 iterations in 1.72 seconds)
Iteration 150: error is 45.060497 (50 iterations in 1.95 seconds)
Iteration 200: error is 45.060497 (50 iterations in 2.09 seconds)
Iteration 250: error is 45.060497 (50 iterations in 2.06 seconds)
Iteration 300: error is 0.176206 (50 iterations in 1.77 seconds)
Iteration 350: error is 0.161241 (50 iterations in 1.63 seconds)
Iteration 400: error is 0.162081 (50 iterations in 1.46 seconds)
Iteration 450: error is 0.161970 (50 iterations in 1.71 seconds)
Iteration 500: error is 0.161862 (50 iterations in 1.77 seconds)
Iteration 550: error is 0.162071 (50 iterations in 1.82 seconds)
Iteration 600: error is 0.162070 (50 iterations in 1.84 seconds)
Iteration 650: error is 0.161656 (50 iterations in 1.83 seconds)
Iteration 700: error is 0.161123 (50 iterations in 1.81 seconds)
Iteration 750: error is 0.162124 (50 iterations in 1.75 seconds)
Iteration 800: error is 0.161912 (50 iterations in 1.55 seconds)
Iteration 850: error is 0.162460 (50 iterations in 1.72 seconds)
Iteration 900: error is 0.161989 (50 iterations in 1.38 seconds)
Iteration 950: error is 0.161813 (50 iterations in 1.39 seconds)
Iteration 1000: error is 0.162155 (50 iterations in 1.39 seconds)
Fitting performed in 34.42 seconds.
delta norm ... done

#dev.off()

Visualization on an existing embedding

Here we use t-SNE embedding from the original publication (in emb variable).

vel <- rvel; arrow.scale=6; cell.alpha=0.4; cell.cex=1; fig.height=4; fig.width=4.5;
show.velocity.on.embedding.cor(emb,vel,n=100,scale='sqrt',cell.colors=ac(cell.colors,alpha=cell.alpha),cex=cell.cex,arrow.scale=arrow.scale,arrow.lwd=1)
delta projections ... sqrt knn ... transition probs ... done

Alternatively, the same function can be used to calculate a velocity vector field:

show.velocity.on.embedding.cor(emb,vel,n=100,scale='sqrt',cell.colors=ac(cell.colors,alpha=cell.alpha),cex=cell.cex,arrow.scale=arrow.scale,show.grid.flow=TRUE,min.grid.cell.mass=0.5,grid.n=20,arrow.lwd=2)
delta projections ... sqrt knn ... transition probs ... done
calculating arrows ... grid estimates ... grid.sd= 1.731182  min.arrow.size= 0.03462363  max.grid.arrow.length= 0.09156871  done

Cell trajectory modeling

A similar function can be used to model central trajectories by directed diffusion on embedding. The main parameters are set up by sigma (which limits the range of how far a cell can jump in terms of distance) and n (how many nearest neighbors are being considered for jumps). The results are sensitive to these parameters, as we don’t have a good way of assessing how much the directional velocity component should compare with random Brownian motion of a cell with the manifold. For instance, relaxing (increasing) sigma, in particular will eventually lead to sympathoblast cells “jumping” the gap into the into the chromaffin differentiation part.

Warning: this simulation takes some time (e.g. a couple of minutes on 40 cores).

x <- show.velocity.on.embedding.eu(emb,vel,n=40,scale='sqrt',cell.colors=ac(cell.colors,alpha=cell.alpha),cex=cell.cex,nPcs=30,sigma=2.5,show.trajectories=TRUE,diffusion.steps=500,n.trajectory.clusters=15,ntop.trajectories=1,embedding.knn=T,control.for.neighborhood.density=TRUE,n.cores=40) 
sqrt scale ... reducing to 30 PCs ... distance ... sigma= 2.5  beta= 1  transition probs ... embedding kNN ... done
simulating diffusion ... constructing path graph ... tracing shortest trajectories ... clustering ... done.

LS0tCnRpdGxlOiAiQ2hyb21hZmZpbiAgZGlmZmVyZW50aWF0aW9uIGFuYWx5c2lzIgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCgpUaGUgbm90ZWJvb2sgc2hvd3MgYW5sYXlzaXMgb2YgYSBTTUFSVC1zZXEyIGRhdGFzZXQsIHdoZXJlIHdlIHN0YXJ0IHdpdGggcGVyLWNlbGwgYmFtIGZpbGVzLgpJdCBzaG93cyBob3cgdG8gZXN0aW1hdGUgZ2VuZS1yZWxhdGl2ZSB2ZWxvY2l0eSAod2l0aCBkaWZmZXJlbnQgcG9vbGluZyBvcHRpb25zKSwgYXMgd2VsbCBhcyBob3cgdG8gZXN0aW1hdGUgZ2VuZS1yZWxhdGl2ZSB2ZWxvY2l0eS4KCiMjIERhdGEgbG9hZGluZwoKTG9hZCB0aGUgdmVsb2N5dG8gcGFja2FnZToKYGBge3J9CmxpYnJhcnkodmVsb2N5dG8uUikKYGBgCgpMb2FkIHRoZSBkYXRhIGFuZCBnZW5vbWUgYW5ub3RhdGlvbnM6CgoodGhpcyBibG9jayB3aWxsIG5vdGUgZXZhbGF1dGUgYnkgZGVmYXVsdCwgYXMgaXQgdGFrZXMgYSB3aGlsZSkKYGBge3IgZXZhbD1GQUxTRX0KIyBpZiB5b3Ugd2FudCB0byBydW4gdGhpcywgcGxlYXNlIGRvd25sb2FkIGJhbSBmaWxlcyAoYW5kIGdlbmVzLnJlZkZsYXQpIGZyb20KIyBodHRwOi8vcGtsYWIubWVkLmhhcnZhcmQuZWR1L3ZlbG9jeXRvL2Nocm9tYWZmaW4vYmFtcy50YXIKIyBhbmQgZXh0cmFjdCBpdCAoInRhciB4dmYgYmFtcy50YXIiKSBpbiB0aGUgd29ya2luZyBkaXJlY3RvcnkuCiMgbm90ZTogdGhlIGZpbGUgaXMgZmFpcmx5IGxhcmdlIC0gNS42IEdCISAKcGF0aCA8LSAiZGF0YS9lMTIuNS5iYW1zIgpmaWxlcyA8LSBzeXN0ZW0ocGFzdGUoJ2ZpbmQnLHBhdGgsJy1uYW1lICIqdW5pcXVlLmJhbSIgLXByaW50JyksaW50ZXJuPVQpCm5hbWVzKGZpbGVzKSA8LSBnc3ViKCIuKlxcLyguKilfdW5pcXVlLmJhbSIsIlxcMSIsZmlsZXMpCiMgcGFyc2UgZ2VuZSBhbm5vdGF0aW9uLCBhbm5vdGF0ZSBiYW0gZmlsZSByZWFkcwpkYXQgPC0gcmVhZC5zbWFydHNlcTIuYmFtcyhmaWxlcywiZGF0YS9nZW5lcy5yZWZGbGF0IixuLmNvcmVzPTQwKQpgYGAKCihpbnN0ZWFkLCB3ZSByZWFkIGluIHRoZSByZXN1bHRpbmcgc3RydWN0dXJlIGZyb20gdGhlIHJkcyBmaWxlKQpgYGB7cn0KZGF0IDwtIHJlYWRSRFModXJsKCJodHRwOi8vcGtsYWIubWVkLmhhcnZhcmQuZWR1L3ZlbG9jeXRvL2Nocm9tYWZmaW4vZGF0LnJkcyIpKQpgYGAKClJlYWQgaW4gY2VsbCBjbHVzdGVyIGFzc2lnbm1lbnQgYW5kIHRTTkUgZW1iZWRkaW5nIHVzZWQgaW4gdGhlIEZ1cmxhbiBldCBhbC4gKFNjaWVuY2UnMTcpLgpgYGB7cn0KY2VsbC5jb2xvcnMgPC0gcmVhZFJEUyh1cmwoImh0dHA6Ly9wa2xhYi5tZWQuaGFydmFyZC5lZHUvdmVsb2N5dG8vY2hyb21hZmZpbi9jZWxsLmNvbG9ycy5yZHMiKSkKZW1iIDwtIHJlYWRSRFModXJsKCJodHRwOi8vcGtsYWIubWVkLmhhcnZhcmQuZWR1L3ZlbG9jeXRvL2Nocm9tYWZmaW4vZW1iZWRkaW5nLnJkcyIpKQpgYGAKCiMjIEdlbmUgZmlsdGVyaW5nClNwbGljZWQgZXhwcmVzc2lvbiBtYWduaXR1ZGUgZGlzdHJpYnV0aW9uIGFjcm9zcyBnZW5lczoKYGBge3J9Cmhpc3QobG9nMTAocm93U3VtcyhkYXQkZW1hdCkrMSksY29sPSd3aGVhdCcseGxhYj0nbG9nMTBbIG51bWJlciBvZiByZWFkcyArIDFdJyxtYWluPSdudW1iZXIgb2YgcmVhZHMgcGVyIGdlbmUnKQpgYGAKCgpTZXQgdXAgZXhwcmVzc2lvbiBtYXRyaWNlcywgZmlsdGVyaW5nIGdlbmVzIHRvIGxlYXZlIHRob3NlIHRoYXQgZXhjZWVkIHNvbWUgcHJlLWRlZmluZWQgZyB0byB0aGUgYXZlcmFnZSBleHByZXNzaW9uIG1hZ25pdHVkZQpgYGB7cn0KIyBleG9uaWMgcmVhZCAoc3BsaWNlZCkgZXhwcmVzc2lvbiBtYXRyaXgKZW1hdCA8LSBkYXQkZW1hdDsKIyBpbnRyb25pYyByZWFkICh1bnNwbGljZWQpIGV4cHJlc3Npb24gbWF0cml4Cm5tYXQgPC0gZGF0JGlvbWF0OwojIHNwYW5uaW5nIHJlYWQgKGludHJvbitleG9uKSBleHByZXNzaW9uIG1hdHJpeApzbWF0IDwtIGRhdCRzbWF0OwojIGZpbHRlciBleHByZXNzaW9uIG1hdHJpY2VzIGJhc2VkIG9uIHNvbWUgbWluaW11bSBtYXgtY2x1c3RlciBhdmVyYWdlcwplbWF0IDwtIGZpbHRlci5nZW5lcy5ieS5jbHVzdGVyLmV4cHJlc3Npb24oZW1hdCxjZWxsLmNvbG9ycyxtaW4ubWF4LmNsdXN0ZXIuYXZlcmFnZSA9IDUpCm5tYXQgPC0gZmlsdGVyLmdlbmVzLmJ5LmNsdXN0ZXIuZXhwcmVzc2lvbihubWF0LGNlbGwuY29sb3JzLG1pbi5tYXguY2x1c3Rlci5hdmVyYWdlID0gMSkKc21hdCA8LSBmaWx0ZXIuZ2VuZXMuYnkuY2x1c3Rlci5leHByZXNzaW9uKHNtYXQsY2VsbC5jb2xvcnMsbWluLm1heC5jbHVzdGVyLmF2ZXJhZ2UgPSAwLjUpCgojIGxvb2sgYXQgdGhlIHJlc3VsdGluZyBnZW5lIHNldApzdHIoaW50ZXJzZWN0KGludGVyc2VjdChyb3duYW1lcyhlbWF0KSxyb3duYW1lcyhubWF0KSkscm93bmFtZXMoc21hdCkpKQoKYGBgCgojIyBTZXZlcmFsIHZhcmlhbnRzIG9mIHZlbG9jaXR5IGVzdGltYXRlcyB1c2luZyBnZW5lLXJlbGF0aXZlIG1vZGVsCgpXZSdsbCBzdGFydCB3aXRoIHdoYXQgaXMgcGVyaGFwcyB0aGUgbW9zdCByb2J1c3QgZXN0aW1hdGUsIHRoYXQgY29tYmluZXMgY2VsbCBrTk4gcG9vbGluZyB3aXRoIHRoZSBnYW1tYSBmaXQgYmFzZWQgb24gYW4gZXh0cmVtZSBxdWFudGlsZXM6CgpVc2luZyBtaW4vbWF4IHF1YW50aWxlIGZpdCwgaW4gd2hpY2ggY2FzZSBnZW5lLXNwZWNpZmljIG9mZnNldHMgZG8gbm90IHJlcXVpcmUgc3Bhbm5pbmcgcmVhZCAoc21hdCkgZml0LgpIZXJlIHRoZSBmaXQgaXMgYmFzZWQgb24gdGhlIHRvcC9ib3R0b20gMiUgb2YgY2VsbHMgKGJ5IHNwbGljZWQgZXhwcmVzc2lvbiBtYWduaXR1ZGUpCmBgYHtyfQpydmVsLnFmIDwtIGdlbmUucmVsYXRpdmUudmVsb2NpdHkuZXN0aW1hdGVzKGVtYXQsbm1hdCxkZWx0YVQ9MSxrQ2VsbHMgPSA1LGZpdC5xdWFudGlsZSA9IDAuMDIpCmBgYAoKV2UgdmlzdWFsaXplIHRoZSB2ZWxvY2l0aWVzIGJ5IHByb2plY3Rpbmcgb2JzZXJ2ZWQgYW5kIGV4dHJhcG9sYXRlZCBjZWxscyBvbnRvIHRoZSBmaXJzdCA1IFBDczoKYGBge3IgZmlnLndpZHRoPTgsIGZpZy5oZWlnaHQ9OH0KcGNhLnZlbG9jaXR5LnBsb3QocnZlbC5xZixuUGNzPTUscGxvdC5jb2xzPTIsY2VsbC5jb2xvcnM9YWMoY2VsbC5jb2xvcnMsYWxwaGE9MC43KSxjZXg9MS4yLHBjb3VudD0wLjEscGMubXVsdGlwbGllcnM9YygxLC0xLC0xLC0xLC0xKSkKYGBgCgoKRml0dGluZyBvZiBpbmRpdmlkdWFsIGdlbmVzIGNhbiBiZSB2aXN1YWxpemVkIHVzaW5nICJzaG93LmdlbmUiIG9wdGlvbi4gVG8gc2F2ZSB0aW1lLCB3ZSdsbCBwYXNzIHByZXZpb3VzbHktY2FsY3VsYXRlZCB2ZWxvY2l0eSAocnZlbC5xZikgdG8gc2F2ZSBjYWxjdWxhdGlvbiB0aW1lOgpgYGB7ciBmaWcud2lkdGg9OCxmaWcuaGVpZ2h0PTIuMn0KIyBkZWZpbmUgY3VzdG9tIHBhbGxldCBmb3IgZXhwcmVzc2lvbiBtYWduaXR1ZGUKZ2VuZS5yZWxhdGl2ZS52ZWxvY2l0eS5lc3RpbWF0ZXMoZW1hdCxubWF0LGRlbHRhVD0xLGtDZWxscyA9IDUsZml0LnF1YW50aWxlID0gMC4wMixvbGQuZml0PXJ2ZWwucWYsc2hvdy5nZW5lPSdDaGdhJyxjZWxsLmVtYj1lbWIsY2VsbC5jb2xvcnM9Y2VsbC5jb2xvcnMpCmBgYAoKCgoKCkFsdGVybmF0aXZlbHksIHdlIGNhbGN1bGF0ZSBnZW5lLXJlbGF0aXZlIHZlbG9jaXR5LCB1c2luZyBrPTUgY2VsbCBrTk4gcG9vbGluZywgYnV0IG5vdyB1c2luZyBlbnRpcmUgcmFuZ2Ugb2YgZXhwcmVzc2lvbiB0byBkZXRlcm1pbmUgc2xvcGUgZ2FtbWEsIGFuZCB1c2luZyBzcGFubmluZyByZWFkcyAoc21hdCkgdG8gZml0IHRoZSBnZW5lIG9mZnNldHMuCmBgYHtyfQpydmVsIDwtIGdlbmUucmVsYXRpdmUudmVsb2NpdHkuZXN0aW1hdGVzKGVtYXQsbm1hdCxzbWF0PXNtYXQsZGVsdGFUPTEsa0NlbGxzID0gNSwgbWluLm5tYXQuZW1hdC5zbG9wZSA9IDAuMSwgbWluLm5tYXQuc21hdC5jb3JyZWxhdGlvbiA9IDAuMSkKYGBgCgpXZSBjYW4gdmlzdWFsaXplIHRoZSB2ZWxvY2l0eSBpbiBQQ0Egc3BhY2U6CmBgYHtyIGZpZy53aWR0aD04LCBmaWcuaGVpZ2h0PTh9CnBjYS52ZWxvY2l0eS5wbG90KHJ2ZWwsblBjcz01LHBsb3QuY29scz0yLGNlbGwuY29sb3JzPWFjKGNlbGwuY29sb3JzLGFscGhhPTAuNyksY2V4PTEuMixwY291bnQ9MC4xLHBjLm11bHRpcGxpZXJzPWMoMSwtMSwxLDEsMSkpCmBgYAoKSGVyZSB3ZSBjYWxjdWxhdGUgdGhlIG1vc3QgYmFzaWMgdmVyc2lvbiBvZiB2ZWxvY2l0eSBlc3RpbWF0ZXMsIHVzaW5nIHJlbGF0aXZlIGdhbW1hIGZpdCwgd2l0aG91dCBjZWxsIGtOTiBzbW9vdGhpbmc6CmBgYHtyfQpydmVsMSA8LSBnZW5lLnJlbGF0aXZlLnZlbG9jaXR5LmVzdGltYXRlcyhlbWF0LG5tYXQsZGVsdGFUPTEsZGVsdGFUMiA9IDEsa0NlbGxzID0gMSkKYGBgCgpgYGB7ciBmaWcud2lkdGg9OCwgZmlnLmhlaWdodD04fQpwY2EudmVsb2NpdHkucGxvdChydmVsMSxuUGNzPTUscGxvdC5jb2xzPTIsY2VsbC5jb2xvcnM9YWMoY2VsbC5jb2xvcnMsYWxwaGE9MC43KSxjZXg9MS4yLHBjb3VudD0wLjEscGMubXVsdGlwbGllcnM9YygxLC0xLDEsMSwxKSkKYGBgCgoKCiMjIFZlbG9jaXR5IGVzdGltYXRlIGJhc2VkIG9uIGdlbmUgc3RydWN0dXJlCgpHZW5vbWUtd2lkZSBtb2RlbCBmaXQ6CmBgYHtyIHdhcm5pbmc9RkFMU0V9CiMgc3RhcnQgd2l0aCB1bmZpbHRlcmVkIG1hdHJpY2VzLCBhcyB3ZSBjYW4gdXNlIG1vcmUgZ2VuZXMgaW4gdGhlc2UgdHlwZXMgb2YgZXN0aW1hdGVzCmVtYXQgPC0gZGF0JGVtYXQ7IG5tYXQgPC0gZGF0JGlvbWF0OyBzbWF0IDwtIGRhdCRzbWF0OwplbWF0IDwtIGZpbHRlci5nZW5lcy5ieS5jbHVzdGVyLmV4cHJlc3Npb24oZW1hdCxjZWxsLmNvbG9ycyxtaW4ubWF4LmNsdXN0ZXIuYXZlcmFnZSA9IDcpCmd2ZWwgPC0gZ2xvYmFsLnZlbGNvaXR5LmVzdGltYXRlcyhlbWF0LCBubWF0LCBydmVsLCBkYXQkYmFzZS5kZiwgc21hdD1zbWF0LCBkZWx0YVQ9MSwga0NlbGxzPTUsIGtHZW5lcyA9IDE1LCBrR2VuZXMudHJpbSA9IDUsIG1pbi5nZW5lLmNlbGxzID0gMCwgbWluLmdlbmUuY29udXRzID0gNTAwKQpgYGAKCgpgYGB7ciBmaWcud2lkdGg9OCwgZmlnLmhlaWdodD04fQpwY2EudmVsb2NpdHkucGxvdChndmVsLG5QY3M9NSxwbG90LmNvbHM9MixjZWxsLmNvbG9ycz1hYyhjZWxsLmNvbG9ycyxhbHBoYT0wLjcpLGNleD0xLjIscGNvdW50PTAuMSxwYy5tdWx0aXBsaWVycz1jKDEsLTEsLTEsMSwxKSkKYGBgCgoKCgpPciBpbiB0U05FIHNwYWNlCmBgYHtyIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD01fQojcGRmKGZpbGU9J3RzbmUuc2hpZnQucGxvdHMucGRmJyxoZWlnaHQ9Nix3aWR0aD0xMikKcGFyKG1mcm93PWMoMSwyKSwgbWFyID0gYygyLjUsMi41LDIuNSwxLjUpLCBtZ3AgPSBjKDIsMC42NSwwKSwgY2V4ID0gMC44NSk7CnggPC0gdFNORS52ZWxvY2l0eS5wbG90KHJ2ZWwsblBjcz0xNSxjZWxsLmNvbG9ycz1jZWxsLmNvbG9ycyxjZXg9MC45LHBlcnBsZXhpdHk9MjAwLG5vcm0ublBjcz1OQSxwY291bnQ9MC4xLHNjYWxlPSdsb2cnLGRvLnBhcj1GKQp4IDwtIHRTTkUudmVsb2NpdHkucGxvdChndmVsLG5QY3M9MTUsY2VsbC5jb2xvcnM9Y2VsbC5jb2xvcnMsY2V4PTAuOSxwZXJwbGV4aXR5PTIwMCxub3JtLm5QY3M9TkEscGNvdW50PTAuMSxzY2FsZT0nbG9nJyxkby5wYXI9RikKI2Rldi5vZmYoKQoKYGBgCgoKIyMgVmlzdWFsaXphdGlvbiBvbiBhbiBleGlzdGluZyBlbWJlZGRpbmcKCkhlcmUgd2UgdXNlIHQtU05FIGVtYmVkZGluZyBmcm9tIHRoZSBvcmlnaW5hbCBwdWJsaWNhdGlvbiAoaW4gZW1iIHZhcmlhYmxlKS4KCmBgYHtyIGZpZy53aWR0aD03LCBmaWcuaGVpZ2h0PTV9CnZlbCA8LSBydmVsOyBhcnJvdy5zY2FsZT02OyBjZWxsLmFscGhhPTAuNDsgY2VsbC5jZXg9MTsgZmlnLmhlaWdodD00OyBmaWcud2lkdGg9NC41OwpzaG93LnZlbG9jaXR5Lm9uLmVtYmVkZGluZy5jb3IoZW1iLHZlbCxuPTEwMCxzY2FsZT0nc3FydCcsY2VsbC5jb2xvcnM9YWMoY2VsbC5jb2xvcnMsYWxwaGE9Y2VsbC5hbHBoYSksY2V4PWNlbGwuY2V4LGFycm93LnNjYWxlPWFycm93LnNjYWxlLGFycm93Lmx3ZD0xKQpgYGAKCkFsdGVybmF0aXZlbHksIHRoZSBzYW1lIGZ1bmN0aW9uIGNhbiBiZSB1c2VkIHRvIGNhbGN1bGF0ZSBhIHZlbG9jaXR5IHZlY3RvciBmaWVsZDoKYGBge3IgZmlnLndpZHRoPTcsIGZpZy5oZWlnaHQ9NX0Kc2hvdy52ZWxvY2l0eS5vbi5lbWJlZGRpbmcuY29yKGVtYix2ZWwsbj0xMDAsc2NhbGU9J3NxcnQnLGNlbGwuY29sb3JzPWFjKGNlbGwuY29sb3JzLGFscGhhPWNlbGwuYWxwaGEpLGNleD1jZWxsLmNleCxhcnJvdy5zY2FsZT1hcnJvdy5zY2FsZSxzaG93LmdyaWQuZmxvdz1UUlVFLG1pbi5ncmlkLmNlbGwubWFzcz0wLjUsZ3JpZC5uPTIwLGFycm93Lmx3ZD0yKQpgYGAKCgojIyBDZWxsIHRyYWplY3RvcnkgbW9kZWxpbmcKQSBzaW1pbGFyIGZ1bmN0aW9uIGNhbiBiZSB1c2VkIHRvIG1vZGVsIGNlbnRyYWwgdHJhamVjdG9yaWVzIGJ5IGRpcmVjdGVkIGRpZmZ1c2lvbiBvbiBlbWJlZGRpbmcuIApUaGUgbWFpbiBwYXJhbWV0ZXJzIGFyZSBzZXQgdXAgYnkgc2lnbWEgKHdoaWNoIGxpbWl0cyB0aGUgcmFuZ2Ugb2YgaG93IGZhciBhIGNlbGwgY2FuIGp1bXAgaW4gdGVybXMgb2YgZGlzdGFuY2UpIGFuZCBuIChob3cgbWFueSBuZWFyZXN0IG5laWdoYm9ycyBhcmUgYmVpbmcgY29uc2lkZXJlZCBmb3IganVtcHMpLiBUaGUgcmVzdWx0cyBhcmUgc2Vuc2l0aXZlIHRvIHRoZXNlIHBhcmFtZXRlcnMsIGFzIHdlIGRvbid0IGhhdmUgYSBnb29kIHdheSBvZiBhc3Nlc3NpbmcgaG93IG11Y2ggdGhlIGRpcmVjdGlvbmFsIHZlbG9jaXR5IGNvbXBvbmVudCBzaG91bGQgY29tcGFyZSB3aXRoIHJhbmRvbSBCcm93bmlhbiBtb3Rpb24gb2YgYSBjZWxsIHdpdGggdGhlIG1hbmlmb2xkLiBGb3IgaW5zdGFuY2UsIHJlbGF4aW5nIChpbmNyZWFzaW5nKSBzaWdtYSwgaW4gcGFydGljdWxhciB3aWxsIGV2ZW50dWFsbHkgbGVhZCB0byBzeW1wYXRob2JsYXN0IGNlbGxzICJqdW1waW5nIiB0aGUgZ2FwIGludG8gdGhlIGludG8gdGhlIGNocm9tYWZmaW4gZGlmZmVyZW50aWF0aW9uIHBhcnQuCgpXYXJuaW5nOiB0aGlzIHNpbXVsYXRpb24gdGFrZXMgc29tZSB0aW1lIChlLmcuIGEgY291cGxlIG9mIG1pbnV0ZXMgb24gNDAgY29yZXMpLgoKYGBge3IgZmlnLndpZHRoPTcsIGZpZy5oZWlnaHQ9NX0KeCA8LSBzaG93LnZlbG9jaXR5Lm9uLmVtYmVkZGluZy5ldShlbWIsdmVsLG49NDAsc2NhbGU9J3NxcnQnLGNlbGwuY29sb3JzPWFjKGNlbGwuY29sb3JzLGFscGhhPWNlbGwuYWxwaGEpLGNleD1jZWxsLmNleCxuUGNzPTMwLHNpZ21hPTIuNSxzaG93LnRyYWplY3Rvcmllcz1UUlVFLGRpZmZ1c2lvbi5zdGVwcz01MDAsbi50cmFqZWN0b3J5LmNsdXN0ZXJzPTE1LG50b3AudHJhamVjdG9yaWVzPTEsZW1iZWRkaW5nLmtubj1ULGNvbnRyb2wuZm9yLm5laWdoYm9yaG9vZC5kZW5zaXR5PVRSVUUsbi5jb3Jlcz00MCkgCgpgYGAKCg==